9.8 KiB
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->charu8->uchars16->shortu16->ushorts32->intu32->uintzeus::CTransform->CTransform4fzeus::CFrustum->CFrustumPlanes- Any other
zeus::class has the prefixed removed, i.e.zeus::CVector3f->CVector3f std::vector->rstl::vectorstd::optional->rstl::optional_objectstd::pair->rstl::pairstd::unique_ptr->rstl::single_ptrorrstl::auto_ptr(auto_ptrhas an adjacent boolean)std::shared_ptr->rstl::rc_ptrorrstl::ncrc_ptrstd::array<T, N> var->T var[N]std::string->rstl::stringstd::string_view->const rstl::string&std::make_unique<T>->new Tstd::moveis removedstd::min->rstl::min_valstd::max->rstl::max_valstd::clamp->CMath::Clamp(depends on context)
Converting globals and getters
Metaforce -> decomp
zeus::CTransform::frontVector()->CTransform4f::GetForward()zeus::skForward->CVector3f::Forwardg_Renderer->gpRenderzeus::degToRad->CRelAngle::FromDegreesmgr.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:
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:
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:
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:
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:
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 enums with a prefix based on the enum name.
Metaforce example:
enum class EFluidState {
EnteredFluid,
InFluid,
LeftFluid,
};
Decomp:
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:
if (msg == EScriptObjectMessage::Deleted) { }
xe8_particleGen = std::make_unique<CElementGen>(particle,
CElementGen::EModelOrientationType::Normal,
flags & 0x2 ? CElementGen::EOptionalSystemFlags::Two
: CElementGen::EOptionalSystemFlags::One);
Decomp:
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:
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:
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:
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:
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:
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:
void CExplosion::Accept(IVisitor& visitor) { visitor.Visit(this); }
Decomp:
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:
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:
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:
mgr.AddObject(new CGameLight(xec_explosionLight, GetAreaIdAlways(), GetActive(),
"ExplodePLight_" + x10_name, GetTransform(), ...));
Decomp:
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:
#pragma once
namespace metaforce {
class CExplosion : public CEffect {
// ...
};
} // namespace metaforce
Decomp header:
#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:
class CScriptTimer : public CEntity {
public:
DEFINE_ENTITY
CScriptTimer(TUniqueId, std::string_view name, ...);
};
Decomp:
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.
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:
void Reset(CStateManager&);
void ApplyTime(float, CStateManager&);
Decomp:
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):
class CFoo {
int x0_field;
int x4_field;
public:
virtual void SomeFunction();
};
Good (vtable at offset 0):
class CFoo {
public:
virtual void SomeFunction();
private:
int x0_field;
int x4_field;
};