2025-02-18 01:04:17 +00:00
|
|
|
#!/usr/bin/python3
|
|
|
|
import lxml.etree, math, sys
|
|
|
|
|
|
|
|
_, inpath, outpath, basename = sys.argv
|
|
|
|
|
|
|
|
tree = lxml.etree.parse(inpath)
|
|
|
|
|
2025-02-18 16:22:39 +00:00
|
|
|
polys = []
|
|
|
|
edge2poly = {}
|
|
|
|
|
|
|
|
# Polygons must be convex to work correctly, but we don't verify that
|
|
|
|
|
|
|
|
def points_to_edges(points):
|
|
|
|
points = points + [points[0]]
|
|
|
|
return zip(points[:-1], points[1:])
|
2025-02-18 01:04:17 +00:00
|
|
|
|
2025-02-18 22:11:20 +00:00
|
|
|
def parse_object_polygon_points(object):
|
|
|
|
polygon_data = object.find("polygon")
|
|
|
|
basex = int(object.attrib["x"])
|
|
|
|
basey = int(object.attrib["y"])
|
|
|
|
if polygon_data is None:
|
|
|
|
# It's a rectangle by default
|
2025-02-19 00:55:56 +00:00
|
|
|
width = int(object.attrib["width"])
|
|
|
|
height = int(object.attrib["height"])
|
2025-02-18 22:11:20 +00:00
|
|
|
points = [(basex, basey), (basex + width, basey), (basex + width, basey + height), (basex, basey + height)]
|
|
|
|
else:
|
|
|
|
points = []
|
|
|
|
for point in polygon_data.attrib["points"].split(" "):
|
|
|
|
x,y=point.split(",")
|
|
|
|
points.append((int(x)+basex, int(y)+basey))
|
|
|
|
# check winding order
|
2025-02-18 01:04:17 +00:00
|
|
|
ax,ay=points[0]
|
|
|
|
bx,by=points[1]
|
|
|
|
cx,cy=points[2]
|
|
|
|
abx,aby=bx-ax,by-ay
|
|
|
|
acx,acy=cx-ax,cy-ay
|
|
|
|
cross = abx*acy-acx*aby
|
|
|
|
# positive for clockwise winding order. Example clockwise winding order: (0,0), (1,0), (0,1)
|
|
|
|
if cross < 0:
|
2025-02-18 16:22:39 +00:00
|
|
|
points = points[::-1]
|
2025-02-18 22:11:20 +00:00
|
|
|
return points
|
|
|
|
|
|
|
|
# Returns P,Q,R such that Px+Qy+R=0 for points on the edge, and >0 for points on the right (inside polygon for clockwise winding order)
|
|
|
|
def get_edge_equation(a, b):
|
|
|
|
(ax,ay),(bx,by)=a,b
|
|
|
|
# Line equation Px+Qy+R=0 from https://math.stackexchange.com/questions/422602/convert-two-points-to-line-eq-ax-by-c-0
|
|
|
|
P,Q,R=ay-by,bx-ax,ax*by-bx*ay
|
|
|
|
# Line equation >0 for points inside the triangle.
|
|
|
|
# Normalize so that P^2+Q^2=1 so that the equation result tells us the distance. This can be used to find the closest triangle to a point.
|
|
|
|
norm=1/math.sqrt(P*P+Q*Q)
|
|
|
|
P *= norm
|
|
|
|
Q *= norm
|
|
|
|
R *= norm
|
|
|
|
return P,Q,R
|
|
|
|
|
2025-02-19 00:55:56 +00:00
|
|
|
def get_bounding_box(points):
|
|
|
|
(x1,y1),(x2,y2) = points[0],points[0]
|
|
|
|
for x,y in points:
|
|
|
|
if x<x1: x1=x
|
|
|
|
if x>x2: x2=x
|
|
|
|
if y<y1: y1=y
|
|
|
|
if y>y2: y2=y
|
|
|
|
return x1,y1,x2,y2
|
|
|
|
|
2025-02-18 22:11:20 +00:00
|
|
|
for object in tree.findall("objectgroup[@name='navmesh']/object"):
|
|
|
|
points = parse_object_polygon_points(object)
|
2025-02-18 01:04:17 +00:00
|
|
|
|
2025-02-18 16:22:39 +00:00
|
|
|
poly_id = len(polys)
|
|
|
|
polys.append(points)
|
2025-02-18 01:04:17 +00:00
|
|
|
|
2025-02-18 16:22:39 +00:00
|
|
|
for edge in points_to_edges(points):
|
2025-02-18 01:04:17 +00:00
|
|
|
edge_id = tuple(sorted(edge))
|
2025-02-18 16:22:39 +00:00
|
|
|
if edge_id not in edge2poly: edge2poly[edge_id] = []
|
|
|
|
edge2poly[edge_id].append(poly_id)
|
|
|
|
assert len(edge2poly[edge_id]) <= 2, "more than 2 polygons share edge "+str(edge_id)
|
2025-02-18 01:04:17 +00:00
|
|
|
|
2025-02-18 16:22:39 +00:00
|
|
|
pathfind_edges = [[] for _ in range(len(polys))]
|
|
|
|
for edgepolys in edge2poly.values():
|
|
|
|
assert len(edgepolys) in (1,2)
|
|
|
|
if len(edgepolys) == 1: continue
|
|
|
|
p1,p2 = edgepolys
|
|
|
|
pathfind_edges[p1].append(p2)
|
|
|
|
pathfind_edges[p2].append(p1)
|
2025-02-18 01:04:17 +00:00
|
|
|
|
2025-02-18 16:22:39 +00:00
|
|
|
pathfind_matrix = [[255 for _1 in range(len(polys))] for _2 in range(len(polys))]
|
2025-02-18 01:04:17 +00:00
|
|
|
# pathfind_matrix[source][dest] is how to get to dest from source
|
|
|
|
|
2025-02-18 16:22:39 +00:00
|
|
|
for dest in range(len(polys)):
|
|
|
|
# Find paths to poly by breadth-first search. We don't take the size of the polygon into account yet.
|
2025-02-18 01:04:17 +00:00
|
|
|
openset = [dest]
|
|
|
|
while len(openset)>0:
|
|
|
|
cur, openset = openset[0], openset[1:]
|
|
|
|
for adjacent in pathfind_edges[cur]:
|
|
|
|
if adjacent != dest and pathfind_matrix[adjacent][dest] == 255:
|
|
|
|
pathfind_matrix[adjacent][dest] = cur
|
|
|
|
openset.append(adjacent)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
out = open(outpath,"w")
|
|
|
|
|
|
|
|
out.write("#include \"compiled_structures.h\"\n")
|
2025-02-18 22:11:20 +00:00
|
|
|
out.write("#include \"objids.h\"\n")
|
2025-02-18 16:22:39 +00:00
|
|
|
out.write(f"static const struct navmesh_tri {basename}_triangles[{len(polys)}] = {{\n");
|
|
|
|
for poly_id,points in enumerate(polys):
|
|
|
|
out.write("\t{\n")
|
|
|
|
out.write("\t\t"+str(len(points))+",\n")
|
|
|
|
out.write("\t\t(const struct navmesh_tri_edge[]){\n")
|
|
|
|
for a,b in points_to_edges(points):
|
2025-02-18 22:11:20 +00:00
|
|
|
P,Q,R = get_edge_equation(a,b)
|
2025-02-18 01:04:17 +00:00
|
|
|
edge_id = tuple(sorted((a,b)))
|
2025-02-18 16:22:39 +00:00
|
|
|
polys_on_edge = edge2poly[edge_id]
|
|
|
|
assert len(polys_on_edge) in (1,2)
|
|
|
|
assert poly_id in polys_on_edge
|
|
|
|
if len(polys_on_edge) == 2:
|
|
|
|
other_poly_id = [x for x in polys_on_edge if x!=poly_id][0]
|
2025-02-18 01:04:17 +00:00
|
|
|
else:
|
2025-02-18 16:22:39 +00:00
|
|
|
other_poly_id = -1
|
2025-02-18 01:04:17 +00:00
|
|
|
|
2025-02-18 22:11:20 +00:00
|
|
|
(ax,ay),(bx,by)=a,b
|
2025-02-18 01:04:17 +00:00
|
|
|
centx, centy = (ax+bx)/2, (ay+by)/2
|
|
|
|
|
2025-02-18 16:22:39 +00:00
|
|
|
out.write(f"\t\t\t{{{P},{Q},{R},{other_poly_id},{{{int(centx)},{int(centy)}}}}},\n")
|
2025-02-18 01:04:17 +00:00
|
|
|
out.write("\t\t},\n")
|
2025-02-18 16:22:39 +00:00
|
|
|
out.write("\t\t(const struct navmesh_point[]){" + ",".join(f"{{{x},{y}}}" for x,y in points) + "},\n")
|
2025-02-18 01:04:17 +00:00
|
|
|
out.write("\t},\n");
|
|
|
|
out.write("};\n")
|
2025-02-18 16:22:39 +00:00
|
|
|
out.write(f"static const unsigned char {basename}_pathfind[{len(polys)*len(polys)}] = {{\n")
|
|
|
|
for dest in range(len(polys)):
|
2025-02-18 01:04:17 +00:00
|
|
|
out.write("\t")
|
2025-02-18 16:22:39 +00:00
|
|
|
for source in range(len(polys)):
|
2025-02-18 01:04:17 +00:00
|
|
|
out.write(str(pathfind_matrix[source][dest])+",")
|
|
|
|
out.write("\n")
|
|
|
|
out.write("};\n")
|
2025-02-18 22:11:20 +00:00
|
|
|
out.write(f"extern const struct navmesh navmesh_{basename} = {{{len(polys)}, {basename}_triangles, {basename}_pathfind}};\n")
|
|
|
|
|
|
|
|
out.write("extern const struct level_predef_data predef_"+basename+" = {\n")
|
|
|
|
out.write("\t(const struct level_clickregion[]){\n") # clickregions start
|
|
|
|
for object in tree.findall("objectgroup[@name='clickable']/object"):
|
|
|
|
out.write("\t\t{\n");
|
|
|
|
points = parse_object_polygon_points(object)
|
2025-02-19 00:55:56 +00:00
|
|
|
x1,y1,x2,y2 = get_bounding_box(points)
|
2025-02-18 22:11:20 +00:00
|
|
|
out.write("\t\t\t"+object.attrib["name"]+",\n")
|
2025-02-19 00:55:56 +00:00
|
|
|
out.write(f"\t\t\t{x1},{y1},{x2-x1},{y2-y1},\n")
|
2025-02-18 22:11:20 +00:00
|
|
|
out.write("\t\t\t" + str(len(points)) + ",\n")
|
|
|
|
out.write("\t\t\t(const struct level_clickregion_edge[]){\n")
|
|
|
|
for a,b in points_to_edges(points):
|
|
|
|
P,Q,R = get_edge_equation(a,b)
|
|
|
|
out.write(f"\t\t\t\t{{{P},{Q},{R}}},\n")
|
|
|
|
out.write("\t\t\t},\n")
|
|
|
|
out.write("\t\t},\n");
|
|
|
|
out.write("\t\t{0}\n")
|
|
|
|
out.write("\t},\n") # clickregions end
|
2025-02-19 16:21:26 +00:00
|
|
|
out.write("\t(const struct level_predef_point[]){\n") # points start
|
|
|
|
for object in tree.findall("objectgroup[@name='points']/object"):
|
|
|
|
x,y=int(object.attrib["x"]),int(object.attrib["y"])
|
|
|
|
# Note: name can contain commas to populate multiple ID fields, or arithmetic expressions
|
|
|
|
out.write(f"\t\t{{{x},{y},{object.attrib['name']}}},\n")
|
|
|
|
out.write("\t\t{0}\n")
|
|
|
|
out.write("\t},\n") # points end
|
2025-02-18 22:11:20 +00:00
|
|
|
out.write("};\n")
|