Skip to content

Commit 701b0dd

Browse files
committed
Improve collision impl
1 parent 3413ebf commit 701b0dd

File tree

8 files changed

+106
-73
lines changed

8 files changed

+106
-73
lines changed

Client/game_sa/CModelInfoSA.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,6 +1540,7 @@ bool CModelInfoSA::SetCustomModel(RpClump* pClump)
15401540
success = pGame->GetRenderWare()->ReplaceWeaponModel(pClump, static_cast<unsigned short>(m_dwModelID));
15411541
break;
15421542
case eModelInfoType::VEHICLE:
1543+
// ReplaceVehicleModele handles collision preservation internally
15431544
success = pGame->GetRenderWare()->ReplaceVehicleModel(pClump, static_cast<unsigned short>(m_dwModelID));
15441545
break;
15451546
case eModelInfoType::ATOMIC:
@@ -1562,9 +1563,8 @@ void CModelInfoSA::RestoreOriginalModel()
15621563
{
15631564
pGame->GetStreaming()->RemoveModel(m_dwModelID);
15641565
}
1565-
15661566
// Reset the stored custom vehicle clump
1567-
m_pCustomClump = NULL;
1567+
m_pCustomClump = nullptr;
15681568
}
15691569

15701570
void CModelInfoSA::SetColModel(CColModel* pColModel)

Client/game_sa/CPoolsSA.cpp

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,27 +88,23 @@ CVehicle* CPoolsSA::AddVehicle(CClientVehicle* pClientVehicle, std::uint16_t mod
8888

8989
CBaseModelInfoSAInterface* pModelInterface = pModelInfo->GetInterface();
9090

91+
// Ensure collision model pointer exists
9192
if (!pModelInterface->pColModel)
9293
{
9394
// Collision model pointer is NULL - try loading
9495
pGame->GetStreaming()->LoadAllRequestedModels(false, "CPoolsSA::AddVehicle");
9596

96-
// Re-check after loading - still NULL means loading failed
97-
if (!pModelInterface->pColModel)
98-
return nullptr;
99-
}
100-
101-
// Check if collision data (m_pColData) is loaded
102-
if (!pModelInterface->pColModel->m_data)
103-
{
104-
// Collision data not loaded - force load
105-
pGame->GetStreaming()->LoadAllRequestedModels(false, "CPoolsSA::AddVehicle");
97+
// Re-fetch interface as loading may have invalidated pointer
98+
pModelInterface = pModelInfo->GetInterface();
10699

107-
// Re-check after loading - still not loaded means loading failed
108-
if (!pModelInterface->pColModel->m_data)
100+
// Still NULL means model has no collision (or loading failed) - block creation
101+
if (!pModelInterface->pColModel)
109102
return nullptr;
110103
}
111104

105+
// Note: Vehicles with custom DFFs (no embedded collision) are handled in CModelInfoSA::SetCustomModel
106+
// where collision is reloaded after SetClump to restore pool-managed collision data
107+
112108
MemSetFast((void*)VAR_CVehicle_Variation1, variation, 1);
113109
MemSetFast((void*)VAR_CVehicle_Variation2, variation2, 1);
114110

Client/game_sa/CRenderWareSA.cpp

Lines changed: 71 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,22 @@ bool CRenderWareSA::ReplaceModel(RpClump* pNew, unsigned short usModelID, DWORD
437437

438438
CBaseModelInfoSAInterface* pModelInfoInterface = pModelInfo->GetInterface();
439439
CBaseModelInfo_SetClump(pModelInfoInterface, pNewClone);
440+
441+
// Re-fetch interface pointer after SetClump (may relocate/change)
442+
pModelInfoInterface = pModelInfo->GetInterface();
443+
444+
// Fix for custom DFF without embedded collision:
445+
// SetClump clears pColModel when DFF has no collision data, but vehicles need collision from .col pool.
446+
// Solution: Remove + Request + Load to restore pool-managed collision from data/vehicles.col
447+
if (dwSetClumpFunction == FUNC_LoadVehicleModel && !pModelInfoInterface->pColModel)
448+
{
449+
pGame->GetStreaming()->RemoveModel(usModelID);
450+
pGame->GetStreaming()->RequestModel(usModelID, 0x16);
451+
pGame->GetStreaming()->LoadAllRequestedModels(false, "CRenderWareSA::ReplaceVehicleModel");
452+
// Re-fetch interface pointer after model reload
453+
pModelInfoInterface = pModelInfo->GetInterface();
454+
}
455+
440456
RpClumpDestroy(pOldClump);
441457
}
442458
}
@@ -470,69 +486,75 @@ bool CRenderWareSA::ReplacePedModel(RpClump* pNew, unsigned short usModelID)
470486
return ReplaceModel(pNew, usModelID, FUNC_LoadPedModel);
471487
}
472488

