Skip to content
17 changes: 17 additions & 0 deletions Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*****************************************************************************/

#include "StdInc.h"
#include <multiplayer/CMultiplayer.h>
#include <game/CColPoint.h>
#include <game/CObjectGroupPhysicalProperties.h>
#include <game/CStreaming.h>
Expand Down Expand Up @@ -83,6 +84,17 @@ size_t EngineStreamingGetBufferSize()
return g_pGame->GetStreaming()->GetStreamingBufferSize();
}

// Opt-in SilentPatch-style fixes for stock vehicle.dff hierarchy typos (wheel_lm_dummy vs wheel_lm, etc.).
void EngineSetVehicleHierarchyTypoFixesEnabled(bool enabled)
{
g_pCore->GetMultiplayer()->SetVehicleHierarchyTypoFixesEnabled(enabled);
}

bool EngineGetVehicleHierarchyTypoFixesEnabled()
{
return g_pCore->GetMultiplayer()->GetVehicleHierarchyTypoFixesEnabled();
}

void CLuaEngineDefs::LoadFunctions()
{
constexpr static const std::pair<const char*, lua_CFunction> functions[]{
Expand Down Expand Up @@ -163,6 +175,8 @@ void CLuaEngineDefs::LoadFunctions()
{"enginePreloadWorldArea", ArgumentParser<EnginePreloadWorldArea>},
{"engineRestreamModel", ArgumentParser<EngineRestreamModel>},
{"engineRestream", ArgumentParser<EngineRestream>},
{"engineSetVehicleHierarchyTypoFixesEnabled", ArgumentParser<EngineSetVehicleHierarchyTypoFixesEnabled>},
{"engineGetVehicleHierarchyTypoFixesEnabled", ArgumentParser<EngineGetVehicleHierarchyTypoFixesEnabled>},

// CLuaCFunctions::AddFunction ( "engineReplaceMatchingAtomics", EngineReplaceMatchingAtomics );
// CLuaCFunctions::AddFunction ( "engineReplaceWheelAtomics", EngineReplaceWheelAtomics );
Expand Down Expand Up @@ -212,6 +226,9 @@ void CLuaEngineDefs::AddClass(lua_State* luaVM)
lua_classfunction(luaVM, "setModelTXDID", "engineSetModelTXDID");
lua_classfunction(luaVM, "resetModelTXDID", "engineResetModelTXDID");

lua_classfunction(luaVM, "setVehicleHierarchyTypoFixesEnabled", "engineSetVehicleHierarchyTypoFixesEnabled");
lua_classfunction(luaVM, "getVehicleHierarchyTypoFixesEnabled", "engineGetVehicleHierarchyTypoFixesEnabled");

lua_registerstaticclass(luaVM, "Engine");

// `EngineStreaming` class
Expand Down
3 changes: 3 additions & 0 deletions Client/multiplayer_sa/CMultiplayerSA.h
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@ class CMultiplayerSA : public CMultiplayer
void SetBoatWaterSplashEnabled(bool bEnabled);
void SetTyreSmokeEnabled(bool bEnabled);

void SetVehicleHierarchyTypoFixesEnabled(bool bEnabled) override;
bool GetVehicleHierarchyTypoFixesEnabled() const override;

void SetLastStaticAnimationPlayed(eAnimGroup dwGroupID, eAnimID dwAnimID, DWORD dwAnimArrayAddress)
{
m_dwLastStaticAnimGroupID = dwGroupID;
Expand Down
82 changes: 82 additions & 0 deletions Client/multiplayer_sa/CMultiplayerSA_VehicleHierarchyTypoFixes.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*****************************************************************************
*
* PROJECT: Multi Theft Auto
* LICENSE: See LICENSE in the top level directory
* FILE: multiplayer_sa/CMultiplayerSA_VehicleHierarchyTypoFixes.cpp
* PURPOSE: Optional CVehicleModelInfo hierarchy compare hook (matches SilentPatch SA)
*
* GTA matches IDE/handling component names to frame names via _stricmp inside
* CVehicleModelInfo setup. Stock models use inconsistent spellings; SilentPatch
* wraps that call to accept both canonical and typo names. We expose the same
* behaviour only when enabled so servers can keep fully vanilla visuals.
*
*****************************************************************************/

#include "StdInc.h"
#include "CMultiplayerSA.h"

#include <utility>

namespace
{
// Off by default (matches stock GTA SA without SilentPatch).
bool g_bVehicleHierarchyTypoFixesEnabled = false;

int(__cdecl* g_pfnOrgStricmp)(const char*, const char*) = nullptr;

// Pairs are { correct name, name as in shipping .dff }. Must be sorted by the second string.
static constexpr std::pair<const char*, const char*> typosAndFixes[] = {
{"boat_moving_hi", "boat_moving"},
{"elevator_r", "elevator"},
{"misc_a", "misca"},
{"misc_b", "miscb"},
{"taillights", "tailights"},
{"taillights2", "tailights2"},
{"transmission_f", "transmision_f"},
{"transmission_r", "transmision_r"},
{"wheel_lm_dummy", "wheel_lm"},
};

int __cdecl Hooked_VehicleHierarchy_Stricmp(const char* dataName, const char* nodeName)
{
if (!g_bVehicleHierarchyTypoFixesEnabled)
return g_pfnOrgStricmp(dataName, nodeName);

const int origComp = g_pfnOrgStricmp(dataName, nodeName);
if (origComp == 0)
return 0;

for (const auto& typo : typosAndFixes)
{
const int nodeComp = _stricmp(typo.second, nodeName);
if (nodeComp > 0)
break;

if (nodeComp == 0 && _stricmp(typo.first, dataName) == 0)
return 0;
}

return origComp;
}
} // namespace

void InitVehicleHierarchyTypoFixesHook()
{
// SA 1.0 US: indirect call to _stricmp while CVehicleModelInfo binds handling/IDE component names to clump frames.
// Same site hooked by SilentPatch (`InterceptCall(0x4C5311, ...)`).
constexpr DWORD CALLSITE = 0x4C5311;
HookCheckOriginalByte(CALLSITE, 0xE8);
const DWORD rel = *reinterpret_cast<DWORD*>(CALLSITE + 1);
g_pfnOrgStricmp = reinterpret_cast<decltype(g_pfnOrgStricmp)>(CALLSITE + 5 + rel);
HookInstallCall(CALLSITE, reinterpret_cast<DWORD>(&Hooked_VehicleHierarchy_Stricmp));
}

void CMultiplayerSA::SetVehicleHierarchyTypoFixesEnabled(bool bEnabled)
{
g_bVehicleHierarchyTypoFixesEnabled = bEnabled;
}

bool CMultiplayerSA::GetVehicleHierarchyTypoFixesEnabled() const
{
return g_bVehicleHierarchyTypoFixesEnabled;
}
12 changes: 12 additions & 0 deletions Client/multiplayer_sa/CMultiplayerSA_VehicleHierarchyTypoFixes.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*****************************************************************************
*
* PROJECT: Multi Theft Auto
* LICENSE: See LICENSE in the top level directory
* FILE: multiplayer_sa/CMultiplayerSA_VehicleHierarchyTypoFixes.h
* PURPOSE: Init for optional vehicle.dff hierarchy typo matching (SilentPatch-style)
*
*****************************************************************************/

#pragma once

void InitVehicleHierarchyTypoFixesHook();
4 changes: 4 additions & 0 deletions Client/multiplayer_sa/CMultiplayerSA_Vehicles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*****************************************************************************/

#include "StdInc.h"
#include "CMultiplayerSA_VehicleHierarchyTypoFixes.h"

static bool __fastcall AreVehicleDoorsUndamageable(CVehicleSAInterface* vehicle)
{
Expand Down Expand Up @@ -72,4 +73,7 @@ static void __declspec(naked) HOOK_CDamageManager__ProgressDoorDamage()
void CMultiplayerSA::InitHooks_Vehicles()
{
EZHookInstall(CDamageManager__ProgressDoorDamage);

// SilentPatch-style optional matching for typo'd vehicle component frame names (see CMultiplayerSA_VehicleHierarchyTypoFixes.cpp).
InitVehicleHierarchyTypoFixesHook();
}
5 changes: 5 additions & 0 deletions Client/sdk/multiplayer/CMultiplayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,11 @@ class CMultiplayer
virtual void SetBoatWaterSplashEnabled(bool bEnabled) = 0;
virtual void SetTyreSmokeEnabled(bool bEnabled) = 0;

// When enabled, matches canonical vehicle component names to the typo'd hierarchy nodes in stock GTA SA models
// (SilentPatch-style fix). Off by default so behaviour stays vanilla unless a script opts in.
virtual void SetVehicleHierarchyTypoFixesEnabled(bool bEnabled) = 0;
virtual bool GetVehicleHierarchyTypoFixesEnabled() const = 0;

virtual eAnimGroup GetLastStaticAnimationGroupID() = 0;
virtual eAnimID GetLastStaticAnimationID() = 0;
virtual DWORD GetLastAnimArrayAddress() = 0;
Expand Down
Loading