Skip to content

Add ability to replace clump model with atomic and vice-versa #4052

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
52 changes: 3 additions & 49 deletions Client/game_sa/CFileLoaderSA.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "gamesa_renderware.h"
#include "CFileLoaderSA.h"
#include "CModelInfoSA.h"
#include "CRenderWareSA.h"

CFileLoaderSA::CFileLoaderSA()
{
Expand Down Expand Up @@ -48,53 +49,6 @@ class CDamagableModelInfo
void CDamagableModelInfo::SetDamagedAtomic(RpAtomic* atomic) { ((void(__thiscall*)(CDamagableModelInfo*, RpAtomic*))0x4C48D0)(this, atomic); }
};

static char* GetFrameNodeName(RwFrame* frame)
{
return ((char*(__cdecl*)(RwFrame*))0x72FB30)(frame);
}

// Originally there was a possibility for this function to cause buffer overflow
// It should be fixed here.
template <size_t OutBuffSize>
void GetNameAndDamage(const char* nodeName, char (&outName)[OutBuffSize], bool& outDamage)
{
const auto nodeNameLen = strlen(nodeName);

const auto NodeNameEndsWith = [=](const char* with) {
const auto withLen = strlen(with);
// dassert(withLen <= nodeNameLen);
return withLen <= nodeNameLen /*dont bother checking otherwise, because it might cause a crash*/
&& strncmp(nodeName + nodeNameLen - withLen, with, withLen) == 0;
};

// Copy `nodeName` into `outName` with `off` trimmed from the end
// Eg.: `dmg_dam` with `off = 4` becomes `dmg`
const auto TerminatedCopy = [&](size_t off) {
dassert(nodeNameLen - off < OutBuffSize);
strncpy_s(outName, nodeName,
std::min(nodeNameLen - off, OutBuffSize - 1)); // By providing `OutBuffSize - 1` it is ensured the array will be null terminated
};

if (NodeNameEndsWith("_dam"))
{
outDamage = true;
TerminatedCopy(sizeof("_dam") - 1);
}
else
{
outDamage = false;
if (NodeNameEndsWith("_l0") || NodeNameEndsWith("_L0"))
{
TerminatedCopy(sizeof("_l0") - 1);
}
else
{
dassert(nodeNameLen < OutBuffSize);
strncpy_s(outName, OutBuffSize, nodeName, OutBuffSize - 1);
}
}
}

static void CVisibilityPlugins_SetAtomicRenderCallback(RpAtomic* pRpAtomic, RpAtomic* (*renderCB)(RpAtomic*))
{
return ((void(__cdecl*)(RpAtomic*, RpAtomic * (*renderCB)(RpAtomic*)))0x7328A0)(pRpAtomic, renderCB);
Expand Down Expand Up @@ -170,9 +124,9 @@ RpAtomic* CFileLoader_SetRelatedModelInfoCB(RpAtomic* atomic, SRelatedModelInfo*
CBaseModelInfoSAInterface* pBaseModelInfo = CModelInfo_ms_modelInfoPtrs[gAtomicModelId];
auto pAtomicModelInfo = reinterpret_cast<CAtomicModelInfo*>(pBaseModelInfo);
RwFrame* pOldFrame = reinterpret_cast<RwFrame*>(atomic->object.object.parent);
char* frameNodeName = GetFrameNodeName(pOldFrame);
char* frameNodeName = CRenderWareSA::GetFrameNodeName(pOldFrame);
bool bDamage = false;
GetNameAndDamage(frameNodeName, name, bDamage);
CRenderWareSA::GetNameAndDamage(frameNodeName, name, bDamage);
CVisibilityPlugins_SetAtomicRenderCallback(atomic, 0);

RpAtomic* pOldAtomic = reinterpret_cast<RpAtomic*>(pBaseModelInfo->pRwObject);
Expand Down
162 changes: 161 additions & 1 deletion Client/game_sa/CModelInfoSA.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ std::map<CTimeInfoSAInterface*, CTimeInfoSAInterface*> CModelInfo
std::unordered_map<DWORD, unsigned short> CModelInfoSA::ms_OriginalObjectPropertiesGroups;
std::unordered_map<DWORD, std::pair<float, float>> CModelInfoSA::ms_VehicleModelDefaultWheelSizes;
std::map<unsigned short, int> CModelInfoSA::ms_DefaultTxdIDMap;
std::unordered_map<std::uint32_t, CBaseModelInfoSAInterface*> CModelInfoSA::m_convertedModelInterfaces;

union tIdeFlags
{
Expand Down Expand Up @@ -1525,7 +1526,8 @@ bool CModelInfoSA::SetCustomModel(RpClump* pClump)
case eModelInfoType::ATOMIC:
case eModelInfoType::LOD_ATOMIC:
case eModelInfoType::TIME:
success = pGame->GetRenderWare()->ReplaceAllAtomicsInModel(pClump, static_cast<unsigned short>(m_dwModelID));
case eModelInfoType::CLUMP:
success = pGame->GetRenderWare()->ReplaceObjectModel(pClump, static_cast<std::uint16_t>(m_dwModelID));
break;
default:
break;
Expand All @@ -1543,6 +1545,26 @@ void CModelInfoSA::RestoreOriginalModel()
pGame->GetStreaming()->RemoveModel(m_dwModelID);
}

// Restore original interface if model was converted
if (MapContains(m_convertedModelInterfaces, m_dwModelID))
{
CBaseModelInfoSAInterface* currentInterface = ppModelInfo[m_dwModelID];
ppModelInfo[m_dwModelID] = MapGet(m_convertedModelInterfaces, m_dwModelID);

if (currentInterface)
{
ppModelInfo[m_dwModelID]->usNumberOfRefs = currentInterface->usNumberOfRefs;
ppModelInfo[m_dwModelID]->pColModel = currentInterface->pColModel;
ppModelInfo[m_dwModelID]->ucAlpha = currentInterface->ucAlpha;
ppModelInfo[m_dwModelID]->usFlags = currentInterface->usFlags;
ppModelInfo[m_dwModelID]->usTextureDictionary = currentInterface->usTextureDictionary;

delete currentInterface;
}

MapRemove(m_convertedModelInterfaces, m_dwModelID);
}

// Reset the stored custom vehicle clump
m_pCustomClump = NULL;
}
Expand Down Expand Up @@ -1812,6 +1834,144 @@ void CModelInfoSA::MakeClumpModel(ushort usBaseID)
CopyStreamingInfoFromModel(usBaseID);
}