473-
// Reads and parses a COL3 file
489+
// Reads and parses a COL file (versions 1-4: COLL, COL2, COL3, COL4)
474490
CColModel* CRenderWareSA::ReadCOL(const SString& buffer)
475491
{
476-
if (buffer.size() < sizeof(ColModelFileHeader) + 16)
477-
return NULL;
492+
// Validate minimum buffer size
493+
if (buffer.size() < sizeof(ColModelFileHeader) + 16) [[unlikely]]
494+
return nullptr;
478495

479-
const ColModelFileHeader& header = *(ColModelFileHeader*)buffer.data();
496+
const auto& header = *reinterpret_cast<const ColModelFileHeader*>(buffer.data());
480497

481-
// Validate version string is null-terminated
482-
bool versionValid = false;
483-
for (std::size_t i = 0; i < sizeof(header.version); ++i)
484-
{
485-
if (header.version[i] == '\0')
486-
{
487-
versionValid = true;
488-
break;
489-
}
490-
}
491-
if (!versionValid)
498+
// Validate version field contains valid COL magic number
499+
// Version is 4-char fixed string (not null-terminated): "COLL", "COL2", "COL3", "COL4"
500+
constexpr std::array<std::array<char, 4>, 4> validVersions = {{
501+
{'C', 'O', 'L', 'L'},
502+
{'C', 'O', 'L', '2'},
503+
{'C', 'O', 'L', '3'},
504+
{'C', 'O', 'L', '4'}
505+
}};
506+
507+
const bool isValidVersion = std::any_of(validVersions.begin(), validVersions.end(),
508+
[&header](const auto& valid) {
509+
return std::equal(valid.begin(), valid.end(), header.version);
510+
});
511+
512+
if (!isValidVersion) [[unlikely]]
492513
{
493-
AddReportLog(8622, "ReadCOL: Invalid header - version field not null-terminated");
514+
// Explicitly limit to 4 characters
515+
AddReportLog(8622, SString("ReadCOL: Invalid version '%c%c%c%c' - expected COLL, COL2, COL3, or COL4",
516+
header.version[0], header.version[1], header.version[2], header.version[3]));
494517
return nullptr;
495518
}
496519

497-
// Load the col model
498-
if (header.version[0] == 'C' && header.version[1] == 'O' && header.version[2] == 'L')
520+
// Ensure name field is null-terminated to prevent buffer overrun
521+
const auto* nameEnd = static_cast<const char*>(std::memchr(header.name, '\0', sizeof(header.name)));
522+
if (!nameEnd) [[unlikely]]
499523
{
500-
// Validate name field is null-terminated to prevent buffer overrun
501-
bool nameValid = false;
502-
for (std::size_t i = 0; i < sizeof(header.name); ++i)
503-
{
504-
if (header.name[i] == '\0')
505-
{
506-
nameValid = true;
507-
break;
508-
}
509-
}
510-
if (!nameValid)
511-
AddReportLog(8623, "ReadCOL: Name field not null-terminated, may be truncated");
512-
513-
unsigned char* pModelData = (unsigned char*)buffer.data() + sizeof(ColModelFileHeader);
524+
AddReportLog(8623, "ReadCOL: Name field not null-terminated, may be truncated");
525+
return nullptr;
526+
}
514527

515-
// Create a new CColModel
516-
CColModelSA* pColModel = new CColModelSA();
528+
// Buffer is not modified by us, but GTA's functions expect non-const
529+
auto* pModelData = const_cast<unsigned char*>(reinterpret_cast<const unsigned char*>(buffer.data())) + sizeof(ColModelFileHeader);
517530

518-
if (header.version[3] == 'L')
519-
{
520-
LoadCollisionModel(pModelData, pColModel->GetInterface(), NULL);
521-
}
522-
else if (header.version[3] == '2')
523-
{
524-
LoadCollisionModelVer2(pModelData, header.size - 0x18, pColModel->GetInterface(), NULL);
525-
}
526-
else if (header.version[3] == '3')
527-
{
528-
LoadCollisionModelVer3(pModelData, header.size - 0x18, pColModel->GetInterface(), NULL);
529-
}
531+
// Create a new CColModel
532+
auto* pColModel = new CColModelSA();
530533

531-
// Return the collision model
532-
return pColModel;
534+
// Load appropriate collision version
535+
switch (header.version[3])
536+
{
537+
case 'L':
538+
LoadCollisionModel(pModelData, pColModel->GetInterface(), nullptr);
539+
break;
540+
case '2':
541+
LoadCollisionModelVer2(pModelData, header.size - 0x18, pColModel->GetInterface(), nullptr);
542+
break;
543+
case '3':
544+
LoadCollisionModelVer3(pModelData, header.size - 0x18, pColModel->GetInterface(), nullptr);
545+
break;
546+
case '4':
547+
// COL4 format has same structure as COL3 with one extra uint32 field in header
548+
// Must use Ver4 loader for correct offset calculations
549+
LoadCollisionModelVer4(pModelData, header.size - 0x18, pColModel->GetInterface(), nullptr);
550+
break;
551+
default:
552+
// Should never reach here due to validation above
553+
delete pColModel;
554+
return nullptr;
533555
}
534556

535-
return NULL;
557+
return pColModel;
536558
}
537559

