papermario/tools/make_npc_structs.py
Maide a8e5d9b1d9
world/area_arn/arn_02 (#236)
* area_arn/arn_02

* Signed StoryProgress enum
2021-03-29 01:05:56 +01:00

540 lines
18 KiB
Python

from pathlib import Path
import argparse
from struct import unpack_from
constants = {}
def get_constants():
global constants
valid_enums = { "ItemId", "PlayerAnim", "ActorID", "Event", "SoundId" }
for enum in valid_enums:
constants[enum] = {}
constants["NPC_SPRITE"] = {}
include_path = Path(Path(__file__).resolve().parent.parent / "include")
enums = Path(include_path / "enums.h").read_text()
for line in enums.splitlines():
this_enum = ""
for enum in valid_enums:
if f"#define {enum}_" in line:
this_enum = enum
break;
if this_enum:
name = line.split(" ",2)[1]
id_ = line.split("0x", 1)[1]
if " " in id_:
id_ = id_.split(" ",1)[0]
constants[this_enum][int(id_, 16)] = name
sprite_path = Path(Path(__file__).resolve().parent.parent / "ver" / "current" / "build" / "include" / "sprite" / "npc")
for file in sprite_path.iterdir():
fd = file.read_text()
for line in fd.splitlines():
if "#define _NPC_SPRITE_" in line:
enum = "NPC_SPRITE"
elif "#define _NPC_PALETTE_" in line:
enum = "NPC_PALETTE"
elif "#define _NPC_ANIM_" in line:
enum = "NPC_ANIM"
else:
continue
name = line.split(" ",2)[1]
id_ = line.split("0x", 1)[1]
if " " in id_:
id_ = id_.split(" ",1)[0]
name = name.split(f"_{enum}_", 1)[1]
if enum == "NPC_SPRITE":
saved_name = name
saved_id = id_
else:
name = name.rsplit(f"{saved_name}_")[1]
if enum == "NPC_SPRITE":
if int(id_, 16) not in constants["NPC_SPRITE"]:
constants[enum][int(id_, 16)] = {"name":"", "palettes":{}, "anims":{}}
constants[enum][int(id_, 16)]["name"] = name
elif enum == "NPC_PALETTE":
constants["NPC_SPRITE"][int(saved_id, 16)]["palettes"][int(id_, 16)] = name
elif enum == "NPC_ANIM":
constants["NPC_SPRITE"][int(saved_id, 16)]["anims"][int(id_, 16)] = name
return
STRUCTS = {}
def parse_var(line):
#print(f"Parsing {line}")
if "*/ " in line:
line = line.split("*/ ",1)[1]
line = line.split(";",1)[0].strip()
if "," in line or "(*" in line:
return (None, None, None, None)
elif "union " in line:
return ("union", None, None, None)
#print(f"Parsed {line}")
if " " in line:
if line.startswith("struct "):
struct, type_, name = line.split(" ")
else:
type_, name = line.split(" ")
else:
type_ = "function"
name = line.split("(", 1)[0]
count = 1
if "[" in name:
name, *count = name.split("[")
counts = 1
count.reverse()
for dim in count:
counts *= int(dim[:-1], 0)
count = counts
is_ptr = "*" in type_ or type_ == "UNK_PTR"
return (type_, name, count, is_ptr)
def parse_file(filename):
fd = filename.read_text().splitlines()
i = 0
while i < len(fd):
#supported = [f"typedef struct {x}" in fd[i] for x in SUPPORTED_STRUCTS]
#if any(supported):
if "typedef struct " in fd[i]:
#supported_name = [SUPPORTED_STRUCTS[i] for i,x in enumerate(supported) if x][0]
supported_name = fd[i].split("typedef struct ", 1)[1].split(" {", 1)[0]
if supported_name == "{":
supported_name = ""
#print(f"Parsing struct \"{supported_name}\"")
struct_to_add = []
i += 1
while ("} " + f"{supported_name.upper()}") not in fd[i].split(";",1)[0].upper():
type_, name, count, ptr = parse_var(fd[i])
if type_ == None:
i += 1
continue
union = []
if type_ == "union":
i += 1
while "}" not in fd[i]:
type_, name, count, ptr = parse_var(fd[i])
union.append({"type":type_, "name": name, "num":count, "ptr":ptr})
i += 1
name = fd[i].split("}", 1)[1].split(";", 1)[0]
#print(supported_name, type_, name, count)
struct_to_add.append({"type":type_, "name": name, "num":count, "ptr":ptr, "union":union})
i += 1
#print(f"Broke on line {fd[i]}")
#print()
if supported_name == "":
supported_name = fd[i].split("} ",1)[1].split(";",1)[0]
if "[" in supported_name:
supported_name = supported_name[:-2]
STRUCTS[supported_name] = struct_to_add
i += 1
def get_structs():
parse_file(Path(Path(__file__).parent.parent / "include" / "map.h"))
parse_file(Path(Path(__file__).parent.parent / "include" / "common_structs.h"))
def get_vals(fd, offset, var):
out = []
arr = []
for i in range(var["num"]):
if var["type"] in STRUCTS:
type_ = "struct"
data = []
for var2 in STRUCTS[var["type"]]:
out3, offset = get_vals(fd, offset, var2)
data.extend(out3)
#if var["num"] == 1:
# out.extend(out2)
#else:
#out.append(out2)
else:
type_ = "int"
if var["type"] == "s8" or var["type"] == "char":
if var["type"] == "char":
type_ = "hex"
data = unpack_from('>b', fd, offset)[0]
offset += 1
elif var["type"] == "u8":
data = unpack_from('>B', fd, offset)[0]
offset += 1
elif var["type"] == "s16" or var["type"] in ("ItemId"):
offset += offset % 2
data = unpack_from('>h', fd, offset)[0]
offset += 2
elif var["type"] == "u16":
offset += offset % 2
data = unpack_from('>H', fd, offset)[0]
offset += 2
elif var["type"] == "s32" or var["type"] in ("NpcId", "NpcAnimID", "MessageID"):
poff = offset
offset += offset % 4
data = unpack_from('>i', fd, offset)[0]
offset += 4
elif var["type"] == "u32":
offset += offset % 4
data = unpack_from('>I', fd, offset)[0]
offset += 4
elif var["type"] == "f32":
offset += offset % 4
data = unpack_from('>f', fd, offset)[0]
type_ = "float"
offset += 4
elif var["type"] == "X32":
offset += offset % 4
data = unpack_from('>f', fd, offset)[0]
type_ = "float"
if data < -1000.0 or data > 1000.0:
type_ = "int"
data = unpack_from('>i', fd, offset)[0]
offset += 4
elif var["ptr"]:
offset += offset % 4
data = unpack_from('>I', fd, offset)[0]
type_ = "ptr"
offset += 4
else:
print(f"Unknown data type \"{var['type']}\"")
exit()
if var["num"] == 1:
out.append({"name":var["name"], "type":type_, "data":data})
else:
arr.append({"name":var["name"], "type":type_, "data":data})
if var["num"] > 1:
out.append(arr)
return out, offset
def INDENT(depth):
return f" " * depth
def print_data(vals, indent, needs_name, is_array=False, is_struct=False):
out = []
for val in vals:
line = ""
if needs_name:
line = INDENT(indent)
#print(val)
# array
if type(val) is list:
line += f".{val[0]['name']} = " + "{ "
struct = type(val[0]["data"]) is list
line += ", ".join(print_data(val, indent + 1, needs_name=False, is_array=True, is_struct=struct))
if struct:
line += "\n" + INDENT(indent) + "},"
else:
line += " },"
else:
if needs_name:
line += f".{val['name']} = "
if type(val["data"]) is list:
if is_struct:
line += "\n"
line += INDENT(indent)
line += "{ "
for x,val2 in enumerate(val["data"]):
if x > 0:
line += ", "
#line += f".{val2['name']} = "
if val2["type"] == "float":
line += f"{val2['data']:.01f}f"
elif val2["type"] == "hex":
line += f"0x{val2['data']:X}"
elif val2["type"] == "ptr":
if val2["data"] == 0:
line += f"NULL"
else:
line += f"0x{val2['data']:08X}"
else:
line += f"{val2['data']}"
line += " }"
if not is_array:
line += ","
else:
if "flags" in val["name"].lower() or "animations" in val["name"].lower():
if val["name"] == "flags":
val["type"] = "ptr"
elif val["name"] == "dropFlags":
val["type"] = "hex"
val["data"] = abs(val["data"])
if val["type"] == "float":
line += f"{val['data']:.01f}f"
elif val["type"] == "hex":
line += f"0x{val['data']:X}"
elif val["type"] == "ptr":
if val["data"] == 0:
line += f"NULL"
else:
line += f"0x{val['data']:08X}"
else:
line += f"{val['data']}"
if not (is_array or is_struct):
line += ","
out.append(line)
return out
def output_type2(fd, offset, var):
vals = []
for var in STRUCTS[args.type]:
tmp, offset = get_vals(fd, offset, var)
vals.extend(tmp)
#print(vals)
#print()
out = [f"{args.type} = " + "{"]
out.extend(print_data(vals, 1, True))
out.append("};")
#print("END======")
#print("\n".join(out))
return "\n".join(out)
def check_list(vals, depth = 0):
for x,val in enumerate(vals):
if type(val) == list:
if check_list(val, depth + 1):
return True
elif val != 0:
return True
return False
def recurse_check_list(vals):
res = 0
for x,val in enumerate(vals):
if type(val) == list:
if check_list(val, 1):
return len(vals) - x
elif val != 0:
return len(vals) - x
return -1
def get_single_struct_vals(fd, i):
vals = []
if not fd[i].rstrip().endswith("},"):
# must be a sub-struct over multiple lines
old_i = i
i += 1
while not ("}," in fd[i] and "." in fd[i+1]):
temp = fd[i].split("{",1)[1].split("}",1)[0].split(", ")
a = []
for x in temp:
x = x.strip()
if x != "":
a.append(int(x, 0))
vals.append(a)
i += 1
else:
# single line
temp = fd[i].split("{",1)[1].split("}",1)[0].split(", ")
a = []
for x in temp:
x = x.strip()
if x != "":
a.append(int(x, 0))
vals.extend(a)
return vals, i
def cull_struct(fd, i):
out = []
vals = []
#print(f"Culling Starting at {fd[i]}")
if not fd[i].rstrip().endswith("},"):
# must be a sub-struct over multiple lines
old_i = i
vals, i = get_single_struct_vals(fd, i)
# reverse and cull entries of only zeros
x = recurse_check_list(vals[::-1])
#print(f"Found first index of empty values at idx {x}, vals: {vals}")
if x < 0:
#print(f"Ending at {fd[i]}")
return None, i
out.append(fd[old_i])
old_i += 1
for y in range(x):
out.append(fd[old_i])
old_i += 1
#print(f"Ending at {fd[i]}")
else:
prefix = fd[i].split("{",1)[0] + "{ "
suffix = " },"
vals, i = get_single_struct_vals(fd, i)
# reverse and cull entries of only zeros
x = recurse_check_list(vals[::-1])
#print(f"Found first index of empty values at idx {x}, vals: {vals}")
if x < 0:
#print(f"Ending at {fd[i]}")
return None, i
#out.append(prefix)
temp = ""
for z,y in enumerate(range(x)):
if z > 0:
prefix += ", "
prefix += f"{vals[y]}"
out.append(prefix + suffix)
#print(f"Ending at {fd[i]}")
return "\n".join(out), i
def MacroReplaceStaticNPC(fd):
replace_cull = { "unk_1C", "movement" }
fd = fd.splitlines()
out = []
i = 0
while i < len(fd):
if any(f".{x}" in fd[i] for x in replace_cull):
vals, i = cull_struct(fd, i)
if vals:
out.append(vals)
i += 1
continue
if ".itemDrops" in fd[i]:
vals, x = cull_struct(fd, i)
indent = len(fd[i].split(".",1)[0]) // 4
new_line = fd[i].split("{",1)[0] + "{\n"
if not vals:
i = x
i += 1
continue
vals += "\n},\n."
vals, _ = get_single_struct_vals(vals.splitlines(), 0)
added_item = False
for item in vals:
if item[0] == 0:
continue
added_item = True
item_name = constants["ItemId"][item[0]]
new_line += " " * (indent+1) + "{ " + item_name + f", {item[1]}, {item[2]}" + " },\n"
if added_item:
new_line += " " * indent + "},"
out.append(new_line)
i = x
elif ".animations" in fd[i]:
indent = len(fd[i].split(".",1)[0]) // 4
new_line = fd[i].split("{",1)[0] + "{\n"
vals, x = get_single_struct_vals(fd, i)
for val in vals:
sprite_id = (val & 0x00FF0000) >> 16
palette_id = (val & 0x0000FF00) >> 8
anim_id = (val & 0x000000FF) >> 0
sprite = constants["NPC_SPRITE"][sprite_id]["name"]
palette = constants["NPC_SPRITE"][sprite_id]["palettes"][palette_id]
anim = constants["NPC_SPRITE"][sprite_id]["anims"][anim_id]
new_line += " " * (indent+1) + f"NPC_ANIM({sprite}, {palette}, {anim}),\n"
new_line += " " * indent + "},"
out.append(new_line)
i = x
elif ".heartDrops" in fd[i] or ".flowerDrops" in fd[i]:
vals, x = get_single_struct_vals(fd, i)
new_line = fd[i].split("{",1)[0]
attempts = vals[0][2]
if ".heartDrops" in fd[i]:
if round(vals[0][1] / 327.67, 2) == 70 and round(vals[0][3] / 327.67, 2) == 50:
new_line += f"STANDARD_HEART_DROPS({attempts}),"
elif round(vals[0][1] / 327.67, 2) == 80 and round(vals[0][3] / 327.67, 2) == 50:
new_line += f"GENEROUS_HEART_DROPS({attempts}),"
elif round(vals[0][1] / 327.67, 2) == 80 and round(vals[0][3] / 327.67, 2) == 60:
new_line += f"GENEROUS_WHEN_LOW_HEART_DROPS({attempts}),"
else:
print(f"Unknown heart drop macro, values were {round(vals[0][1] / 327.67, 2)} and {round(vals[0][3] / 327.67, 2)}")
exit()
else:
if round(vals[0][1] / 327.67, 2) == 50 and round(vals[0][3] / 327.67, 2) == 40:
new_line += f"STANDARD_FLOWER_DROPS({attempts}),"
elif round(vals[0][1] / 327.67, 2) == 70 and round(vals[0][3] / 327.67, 2) == 50:
new_line += f"GENEROUS_WHEN_LOW_FLOWER_DROPS({attempts}),"
elif round(vals[0][1] / 327.67, 2) == 40 and round(vals[0][3] / 327.67, 2) == 40:
new_line += f"REDUCED_FLOWER_DROPS({attempts}),"
else:
print(f"Unknown heart drop macro, values were {round(vals[0][1] / 327.67, 2)} and {round(vals[0][3] / 327.67, 2)}")
exit()
out.append(new_line)
i = x
else:
out.append(fd[i])
i += 1
return "\n".join(out)
def MacroReplaceNpcSettings(fd):
replace_cull = { "unk_00", "unk_24" }
fd = fd.splitlines()
out = []
i = 0
while i < len(fd):
if any(f".{x}" in fd[i] for x in replace_cull):
vals, i = cull_struct(fd, i)
if vals:
out.append(vals)
else:
out.append(fd[i])
i += 1
return "\n".join(out)
parser = argparse.ArgumentParser()
parser.add_argument("file", type=str, help="File to decompile struct from")
parser.add_argument("offset", type=lambda x: int(x, 0), help="Offset to decompile struct from")
parser.add_argument("type", type=str, help="Struct type to decompile")
args = parser.parse_args()
get_constants()
get_structs()
if args.type not in STRUCTS:
print(f"Unknown struct type {args.type}")
exit()
'''
out = [f"{args.type} = " + "{\n"]
offset = args.offset
for var in STRUCTS[args.type]:
line, offset = output_type(fd, offset, var, 1)
out.append(line)
out.append("};")
'''
fd = Path(args.file).resolve().read_bytes()
out = output_type2(fd, args.offset, STRUCTS[args.type])
if args.type.lower() == "staticnpc":
print(MacroReplaceStaticNPC(out))
elif args.type.lower() == "npcaisettings":
print(out)
elif args.type.lower() == "npcsettings":
print(MacroReplaceNpcSettings(out))
else:
print(f"Add me type: {args.type}")