From 355cfc752709c5d09e8064b50aa49308f5279c76 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Mon, 23 Feb 2026 00:30:14 -0700 Subject: [PATCH] Some docs & scripts for automation --- CONTRIBUTING.md | 184 ------------- docs/map.md | 78 ++++++ docs/metaforce.md | 410 ++++++++++++++++++++++++++++ scripts/rename_symbol.py | 95 +++++++ scripts/symbols.py | 143 ++++++++++ src/MetroidPrime/Tweaks/CTweaks.cpp | 4 +- 6 files changed, 727 insertions(+), 187 deletions(-) create mode 100644 docs/map.md create mode 100644 docs/metaforce.md create mode 100644 scripts/rename_symbol.py create mode 100644 scripts/symbols.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b5b4e952..96b0cefb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,187 +28,3 @@ With CodeWarrior, the `-inline deferred` flag reverses the function order in a t When you have a function mismatch that you want help on, you can upload a scratch to [decomp.me](https://decomp.me): - Use `tools/decompctx.py src/path/to/file.cpp` to generate `ctx.c` which you can put in the "Context" field. - Set preset to `Metroid Prime (USA)`. - -## Metaforce notes - -Metaforce is a non-matching decompilation, and often uses modern C++ features that won't work in C++98. This aims to be a (non-exhaustive) reference for things to watch out for when converting Metaforce code into decomp-matching code. - -### Converting types - -The python script at `tool/metaforce_renames.py` automates many simple renames listed next, but not everything works perfectly. - -Metaforce -> decomp -- `s8` -> `char` -- `u8` -> `uchar` -- `s16` -> `short` -- `u16` -> `ushort` -- `s32` -> `int` -- `u32` -> `uint` -- `zeus::CTransform` -> `CTransform4f` -- `zeus::CFrustum` -> `CFrustumPlanes` -- Any other `zeus::` class has the prefixed removed, i.e. `zeus::CVector3f` -> `CVector3f` -- `std::vector` -> `rstl::vector` -- `std::optional` -> `rstl::optional_object` -- `std::pair` -> `rstl::pair` -- `std::unique_ptr` -> `rstl::single_ptr` or `rstl::auto_ptr` (`auto_ptr` has an adjacent boolean) -- `std::shared_ptr` -> `rstl::rc_ptr` or `rstl::ncrc_ptr` -- `std::array var` -> `T var[N]` -- `std::string` -> `rstl::string` -- `std::string_view` -> `const rstl::string&` -- `std::make_unique` -> `new T` -- `std::move` is removed -- `std::min` -> `rstl::min_val` -- `std::max` -> `rstl::max_val` -- `std::clamp` -> `CMath::Clamp` (depends on context) - -### Converting globals and getters - -Metaforce -> decomp -- `zeus::CTransform::frontVector()` -> `CTransform4f::GetForward()` -- `zeus::skForward` -> `CVector3f::Forward` -- `g_Renderer` -> `gpRender` -- `zeus::degToRad` -> `CRelAngle::FromDegrees` - -### Class definitions & constructors - -Metaforce often uses default values inside of class definitions, like the ` = false` below: - -```c++ -TAreaId x4_areaId; -TUniqueId x8_uid; -TEditorId xc_editorId; -std::string x10_name; -std::vector x20_conns; -bool x30_24_active : 1; -bool x30_25_inGraveyard : 1 = false; -bool x30_26_scriptingBlocked : 1 = false; -bool x30_27_inUse : 1; -``` - -This is unsupported in C++98, and need to be removed: - -```c++ -TAreaId x4_areaId; -TUniqueId x8_uid; -TEditorId xc_editorId; -rstl::string x10_name; -rstl::vector< SConnection > x20_conns; -bool x30_24_active : 1; -bool x30_25_inGraveyard : 1; -bool x30_26_scriptingBlocked : 1; -bool x30_27_notInArea : 1; -``` - -Instead, these values are set in the constructor like so: - -```c++ -CEntity::CEntity(TUniqueId id, const CEntityInfo& info, bool active, const rstl::string& name) -: x4_areaId(info.GetAreaId()) -, x8_uid(id) -, xc_editorId(info.GetEditorId()) -, x10_name(name) -, x20_conns(info.GetConnectionList()) -, x30_24_active(active) -, x30_25_inGraveyard(false) // <-- -, x30_26_scriptingBlocked(false) // <-- -, x30_27_notInArea(x4_areaId == kInvalidAreaId) {} -``` - -### Getters / Setters - -In retail, classes almost always have private members, and use getters/setters. Metaforce often doesn't represent this, and may require transitioning. - -Be sure to search the demo symbol map for hints on names, getters and setters. - -Example in Metaforce: - -```c++ -void CActor::SetTranslation(const zeus::CVector3f& tr) { - x34_transform.origin = tr; - xe4_27_notInSortedLists = true; - xe4_28_transformDirty = true; - xe4_29_actorLightsDirty = true; -} -``` - -Would convert to: - -```c++ -void CActor::SetTranslation(const CVector3f& pos) { - x34_transform.SetTranslation(pos); - SetTransformDirty(true); - SetTransformDirtySpare(true); - SetPreRenderHasMoved(true); -} -``` - -Note that Metaforce had somewhat inaccurate names for these fields, and the real names for the setters were located in the demo map under `SetTranslation__6CActorFRC9CVector3f`. - -### Enums - -Metaforce almost exclusively uses `enum class`, which isn't supported in C++98. These will be transitioned to standard `enum`s with a prefix based on the enum name. - -Metaforce example: - -```c++ -enum class EFluidState { - EnteredFluid, - InFluid, - LeftFluid, -}; -``` - -Decomp: - -```c++ -enum EFluidState { - kFS_EnteredFluid, - kFS_InFluid, - kFS_LeftFluid, -}; -``` - -### Iterators - -Metaforce makes use of ranged-for and ``, neither of which can be used in C++98. - -Commonly, you'll see: - -```c++ -for (const SConnection& conn : x20_conns) { - if (conn.x0_state == state && conn.x4_msg != skipMsg) { - mgr.SendScriptMsg(x8_uid, conn.x8_objId, conn.x4_msg, state); - } -} -``` - -This will be converted to use `rstl::vector::iterator` or `const_iterator` like so: - -```c++ -rstl::vector< SConnection >::const_iterator it = GetConnections().begin(); -for (; it != GetConnections().end(); ++it) { - if (it->x0_state == state && it->x4_msg != skipMsg) { - mgr.SendScriptMsg(GetUniqueId(), it->x8_objId, it->x4_msg, state); - } -} -``` - -Though sometimes the original code will instead use an indexed for loop instead, which will generate different code. - -### Const correctness - - - -A common thing you'll see in Metaforce are `const` overloads for getters: - -```c++ -CWorld* GetWorld() { return x850_world.get(); } -const CWorld* GetWorld() const { return x850_world.get(); } -``` - -However, in retail, the standard is to use `GetX()` for const and `X()` for non-const, like so: - -```c++ -CWorld* World() { return x850_world.get(); } -const CWorld* GetWorld() const { return x850_world.get(); } -``` diff --git a/docs/map.md b/docs/map.md new file mode 100644 index 00000000..df407ae4 --- /dev/null +++ b/docs/map.md @@ -0,0 +1,78 @@ +# Using the debug map + +An early demo build of Metroid Prime shipped with a debug map (MetaforceCWD.MAP). The debug build had no inlining, so all functions (including normally-inlined functions) are listed in the map file. + +However, there are a few caveats: + +1. Given that this is for an early demo build, it does not contain everything present in the retail release. Some functions may have changed prototypes or been added/removed entirely. +2. Functions only show up _once_ in the link map, even if they are used in multiple translation units. However, the map _does_ list what other translation units reference that function, although we lose the information about the exact call sites. For example, the first usage of `CVector3f::GetX()` is shown as: + ``` + 11] GetX__9CVector3fCFv (func,weak) found in CStateManager.o + 11] >>> UNREFERENCED DUPLICATE GetX__9CVector3fCFv + 11] >>> (func,weak) found in Kyoto_CWD.a CPoseAsTransformsVariableSize.cpp + 11] >>> (func,weak) found in Kyoto_CWD.a CParticleElectric.cpp + 11] >>> (func,weak) found in Kyoto_CWD.a CAdditiveAnimPlayback.cpp + ... (truncated for brevity) + ``` + +decomp-toolkit has a convenience command for querying the map file for information about a given symbol: +``` +$ build/tools/dtk map symbol orig/MetaforceCWD.MAP GetX__9CVector3fCFv + INFO Processing map... + INFO Done! +Located symbol CVector3f::GetX() const (Function,Weak) @ .text:0x800C22EC [CStateManager.o] + +Known referenced from: +>>> CGraphics::SetViewMatrix() (Function, Global) @ .text:0x804D9200 [Kyoto_CWD.a DolphinCGraphics.cpp] + +Generated in TUs: +>>> CStateManager.o +>>> Kyoto_CWD.a CPoseAsTransformsVariableSize.cpp +>>> Kyoto_CWD.a CParticleElectric.cpp +>>> Kyoto_CWD.a CAdditiveAnimPlayback.cpp +... (truncated for brevity) +``` + +Here, we can see that `CVector3f::GetX()` first appears used in `CGraphics::SetViewMatrix()`. It is also used in many other translation units, which are listed under "Generated in TUs", we just don't know exactly where. + +decomp-toolkit also provides a convenience command to list all entries that appear for a given translation unit: +``` +$ build/tools/dtk map entries orig/MetaforceCWD.MAP 'CollisionCWD.a CMRay.cpp' +Entries for CollisionCWD.a CMRay.cpp: +>>> CBasics::GetFalseValue() # ignore this, it's a debug helper in every TU +>>> CVector3f::GetZ() const +>>> CVector3f::GetY() const +>>> CVector3f::GetX() const +>>> CVector3f::CVector3f(const CVector3f&) +>>> CVector3f::CVector3f(float, float, float) +>>> operator-(const CVector3f&, const CVector3f&) +>>> operator+(const CVector3f&, const CVector3f&) +>>> operator*(const CVector3f&, float) +>>> CMRay::GetInvUnscaledTransformRay(const CTransform4f&) const (Function,Global) @ .text:0x8046DA64 [CollisionCWD.a CMRay.cpp] +>>> CMRay::CMRay(const CVector3f&, const CVector3f&, float, float) (Function,Global) @ .text:0x8046D7B4 [CollisionCWD.a CMRay.cpp] +>>> CMRay::CMRay(const CVector3f&, const CVector3f&, float) (Function,Global) @ .text:0x8046D850 [CollisionCWD.a CMRay.cpp] +``` + +This tells us that `CMRay.cpp` uses `CVector3f::GetX()` _somewhere_ in the TU (in the map, it shows up as `11] >>> UNREFERENCED DUPLICATE GetX__9CVector3fCFv` -> `11] >>> (func,weak) found in CollisionCWD.a CMRay.cpp`), among other `CVector3f` functions. + +In fact, if we pull up the source for `operator+(const CVector3f&, const CVector3f&)`, we can see that it calls `GetX()`, `GetY()`, `GetZ()` and `CVector3f(float, float, float)`: + +```cpp +inline CVector3f operator+(const CVector3f& lhs, const CVector3f& rhs) { + float x = lhs.GetX() + rhs.GetX(); + float y = lhs.GetY() + rhs.GetY(); + float z = lhs.GetZ() + rhs.GetZ(); + return CVector3f(x, y, z); +} +``` + +and the `CMRay` constructor uses `operator-`, `operator+`, and `operator*`: + +```cpp +CMRay::CMRay(const CVector3f& start, const CVector3f& dir, float length) +: mStart(start) +, mEnd(start + length * dir) +, mDelta(mEnd - mStart) +``` + +so we can conclude that we're using all of the necessary inlined functions here. diff --git a/docs/metaforce.md b/docs/metaforce.md new file mode 100644 index 00000000..3576896a --- /dev/null +++ b/docs/metaforce.md @@ -0,0 +1,410 @@ +# Converting Metaforce code + +Metaforce is a non-matching decompilation, and often uses modern C++ features that won't work in C++98. This aims to be a (non-exhaustive) reference for things to watch out for when converting Metaforce code into decomp-matching code. + +## Converting types + +The python script at `tool/metaforce_renames.py` automates many simple renames listed next, but not everything works perfectly. + +Metaforce -> decomp + +- `s8` -> `char` +- `u8` -> `uchar` +- `s16` -> `short` +- `u16` -> `ushort` +- `s32` -> `int` +- `u32` -> `uint` +- `zeus::CTransform` -> `CTransform4f` +- `zeus::CFrustum` -> `CFrustumPlanes` +- Any other `zeus::` class has the prefixed removed, i.e. `zeus::CVector3f` -> `CVector3f` +- `std::vector` -> `rstl::vector` +- `std::optional` -> `rstl::optional_object` +- `std::pair` -> `rstl::pair` +- `std::unique_ptr` -> `rstl::single_ptr` or `rstl::auto_ptr` (`auto_ptr` has an adjacent boolean) +- `std::shared_ptr` -> `rstl::rc_ptr` or `rstl::ncrc_ptr` +- `std::array var` -> `T var[N]` +- `std::string` -> `rstl::string` +- `std::string_view` -> `const rstl::string&` +- `std::make_unique` -> `new T` +- `std::move` is removed +- `std::min` -> `rstl::min_val` +- `std::max` -> `rstl::max_val` +- `std::clamp` -> `CMath::Clamp` (depends on context) + +## Converting globals and getters + +Metaforce -> decomp + +- `zeus::CTransform::frontVector()` -> `CTransform4f::GetForward()` +- `zeus::skForward` -> `CVector3f::Forward` +- `g_Renderer` -> `gpRender` +- `zeus::degToRad` -> `CRelAngle::FromDegrees` +- `mgr.FreeScriptObject()` -> `mgr.DeleteObjectRequest()` +- `mgr.GetActiveRandom()` -> `mgr.Random()` +- `GetAreaIdAlways()` -> `GetCurrentAreaId()` + +## Class definitions & constructors + +Metaforce often uses default values inside of class definitions, like the `= false` below: + +```c++ +TAreaId x4_areaId; +TUniqueId x8_uid; +TEditorId xc_editorId; +std::string x10_name; +std::vector x20_conns; +bool x30_24_active : 1; +bool x30_25_inGraveyard : 1 = false; +bool x30_26_scriptingBlocked : 1 = false; +bool x30_27_inUse : 1; +``` + +This is unsupported in C++98, and need to be removed: + +```c++ +TAreaId x4_areaId; +TUniqueId x8_uid; +TEditorId xc_editorId; +rstl::string x10_name; +rstl::vector< SConnection > x20_conns; +bool x30_24_active : 1; +bool x30_25_inGraveyard : 1; +bool x30_26_scriptingBlocked : 1; +bool x30_27_notInArea : 1; +``` + +Instead, these values are set in the constructor like so: + +```c++ +CEntity::CEntity(TUniqueId id, const CEntityInfo& info, bool active, const rstl::string& name) +: x4_areaId(info.GetAreaId()) +, x8_uid(id) +, xc_editorId(info.GetEditorId()) +, x10_name(name) +, x20_conns(info.GetConnectionList()) +, x30_24_active(active) +, x30_25_inGraveyard(false) // <-- +, x30_26_scriptingBlocked(false) // <-- +, x30_27_notInArea(x4_areaId == kInvalidAreaId) {} +``` + +## Getters / Setters + +In retail, classes almost always have private members, and use getters/setters. Metaforce often doesn't represent this, and may require transitioning. + +Be sure to search the demo symbol map for hints on names, getters and setters. + +Example in Metaforce: + +```c++ +void CActor::SetTranslation(const zeus::CVector3f& tr) { + x34_transform.origin = tr; + xe4_27_notInSortedLists = true; + xe4_28_transformDirty = true; + xe4_29_actorLightsDirty = true; +} +``` + +Would convert to: + +```c++ +void CActor::SetTranslation(const CVector3f& pos) { + x34_transform.SetTranslation(pos); + SetTransformDirty(true); + SetTransformDirtySpare(true); + SetPreRenderHasMoved(true); +} +``` + +Note that Metaforce had somewhat inaccurate names for these fields, and the real names for the setters were located in the demo map under `SetTranslation__6CActorFRC9CVector3f`. + +## Enums + +Metaforce almost exclusively uses `enum class`, which isn't supported in C++98. These will be transitioned to standard `enum`s with a prefix based on the enum name. + +Metaforce example: + +```c++ +enum class EFluidState { + EnteredFluid, + InFluid, + LeftFluid, +}; +``` + +Decomp: + +```c++ +enum EFluidState { + kFS_EnteredFluid, + kFS_InFluid, + kFS_LeftFluid, +}; +``` + +### Enum scoping + +Since `enum class` provides scoping but regular `enum` doesn't, Metaforce uses fully-qualified enum values that need to be converted to the prefixed form. + +Metaforce: + +```c++ +if (msg == EScriptObjectMessage::Deleted) { } +xe8_particleGen = std::make_unique(particle, + CElementGen::EModelOrientationType::Normal, + flags & 0x2 ? CElementGen::EOptionalSystemFlags::Two + : CElementGen::EOptionalSystemFlags::One); +``` + +Decomp: + +```c++ +if (msg == kSM_Deleted) { } +xe8_particleGen = rs_new CElementGen(particle, + CElementGen::kMOT_Normal, + flags & 0x2 ? CElementGen::kOSF_Two + : CElementGen::kOSF_One); +``` + +## Iterators + +Metaforce makes use of ranged-for and ``, neither of which can be used in our C++98 codebase. + +Commonly, you'll see: + +```c++ +for (const SConnection& conn : x20_conns) { + if (conn.x0_state == state && conn.x4_msg != skipMsg) { + mgr.SendScriptMsg(x8_uid, conn.x8_objId, conn.x4_msg, state); + } +} +``` + +This will be converted to use `rstl::vector::iterator` or `const_iterator` like so: + +```c++ +rstl::vector< SConnection >::const_iterator it = GetConnections().begin(); +for (; it != GetConnections().end(); ++it) { + if (it->x0_state == state && it->x4_msg != skipMsg) { + mgr.SendScriptMsg(GetUniqueId(), it->x8_objId, it->x4_msg, state); + } +} +``` + +This can be simplified with the `AUTO` macro: + +```c++ +for (AUTO(it, GetConnections().begin()); it != GetConnections().end(); ++it) { +``` + +Though sometimes the original code will instead use an indexed for loop instead, which will generate different code. + +## Const correctness + + + +A common thing you'll see in Metaforce are `const` overloads for getters: + +```c++ +CWorld* GetWorld() { return x850_world.get(); } +const CWorld* GetWorld() const { return x850_world.get(); } +``` + +However, in retail, the standard is to use `GetX()` for const and `X()` for non-const, like so: + +```c++ +CWorld* World() { return x850_world.get(); } +const CWorld* GetWorld() const { return x850_world.get(); } +``` + +## Accept visitor syntax + +Metaforce passes `this` pointer to visitor, but decomp dereferences to pass by reference. + +Metaforce: + +```c++ +void CExplosion::Accept(IVisitor& visitor) { visitor.Visit(this); } +``` + +Decomp: + +```c++ +void CExplosion::Accept(IVisitor& visitor) { visitor.Visit(*this); } +``` + +## Switch vs if-else chains + +In `AcceptScriptMsg` and similar message-handling functions, the decomp typically uses switch statements rather than if-else chains. This is often required for matching. + +Metaforce: + +```c++ +void CScriptTimer::AcceptScriptMsg(EScriptObjectMessage msg, TUniqueId objId, CStateManager& mgr) { + if (GetActive()) { + if (msg == EScriptObjectMessage::Start) { + StartTiming(true); + } else if (msg == EScriptObjectMessage::Stop) { + StartTiming(false); + } else if (msg == EScriptObjectMessage::Reset) { + Reset(mgr); + } + } + CEntity::AcceptScriptMsg(msg, objId, mgr); +} +``` + +Decomp: + +```c++ +void CScriptTimer::AcceptScriptMsg(EScriptObjectMessage msg, TUniqueId objId, CStateManager& mgr) { + switch (msg) { + case kSM_Start: + if (GetActive()) { + StartTiming(true); + } + break; + case kSM_Stop: + if (GetActive()) { + StartTiming(false); + } + break; + case kSM_Reset: + if (GetActive()) { + Reset(mgr); + } + break; + } + CEntity::AcceptScriptMsg(msg, objId, mgr); +} +``` + +## String literals + +When constructing `rstl::string` from string literals, use `rstl::string_l()` wrapper. + +Metaforce: + +```c++ +mgr.AddObject(new CGameLight(xec_explosionLight, GetAreaIdAlways(), GetActive(), + "ExplodePLight_" + x10_name, GetTransform(), ...)); +``` + +Decomp: + +```c++ +mgr.AddObject(rs_new CGameLight(xec_explosionLight, GetCurrentAreaId(), GetActive(), + rstl::string_l("ExplodePLight_") + GetDebugName(), + GetTransform(), ...)); +``` + +## Header files + +### Namespace wrapping & header guards + +Metaforce wraps all code in `namespace metaforce { ... }`. This needs to be removed entirely from both headers and source files. + +Metaforce uses `#pragma once`, but decomp uses traditional header guards. + +Metaforce header: + +```c++ +#pragma once +namespace metaforce { +class CExplosion : public CEffect { + // ... +}; +} // namespace metaforce +``` + +Decomp header: + +```c++ +#ifndef _CEXPLOSION +#define _CEXPLOSION +class CExplosion : public CEffect { + // ... +}; +#endif // _CEXPLOSION +``` + +### DEFINE_ENTITY macro + +Metaforce uses a `DEFINE_ENTITY` macro in class definitions that should be removed. + +Metaforce: + +```c++ +class CScriptTimer : public CEntity { +public: + DEFINE_ENTITY + CScriptTimer(TUniqueId, std::string_view name, ...); +}; +``` + +Decomp: + +```c++ +class CScriptTimer : public CEntity { +public: + CScriptTimer(TUniqueId, const rstl::string& name, ...); +}; +``` + +### CHECK_SIZEOF assertions + +Add `CHECK_SIZEOF` assertions at the end of class definitions to verify the class size is as expected. + +```c++ +class CEffect : public CActor { +public: + // ... +}; +CHECK_SIZEOF(CEffect, 0xe8) +``` + +### Parameter names in declarations + +Include parameter names in function declarations in headers (and keep them synchronized with the implementation). + +Metaforce: + +```c++ +void Reset(CStateManager&); +void ApplyTime(float, CStateManager&); +``` + +Decomp: + +```c++ +void Reset(CStateManager& mgr); +void ApplyTime(float dt, CStateManager& mgr); +``` + +### Class member ordering + +MWCC places the vtable at different offsets depending on member ordering. Always define public functions first, then private/public data members. This ensures the vtable is placed at offset 0. + +Bad (vtable placed after fields): + +```c++ +class CFoo { + int x0_field; + int x4_field; +public: + virtual void SomeFunction(); +}; +``` + +Good (vtable at offset 0): + +```c++ +class CFoo { +public: + virtual void SomeFunction(); + +private: + int x0_field; + int x4_field; +}; +``` diff --git a/scripts/rename_symbol.py b/scripts/rename_symbol.py new file mode 100644 index 00000000..f597c761 --- /dev/null +++ b/scripts/rename_symbol.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path +from typing import Iterable + +from symbols import Symbol, parse_symbols_file, write_symbols_file + +ROOT = Path(__file__).resolve().parent.parent + + +def detect_version() -> str: + ninja_path = ROOT / "build.ninja" + if ninja_path.is_file(): + text = ninja_path.read_text() + match = re.search(r"build/([A-Za-z0-9_]+)/obj/", text) + if match: + return match.group(1) + match = re.search(r"build/([A-Za-z0-9_]+)/", text) + if match: + return match.group(1) + + config_dir = ROOT / "config" + configs: Iterable[Path] = sorted(p for p in config_dir.iterdir() if p.is_dir()) + configs_list = [p.name for p in configs] + if len(configs_list) == 1: + return configs_list[0] + + raise SystemExit("Unable to determine version from build.ninja. Please run configure or add a build directory.") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Rename a symbol in config/{version}/symbols.txt") + parser.add_argument("old_name", help="Current name of the symbol to rename") + parser.add_argument("new_name", help="New name to assign to the symbol") + parser.add_argument( + "--scope", + choices=["local", "global", "weak"], + help="Update the scope attribute for the symbol", + ) + return parser.parse_args() + + +def find_symbol(symbols: list[Symbol], name: str) -> Symbol | None: + for symbol in symbols: + if symbol.name == name: + return symbol + return None + + +def get_scope(symbol: Symbol) -> str: + scope = symbol.attrs.get("scope") + return scope if scope is not None else "global" + + +def main() -> None: + args = parse_args() + version = detect_version() + symbols_path = ROOT / "config" / version / "symbols.txt" + + if not symbols_path.is_file(): + raise SystemExit(f"symbols.txt not found for version '{version}' at {symbols_path}") + + symbols = parse_symbols_file(symbols_path) + + symbol = find_symbol(symbols, args.old_name) + if symbol is None: + raise SystemExit(f"symbol '{args.old_name}' not found in {symbols_path}") + + new_scope = args.scope or get_scope(symbol) + + if new_scope == "global": + for other in symbols: + if other is symbol: + continue + if other.name == args.new_name and get_scope(other) == "global": + raise SystemExit(f"a global symbol named '{args.new_name}' already exists in {symbols_path}") + + symbol.name = args.new_name + if args.scope: + symbol.replace_attr("scope", args.scope) + + write_symbols_file(symbols_path, symbols) + print(f"Renamed '{args.old_name}' -> '{args.new_name}' in {symbols_path}") + if args.scope: + print(f"Updated scope to '{args.scope}'") + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + sys.exit(130) diff --git a/scripts/symbols.py b/scripts/symbols.py new file mode 100644 index 00000000..24988122 --- /dev/null +++ b/scripts/symbols.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, Iterable, List + +# Attributes that should be written in decimal form. +DEC_ATTRS = ["align"] +# Attributes that should be written in hexadecimal form. +HEX_ATTRS = ["size"] +NUM_ATTRS = DEC_ATTRS + HEX_ATTRS +# Boolean attributes are represented by their presence only. +BOOL_ATTRS = ["hidden", "force_active", "noreloc", "noexport", "stripped"] + + +def _parse_int(value: str) -> int: + value = value.strip().lower() + base = 16 if value.startswith("0x") else 10 + return int(value, base) + + +def _format_int(key: str, value: int) -> str: + if key in HEX_ATTRS: + return f"0x{value:X}" + return str(value) + + +@dataclass +class Symbol: + name: str + section: str + address: int + attrs: Dict[str, Any] = field(default_factory=dict) + + def replace_attr(self, key: str, value: Any) -> None: + """Replace or add an attribute.""" + self.attrs[key] = value + + @classmethod + def from_line(cls, line: str) -> "Symbol": + match = re.match( + r"^(?P\S+)\s*=\s*(?P
[^:]+):(?P
[^;]+);\s*(?://\s*(?P.*))?$", + line, + ) + if not match: + raise ValueError("line does not match symbol format") + + name = match.group("name").strip() + section = match.group("section").strip() + address_str = match.group("address").strip() + + try: + address = _parse_int(address_str) + except ValueError as exc: + raise ValueError(f"invalid address '{address_str}'") from exc + + raw_attrs = match.group("attrs") or "" + attrs: Dict[str, Any] = {} + + for token in raw_attrs.split(): + if ":" in token: + key, raw_val = token.split(":", 1) + key = key.strip() + raw_val = raw_val.strip() + if key in NUM_ATTRS: + try: + attrs[key] = _parse_int(raw_val) + except ValueError as exc: + raise ValueError(f"invalid numeric value for '{key}': '{raw_val}'") from exc + else: + attrs[key] = raw_val + else: + # Boolean flags are represented by presence only. + attrs[token.strip()] = True + + return cls(name=name, section=section, address=address, attrs=attrs) + + def to_line(self) -> str: + addr = f"0x{self.address:X}" + parts: List[str] = [] + + ordered_keys: Iterable[str] = [ + "type", + "size", + "scope", + "align", + "data", + *BOOL_ATTRS, + ] + + # Add known keys in preferred order, then any remaining in sorted order. + seen = set() + for key in ordered_keys: + if key in self.attrs: + rendered = self._format_attr(key, self.attrs[key]) + if rendered: + parts.append(rendered) + seen.add(key) + + for key in sorted(k for k in self.attrs.keys() if k not in seen): + rendered = self._format_attr(key, self.attrs[key]) + if rendered: + parts.append(rendered) + + attrs_str = "" + if parts: + attrs_str = " // " + " ".join(parts) + + return f"{self.name} = {self.section}:{addr};{attrs_str}" + + def _format_attr(self, key: str, value: Any) -> str: + if isinstance(value, bool): + return key if value else "" + if key in NUM_ATTRS: + try: + number = _parse_int(value) if isinstance(value, str) else int(value) + except (TypeError, ValueError) as exc: + raise ValueError(f"invalid numeric value for '{key}': {value}") from exc + return f"{key}:{_format_int(key, number)}" + return f"{key}:{value}" + + +def parse_symbols_file(file_path: str | Path) -> List[Symbol]: + """Parse a symbols.txt file into a list of Symbol objects.""" + path = Path(file_path) + symbols: List[Symbol] = [] + for idx, line in enumerate(path.read_text().splitlines(), start=1): + stripped = line.strip() + if not stripped or stripped.startswith("//") or stripped.startswith("#"): + continue + try: + symbols.append(Symbol.from_line(stripped)) + except ValueError as exc: + raise ValueError(f"{path}:{idx}: {exc}") from exc + return symbols + + +def write_symbols_file(file_path: str | Path, symbols: List[Symbol]) -> None: + """Write a list of Symbol objects back to a symbols.txt file.""" + path = Path(file_path) + lines = [symbol.to_line() for symbol in symbols] + path.write_text("\n".join(lines) + "\n") diff --git a/src/MetroidPrime/Tweaks/CTweaks.cpp b/src/MetroidPrime/Tweaks/CTweaks.cpp index 26834351..c54d893c 100644 --- a/src/MetroidPrime/Tweaks/CTweaks.cpp +++ b/src/MetroidPrime/Tweaks/CTweaks.cpp @@ -19,9 +19,7 @@ #include // clang-format on -#include "Kyoto/CResFactory.hpp" - -#include +#include CTweakPlayer* gpTweakPlayer = nullptr; CTweakBall* gpTweakBall = nullptr;