mirror of
https://github.com/zeldaret/botw
synced 2026-10-20 08:47:15 -04:00
f4cd38c43b
Link order shows one sorted actor tree, not a separate AI project: the Action/AI/Query folders and per-feature folders (Camera, Player, Horse, ...) come out of the same archive rounds as the actor classes. (AI assisted using Claude Fable 5.1 and Claude Opus 5.5)
273 lines
10 KiB
Python
Executable File
273 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import enum
|
|
|
|
import cxxfilt
|
|
import zlib
|
|
from typing import List, Dict, Iterable, Optional, Set
|
|
|
|
from pathlib import Path
|
|
import textwrap
|
|
import ai_common
|
|
from common.util import elf
|
|
|
|
|
|
def get_member_name(entry) -> str:
|
|
type_ = entry["type"]
|
|
if type_ == "dynamic_param":
|
|
return f'm{entry["param_name"]}_d'
|
|
elif type_ == "dynamic2_param":
|
|
return f'm{entry["param_name"]}_d'
|
|
elif type_ == "static_param":
|
|
return f'm{entry["param_name"]}_s'
|
|
elif type_ == "map_unit_param":
|
|
return f'm{entry["param_name"]}_m'
|
|
elif type_ == "aitree_variable":
|
|
return f'm{entry["param_name"]}_a'
|
|
else:
|
|
assert False
|
|
|
|
|
|
def generate_action_loadparam_body(info: list) -> str:
|
|
out = []
|
|
for entry in info:
|
|
type_ = entry["type"]
|
|
if type_ == "dynamic_param":
|
|
if entry["param_name"]:
|
|
out.append(f'getDynamicParam(&{get_member_name(entry)}, "{entry["param_name"]}");')
|
|
elif type_ == "dynamic2_param":
|
|
if entry["param_name"]:
|
|
out.append(f'getDynamicParam2(&{get_member_name(entry)}, "{entry["param_name"]}");')
|
|
elif type_ == "static_param":
|
|
if entry["param_name"]:
|
|
out.append(f'getStaticParam(&{get_member_name(entry)}, "{entry["param_name"]}");')
|
|
elif type_ == "map_unit_param":
|
|
if entry["param_name"]:
|
|
out.append(f'getMapUnitParam(&{get_member_name(entry)}, "{entry["param_name"]}");')
|
|
elif type_ == "aitree_variable":
|
|
if entry["param_name"]:
|
|
out.append(f'getAITreeVariable(&{get_member_name(entry)}, "{entry["param_name"]}");')
|
|
elif type_ == "call":
|
|
fn_name: str = entry["fn"]
|
|
if fn_name.startswith("_ZN") and fn_name.endswith("11loadParams_Ev"):
|
|
parent_class_name = cxxfilt.demangle(fn_name).split("::")[-2]
|
|
out.append(f"{parent_class_name}::loadParams_();")
|
|
else:
|
|
out.append(f"// FIXME: CALL {fn_name} @ {entry['addr']:#x}")
|
|
else:
|
|
raise AssertionError(f"unknown type: {type_}")
|
|
|
|
return "\n".join(out)
|
|
|
|
|
|
def generate_action_param_member_vars(parent: str, info: list) -> str:
|
|
out = []
|
|
|
|
# Ignore duplicate calls to getXXXXXParam
|
|
params_dict = dict()
|
|
for entry in info:
|
|
offset: Optional[int] = entry.get("param_offset")
|
|
if offset is not None:
|
|
params_dict[offset] = entry
|
|
params = list(params_dict.values())
|
|
params.sort(key=lambda entry: entry["param_offset"])
|
|
|
|
if not parent and params:
|
|
first_offset: int = params[0]["param_offset"]
|
|
sizeof_action = 0x20
|
|
diff = first_offset - sizeof_action
|
|
assert diff >= 0
|
|
if diff > 0:
|
|
out.append(f"// FIXME: remove this")
|
|
out.append(f"u8 pad_0x20[{diff:#x}];")
|
|
|
|
for entry in params:
|
|
if not entry["param_name"]:
|
|
continue
|
|
out.append(f"// {entry['type']} at offset {entry['param_offset']:#x}")
|
|
out.append(f"{entry['param_type']} {get_member_name(entry)}{{}};")
|
|
return "\n".join(out)
|
|
|
|
|
|
@enum.unique
|
|
class CommonVIndex(enum.IntEnum):
|
|
Dtor = 2
|
|
OneShot = 10
|
|
Init = 11
|
|
Enter = 12
|
|
Leave = 14
|
|
LoadParams = 15
|
|
Calc = 31
|
|
|
|
|
|
def generate_action(class_dir: Path, name: str, info: list, parent: str, seen_virtual_functions: Set[int],
|
|
vtable: int) -> None:
|
|
name = name[0].upper() + name[1:]
|
|
if parent:
|
|
parent = parent[0].upper() + parent[1:]
|
|
|
|
cpp_class_name = f"{name}"
|
|
header_file_name = f"action{name}.h"
|
|
|
|
parent_class_name = parent if parent else 'ksys::act::ai::Action'
|
|
|
|
own_virtual_functions: Set[int] = set()
|
|
for i, fn in enumerate(elf.get_vtable_fns_from_base_elf(vtable, 32)):
|
|
if i not in CommonVIndex.__members__.values() or fn in seen_virtual_functions:
|
|
continue
|
|
own_virtual_functions.add(i)
|
|
seen_virtual_functions.add(fn)
|
|
|
|
# Header
|
|
out = []
|
|
out.append("#pragma once")
|
|
out.append("")
|
|
if parent:
|
|
out.append(f'#include "{ai_common.find_header(class_dir, f"action{parent}.h")}"')
|
|
out.append('#include "KingSystem/ActorSystem/actAiAction.h"')
|
|
out.append("")
|
|
out.append("namespace uking::action {")
|
|
out.append("")
|
|
out.append(f"class {cpp_class_name} : public {parent_class_name} {{")
|
|
out.append(f" SEAD_RTTI_OVERRIDE({cpp_class_name}, {parent_class_name})")
|
|
out.append("public:")
|
|
out.append(f" explicit {cpp_class_name}(const InitArg& arg);")
|
|
if CommonVIndex.Dtor in own_virtual_functions:
|
|
out.append(f" ~{cpp_class_name}() override;")
|
|
out.append("")
|
|
if CommonVIndex.Init in own_virtual_functions:
|
|
out.append(" bool init_(sead::Heap* heap) override;")
|
|
if CommonVIndex.Enter in own_virtual_functions:
|
|
out.append(" void enter_(ksys::act::ai::InlineParamPack* params) override;")
|
|
if CommonVIndex.Leave in own_virtual_functions:
|
|
out.append(" void leave_() override;")
|
|
if CommonVIndex.LoadParams in own_virtual_functions:
|
|
out.append(" void loadParams_() override;")
|
|
out.append("")
|
|
out.append("protected:")
|
|
if CommonVIndex.Calc in own_virtual_functions:
|
|
out.append(" void calc_() override;")
|
|
out.append("")
|
|
out.append(textwrap.indent(generate_action_param_member_vars(parent, info), " " * 4))
|
|
out.append("};") # =================================== end of class
|
|
out.append("")
|
|
out.append("} // namespace uking::action")
|
|
out.append("")
|
|
(class_dir / header_file_name).write_text("\n".join(out))
|
|
|
|
# .cpp
|
|
out = []
|
|
out.append(f'#include "{ai_common.find_header(class_dir, header_file_name)}"')
|
|
out.append("")
|
|
out.append("namespace uking::action {")
|
|
out.append("")
|
|
out.append(f"{cpp_class_name}::{cpp_class_name}(const InitArg& arg) : {parent_class_name}(arg) {{}}")
|
|
out.append("")
|
|
if CommonVIndex.Dtor in own_virtual_functions:
|
|
out.append(f"{cpp_class_name}::~{cpp_class_name}() = default;")
|
|
out.append("")
|
|
if CommonVIndex.Init in own_virtual_functions:
|
|
out.append(f"bool {cpp_class_name}::init_(sead::Heap* heap) {{")
|
|
out.append(f" return {parent_class_name}::init_(heap);")
|
|
out.append(f"}}")
|
|
out.append("")
|
|
if CommonVIndex.Enter in own_virtual_functions:
|
|
out.append(f"void {cpp_class_name}::enter_(ksys::act::ai::InlineParamPack* params) {{")
|
|
out.append(f" {parent_class_name}::enter_(params);")
|
|
out.append(f"}}")
|
|
out.append("")
|
|
if CommonVIndex.Leave in own_virtual_functions:
|
|
out.append(f"void {cpp_class_name}::leave_() {{")
|
|
out.append(f" {parent_class_name}::leave_();")
|
|
out.append(f"}}")
|
|
out.append("")
|
|
if CommonVIndex.LoadParams in own_virtual_functions:
|
|
out.append(f"void {cpp_class_name}::loadParams_() {{")
|
|
out.append(textwrap.indent(generate_action_loadparam_body(info), " " * 4))
|
|
out.append(f"}}")
|
|
out.append("")
|
|
if CommonVIndex.Calc in own_virtual_functions:
|
|
out.append(f"void {cpp_class_name}::calc_() {{")
|
|
out.append(f" {parent_class_name}::calc_();")
|
|
out.append(f"}}")
|
|
out.append("")
|
|
out.append("} // namespace uking::action")
|
|
out.append("")
|
|
(class_dir / f"action{name}.cpp").write_text("\n".join(out))
|
|
|
|
|
|
def generate_action_factories(class_dir: Path, actions: Iterable[str]) -> None:
|
|
out = []
|
|
out.append("""\
|
|
// DO NOT MAKE MAJOR EDITS. This file is automatically generated.
|
|
// For major edits, please edit the generator script (ai_generate_queries.py) instead.
|
|
// If edits are made to this file, make sure they are not lost when the generator is re-run.
|
|
""")
|
|
out.append('#include "Game/Actor/aiActionFactories.h"')
|
|
out.append('#include <array>')
|
|
for name in actions:
|
|
name = name[0].upper() + name[1:]
|
|
out.append(f'#include "{ai_common.find_header(class_dir, f"action{name}.h")}"')
|
|
out.append('#include "KingSystem/ActorSystem/actAiAction.h"')
|
|
out.append('')
|
|
out.append('namespace uking {')
|
|
out.append('')
|
|
out.append('using Factory = ksys::act::ai::ActionFactory;')
|
|
out.append('')
|
|
out.append('static Factory sActionFactories[] = {')
|
|
for name in sorted(actions, key=lambda name: zlib.crc32(name.encode())):
|
|
class_name = "action::" + name[0].upper() + name[1:]
|
|
out.append(f' {{0x{zlib.crc32(name.encode()):08x}, Factory::make<{class_name}>}},')
|
|
out.append('};')
|
|
out.append('')
|
|
out.append('void initActionFactories() {')
|
|
out.append(' ksys::act::ai::Actions::setFactories(std::size(sActionFactories), sActionFactories);')
|
|
out.append('}')
|
|
out.append('')
|
|
out.append('} // namespace uking')
|
|
(class_dir.parent / f"aiActionFactories.cpp").write_text("\n".join(out))
|
|
|
|
|
|
def main() -> None:
|
|
src_root = Path(__file__).parent.parent
|
|
class_dir = src_root / "src" / "Game" / "Actor" / "Action"
|
|
class_dir.mkdir(exist_ok=True)
|
|
|
|
action_vtables: Dict[str, List[int]] = ai_common.get_vtables()["Action"]
|
|
action_params = ai_common.get_action_params()
|
|
vtable_names = ai_common.get_action_vtable_names()
|
|
|
|
seen_virtual_functions = set()
|
|
seen_virtual_functions.update(elf.get_vtable_fns_from_base_elf(0x24d8d68, 31))
|
|
seen_virtual_functions.update(elf.get_vtable_fns_from_base_elf(0x25129f0, 32))
|
|
|
|
generated = set()
|
|
for vtables in action_vtables.values():
|
|
vtables = list(dict.fromkeys(vtables))
|
|
for i in range(len(vtables)):
|
|
# This skips the first base class.
|
|
if i == 0:
|
|
continue
|
|
|
|
vtable_parent = vtables[i - 1]
|
|
vtable = vtables[i]
|
|
|
|
# This skips any other base class.
|
|
if vtable in ai_common.BaseClasses:
|
|
continue
|
|
|
|
action_name = vtable_names[vtable]
|
|
parent_name = vtable_names[vtable_parent]
|
|
if vtable_parent in ai_common.BaseClasses:
|
|
parent_name = ""
|
|
|
|
if vtable not in generated:
|
|
generated.add(vtable)
|
|
generate_action(class_dir, action_name, action_params[action_name], parent_name, seen_virtual_functions,
|
|
vtable)
|
|
|
|
generate_action_factories(class_dir, action_vtables.keys())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|