mirror of
https://github.com/PrimeDecomp/prime.git
synced 2026-07-12 18:18:56 -07:00
Some docs & scripts for automation
This commit is contained in:
-184
@@ -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
@@ -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.
|
||||
@@ -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;
|
||||
};
|
||||
```
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user