mirror of https://github.com/zeldaret/botw.git
				
				
				
			
		
			
				
	
	
		
			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 "Game/AI/Action/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 "Game/AI/Action/{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/AI/aiActionFactories.h"')
 | 
						|
    out.append('#include <array>')
 | 
						|
    for name in actions:
 | 
						|
        name = name[0].upper() + name[1:]
 | 
						|
        out.append(f'#include "Game/AI/Action/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" / "AI" / "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()
 |