538560
// Loads all atomics from a clump into a container struct and returns the number of atomics it loaded

Client/game_sa/CRenderWareSA.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class CRenderWareSA : public CRenderWare
5454
// Destroys a texture
5555
void DestroyTexture(RwTexture* pTex);
5656

57-
// Reads and parses a COL3 file with an optional collision key name
57+
// Reads and parses a COL file (versions 1-4: COLL, COL2, COL3, COL4)
5858
CColModel* ReadCOL(const SString& buffer);
5959

6060
// Replaces a CColModel for a specific object identified by the object id (usModelID)

Client/game_sa/CVehicleSA.cpp

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1784,20 +1784,20 @@ void* CVehicleSA::GetPrivateSuspensionLines()
17841784
void CVehicleSA::CopyGlobalSuspensionLinesToPrivate()
17851785
{
17861786
CModelInfo* pModelInfo = pGame->GetModelInfo(GetModelIndex());
1787-
if (!pModelInfo) [[unlikely]]
1787+
if (!pModelInfo)
17881788
return;
17891789

17901790
// Protect collision model from streaming GC
17911791
CColModelGuard guard(static_cast<CModelInfoSA*>(pModelInfo));
1792-
if (!guard.IsValid()) [[unlikely]]
1792+
if (!guard.IsValid())
17931793
return;
17941794

17951795
CColDataSA* pColData = guard.GetColData();
1796-
if (!pColData || !pColData->m_suspensionLines) [[unlikely]]
1796+
if (!pColData || !pColData->m_suspensionLines)
17971797
return;
17981798

17991799
void* pPrivateLines = GetPrivateSuspensionLines();
1800-
if (!pPrivateLines) [[unlikely]]
1800+
if (!pPrivateLines)
18011801
return;
18021802

18031803
// Determine copy size based on vehicle type
@@ -1828,13 +1828,13 @@ void CVehicleSA::CopyGlobalSuspensionLinesToPrivate()
18281828
void CVehicleSA::RecalculateSuspensionLines()
18291829
{
18301830
CHandlingEntry* pHandlingEntry = GetHandlingData();
1831-
if (!pHandlingEntry) [[unlikely]]
1831+
if (!pHandlingEntry)
18321832
return;
18331833

18341834
const std::uint32_t dwModel = GetModelIndex();
18351835

18361836
CModelInfo* pModelInfo = pGame->GetModelInfo(dwModel);
1837-
if (!pModelInfo) [[unlikely]]
1837+
if (!pModelInfo)
18381838
return;
18391839

18401840
// Only for vehicles with suspension lines
@@ -1848,11 +1848,11 @@ void CVehicleSA::RecalculateSuspensionLines()
18481848

18491849
// Protect collision model before accessing suspension data
18501850
CColModelGuard guard(static_cast<CModelInfoSA*>(pModelInfo));
1851-
if (!guard.IsValid()) [[unlikely]]
1851+
if (!guard.IsValid())
18521852
return;
18531853

18541854
CVehicleSAInterface* pVehicleInterface = GetVehicleInterface();
1855-
if (!pVehicleInterface) [[unlikely]]
1855+
if (!pVehicleInterface)
18561856
return;
18571857

18581858
// Safe to call now - collision is protected by guard

Client/game_sa/gamesa_renderware.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ typedef void(__cdecl* LoadCollisionModel_t)(unsigned char*, CColModelSAInterface
208208
typedef void(__cdecl* LoadCollisionModelVer2_t)(unsigned char*, unsigned int, CColModelSAInterface*, const char*);
209209
typedef void(__cdecl* LoadCollisionModelVer3_t)(unsigned char*, unsigned int, CColModelSAInterface*,
210210
const char*); // buf, bufsize, ccolmodel&, keyname
211+
typedef void(__cdecl* LoadCollisionModelVer4_t)(unsigned char*, unsigned int, CColModelSAInterface*,
212+
const char*); // buf, bufsize, ccolmodel&, keyname
211213
typedef bool(__cdecl* CTxdStore_LoadTxd_t)(unsigned int id, RwStream* filename);
212214
typedef void(__cdecl* CTxdStore_RemoveTxd_t)(unsigned int id);
213215
typedef void(__cdecl* CTxdStore_RemoveRef_t)(unsigned int id);
@@ -222,6 +224,7 @@ RWFUNC(LoadModel_t LoadModel, (LoadModel_t)0xDEAD)
222224
RWFUNC(LoadCollisionModel_t LoadCollisionModel, (LoadCollisionModel_t)0xDEAD)
223225
RWFUNC(LoadCollisionModelVer2_t LoadCollisionModelVer2, (LoadCollisionModelVer2_t)0xDEAD)
224226
RWFUNC(LoadCollisionModelVer3_t LoadCollisionModelVer3, (LoadCollisionModelVer3_t)0xDEAD)
227+
RWFUNC(LoadCollisionModelVer4_t LoadCollisionModelVer4, (LoadCollisionModelVer4_t)0xDEAD)
225228
RWFUNC(CTxdStore_LoadTxd_t CTxdStore_LoadTxd, (CTxdStore_LoadTxd_t)0xDEAD)
226229
RWFUNC(CTxdStore_GetTxd_t CTxdStore_GetTxd, (CTxdStore_GetTxd_t)0xDEAD)
227230
RWFUNC(CTxdStore_RemoveTxd_t CTxdStore_RemoveTxd, (CTxdStore_RemoveTxd_t)0xDEAD)

Client/game_sa/gamesa_renderware.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ void InitRwFunctions()
9696
LoadCollisionModel = (LoadCollisionModel_t)0x00537580;
9797
LoadCollisionModelVer2 = (LoadCollisionModelVer2_t)0x00537EE0;
9898
LoadCollisionModelVer3 = (LoadCollisionModelVer3_t)0x00537CE0;
99+
LoadCollisionModelVer4 = (LoadCollisionModelVer4_t)0x00537AE0;
99100
CTxdStore_LoadTxd = (CTxdStore_LoadTxd_t)0x00731DD0;
100101
CTxdStore_GetTxd = (CTxdStore_GetTxd_t)0x00408340;
101102
CTxdStore_RemoveTxd = (CTxdStore_RemoveTxd_t)0x00731E90;

Client/mods/deathmatch/logic/CClientColModel.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,5 +157,16 @@ void CClientColModel::InternalRestore(unsigned short usModel)
157157
// Return true if data looks like COL file contents
158158
bool CClientColModel::IsCOLData(const SString& strData)
159159
{
160-
return strData.length() > 32 && memcmp(strData, "COL", 3) == 0 && strData[7] == 0;
160+
// COL file format: version[4] + size[4] + name[24] = minimum 32 bytes
161+
// Version is 4-char string: "COLL", "COL2", "COL3", or "COL4" (not null-terminated)
162+
if (strData.length() < 32)
163+
return false;
164+
165+
// Check if starts with "COL" magic number
166+
if (memcmp(strData, "COL", 3) != 0)
167+
return false;
168+
169+
// Validate 4th character is valid version indicator
170+
const char versionChar = strData[3];
171+
return versionChar == 'L' || versionChar == '2' || versionChar == '3' || versionChar == '4';
161172
}

0 commit comments

Comments
 (0)