Some docs & scripts for automation

This commit is contained in:
Luke Street
2026-02-23 00:34:24 -07:00
parent 7bb60630c0
commit 355cfc7527
6 changed files with 727 additions and 187 deletions
-184
View File
@@ -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<T, N> var` -> `T var[N]`
- `std::string` -> `rstl::string`
- `std::string_view` -> `const rstl::string&`
- `std::make_unique<T>` -> `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<SConnection> 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 `<algorithm>`, 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<T>::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
<!-- Metaforce functions and parameters often have different `const`ness than retail, which can lead to different code generation. -->
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(); }
```
+78
View File
@@ -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.
+410
View File
@@ -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<T, N> var` -> `T var[N]`
- `std::string` -> `rstl::string`
- `std::string_view` -> `const rstl::string&`
- `std::make_unique<T>` -> `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<SConnection> 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<CElementGen>(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 `<algorithm>`, 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<T>::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
<!-- Metaforce functions and parameters often have different `const`ness than retail, which can lead to different code generation. -->
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;
};
```
+95
View File
@@ -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)
+143
View File
@@ -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<name>\S+)\s*=\s*(?P<section>[^:]+):(?P<address>[^;]+);\s*(?://\s*(?P<attrs>.*))?$",
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")
+1 -3
View File
@@ -19,9 +19,7 @@
#include <MetroidPrime/Tweaks/CTweaks.hpp>
// clang-format on
#include "Kyoto/CResFactory.hpp"
#include <Kyoto/CresFactory.hpp>
#include <Kyoto/CResFactory.hpp>
CTweakPlayer* gpTweakPlayer = nullptr;
CTweakBall* gpTweakBall = nullptr;