bool CModelInfoSA::ConvertToClump()
{
if (GetModelType() == eModelInfoType::CLUMP)
return false;

// Get current interface
CBaseModelInfoSAInterface* currentModelInterface = ppModelInfo[m_dwModelID];
if (!currentModelInterface)
return false;

m_lastInterfaceVTBL = currentModelInterface->VFTBL;

// Create new clump interface
CClumpModelInfoSAInterface* newClumpInterface = new CClumpModelInfoSAInterface();
MemCpyFast(newClumpInterface, currentModelInterface, sizeof(CBaseModelInfoSAInterface));
newClumpInterface->m_nAnimFileIndex = -1;

// We do not destroy or set pRwObject to nullptr here
// because our IsLoaded code expects the RwObject to exist.
// We destroy the old RwObject in CRenderWareSA::ReplaceAllAtomicsInModel after passing the IsLoaded condition in the SetCustomModel.

// Set CClumpModelInfo vtbl after copying data
newClumpInterface->VFTBL = reinterpret_cast<CBaseModelInfo_SA_VTBL*>(VTBL_CClumpModelInfo);

// Set new interface for ModelInfo
ppModelInfo[m_dwModelID] = newClumpInterface;

// Store original (only) interface
if (!MapContains(m_convertedModelInterfaces, m_dwModelID))
MapSet(m_convertedModelInterfaces, m_dwModelID, currentModelInterface);
else
m_lastConversionInterface = currentModelInterface;

return true;
}

