@@ -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)
474490CColModel* 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
0 commit comments