diff --git a/Client/game_sa/CFileLoaderSA.cpp b/Client/game_sa/CFileLoaderSA.cpp index 67cb476cf5..feb6d89be4 100644 --- a/Client/game_sa/CFileLoaderSA.cpp +++ b/Client/game_sa/CFileLoaderSA.cpp @@ -12,6 +12,7 @@ #include "gamesa_renderware.h" #include "CFileLoaderSA.h" #include "CModelInfoSA.h" +#include "CRenderWareSA.h" CFileLoaderSA::CFileLoaderSA() { @@ -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); @@ -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); diff --git a/Client/game_sa/CModelInfoSA.cpp b/Client/game_sa/CModelInfoSA.cpp index 715515f488..07f152eb71 100644 --- a/Client/game_sa/CModelInfoSA.cpp +++ b/Client/game_sa/CModelInfoSA.cpp @@ -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 { @@ -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; @@ -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; } @@ -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(); diff --git a/Client/game_sa/CModelInfoSA.h b/Client/game_sa/CModelInfoSA.h index 6d0e00c758..9ccbf43aae 100644 --- a/Client/game_sa/CModelInfoSA.h +++ b/Client/game_sa/CModelInfoSA.h @@ -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 { @@ -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"); @@ -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; @@ -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 @@ -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(); @@ -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; diff --git a/Client/game_sa/CRenderWareSA.cpp b/Client/game_sa/CRenderWareSA.cpp index 56f5ce34e4..c95e45cfa3 100644 --- a/Client/game_sa/CRenderWareSA.cpp +++ b/Client/game_sa/CRenderWareSA.cpp @@ -23,6 +23,7 @@ #include "CRenderWareSA.ShaderMatching.h" #include "gamesa_renderware.h" #include "gamesa_renderware.hpp" +#include "CVisibilityPluginsSA.h" extern CCoreInterface* g_pCore; extern CGameSA* pGame; @@ -476,52 +477,156 @@ unsigned int CRenderWareSA::LoadAtomics(RpClump* pClump, RpAtomicContainer* pAto return data.uiReplacements; } -// Replaces all atomics for a specific model -typedef struct +struct SCDamageableModelinfo { - unsigned short usTxdID; - RpClump* pClump; -} SAtomicsReplacer; -bool AtomicsReplacer(RpAtomic* pAtomic, void* data) + CBaseModelInfoSAInterface* baseInterface; + std::uint32_t modelId; + RpClump* clump; +}; + +bool CRenderWareSA::ReplaceObjectModel(RpClump* newClump, std::uint16_t modelID) { - SAtomicsReplacer* pData = reinterpret_cast<SAtomicsReplacer*>(data); - SRelatedModelInfo relatedModelInfo = {0}; - relatedModelInfo.pClump = pData->pClump; - relatedModelInfo.bDeleteOldRwObject = true; - CFileLoader_SetRelatedModelInfoCB(pAtomic, &relatedModelInfo); + CModelInfo* pModelInfo = pGame->GetModelInfo(modelID); + if (!pModelInfo) + return false; - // The above function adds a reference to the model's TXD by either - // calling CAtomicModelInfo::SetAtomic or CDamagableModelInfo::SetDamagedAtomic. Remove it again. - CTxdStore_RemoveRef(pData->usTxdID); - return true; -} + CBaseModelInfoSAInterface* currentModelInterface = pModelInfo->GetInterface(); -bool CRenderWareSA::ReplaceAllAtomicsInModel(RpClump* pNew, unsigned short usModelID) -{ - CModelInfo* pModelInfo = pGame->GetModelInfo(usModelID); + // Comparing two clumps here leads to an unknown crash during object creation (std::bad_alloc) in the SA code + if ((newClump == reinterpret_cast<RpClump*>(currentModelInterface->pRwObject) || DoContainTheSameGeometry(newClump, nullptr, reinterpret_cast<RpAtomic*>(currentModelInterface->pRwObject))) && pModelInfo->IsSameModelType()) + return true; - if (pModelInfo) + // We need to make a copy here because DeleteRwObject removes atomics from our new clump + RpClump* clonedClump = RpClumpClone(newClump); + if (!clonedClump) + return false; + + // Add extra reference to prevent the TXD from being unloaded after calling DeleteRwObject + bool extraRefAdded = false; + if (currentModelInterface->pRwObject) { - RpAtomic* pOldAtomic = (RpAtomic*)pModelInfo->GetRwObject(); + CTxdStore_AddRef(currentModelInterface->usTextureDictionary); + extraRefAdded = true; + } - if (reinterpret_cast<RpClump*>(pOldAtomic) != pNew && !DoContainTheSameGeometry(pNew, NULL, pOldAtomic)) + // Destroy RwObject from original interface after type conversion + CBaseModelInfoSAInterface* originalInterface = pModelInfo->GetOriginalInterface(); + if (originalInterface && originalInterface->pRwObject) + { + originalInterface->DeleteRwObject(); + + // If the originalInterface has an existing RwObject, it points to the same one as copyCurrentModelInterface + currentModelInterface->pRwObject = nullptr; + } + + // The user can convert the same model multiple times, but its original model interface is saved only once, + // so we remove the RwObject and the interface created during the last conversion + CBaseModelInfoSAInterface* lastConversionInterface = pModelInfo->GetLastConversionInterface(); + if (lastConversionInterface) + { + if (lastConversionInterface->pRwObject == currentModelInterface->pRwObject) + currentModelInterface->pRwObject = nullptr; + + lastConversionInterface->DeleteRwObject(); + delete lastConversionInterface; + + pModelInfo->SetLastConversionInterface(nullptr); + } + + // Destroy current (old) RwObject + if (currentModelInterface->pRwObject) + currentModelInterface->DeleteRwObject(); + + // Check model type + switch (pModelInfo->GetModelType()) + { + case eModelInfoType::ATOMIC: { - // Clone the clump that's to be replaced (FUNC_AtomicsReplacer removes the atomics from the source clump) - RpClump* pCopy = RpClumpClone(pNew); + RpAtomic* atomic = GetFirstAtomic(clonedClump); + if (!atomic) + return false; - // Replace the atomics - SAtomicsReplacer data; - data.usTxdID = ((CBaseModelInfoSAInterface**)ARRAY_ModelInfo)[usModelID]->usTextureDictionary; - data.pClump = pCopy; + // Create main root frame + RpAtomicSetFrame(atomic, RwFrameCreate()); - MemPutFast<DWORD>((DWORD*)DWORD_AtomicsReplacerModelID, usModelID); - RpClumpForAllAtomics(pCopy, AtomicsReplacer, &data); + // Is damageable object? + if (currentModelInterface->IsDamageAtomicVTBL()) + { + SCDamageableModelinfo damagedInfo{}; + damagedInfo.modelId = modelID; + damagedInfo.baseInterface = currentModelInterface; + damagedInfo.clump = clonedClump; + + RpClumpForAllAtomics(clonedClump, + [](RpAtomic* atomic, void* data) -> bool { + auto* damagedInfo = static_cast<SCDamageableModelinfo*>(data); + RwFrame* frame = RpGetFrame(atomic); + if (!frame) + return false; + + bool damage = false; + char name[24]; + GetNameAndDamage(GetFrameNodeName(frame), name, damage); + + if (damage) + static_cast<CDamageableModelInfoSAInterface*>(damagedInfo->baseInterface)->SetDamagedAtomic(atomic); + else + static_cast<CAtomicModelInfoSAInterface*>(damagedInfo->baseInterface)->SetAtomic(atomic); + + // Create main root frame + RpAtomicSetFrame(atomic, RwFrameCreate()); + pGame->GetVisibilityPlugins()->SetAtomicId(atomic, damagedInfo->modelId); + + // Remove our atomic from temp clump + RpClumpRemoveAtomic(damagedInfo->clump, atomic); + return false; + }, &damagedInfo); + } + else // or standard object? + { + static_cast<CAtomicModelInfoSAInterface*>(currentModelInterface)->SetAtomic(atomic); + pGame->GetVisibilityPlugins()->SetAtomicId(atomic, modelID); + + // Remove our atomic from temp clump + RpClumpRemoveAtomic(clonedClump, atomic); + } + + // Destroy empty clump + RpClumpDestroy(clonedClump); + break; + } + case eModelInfoType::TIME: + { + RpAtomic* atomic = GetFirstAtomic(clonedClump); + if (!atomic) + return false; - // Get rid of the now empty copied clump - RpClumpDestroy(pCopy); + // Create main root frame + RpAtomicSetFrame(atomic, RwFrameCreate()); + + static_cast<CTimeModelInfoSAInterface*>(currentModelInterface)->SetAtomic(atomic); + pGame->GetVisibilityPlugins()->SetAtomicId(atomic, modelID); + + // Remove our atomic from temp clump + RpClumpRemoveAtomic(clonedClump, atomic); + + // Destroy empty clump + RpClumpDestroy(clonedClump); + break; + } + case eModelInfoType::CLUMP: + { + // We do not delete or clear the clump copy here + // The copy will be removed when DestroyRwObject is called in CClumpModelInfo + static_cast<CClumpModelInfoSAInterface*>(currentModelInterface)->SetClump(clonedClump); + break; } } + // Remove extra ref + if (extraRefAdded) + CTxdStore_RemoveRef(currentModelInterface->usTextureDictionary); + return true; } @@ -927,6 +1032,30 @@ RwFrame* CRenderWareSA::GetFrameFromName(RpClump* pRoot, SString strName) return RwFrameFindFrame(RpGetFrame(pRoot), strName); } +RpAtomic* CRenderWareSA::GetFirstAtomic(RpClump* clump) +{ + return ((RpAtomic*(__cdecl*)(RpClump*))0x734820)(clump); +} + +char* CRenderWareSA::GetFrameNodeName(RwFrame* frame) +{ + return ((char*(__cdecl*)(RwFrame*))0x72FB30)(frame); +} + +std::uint32_t CRenderWareSA::RpGeometryGet2dFxCount(RpGeometry* geometry) +{ + if (!geometry) + return 0; + + auto* plugin = RWPLUGINOFFSET(std::uint32_t, geometry, *(int*)0xC3A1E0); // g2dEffectPluginOffset + return plugin ? *plugin : 0; +} + +RpAtomic* CRenderWareSA::Get2DEffectAtomic(RpClump* clump) +{ + return ((RpAtomic*(__cdecl*)(RpClump*))0x734880)(clump); +} + void CRenderWareSA::CMatrixToRwMatrix(const CMatrix& mat, RwMatrix& rwOutMatrix) { rwOutMatrix.right = (RwV3d&)mat.vRight; diff --git a/Client/game_sa/CRenderWareSA.h b/Client/game_sa/CRenderWareSA.h index d79d930720..3be13b17a0 100644 --- a/Client/game_sa/CRenderWareSA.h +++ b/Client/game_sa/CRenderWareSA.h @@ -60,9 +60,6 @@ class CRenderWareSA : public CRenderWare // Loads all atomics from a clump into a container struct and returns the number of atomics it loaded unsigned int LoadAtomics(RpClump* pClump, RpAtomicContainer* pAtomics); - // Replaces all atomics for a specific model - bool ReplaceAllAtomicsInModel(RpClump* pSrc, unsigned short usModelID) override; - // Replaces all atomics in a clump void ReplaceAllAtomicsInClump(RpClump* pDst, RpAtomicContainer* pAtomics, unsigned int uiAtomics); @@ -80,11 +77,12 @@ class CRenderWareSA : public CRenderWare // Replaces a CClumpModelInfo clump with a new clump bool ReplaceWeaponModel(RpClump* pNew, unsigned short usModelID) override; - bool ReplacePedModel(RpClump* pNew, unsigned short usModelID) override; - bool ReplaceModel(RpClump* pNew, unsigned short usModelID, DWORD dwSetClumpFunction); + // Replaces object model (clump or atomic) + bool ReplaceObjectModel(RpClump* newClump, std::uint16_t modelID) override; + // Replaces dynamic parts of the vehicle (models that have two different versions: 'ok' and 'dam'), such as doors // szName should be without the part suffix (e.g. 'door_lf' or 'door_rf', and not 'door_lf_dummy') bool ReplacePartModels(RpClump* pClump, RpAtomicContainer* pAtomics, unsigned int uiAtomics, const char* szName); @@ -122,6 +120,57 @@ class CRenderWareSA : public CRenderWare CModelTexturesInfo* GetModelTexturesInfo(ushort usModelId); RwFrame* GetFrameFromName(RpClump* pRoot, SString strName); + RpAtomic* GetFirstAtomic(RpClump* clump) override; + + static char* GetFrameNodeName(RwFrame* frame); + + std::uint32_t RpGeometryGet2dFxCount(RpGeometry* geometry) override; + RpAtomic* Get2DEffectAtomic(RpClump* clump) override; + + // Originally there was a possibility for this function to cause buffer overflow + // It should be fixed here. + template <size_t OutBuffSize> + static 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 StaticSetHooks(); static void StaticSetClothesReplacingHooks(); diff --git a/Client/game_sa/CVisibilityPluginsSA.cpp b/Client/game_sa/CVisibilityPluginsSA.cpp index b5fe2d92c6..c4cab5c173 100644 --- a/Client/game_sa/CVisibilityPluginsSA.cpp +++ b/Client/game_sa/CVisibilityPluginsSA.cpp @@ -54,6 +54,14 @@ int CVisibilityPluginsSA::GetAtomicId(RwObject* pAtomic) return iResult; } +void CVisibilityPluginsSA::SetAtomicId(RpAtomic* atomic, int id) +{ + if (!atomic) + return; + + ((void(__cdecl*)(RpAtomic*, int))FUNC_CVisibilityPlugins_SetAtomicId)(atomic, id); +} + bool CVisibilityPluginsSA::InsertEntityIntoEntityList(void* entity, float distance, void* callback) { return ((bool(_cdecl*)(void*, float, void*))FUNC_CVisibilityPlugins_InsertEntityIntoEntityList)(entity, distance, callback); diff --git a/Client/game_sa/CVisibilityPluginsSA.h b/Client/game_sa/CVisibilityPluginsSA.h index f4a583bcf2..81ffc736eb 100644 --- a/Client/game_sa/CVisibilityPluginsSA.h +++ b/Client/game_sa/CVisibilityPluginsSA.h @@ -15,12 +15,14 @@ #define FUNC_CVisiblityPlugins_SetClumpAlpha 0x732B00 #define FUNC_CVisibilityPlugins_GetAtomicId 0x732370 +#define FUNC_CVisibilityPlugins_SetAtomicId 0x732230 class CVisibilityPluginsSA : public CVisibilityPlugins { public: void SetClumpAlpha(RpClump* pClump, int iAlpha); int GetAtomicId(RwObject* pAtomic); + void SetAtomicId(RpAtomic* atomic, int id) override; bool InsertEntityIntoEntityList(void* entity, float distance, void* callback); }; diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp index 0ad88113b1..4a57ab30c5 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp @@ -147,6 +147,7 @@ void CLuaEngineDefs::LoadFunctions() {"engineGetPoolUsedCapacity", ArgumentParser<EngineGetPoolUsedCapacity>}, {"engineSetPoolCapacity", ArgumentParser<EngineSetPoolCapacity>}, {"enginePreloadWorldArea", ArgumentParser<EnginePreloadWorldArea>}, + {"engineConvertModelToType", ArgumentParser<EngineConvertModelToType>}, // CLuaCFunctions::AddFunction ( "engineReplaceMatchingAtomics", EngineReplaceMatchingAtomics ); // CLuaCFunctions::AddFunction ( "engineReplaceWheelAtomics", EngineReplaceWheelAtomics ); @@ -2568,3 +2569,34 @@ void CLuaEngineDefs::EnginePreloadWorldArea(CVector position, std::optional<Prel if (option == PreloadAreaOption::ALL || option == PreloadAreaOption::COLLISIONS) g_pGame->GetStreaming()->LoadSceneCollision(&position); } + +bool CLuaEngineDefs::EngineConvertModelToType(std::uint32_t model, eClientModelType type) +{ + if (!CClientObjectManager::IsValidModel(model)) + throw LuaFunctionError("Invalid model"); + + if (type == eClientModelType::PED || type == eClientModelType::VEHICLE || type == eClientModelType::TXD) + throw LuaFunctionError("The argument 'model-type' is invalid"); + + CModelInfo* modelInfo = g_pGame->GetModelInfo(model); + if (!modelInfo) + return false; + + // We need to stream out the model, otherwise it can crash + g_pClientGame->GetObjectManager()->RestreamObjects(model); + modelInfo->RestreamIPL(); + + switch (type) + { + case eClientModelType::OBJECT: + return modelInfo->ConvertToAtomic(false); + case eClientModelType::OBJECT_DAMAGEABLE: + return modelInfo->ConvertToAtomic(true); + case eClientModelType::CLUMP: + return modelInfo->ConvertToClump(); + case eClientModelType::TIMED_OBJECT: + return modelInfo->ConvertToTimedObject(); + } + + return false; +} diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.h b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.h index a67ecfc68d..85d013a426 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.h +++ b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.h @@ -94,6 +94,8 @@ class CLuaEngineDefs : public CLuaDefs static void EnginePreloadWorldArea(CVector position, std::optional<PreloadAreaOption> option); + static bool EngineConvertModelToType(std::uint32_t model, eClientModelType type); + private: static void AddEngineColClass(lua_State* luaVM); static void AddEngineTxdClass(lua_State* luaVM); diff --git a/Client/sdk/game/CModelInfo.h b/Client/sdk/game/CModelInfo.h index 8b27fc0304..d42589ee6f 100644 --- a/Client/sdk/game/CModelInfo.h +++ b/Client/sdk/game/CModelInfo.h @@ -192,6 +192,14 @@ class CModelInfo virtual bool IsAlphaTransparencyEnabled() = 0; virtual void ResetAlphaTransparency() = 0; + virtual bool ConvertToClump() = 0; + virtual bool ConvertToAtomic(bool damageable) = 0; + virtual bool ConvertToTimedObject() = 0; + virtual CBaseModelInfoSAInterface* GetLastConversionInterface() const noexcept = 0; + virtual void SetLastConversionInterface(CBaseModelInfoSAInterface* lastInterface) noexcept = 0; + virtual CBaseModelInfoSAInterface* GetOriginalInterface() const = 0; + virtual bool IsSameModelType() = 0; + // ONLY use for CVehicleModelInfos virtual short GetAvailableVehicleMod(unsigned short usSlot) = 0; virtual bool IsUpgradeAvailable(eVehicleUpgradePosn posn) = 0; diff --git a/Client/sdk/game/CRenderWare.h b/Client/sdk/game/CRenderWare.h index 92686978b1..e65c1c1208 100644 --- a/Client/sdk/game/CRenderWare.h +++ b/Client/sdk/game/CRenderWare.h @@ -26,6 +26,8 @@ struct RwMatrix; struct RwTexDictionary; struct RwTexture; struct RpClump; +struct RpAtomic; +struct RpGeometry; typedef CShaderItem CSHADERDUMMY; @@ -87,7 +89,6 @@ class CRenderWare virtual void DestroyTexture(RwTexture* pTex) = 0; virtual void ReplaceCollisions(CColModel* pColModel, unsigned short usModelID) = 0; virtual unsigned int LoadAtomics(RpClump* pClump, RpAtomicContainer* pAtomics) = 0; - virtual bool ReplaceAllAtomicsInModel(RpClump* pSrc, unsigned short usModelID) = 0; virtual void ReplaceAllAtomicsInClump(RpClump* pDst, RpAtomicContainer* pAtomics, unsigned int uiAtomics) = 0; virtual void ReplaceWheels(RpClump* pClump, RpAtomicContainer* pAtomics, unsigned int uiAtomics, const char* szWheel) = 0; virtual void RepositionAtomic(RpClump* pDst, RpClump* pSrc, const char* szName) = 0; @@ -95,6 +96,7 @@ class CRenderWare virtual bool ReplaceVehicleModel(RpClump* pNew, unsigned short usModelID) = 0; virtual bool ReplaceWeaponModel(RpClump* pNew, unsigned short usModelID) = 0; virtual bool ReplacePedModel(RpClump* pNew, unsigned short usModelID) = 0; + virtual bool ReplaceObjectModel(RpClump* newClump, std::uint16_t modelID) = 0; virtual bool ReplacePartModels(RpClump* pClump, RpAtomicContainer* pAtomics, unsigned int uiAtomics, const char* szName) = 0; virtual void PulseWorldTextureWatch() = 0; virtual void GetModelTextureNames(std::vector<SString>& outNameList, ushort usModelID) = 0; @@ -110,6 +112,11 @@ class CRenderWare virtual void RemoveClientEntityRefs(CClientEntityBase* pClientEntity) = 0; virtual void RemoveShaderRefs(CSHADERDUMMY* pShaderItem) = 0; virtual RwFrame* GetFrameFromName(RpClump* pRoot, SString strName) = 0; + virtual RpAtomic* GetFirstAtomic(RpClump* clump) = 0; + + virtual std::uint32_t RpGeometryGet2dFxCount(RpGeometry* geometry) = 0; + virtual RpAtomic* Get2DEffectAtomic(RpClump* clump) = 0; + virtual bool RightSizeTxd(const SString& strInTxdFilename, const SString& strOutTxdFilename, uint uiSizeLimit) = 0; virtual void TxdForceUnload(ushort usTxdId, bool bDestroyTextures) = 0; diff --git a/Client/sdk/game/CVisibilityPlugins.h b/Client/sdk/game/CVisibilityPlugins.h index f2e948f65a..1cb16960c9 100644 --- a/Client/sdk/game/CVisibilityPlugins.h +++ b/Client/sdk/game/CVisibilityPlugins.h @@ -15,6 +15,7 @@ #define ATOMIC_ID_FLAG_TWO_VERSIONS_DAMAGED 2 struct RpClump; +struct RpAtomic; struct RwObject; class CVisibilityPlugins @@ -22,6 +23,7 @@ class CVisibilityPlugins public: virtual void SetClumpAlpha(RpClump* pClump, int iAlpha) = 0; virtual int GetAtomicId(RwObject* pAtomic) = 0; + virtual void SetAtomicId(RpAtomic* atomic, int id) = 0; virtual bool InsertEntityIntoEntityList(void* entity, float distance, void* callback) = 0; };