bool CModelInfoSA::ConvertToAtomic(bool damageable)
{
// Get current interface
CBaseModelInfoSAInterface* currentModelInterface = ppModelInfo[m_dwModelID];
if (!currentModelInterface)
return false;

if (GetModelType() == eModelInfoType::ATOMIC && ((damageable && currentModelInterface->IsDamageAtomicVTBL()) || (!damageable && currentModelInterface->IsAtomicVTBL())))
return false;

m_lastInterfaceVTBL = currentModelInterface->VFTBL;

// Create new atomic interface
CAtomicModelInfoSAInterface* newAtomicInterface = nullptr;
CDamageableModelInfoSAInterface* newDamageableAtomicInterface = nullptr;

// We do not destroy or set pRwObject to nullptr here
// because our IsLoaded code expects the RwObject to exist.
// We destroy the old RwObject in CRenderWareSA::ReplaceAllAtomicsInModel after passing the IsLoaded condition in the SetCustomModel.

if (damageable)
{
newDamageableAtomicInterface = new CDamageableModelInfoSAInterface();
MemCpyFast(newDamageableAtomicInterface, currentModelInterface, sizeof(CDamageableModelInfoSAInterface));
newDamageableAtomicInterface->m_damagedAtomic = nullptr;

// Set CDamageAtomicModelInfo vtbl after copying data
newDamageableAtomicInterface->VFTBL = reinterpret_cast<CBaseModelInfo_SA_VTBL*>(VTBL_CDamageAtomicModelInfo);
}
else
{
newAtomicInterface = new CAtomicModelInfoSAInterface();
MemCpyFast(newAtomicInterface, currentModelInterface, sizeof(CAtomicModelInfoSAInterface));

// Set CAtomicModelInfo vtbl after copying data
newAtomicInterface->VFTBL = reinterpret_cast<CBaseModelInfo_SA_VTBL*>(VTBL_CAtomicModelInfo);
}

// Set new interface for ModelInfo
ppModelInfo[m_dwModelID] = damageable ? newDamageableAtomicInterface : newAtomicInterface;

// Store original (only) interface
if (!MapContains(m_convertedModelInterfaces, m_dwModelID))
MapSet(m_convertedModelInterfaces, m_dwModelID, currentModelInterface);
else
m_lastConversionInterface = currentModelInterface;

return true;
}

bool CModelInfoSA::ConvertToTimedObject()
{
if (GetModelType() == eModelInfoType::TIME)
return false;

// Get current interface
CBaseModelInfoSAInterface* currentModelInterface = ppModelInfo[m_dwModelID];
if (!currentModelInterface)
return false;

m_lastInterfaceVTBL = currentModelInterface->VFTBL;

// Create new interface
CTimeModelInfoSAInterface* newTimedInterface = new CTimeModelInfoSAInterface();
MemCpyFast(newTimedInterface, currentModelInterface, sizeof(CTimeModelInfoSAInterface));
newTimedInterface->timeInfo.m_wOtherTimeModel = 0;

// We do not destroy or set pRwObject to nullptr here
// because our IsLoaded code expects the RwObject to exist.
// We destroy the old RwObject in CRenderWareSA::ReplaceAllAtomicsInModel after passing the IsLoaded condition in the SetCustomModel.

// Set CTimeModelInfo vtbl after copying data
newTimedInterface->VFTBL = reinterpret_cast<CBaseModelInfo_SA_VTBL*>(VTBL_CTimeModelInfo);

// Set new interface for ModelInfo
ppModelInfo[m_dwModelID] = newTimedInterface;

// Store original (only) interface
if (!MapContains(m_convertedModelInterfaces, m_dwModelID))
MapSet(m_convertedModelInterfaces, m_dwModelID, currentModelInterface);
else
m_lastConversionInterface = currentModelInterface;

return true;
}

CBaseModelInfoSAInterface* CModelInfoSA::GetOriginalInterface() const
{
if (!MapContains(m_convertedModelInterfaces, m_dwModelID))
return nullptr;

return MapGet(m_convertedModelInterfaces, m_dwModelID);
}

bool CModelInfoSA::IsSameModelType()
{
if (!m_lastInterfaceVTBL)
m_lastInterfaceVTBL = m_pInterface->VFTBL;

return m_lastInterfaceVTBL == m_pInterface->VFTBL;
}

void CModelInfoSA::MakeVehicleAutomobile(ushort usBaseID)
{
CVehicleModelInfoSAInterface* m_pInterface = new CVehicleModelInfoSAInterface();
Expand Down
54 changes: 50 additions & 4 deletions Client/game_sa/CModelInfoSA.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ static void* ARRAY_ModelInfo = *(void**)(0x403DA4 + 3);

#define VAR_CTempColModels_ModelPed1 0x968DF0

#define VTBL_CClumpModelInfo 0x85BD30
#define VTBL_CAtomicModelInfo 0x85BBF0
#define VTBL_CDamageAtomicModelInfo 0x85BC30
#define VTBL_CTimeModelInfo 0x85BCB0

class CBaseModelInfoSAInterface;
class CModelInfoSAInterface
{
Expand Down Expand Up @@ -232,6 +237,13 @@ class CBaseModelInfoSAInterface
// +726 = Word array as referenced in CVehicleModelInfo::GetVehicleUpgrade(int)
// +762 = Array of WORD containing something relative to paintjobs
// +772 = Anim file index

void DeleteRwObject() { ((void(__thiscall*)(void*))VFTBL->DeleteRwObject)(this); }

bool IsAtomicVTBL() const { return VFTBL == reinterpret_cast<CBaseModelInfo_SA_VTBL*>(VTBL_CAtomicModelInfo); }
bool IsDamageAtomicVTBL() const { return VFTBL == reinterpret_cast<CBaseModelInfo_SA_VTBL*>(VTBL_CDamageAtomicModelInfo); }
bool IsTimedObjectVTBL() const { return VFTBL == reinterpret_cast<CBaseModelInfo_SA_VTBL*>(VTBL_CTimeModelInfo); }
bool IsClumpVTBL() const { return VFTBL == reinterpret_cast<CBaseModelInfo_SA_VTBL*>(VTBL_CClumpModelInfo); }
};
static_assert(sizeof(CBaseModelInfoSAInterface) == 0x20, "Invalid size for CBaseModelInfoSAInterface");

Expand All @@ -250,11 +262,28 @@ class CClumpModelInfoSAInterface : public CBaseModelInfoSAInterface
union
{
char* m_animFileName;
uint32_t m_nAnimFileIndex;
std::int32_t m_nAnimFileIndex;
};

void DeleteRwObject() { ((void(__thiscall*)(CClumpModelInfoSAInterface*))0x4C4E70)(this); }
void SetClump(RpClump* clump) { ((void(__thiscall*)(CClumpModelInfoSAInterface*, RpClump*))0x4C4F70)(this, clump); }
};

class CAtomicModelInfoSAInterface : public CBaseModelInfoSAInterface
{
public:
void DeleteRwObject() { ((void(__thiscall*)(CAtomicModelInfoSAInterface*))0x4C4440)(this); }
void SetAtomic(RpAtomic* atomic) { ((void(__thiscall*)(CAtomicModelInfoSAInterface*, RpAtomic*))0x4C4360)(this, atomic); }
};

class CTimeModelInfoSAInterface : public CBaseModelInfoSAInterface
class CLodAtomicModelInfoSAInterface : public CAtomicModelInfoSAInterface
{
public:
std::int16_t numChildren; // num child higher level LODs
std::int16_t numChildrenRendered; // num child higher level LODs that have been rendered
};

class CTimeModelInfoSAInterface : public CAtomicModelInfoSAInterface
{
public:
CTimeInfoSAInterface timeInfo;
Expand All @@ -268,10 +297,12 @@ class CVehicleModelUpgradePosnDesc
};
static_assert(sizeof(CVehicleModelUpgradePosnDesc) == 0x20, "Invalid size of CVehicleModelUpgradePosnDesc class");

class CDamageableModelInfoSAInterface : public CBaseModelInfoSAInterface
class CDamageableModelInfoSAInterface : public CAtomicModelInfoSAInterface
{
public:
void* m_damagedAtomic;
RpAtomic* m_damagedAtomic;

void SetDamagedAtomic(RpAtomic* atomic) { ((void(__thiscall*)(CDamageableModelInfoSAInterface*, RpAtomic*))0x4C48D0)(this, atomic); }
};

class CVehicleModelVisualInfoSAInterface // Not sure about this name. If somebody knows more, please change
Expand Down Expand Up @@ -356,6 +387,12 @@ class CModelInfoSA : public CModelInfo
static std::map<unsigned short, int> ms_DefaultTxdIDMap;
SVehicleSupportedUpgrades m_ModelSupportedUpgrades;

void* m_lastInterfaceVTBL{nullptr};
CBaseModelInfoSAInterface* m_lastConversionInterface{nullptr};

// Store original model interfaces after type conersion
static std::unordered_map<std::uint32_t, CBaseModelInfoSAInterface*> m_convertedModelInterfaces;

public:
CModelInfoSA();

Expand Down Expand Up @@ -489,6 +526,15 @@ class CModelInfoSA : public CModelInfo
void RestoreObjectPropertiesGroup();
static void RestoreAllObjectsPropertiesGroups();

// Model type conversion functions
bool ConvertToClump() override;
bool ConvertToAtomic(bool damageable) override;
bool ConvertToTimedObject() override;
CBaseModelInfoSAInterface* GetLastConversionInterface() const noexcept override { return m_lastConversionInterface; }
void SetLastConversionInterface(CBaseModelInfoSAInterface* lastInterace) noexcept override { m_lastConversionInterface = lastInterace; }
CBaseModelInfoSAInterface* GetOriginalInterface() const override;
bool IsSameModelType() override;

// Vehicle towing functions
bool IsTowableBy(CModelInfo* towingModel) override;

Expand Down
Loading
Loading