From 1032a725f2ec7542c04f256f5247a4c37473a674 Mon Sep 17 00:00:00 2001 From: Flashmyname Date: Sat, 5 Sep 2026 18:39:49 +0200 Subject: [PATCH 1/4] Allow pickups and projectiles as engineApplyShaderToWorldTexture targets The objects behind pickups and projectiles are never registered in the object pool, so the render hook could not resolve them and a shader targeted at one matched nothing while the call still returned true. Resolve them in the entity render handler when the pool lookup misses. CPools::GetClientEntity is left alone, its other callers rely on getting NULL for these objects. Element types the render hooks cannot tell apart now fail with a script warning instead of silently returning true. Part of #1111. --- Client/mods/deathmatch/logic/CClientGame.cpp | 14 +++++++++ .../mods/deathmatch/logic/CClientPickup.cpp | 5 ++++ .../deathmatch/logic/CClientPickupManager.cpp | 25 ++++++++++++++++ .../deathmatch/logic/CClientPickupManager.h | 10 +++++++ .../logic/CClientProjectileManager.cpp | 3 +- .../logic/luadefs/CLuaEngineDefs.cpp | 30 +++++++++++++++++-- 6 files changed, 84 insertions(+), 3 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 6ed7e017179..cc0d543fac8 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -3761,6 +3761,15 @@ void CClientGame::StaticGameEntityRenderHandler(CEntitySAInterface* pGameEntity) CPools* pPools = g_pGame->GetPools(); // Map to client entity and pass to the texture replacer CClientEntity* pClientEntity = pPools->GetClientEntity((DWORD*)pGameEntity); + if (!pClientEntity) + { + // Pickup and projectile objects are not registered in the object pool. The interface type is not visible + // here, so every miss pays for the two lookups: a hash find while pickups exist and a walk of the (short) projectile list. + CClientManager* pManager = g_pClientGame->GetManager(); + pClientEntity = pManager->GetPickupManager()->GetPickupByGameObject(pGameEntity); + if (!pClientEntity) + pClientEntity = pManager->GetProjectileManager()->Get(pGameEntity); + } if (pClientEntity) { int iTypeMask; @@ -3776,6 +3785,8 @@ void CClientGame::StaticGameEntityRenderHandler(CEntitySAInterface* pGameEntity) iTypeMask = TYPE_MASK_VEHICLE; break; case CCLIENTOBJECT: + case CCLIENTPICKUP: + case CCLIENTPROJECTILE: iTypeMask = TYPE_MASK_OBJECT; break; case CCLIENTBUILDING: @@ -5033,6 +5044,9 @@ bool CClientGame::VehicleFellThroughMapHandler(CVehicleSAInterface* pVehicleInte // Clear stale pool entries to prevent dangling pointer crashes in GetClientEntity/GetEntity. void CClientGame::GameObjectDestructHandler(CEntitySAInterface* pObject) { + if (m_pManager) + m_pManager->GetPickupManager()->OnGameObjectDestroyed(pObject); + if (auto* pSlot = g_pGame->GetPools()->GetObject(reinterpret_cast(pObject))) { pSlot->pEntity = nullptr; diff --git a/Client/mods/deathmatch/logic/CClientPickup.cpp b/Client/mods/deathmatch/logic/CClientPickup.cpp index bf51ac1d9c8..8b6f76221e4 100644 --- a/Client/mods/deathmatch/logic/CClientPickup.cpp +++ b/Client/mods/deathmatch/logic/CClientPickup.cpp @@ -188,6 +188,8 @@ bool CClientPickup::Create() return false; } + m_pPickupManager->RegisterGameObject(m_pObject->GetInterface(), this); + // Create our collision sphere m_pCollision = new CClientColSphere(g_pClientGame->GetManager(), ElementID(INVALID_ELEMENT_ID), m_vecPosition, 1.0f); m_pCollision->m_pOwningPickup = this; @@ -220,6 +222,9 @@ void CClientPickup::Destroy() // Delete the pickup if (m_pPickup) { + if (m_pObject) + m_pPickupManager->UnregisterGameObject(m_pObject->GetInterface(), this); + // Clear object reference before Remove() to prevent dangling pointer m_pObject = nullptr; m_pPickup->Remove(); diff --git a/Client/mods/deathmatch/logic/CClientPickupManager.cpp b/Client/mods/deathmatch/logic/CClientPickupManager.cpp index 6cc7b07c3ca..5a5e15433d6 100644 --- a/Client/mods/deathmatch/logic/CClientPickupManager.cpp +++ b/Client/mods/deathmatch/logic/CClientPickupManager.cpp @@ -70,6 +70,7 @@ void CClientPickupManager::DeleteAll() // Clear the list m_List.clear(); + m_GameObjectMap.clear(); // Restore previous processing state if (!wasDisabled) @@ -124,6 +125,30 @@ unsigned short CClientPickupManager::GetWeaponModel(unsigned int uiWeaponID) return 0; } +CClientPickup* CClientPickupManager::GetPickupByGameObject(const CEntitySAInterface* pGameObject) const +{ + auto iter = m_GameObjectMap.find(pGameObject); + return iter != m_GameObjectMap.end() ? iter->second : nullptr; +} + +void CClientPickupManager::OnGameObjectDestroyed(const CEntitySAInterface* pGameObject) +{ + m_GameObjectMap.erase(pGameObject); +} + +void CClientPickupManager::RegisterGameObject(const CEntitySAInterface* pGameObject, CClientPickup* pPickup) +{ + m_GameObjectMap[pGameObject] = pPickup; +} + +void CClientPickupManager::UnregisterGameObject(const CEntitySAInterface* pGameObject, const CClientPickup* pPickup) +{ + // The object slot can already belong to a newer pickup + auto iter = m_GameObjectMap.find(pGameObject); + if (iter != m_GameObjectMap.end() && iter->second == pPickup) + m_GameObjectMap.erase(iter); +} + void CClientPickupManager::RemoveFromList(CClientPickup* pPickup) { if (!m_bDontRemoveFromList) diff --git a/Client/mods/deathmatch/logic/CClientPickupManager.h b/Client/mods/deathmatch/logic/CClientPickupManager.h index 384a8424750..71d8f2eecdd 100644 --- a/Client/mods/deathmatch/logic/CClientPickupManager.h +++ b/Client/mods/deathmatch/logic/CClientPickupManager.h @@ -15,6 +15,9 @@ class CClientPickupManager; #include "CClientManager.h" #include "CClientPickup.h" #include +#include + +class CEntitySAInterface; class CClientPickupManager { @@ -45,17 +48,24 @@ class CClientPickupManager void RestreamPickups(unsigned short usModel); void RestreamAllPickups(); + CClientPickup* GetPickupByGameObject(const CEntitySAInterface* pGameObject) const; + void OnGameObjectDestroyed(const CEntitySAInterface* pGameObject); + private: CClientPickupManager(CClientManager* pManager); ~CClientPickupManager(); void RemoveFromList(CClientPickup* pPickup); + void RegisterGameObject(const CEntitySAInterface* pGameObject, CClientPickup* pPickup); + void UnregisterGameObject(const CEntitySAInterface* pGameObject, const CClientPickup* pPickup); CClientManager* m_pManager; std::list m_List; bool m_bDontRemoveFromList; + std::unordered_map m_GameObjectMap; + bool m_bPickupProcessingDisabled; static unsigned int m_uiPickupCount; }; diff --git a/Client/mods/deathmatch/logic/CClientProjectileManager.cpp b/Client/mods/deathmatch/logic/CClientProjectileManager.cpp index 862ebc68c40..74ccf6fba4b 100644 --- a/Client/mods/deathmatch/logic/CClientProjectileManager.cpp +++ b/Client/mods/deathmatch/logic/CClientProjectileManager.cpp @@ -88,7 +88,8 @@ CClientProjectile* CClientProjectileManager::Get(CEntitySAInterface* pProjectile list::iterator iter = m_List.begin(); for (; iter != m_List.end(); iter++) { - if ((*iter)->GetGameEntity()->GetInterface() == pProjectile) + CEntity* pGameEntity = (*iter)->GetGameEntity(); + if (pGameEntity && pGameEntity->GetInterface() == pProjectile) { return (*iter); } diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp index d8d393e085e..44ea554c92d 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp @@ -1303,6 +1303,32 @@ int CLuaEngineDefs::EngineReplaceVehiclePart(lua_State* luaVM) return 1; } +// Element types the texture replacer can tell apart while they render +static bool IsShaderTargetElement(CClientEntity* pElement) +{ + switch (pElement->GetType()) + { + case CCLIENTPED: + case CCLIENTPLAYER: + case CCLIENTVEHICLE: + case CCLIENTOBJECT: + case CCLIENTWEAPON: + case CCLIENTBUILDING: + case CCLIENTPICKUP: + case CCLIENTPROJECTILE: + return true; + default: + return false; + } +} + +static void ReadShaderTargetElement(CScriptArgReader& argStream, CClientEntity*& pElement) +{ + argStream.ReadUserData(pElement, nullptr); + if (!argStream.HasErrors() && pElement && !IsShaderTargetElement(pElement)) + argStream.SetCustomError("targetElement must be a ped, player, vehicle, object, weapon, building, pickup or projectile", "Bad argument"); +} + int CLuaEngineDefs::EngineApplyShaderToWorldTexture(lua_State* luaVM) { // bool engineApplyShaderToWorldTexture ( element shader, string textureName, [ element targetElement, bool appendLayers ] ) @@ -1314,7 +1340,7 @@ int CLuaEngineDefs::EngineApplyShaderToWorldTexture(lua_State* luaVM) CScriptArgReader argStream(luaVM); argStream.ReadUserData(pShader); argStream.ReadString(strTextureNameMatch); - argStream.ReadUserData(pElement, NULL); + ReadShaderTargetElement(argStream, pElement); argStream.ReadBool(bAppendLayers, true); if (!argStream.HasErrors()) @@ -1342,7 +1368,7 @@ int CLuaEngineDefs::EngineRemoveShaderFromWorldTexture(lua_State* luaVM) CScriptArgReader argStream(luaVM); argStream.ReadUserData(pShader); argStream.ReadString(strTextureNameMatch); - argStream.ReadUserData(pElement, NULL); + ReadShaderTargetElement(argStream, pElement); if (!argStream.HasErrors()) { From d843a17fe4a97b8d06a1c907f9fb2a8532008a90 Mon Sep 17 00:00:00 2001 From: Flashmyname Date: Sat, 5 Sep 2026 18:39:50 +0200 Subject: [PATCH 2/4] Allow searchlights as engineApplyShaderToWorldTexture targets Searchlights are not game entities, so the entity render hook never saw them. The cone is drawn by MTA itself, so tag that draw with the element around the RenderHeliLight call. The cone binds no texture and fell into the generic unnamed bucket together with every other untextured draw, so it gets a name of its own, "searchlight". The ground spot is stored with the texture the game also uses for the light pools of street lamps, so a copy of it is registered as "shad_searchlight" and a shader on the spots leaves the lamps alone. Part of #1111. --- .../CRenderItemManager.TextureReplace.cpp | 2 +- Client/game_sa/CPointLightsSA.cpp | 9 ++ .../game_sa/CRenderWareSA.ShaderSupport.cpp | 99 +++++++++++++++++-- Client/game_sa/CRenderWareSA.ShaderSupport.h | 3 + Client/game_sa/CRenderWareSA.h | 4 + Client/game_sa/gamesa_renderware.h | 2 + Client/game_sa/gamesa_renderware.hpp | 1 + .../deathmatch/logic/CClientSearchLight.cpp | 10 +- .../logic/luadefs/CLuaEngineDefs.cpp | 3 +- Client/sdk/game/CRenderWare.h | 1 + Client/sdk/game/RenderWare.h | 6 ++ 11 files changed, 130 insertions(+), 10 deletions(-) diff --git a/Client/core/Graphics/CRenderItemManager.TextureReplace.cpp b/Client/core/Graphics/CRenderItemManager.TextureReplace.cpp index 284de0fab1d..d89c0d29fe5 100644 --- a/Client/core/Graphics/CRenderItemManager.TextureReplace.cpp +++ b/Client/core/Graphics/CRenderItemManager.TextureReplace.cpp @@ -80,7 +80,7 @@ void CRenderItemManager::RemoveClientEntityRefs(CClientEntityBase* pClientEntity SShaderItemLayers* CRenderItemManager::GetAppliedShaderForD3DData(CD3DDUMMY* pD3DData) { // Save texture usage for later - MapInsert(m_FrameTextureUsage, pD3DData); + MapInsert(m_FrameTextureUsage, m_pRenderWare->ResolveD3DData(pD3DData)); return m_pRenderWare->GetAppliedShaderForD3DData(pD3DData); } diff --git a/Client/game_sa/CPointLightsSA.cpp b/Client/game_sa/CPointLightsSA.cpp index 8d0a22ecfa7..8aeda447f5b 100644 --- a/Client/game_sa/CPointLightsSA.cpp +++ b/Client/game_sa/CPointLightsSA.cpp @@ -12,6 +12,10 @@ #include "StdInc.h" #include "CPointLightsSA.h" #include "CEntitySA.h" +#include "CRenderWareSA.h" +#include "CGameSA.h" + +extern CGameSA* pGame; using CHeli_SearchLightCone_t = void(__cdecl*)(int handleId, CVector startPos, CVector endPos, float radius1, float unknownConstant, int unkown1, bool renderSpot, CVector* unkown3, CVector* unkown4, CVector* unknown5, int unknown6, float radius2); @@ -119,6 +123,11 @@ static void CSearchLight_RenderShadow(char type, void* texture, CVector* pos, fl // Get original color intensity multiplier float colorIntensity = static_cast(intensity) / 128.0f; + // The game draws the spot with the texture of the street lamp light pools, so a shader could not address + // one without the other. Give the searchlights a texture of their own. + if (RwTexture* pSpotTexture = pGame->GetRenderWareSA()->GetSearchLightSpotTexture()) + texture = pSpotTexture; + // CShadows::StoreShadowToBeRendered ((void(__cdecl*)(char, void*, CVector*, float, float, float, float, std::int16_t, unsigned char, unsigned char, unsigned char, float, bool, float, void*, bool))0x707390)(type, texture, pos, x1, y1, x2, y2, intensity, static_cast(searchLightColor.R * colorIntensity), diff --git a/Client/game_sa/CRenderWareSA.ShaderSupport.cpp b/Client/game_sa/CRenderWareSA.ShaderSupport.cpp index aa2927788c9..d0f233e1684 100644 --- a/Client/game_sa/CRenderWareSA.ShaderSupport.cpp +++ b/Client/game_sa/CRenderWareSA.ShaderSupport.cpp @@ -15,6 +15,7 @@ #include "CGameSA.h" #include "CRenderWareSA.ShaderMatching.h" #include "CRenderWareSA.ShaderSupport.h" +#include "gamesa_renderware.h" extern CCoreInterface* g_pCore; extern CGameSA* pGame; @@ -25,6 +26,8 @@ extern CGameSA* pGame; #define ADDR_CClothesBuilder_ConstructTextures_End 0x05A6520 #define ADDR_CVehicle_DoHeadLightBeam_RenderPrimitive 0x06E13CD #define ADDR_CHeli_SearchLightCone_RenderPrimitive 0x06C62AD +#define VAR_CShadows_gpShadowExplosionTex 0x0C403F4 +#define FAKE_NAME_SEARCHLIGHT_SPOT "shad_searchlight" #define ADDR_CWaterCannon_Render_RenderPrimitive 0x072956B enum @@ -36,7 +39,9 @@ enum RT_3DNI, }; -int CRenderWareSA::ms_iRenderingType = 0; +int CRenderWareSA::ms_iRenderingType = 0; +CD3DDUMMY* CRenderWareSA::ms_pNoTextureD3DData = FAKE_D3DTEXTURE_NO_TEXTURE; +RwTexture* CRenderWareSA::ms_pSearchLightSpotTexture = nullptr; //////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// @@ -428,6 +433,82 @@ void CRenderWareSA::SetRenderingClientEntity(CClientEntityBase* pClientEntity, u m_iRenderingEntityType = iTypeMask; } +//////////////////////////////////////////////////////////////// +// +// CRenderWareSA::GetSearchLightSpotTexture +// +// The ground spot of a searchlight is drawn by CShadows with the same texture as the +// light pools of street lamps. Give the searchlights a copy of that texture under a +// name of their own, so a shader can address the spots without touching the lamps +// while the spots look exactly as they do in the game. +// +//////////////////////////////////////////////////////////////// +RwTexture* CRenderWareSA::GetSearchLightSpotTexture() +{ + if (ms_pSearchLightSpotTexture) + return ms_pSearchLightSpotTexture; + + RwTexture* pSource = *reinterpret_cast(VAR_CShadows_gpShadowExplosionTex); + if (!pSource || !pSource->raster || !pSource->raster->renderResource) + return nullptr; + + // Read the game texture as plain BGRA rows (the pixels manager also decompresses DXT) + CPixelsManagerInterface* pPixelsManager = g_pCore->GetGraphics()->GetPixelsManager(); + CPixels sourcePixels; + uint uiWidth = 0, uiHeight = 0; + if (!pPixelsManager->GetTexturePixels(reinterpret_cast(pSource->raster->renderResource), sourcePixels) || + !pPixelsManager->GetPixelsSize(sourcePixels, uiWidth, uiHeight) || uiWidth == 0 || uiHeight == 0) + return nullptr; + + RwRaster* pRaster = RwRasterCreate(uiWidth, uiHeight, 32, RASTER_TYPE_TEXTURE | RASTER_FORMAT_8888); + if (!pRaster) + return nullptr; + + if (!RwRasterLock(pRaster, 0, RASTER_LOCK_WRITE) || !pRaster->pixels) + { + RwRasterDestroy(pRaster); + return nullptr; + } + + // The sdk struct calls it numLevels, but RenderWare keeps the row stride of the locked raster there + const uint uiSourcePitch = uiWidth * sizeof(DWORD); + int iStride = pRaster->numLevels; + if (iStride <= 0) + iStride = uiSourcePitch; + for (uint y = 0; y < uiHeight; y++) + memcpy(pRaster->pixels + y * iStride, sourcePixels.GetData() + y * uiSourcePitch, uiSourcePitch); + RwRasterUnlock(pRaster); + + RwTexture* pTexture = RwTextureCreate(pRaster); + if (!pTexture) + { + RwRasterDestroy(pRaster); + return nullptr; + } + strncpy(pTexture->name, FAKE_NAME_SEARCHLIGHT_SPOT, RW_TEXTURE_NAME_LENGTH - 1); + pTexture->flags = pSource->flags; + ms_pSearchLightSpotTexture = pTexture; + + // Make it known to the texture replacer under its own name + OnTextureStreamIn(CreateTexInfo(pTexture, FAKE_NAME_SEARCHLIGHT_SPOT, reinterpret_cast(pRaster->renderResource))); + return pTexture; +} + +//////////////////////////////////////////////////////////////// +// +// CRenderWareSA::ResolveD3DData +// +// Draws without a texture are keyed by a fake texture ('unnamed', or 'searchlight' for +// the helicopter searchlight cones) when doing a 3d model like render +// +//////////////////////////////////////////////////////////////// +CD3DDUMMY* CRenderWareSA::ResolveD3DData(CD3DDUMMY* pD3DData) +{ + if (pD3DData == nullptr && CRenderWareSA::ms_iRenderingType == RT_NONE) + return CRenderWareSA::ms_pNoTextureD3DData; + return pD3DData; +} + //////////////////////////////////////////////////////////////// // // CRenderWareSA::GetAppliedShaderForD3DData @@ -439,11 +520,7 @@ SShaderItemLayers* CRenderWareSA::GetAppliedShaderForD3DData(CD3DDUMMY* pD3DData { m_uiReplacementRequestCounter++; - // If rendering with no texture, and doing an 3d model like render, use the 'unnamed' texinfo - if (pD3DData == NULL && CRenderWareSA::ms_iRenderingType == RT_NONE) - pD3DData = FAKE_D3DTEXTURE_NO_TEXTURE; - - STexInfo* pTexInfo = MapFindRef(m_D3DDataTexInfoMap, pD3DData); + STexInfo* pTexInfo = MapFindRef(m_D3DDataTexInfoMap, ResolveD3DData(pD3DData)); if (!pTexInfo) return NULL; @@ -602,6 +679,12 @@ void CRenderWareSA::Initialize() STexInfo* pTexInfo = CreateTexInfo(FAKE_RWTEXTURE_NO_TEXTURE, FAKE_NAME_NO_TEXTURE, FAKE_D3DTEXTURE_NO_TEXTURE); OnTextureStreamIn(pTexInfo); } + if (!MapContains(m_D3DDataTexInfoMap, FAKE_D3DTEXTURE_SEARCHLIGHT)) + { + // Helicopter searchlight cones draw without a texture too, give them a name of their own + STexInfo* pTexInfo = CreateTexInfo(FAKE_RWTEXTURE_SEARCHLIGHT, FAKE_NAME_SEARCHLIGHT, FAKE_D3DTEXTURE_SEARCHLIGHT); + OnTextureStreamIn(pTexInfo); + } } //////////////////////////////////////////////////////////////// @@ -768,6 +851,9 @@ __declspec(noinline) void OnMY_RwIm3DRenderIndexedPrimitive_Pre(DWORD dwAddrCall dwAddrCalledFrom == ADDR_CWaterCannon_Render_RenderPrimitive) { CRenderWareSA::ms_iRenderingType = RT_NONE; // Treat these items like world models + // The searchlight cones get a texture name of their own, so a shader can target them without every other untextured draw + if (dwAddrCalledFrom == ADDR_CHeli_SearchLightCone_RenderPrimitive) + CRenderWareSA::ms_pNoTextureD3DData = FAKE_D3DTEXTURE_SEARCHLIGHT; } else { @@ -778,6 +864,7 @@ __declspec(noinline) void OnMY_RwIm3DRenderIndexedPrimitive_Pre(DWORD dwAddrCall __declspec(noinline) void OnMY_RwIm3DRenderIndexedPrimitive_Post(DWORD dwAddrCalledFrom) { CRenderWareSA::ms_iRenderingType = RT_NONE; + CRenderWareSA::ms_pNoTextureD3DData = FAKE_D3DTEXTURE_NO_TEXTURE; } // Hook info diff --git a/Client/game_sa/CRenderWareSA.ShaderSupport.h b/Client/game_sa/CRenderWareSA.ShaderSupport.h index a91f90044ac..19f596139c5 100644 --- a/Client/game_sa/CRenderWareSA.ShaderSupport.h +++ b/Client/game_sa/CRenderWareSA.ShaderSupport.h @@ -25,6 +25,9 @@ #define FAKE_D3DTEXTURE_NO_TEXTURE ((CD3DDUMMY*)-9) #define FAKE_RWTEXTURE_NO_TEXTURE ((RwTexture*)-10) #define FAKE_NAME_NO_TEXTURE "unnamed" +#define FAKE_D3DTEXTURE_SEARCHLIGHT ((CD3DDUMMY*)-11) +#define FAKE_RWTEXTURE_SEARCHLIGHT ((RwTexture*)-12) +#define FAKE_NAME_SEARCHLIGHT "searchlight" class CMatchChannel; class CMatchChannelManager; diff --git a/Client/game_sa/CRenderWareSA.h b/Client/game_sa/CRenderWareSA.h index 1fc217ad967..6112e19e0b5 100644 --- a/Client/game_sa/CRenderWareSA.h +++ b/Client/game_sa/CRenderWareSA.h @@ -101,6 +101,7 @@ class CRenderWareSA : public CRenderWare const char* GetTextureName(CD3DDUMMY* pD3DData); void SetRenderingClientEntity(CClientEntityBase* pClientEntity, ushort usModelId, int iTypeMask); SShaderItemLayers* GetAppliedShaderForD3DData(CD3DDUMMY* pD3DData); + CD3DDUMMY* ResolveD3DData(CD3DDUMMY* pD3DData); void AppendAdditiveMatch(CSHADERDUMMY* pShaderData, CClientEntityBase* pClientEntity, const char* strTextureNameMatch, float fShaderPriority, bool bShaderLayered, int iTypeMask, uint uiShaderCreateTime, bool bShaderUsesVertexShader, bool bAppendLayers); void AppendSubtractiveMatch(CSHADERDUMMY* pShaderData, CClientEntityBase* pClientEntity, const char* strTextureNameMatch); @@ -167,4 +168,7 @@ class CRenderWareSA : public CRenderWare bool m_bGTAVertexShadersEnabled; std::set m_SpecialTextures; static int ms_iRenderingType; + static CD3DDUMMY* ms_pNoTextureD3DData; // Fake texture used for draws without a texture + static RwTexture* ms_pSearchLightSpotTexture; + RwTexture* GetSearchLightSpotTexture(); }; diff --git a/Client/game_sa/gamesa_renderware.h b/Client/game_sa/gamesa_renderware.h index 224970c17cd..55b7ed42bee 100644 --- a/Client/game_sa/gamesa_renderware.h +++ b/Client/game_sa/gamesa_renderware.h @@ -98,6 +98,7 @@ typedef int(__cdecl* rwD3D9NativeTextureRead_t)(RwStream* stream, RwTexture** te typedef RwRaster*(__cdecl* RwRasterUnlock_t)(RwRaster* raster); typedef RwRaster*(__cdecl* RwRasterLock_t)(RwRaster* raster, unsigned char level, int lockmode); typedef RwRaster*(__cdecl* RwRasterCreate_t)(int width, int height, int depth, int flags); +typedef int(__cdecl* RwRasterDestroy_t)(RwRaster* raster); typedef RwTexture*(__cdecl* RwTextureCreate_t)(RwRaster* raster); typedef RpMaterial*(__cdecl* RpMaterialSetTexture_t)(RpMaterial* mat, RwTexture* tex); typedef RpHAnimHierarchy*(__cdecl* GetAnimHierarchyFromClump_t)(RpClump*); @@ -188,6 +189,7 @@ RWFUNC(RwTextureDestroy_t RwTextureDestroy, (RwTextureDestroy_t)0xDEAD) RWFUNC(RwRasterUnlock_t RwRasterUnlock, (RwRasterUnlock_t)0xDEAD) RWFUNC(RwRasterLock_t RwRasterLock, (RwRasterLock_t)0xDEAD) RWFUNC(RwRasterCreate_t RwRasterCreate, (RwRasterCreate_t)0xDEAD) +RWFUNC(RwRasterDestroy_t RwRasterDestroy, (RwRasterDestroy_t)0xDEAD) RWFUNC(RwTextureCreate_t RwTextureCreate, (RwTextureCreate_t)0xDEAD) RWFUNC(RpMaterialSetTexture_t RpMaterialSetTexture, (RpMaterialSetTexture_t)0xDEAD) RWFUNC(GetAnimHierarchyFromClump_t GetAnimHierarchyFromClump, (GetAnimHierarchyFromClump_t)0xDEAD) diff --git a/Client/game_sa/gamesa_renderware.hpp b/Client/game_sa/gamesa_renderware.hpp index 4f9bd21484c..14b04cf80cc 100644 --- a/Client/game_sa/gamesa_renderware.hpp +++ b/Client/game_sa/gamesa_renderware.hpp @@ -82,6 +82,7 @@ void InitRwFunctions() RwRasterUnlock = (RwRasterUnlock_t)0x007FAEC0; RwRasterLock = (RwRasterLock_t)0x007FB2D0; RwRasterCreate = (RwRasterCreate_t)0x007FB230; + RwRasterDestroy = (RwRasterDestroy_t)0x007FB020; RwTextureCreate = (RwTextureCreate_t)0x007F37C0; RpMaterialSetTexture = (RpMaterialSetTexture_t)0x0074DBC0; GetAnimHierarchyFromClump = (GetAnimHierarchyFromClump_t)0x734B10; diff --git a/Client/mods/deathmatch/logic/CClientSearchLight.cpp b/Client/mods/deathmatch/logic/CClientSearchLight.cpp index deece435bb1..5b3c8f8c222 100644 --- a/Client/mods/deathmatch/logic/CClientSearchLight.cpp +++ b/Client/mods/deathmatch/logic/CClientSearchLight.cpp @@ -32,6 +32,12 @@ void CClientSearchLight::Render() { DoAttaching(); - if (IsStreamedIn()) - g_pGame->GetPointLights()->RenderHeliLight(m_StartPosition, m_EndPosition, m_StartRadius, m_EndRadius, m_bRenderSpot, m_color); + if (!IsStreamedIn()) + return; + + // Tell the texture replacer which element the cone belongs to + CRenderWare* pRenderWare = g_pGame->GetRenderWare(); + pRenderWare->SetRenderingClientEntity(this, 0xFFFF, TYPE_MASK_OTHER); + g_pGame->GetPointLights()->RenderHeliLight(m_StartPosition, m_EndPosition, m_StartRadius, m_EndRadius, m_bRenderSpot, m_color); + pRenderWare->SetRenderingClientEntity(nullptr, 0xFFFF, TYPE_MASK_WORLD); } diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp index 44ea554c92d..11e85c89b95 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp @@ -1316,6 +1316,7 @@ static bool IsShaderTargetElement(CClientEntity* pElement) case CCLIENTBUILDING: case CCLIENTPICKUP: case CCLIENTPROJECTILE: + case CCLIENTSEARCHLIGHT: return true; default: return false; @@ -1326,7 +1327,7 @@ static void ReadShaderTargetElement(CScriptArgReader& argStream, CClientEntity*& { argStream.ReadUserData(pElement, nullptr); if (!argStream.HasErrors() && pElement && !IsShaderTargetElement(pElement)) - argStream.SetCustomError("targetElement must be a ped, player, vehicle, object, weapon, building, pickup or projectile", "Bad argument"); + argStream.SetCustomError("targetElement must be a ped, player, vehicle, object, weapon, building, pickup, projectile or searchlight", "Bad argument"); } int CLuaEngineDefs::EngineApplyShaderToWorldTexture(lua_State* luaVM) diff --git a/Client/sdk/game/CRenderWare.h b/Client/sdk/game/CRenderWare.h index 760fe299253..2db10c53849 100644 --- a/Client/sdk/game/CRenderWare.h +++ b/Client/sdk/game/CRenderWare.h @@ -107,6 +107,7 @@ class CRenderWare virtual void SetRenderingClientEntity(CClientEntityBase* pClientEntity, ushort usModelId, int iTypeMask) = 0; virtual SShaderItemLayers* GetAppliedShaderForD3DData(CD3DDUMMY* pD3DData) = 0; + virtual CD3DDUMMY* ResolveD3DData(CD3DDUMMY* pD3DData) = 0; virtual void AppendAdditiveMatch(CSHADERDUMMY* pShaderData, CClientEntityBase* pClientEntity, const char* strTextureNameMatch, float fShaderPriority, bool bShaderLayered, int iTypeMask, uint uiShaderCreateTime, bool bShaderUsesVertexShader, bool bAppendLayers) = 0; virtual void AppendSubtractiveMatch(CSHADERDUMMY* pShaderData, CClientEntityBase* pClientEntity, const char* strTextureNameMatch) = 0; diff --git a/Client/sdk/game/RenderWare.h b/Client/sdk/game/RenderWare.h index c0a4257b059..6ff56c98df0 100644 --- a/Client/sdk/game/RenderWare.h +++ b/Client/sdk/game/RenderWare.h @@ -173,6 +173,12 @@ enum RwRasterLockFlags RASTER_LOCK_READ = 2, RASTER_LOCK_LAST = RW_STRUCT_ALIGN }; +enum RwRasterFlags +{ + RASTER_TYPE_TEXTURE = 0x04, + RASTER_FORMAT_8888 = 0x0500, + RASTER_FLAGS_LAST = RW_STRUCT_ALIGN +}; enum RwTransformOrder { TRANSFORM_INITIAL = 0, From 1e35369f162cc6a216644133efd06016845f2aa5 Mon Sep 17 00:00:00 2001 From: Flashmyname Date: Sat, 5 Sep 2026 18:39:51 +0200 Subject: [PATCH 3/4] Allow blips as engineApplyShaderToWorldTexture targets Radar blips are drawn one sprite at a time by CRadar::DrawCoordBlip with no game entity behind them. Hook both call sites, the regular pass and the one for the waypoint icon, and tag the draw through a game marker to element map kept by CClientRadarMarkerManager. Not covered: icons 0 and 1, which are drawn without a texture, blips the game creates itself, and the F11 map, which has its own textures. Part of #1111. --- Client/game_sa/CRadarSA.cpp | 7 ++ Client/game_sa/CRadarSA.h | 1 + Client/mods/deathmatch/logic/CClientGame.cpp | 19 ++++++ Client/mods/deathmatch/logic/CClientGame.h | 1 + .../deathmatch/logic/CClientRadarMarker.cpp | 2 + .../logic/CClientRadarMarkerManager.cpp | 7 ++ .../logic/CClientRadarMarkerManager.h | 9 +++ .../logic/luadefs/CLuaEngineDefs.cpp | 4 +- Client/multiplayer_sa/CMultiplayerSA.h | 1 + .../CMultiplayerSA_Rendering.cpp | 65 +++++++++++++++++++ Client/sdk/game/CRadar.h | 1 + Client/sdk/multiplayer/CMultiplayer.h | 2 + 12 files changed, 118 insertions(+), 1 deletion(-) diff --git a/Client/game_sa/CRadarSA.cpp b/Client/game_sa/CRadarSA.cpp index 1ae2a6a9cc9..ab89b290f9f 100644 --- a/Client/game_sa/CRadarSA.cpp +++ b/Client/game_sa/CRadarSA.cpp @@ -57,6 +57,13 @@ CMarker* CRadarSA::GetFreeMarker() return Markers[Index]; } +CMarker* CRadarSA::GetMarker(int iIndex) +{ + if (iIndex < 0 || iIndex >= MAX_MARKERS) + return nullptr; + return Markers[iIndex]; +} + void CRadarSA::DrawAreaOnRadar(float fX1, float fY1, float fX2, float fY2, const SharedUtil::SColor color) { // Convert color to required abgr at the last moment diff --git a/Client/game_sa/CRadarSA.h b/Client/game_sa/CRadarSA.h index d060b0b9152..d3ad40aa8ca 100644 --- a/Client/game_sa/CRadarSA.h +++ b/Client/game_sa/CRadarSA.h @@ -27,5 +27,6 @@ class CRadarSA : public CRadar ~CRadarSA(); CMarker* CreateMarker(CVector* vecPosition); CMarker* GetFreeMarker(); + CMarker* GetMarker(int iIndex); void DrawAreaOnRadar(float fX1, float fY1, float fX2, float fY2, const SharedUtil::SColor color); }; diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index cc0d543fac8..1d0eb819c5b 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -313,6 +314,7 @@ CClientGame::CClientGame(bool bLocalPlay) : m_ServerInfo(new CServerInfo()) g_pMultiplayer->SetGameModelRemoveHandler(CClientGame::StaticGameModelRemoveHandler); g_pMultiplayer->SetGameRunNamedAnimDestructorHandler(CClientGame::StaticGameRunNamedAnimDestructorHandler); g_pMultiplayer->SetGameEntityRenderHandler(CClientGame::StaticGameEntityRenderHandler); + g_pMultiplayer->SetRadarBlipRenderHandler(CClientGame::StaticRadarBlipRenderHandler); g_pMultiplayer->SetFxSystemDestructionHandler(CClientGame::StaticFxSystemDestructionHandler); g_pMultiplayer->SetDrivebyAnimationHandler(CClientGame::StaticDrivebyAnimationHandler); g_pMultiplayer->SetPedStepHandler(CClientGame::StaticPedStepHandler); @@ -528,6 +530,7 @@ CClientGame::~CClientGame() g_pMultiplayer->SetGameModelRemoveHandler(NULL); g_pMultiplayer->SetGameRunNamedAnimDestructorHandler(nullptr); g_pMultiplayer->SetGameEntityRenderHandler(NULL); + g_pMultiplayer->SetRadarBlipRenderHandler(nullptr); g_pMultiplayer->SetDrivebyAnimationHandler(nullptr); g_pMultiplayer->SetPedStepHandler(nullptr); g_pMultiplayer->SetVehicleWeaponHitHandler(nullptr); @@ -3804,6 +3807,22 @@ void CClientGame::StaticGameEntityRenderHandler(CEntitySAInterface* pGameEntity) g_pGame->GetRenderWare()->SetRenderingClientEntity(NULL, 0xFFFF, TYPE_MASK_WORLD); } +void CClientGame::StaticRadarBlipRenderHandler(int iBlipIndex) +{ + if (iBlipIndex >= 0) + { + CMarker* pGameMarker = g_pGame->GetRadar()->GetMarker(iBlipIndex); + CClientRadarMarker* pBlip = g_pClientGame->GetManager()->GetRadarMarkerManager()->GetMarkerByGameMarker(pGameMarker); + if (pBlip) + { + g_pGame->GetRenderWare()->SetRenderingClientEntity(pBlip, 0xFFFF, TYPE_MASK_OTHER); + return; + } + } + + g_pGame->GetRenderWare()->SetRenderingClientEntity(nullptr, 0xFFFF, TYPE_MASK_WORLD); +} + void CClientGame::StaticTaskSimpleBeHitHandler(CPedSAInterface* pPedAttacker, ePedPieceTypes hitBodyPart, int hitBodySide, int weaponId) { g_pClientGame->TaskSimpleBeHitHandler(pPedAttacker, hitBodyPart, hitBodySide, weaponId); diff --git a/Client/mods/deathmatch/logic/CClientGame.h b/Client/mods/deathmatch/logic/CClientGame.h index f010e6d0c57..cc978a6ada7 100644 --- a/Client/mods/deathmatch/logic/CClientGame.h +++ b/Client/mods/deathmatch/logic/CClientGame.h @@ -611,6 +611,7 @@ class CClientGame static void StaticGameRunNamedAnimDestructorHandler(class CTaskSimpleRunNamedAnimSAInterface* pTask); static bool StaticWorldSoundHandler(const SWorldSoundEvent& event); static void StaticGameEntityRenderHandler(CEntitySAInterface* pEntity); + static void StaticRadarBlipRenderHandler(int iBlipIndex); static void StaticTaskSimpleBeHitHandler(CPedSAInterface* pPedAttacker, ePedPieceTypes hitBodyPart, int hitBodySide, int weaponId); static void StaticFxSystemDestructionHandler(void* pFxSAInterface); static void StaticPedStepHandler(CPedSAInterface* pPed, bool bFoot); diff --git a/Client/mods/deathmatch/logic/CClientRadarMarker.cpp b/Client/mods/deathmatch/logic/CClientRadarMarker.cpp index be13f67b3ff..5f48e73ff2c 100644 --- a/Client/mods/deathmatch/logic/CClientRadarMarker.cpp +++ b/Client/mods/deathmatch/logic/CClientRadarMarker.cpp @@ -227,6 +227,7 @@ void CClientRadarMarker::CreateMarker() m_pMarker->SetScale(m_usScale); m_pMarker->SetColor(color); m_pMarker->SetSprite(static_cast(m_ulSprite)); + m_pRadarMarkerManager->RegisterGameMarker(m_pMarker, this); } } } @@ -236,6 +237,7 @@ void CClientRadarMarker::DestroyMarker() { if (m_pMarker) { + m_pRadarMarkerManager->UnregisterGameMarker(m_pMarker); m_pMarker->Remove(); m_pMarker = NULL; } diff --git a/Client/mods/deathmatch/logic/CClientRadarMarkerManager.cpp b/Client/mods/deathmatch/logic/CClientRadarMarkerManager.cpp index e7f00c96cf0..6c965482bd1 100644 --- a/Client/mods/deathmatch/logic/CClientRadarMarkerManager.cpp +++ b/Client/mods/deathmatch/logic/CClientRadarMarkerManager.cpp @@ -54,9 +54,16 @@ void CClientRadarMarkerManager::DeleteAll() // Clear the list m_Markers.clear(); + m_GameMarkerMap.clear(); m_bCanRemoveFromList = true; } +CClientRadarMarker* CClientRadarMarkerManager::GetMarkerByGameMarker(const CMarker* pGameMarker) const +{ + auto iter = m_GameMarkerMap.find(pGameMarker); + return iter != m_GameMarkerMap.end() ? iter->second : nullptr; +} + CClientRadarMarker* CClientRadarMarkerManager::Get(ElementID ID) { // Grab the element with the given id. Check its type. diff --git a/Client/mods/deathmatch/logic/CClientRadarMarkerManager.h b/Client/mods/deathmatch/logic/CClientRadarMarkerManager.h index 8fd0450ca22..9e7eef6690d 100644 --- a/Client/mods/deathmatch/logic/CClientRadarMarkerManager.h +++ b/Client/mods/deathmatch/logic/CClientRadarMarkerManager.h @@ -14,6 +14,9 @@ class CClientRadarMarkerManager; #include "CClientRadarMarker.h" #include +#include + +class CMarker; class CClientRadarMarkerManager { @@ -38,9 +41,13 @@ class CClientRadarMarkerManager bool Exists(CClientRadarMarker* pMarker); static bool IsValidIcon(unsigned long ulIcon) noexcept { return ulIcon <= RADAR_MARKER_LIMIT; } + CClientRadarMarker* GetMarkerByGameMarker(const CMarker* pGameMarker) const; + private: void AddToList(CClientRadarMarker* pMarker) { m_Markers.push_back(pMarker); }; void RemoveFromList(CClientRadarMarker* pMarker); + void RegisterGameMarker(const CMarker* pGameMarker, CClientRadarMarker* pMarker) { m_GameMarkerMap[pGameMarker] = pMarker; } + void UnregisterGameMarker(const CMarker* pGameMarker) { m_GameMarkerMap.erase(pGameMarker); } void OrderMarkers(); static bool CompareOrderingIndex(CClientRadarMarker* p1, CClientRadarMarker* p2); @@ -49,6 +56,8 @@ class CClientRadarMarkerManager bool m_bCanRemoveFromList; std::list m_Markers; + std::unordered_map m_GameMarkerMap; + unsigned short m_usDimension; bool m_bOrderOnPulse; CVector m_vecCameraPosition; diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp index 11e85c89b95..5d0ced42ac0 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp @@ -1317,6 +1317,7 @@ static bool IsShaderTargetElement(CClientEntity* pElement) case CCLIENTPICKUP: case CCLIENTPROJECTILE: case CCLIENTSEARCHLIGHT: + case CCLIENTRADARMARKER: return true; default: return false; @@ -1327,7 +1328,8 @@ static void ReadShaderTargetElement(CScriptArgReader& argStream, CClientEntity*& { argStream.ReadUserData(pElement, nullptr); if (!argStream.HasErrors() && pElement && !IsShaderTargetElement(pElement)) - argStream.SetCustomError("targetElement must be a ped, player, vehicle, object, weapon, building, pickup, projectile or searchlight", "Bad argument"); + argStream.SetCustomError("targetElement must be a ped, player, vehicle, object, weapon, building, pickup, projectile, searchlight or blip", + "Bad argument"); } int CLuaEngineDefs::EngineApplyShaderToWorldTexture(lua_State* luaVM) diff --git a/Client/multiplayer_sa/CMultiplayerSA.h b/Client/multiplayer_sa/CMultiplayerSA.h index 9f85b307909..5ce965d8fe8 100644 --- a/Client/multiplayer_sa/CMultiplayerSA.h +++ b/Client/multiplayer_sa/CMultiplayerSA.h @@ -148,6 +148,7 @@ class CMultiplayerSA : public CMultiplayer void SetGameModelRemoveHandler(GameModelRemoveHandler* pHandler); void SetGameRunNamedAnimDestructorHandler(GameRunNamedAnimDestructorHandler* pHandler); void SetGameEntityRenderHandler(GameEntityRenderHandler* pHandler); + void SetRadarBlipRenderHandler(RadarBlipRenderHandler* pHandler); void SetFxSystemDestructionHandler(FxSystemDestructionHandler* pHandler); void SetDrivebyAnimationHandler(DrivebyAnimationHandler* pHandler); void SetPedStepHandler(PedStepHandler* pHandler); diff --git a/Client/multiplayer_sa/CMultiplayerSA_Rendering.cpp b/Client/multiplayer_sa/CMultiplayerSA_Rendering.cpp index 77c49d8de2b..63f76dfd5c4 100644 --- a/Client/multiplayer_sa/CMultiplayerSA_Rendering.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA_Rendering.cpp @@ -12,6 +12,7 @@ #include extern CCoreInterface* g_pCore; GameEntityRenderHandler* pGameEntityRenderHandler = nullptr; +RadarBlipRenderHandler* pRadarBlipRenderHandler = nullptr; PreRenderSkyHandler* pPreRenderSkyHandlerHandler = nullptr; RenderHeliLightHandler* pRenderHeliLightHandler = nullptr; RenderEverythingBarRoadsHandler* pRenderEverythingBarRoadsHandler = nullptr; @@ -733,6 +734,57 @@ static void __declspec(naked) HOOK_RenderEffects_HeliLight() // clang-format on } +////////////////////////////////////////////////////////////////////////////////////////// +// +// CRadar::DrawBlips +// +// Detect radar blip rendering +// +////////////////////////////////////////////////////////////////////////////////////////// +void OnMY_CRadar_DrawCoordBlip_Pre(int iBlipIndex) +{ + if (pRadarBlipRenderHandler) + pRadarBlipRenderHandler(iBlipIndex); +} + +void OnMY_CRadar_DrawCoordBlip_Post() +{ + if (pRadarBlipRenderHandler) + pRadarBlipRenderHandler(-1); +} + +// Hook info +#define HOOKPOS_CRadar_DrawBlips_CoordBlip 0x588347 +#define HOOKPOS_CRadar_DrawBlips_CoordBlipWaypoint 0x588411 // second pass for the waypoint icon +#define FUNC_CRadar_DrawCoordBlip 0x586D60 +static void __declspec(naked) HOOK_CRadar_DrawCoordBlip() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + pushad + push dword ptr [esp+24h] + call OnMY_CRadar_DrawCoordBlip_Pre + add esp, 4*1 + popad + + // Original call with its arguments + push dword ptr [esp+8] + push dword ptr [esp+8] + mov eax, FUNC_CRadar_DrawCoordBlip + call eax + add esp, 4*2 + + pushad + call OnMY_CRadar_DrawCoordBlip_Post + popad + retn + } + // clang-format on +} + ////////////////////////////////////////////////////////////////////////////////////////// // // CMultiplayerSA::SetGameEntityRenderHandler @@ -744,6 +796,17 @@ void CMultiplayerSA::SetGameEntityRenderHandler(GameEntityRenderHandler* pHandle pGameEntityRenderHandler = pHandler; } +////////////////////////////////////////////////////////////////////////////////////////// +// +// CMultiplayerSA::SetRadarBlipRenderHandler +// +// +////////////////////////////////////////////////////////////////////////////////////////// +void CMultiplayerSA::SetRadarBlipRenderHandler(RadarBlipRenderHandler* pHandler) +{ + pRadarBlipRenderHandler = pHandler; +} + ////////////////////////////////////////////////////////////////////////////////////////// // // CMultiplayerSA::SetPreRenderSkyHandler @@ -962,4 +1025,6 @@ void CMultiplayerSA::InitHooks_Rendering() EZHookInstallChecked(RwCameraSetNearClipPlane); EZHookInstall(RenderEffects_HeliLight); EZHookInstall(CRenderer_EverythingBarRoads); + HookInstallCall(HOOKPOS_CRadar_DrawBlips_CoordBlip, (DWORD)HOOK_CRadar_DrawCoordBlip); + HookInstallCall(HOOKPOS_CRadar_DrawBlips_CoordBlipWaypoint, (DWORD)HOOK_CRadar_DrawCoordBlip); } diff --git a/Client/sdk/game/CRadar.h b/Client/sdk/game/CRadar.h index 4a373c33442..fd5e860658c 100644 --- a/Client/sdk/game/CRadar.h +++ b/Client/sdk/game/CRadar.h @@ -21,5 +21,6 @@ class CRadar public: virtual CMarker* CreateMarker(CVector* vecPosition) = 0; virtual CMarker* GetFreeMarker() = 0; + virtual CMarker* GetMarker(int iIndex) = 0; virtual void DrawAreaOnRadar(float fX1, float fY1, float fX2, float fY2, const SharedUtil::SColor color) = 0; }; diff --git a/Client/sdk/multiplayer/CMultiplayer.h b/Client/sdk/multiplayer/CMultiplayer.h index 28d2e074088..eb1e031b414 100644 --- a/Client/sdk/multiplayer/CMultiplayer.h +++ b/Client/sdk/multiplayer/CMultiplayer.h @@ -132,6 +132,7 @@ typedef void(GameProjectileDestructHandler)(CEntitySAInterface* pProjectile); typedef void(GameModelRemoveHandler)(ushort usModelId); typedef void(GameRunNamedAnimDestructorHandler)(class CTaskSimpleRunNamedAnimSAInterface* pTask); typedef void(GameEntityRenderHandler)(CEntitySAInterface* pEntity); +typedef void(RadarBlipRenderHandler)(int iBlipIndex); typedef void(FxSystemDestructionHandler)(void* pFxSA); typedef AnimationId(DrivebyAnimationHandler)(AnimationId animGroup, AssocGroupId animId); typedef void(PedStepHandler)(CPedSAInterface* pPed, bool bFoot); @@ -258,6 +259,7 @@ class CMultiplayer virtual void SetGameModelRemoveHandler(GameModelRemoveHandler* pHandler) = 0; virtual void SetGameRunNamedAnimDestructorHandler(GameRunNamedAnimDestructorHandler* pHandler) = 0; virtual void SetGameEntityRenderHandler(GameEntityRenderHandler* pHandler) = 0; + virtual void SetRadarBlipRenderHandler(RadarBlipRenderHandler* pHandler) = 0; virtual void SetFxSystemDestructionHandler(FxSystemDestructionHandler* pHandler) = 0; virtual void SetDrivebyAnimationHandler(DrivebyAnimationHandler* pHandler) = 0; virtual void SetPedStepHandler(PedStepHandler* pHandler) = 0; From 479761888671d85dde3b96ece3d52340b204fd2f Mon Sep 17 00:00:00 2001 From: Flashmyname Date: Sat, 5 Sep 2026 18:39:52 +0200 Subject: [PATCH 4/4] Allow markers as engineApplyShaderToWorldTexture targets Markers have no game entity behind them either. Cylinders, arrows, rings and the checkpoint tube are C3dMarker objects, corona markers are registered coronas, and both carry the identifier MTA gave them, so hook the per-marker draw in the two render loops and resolve that identifier back to the element. The same corona hook, together with one on CHeli::RenderAllHeliSearchLights, covers the searchlight of a game helicopter, which is registered with the vehicle pointer plus 0xB as its id. Part of #1111. --- .../mods/deathmatch/logic/CClient3DMarker.cpp | 2 + .../deathmatch/logic/CClientCheckpoint.cpp | 6 + .../mods/deathmatch/logic/CClientCorona.cpp | 2 + Client/mods/deathmatch/logic/CClientGame.cpp | 31 ++++ Client/mods/deathmatch/logic/CClientGame.h | 1 + .../deathmatch/logic/CClientMarkerManager.cpp | 7 + .../deathmatch/logic/CClientMarkerManager.h | 8 ++ .../logic/luadefs/CLuaEngineDefs.cpp | 3 +- Client/multiplayer_sa/CMultiplayerSA.h | 1 + .../CMultiplayerSA_Rendering.cpp | 135 ++++++++++++++++++ Client/sdk/multiplayer/CMultiplayer.h | 2 + 11 files changed, 197 insertions(+), 1 deletion(-) diff --git a/Client/mods/deathmatch/logic/CClient3DMarker.cpp b/Client/mods/deathmatch/logic/CClient3DMarker.cpp index 0ceeb75d200..b1852da6be2 100644 --- a/Client/mods/deathmatch/logic/CClient3DMarker.cpp +++ b/Client/mods/deathmatch/logic/CClient3DMarker.cpp @@ -25,10 +25,12 @@ CClient3DMarker::CClient3DMarker(CClientMarker* pThis) m_dwType = static_cast(T3DMarkerType::MARKER3D_CYLINDER2); m_pMarker = NULL; m_ulIdentifier = (DWORD)this; + g_pClientGame->GetManager()->GetMarkerManager()->RegisterIdentifier(m_ulIdentifier, pThis); } CClient3DMarker::~CClient3DMarker() { + g_pClientGame->GetManager()->GetMarkerManager()->UnregisterIdentifier(m_ulIdentifier); } unsigned long CClient3DMarker::Get3DMarkerType() diff --git a/Client/mods/deathmatch/logic/CClientCheckpoint.cpp b/Client/mods/deathmatch/logic/CClientCheckpoint.cpp index 1a523ebd680..1bc327ad137 100644 --- a/Client/mods/deathmatch/logic/CClientCheckpoint.cpp +++ b/Client/mods/deathmatch/logic/CClientCheckpoint.cpp @@ -20,6 +20,7 @@ CClientCheckpoint::CClientCheckpoint(CClientMarker* pThis) // Init m_pThis = pThis; m_pCheckpoint = NULL; + m_dwIdentifier = 0; m_bStreamedIn = false; m_bVisible = true; m_uiIcon = CClientCheckpoint::ICON_NONE; @@ -37,6 +38,8 @@ CClientCheckpoint::~CClientCheckpoint() { // Eventually destroy the checkpoint Destroy(); + if (m_dwIdentifier) + g_pClientGame->GetManager()->GetMarkerManager()->UnregisterIdentifier(m_dwIdentifier); CClientEntityRefManager::RemoveEntityRefs(0, &m_pThis, NULL); } @@ -350,12 +353,15 @@ void CClientCheckpoint::Create(unsigned long ulIdentifier) if (ulIdentifier == 0) { s_ulIdentifier++; + if (m_dwIdentifier) + g_pClientGame->GetManager()->GetMarkerManager()->UnregisterIdentifier(m_dwIdentifier); m_dwIdentifier = s_ulIdentifier; } else { m_dwIdentifier = ulIdentifier; } + g_pClientGame->GetManager()->GetMarkerManager()->RegisterIdentifier(m_dwIdentifier, m_pThis); // Create it m_pCheckpoint = diff --git a/Client/mods/deathmatch/logic/CClientCorona.cpp b/Client/mods/deathmatch/logic/CClientCorona.cpp index c985fc20780..da90e65a34c 100644 --- a/Client/mods/deathmatch/logic/CClientCorona.cpp +++ b/Client/mods/deathmatch/logic/CClientCorona.cpp @@ -28,6 +28,7 @@ CClientCorona::CClientCorona(CClientMarker* pThis) // Pick an unique identifier static unsigned long ulIdentifier = 0xFFFFFFFF; m_ulIdentifier = --ulIdentifier; + g_pClientGame->GetManager()->GetMarkerManager()->RegisterIdentifier(m_ulIdentifier, pThis); } CClientCorona::~CClientCorona() @@ -38,6 +39,7 @@ CClientCorona::~CClientCorona() { pCorona->Disable(); } + g_pClientGame->GetManager()->GetMarkerManager()->UnregisterIdentifier(m_ulIdentifier); CClientEntityRefManager::RemoveEntityRefs(0, &m_pThis, NULL); } diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 1d0eb819c5b..f5cb50086e1 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -315,6 +315,7 @@ CClientGame::CClientGame(bool bLocalPlay) : m_ServerInfo(new CServerInfo()) g_pMultiplayer->SetGameRunNamedAnimDestructorHandler(CClientGame::StaticGameRunNamedAnimDestructorHandler); g_pMultiplayer->SetGameEntityRenderHandler(CClientGame::StaticGameEntityRenderHandler); g_pMultiplayer->SetRadarBlipRenderHandler(CClientGame::StaticRadarBlipRenderHandler); + g_pMultiplayer->SetMarkerRenderHandler(CClientGame::StaticMarkerRenderHandler); g_pMultiplayer->SetFxSystemDestructionHandler(CClientGame::StaticFxSystemDestructionHandler); g_pMultiplayer->SetDrivebyAnimationHandler(CClientGame::StaticDrivebyAnimationHandler); g_pMultiplayer->SetPedStepHandler(CClientGame::StaticPedStepHandler); @@ -531,6 +532,7 @@ CClientGame::~CClientGame() g_pMultiplayer->SetGameRunNamedAnimDestructorHandler(nullptr); g_pMultiplayer->SetGameEntityRenderHandler(NULL); g_pMultiplayer->SetRadarBlipRenderHandler(nullptr); + g_pMultiplayer->SetMarkerRenderHandler(nullptr); g_pMultiplayer->SetDrivebyAnimationHandler(nullptr); g_pMultiplayer->SetPedStepHandler(nullptr); g_pMultiplayer->SetVehicleWeaponHitHandler(nullptr); @@ -3823,6 +3825,35 @@ void CClientGame::StaticRadarBlipRenderHandler(int iBlipIndex) g_pGame->GetRenderWare()->SetRenderingClientEntity(nullptr, 0xFFFF, TYPE_MASK_WORLD); } +// CHeli::ProcessControl registers the searchlight cone and corona of a helicopter with the vehicle pointer plus this +static constexpr unsigned long HELI_SEARCHLIGHT_IDENTIFIER_OFFSET = 0xB; + +void CClientGame::StaticMarkerRenderHandler(unsigned long ulIdentifier) +{ + if (ulIdentifier) + { + CClientEntity* pEntity = g_pClientGame->GetManager()->GetMarkerManager()->GetEntityByIdentifier(ulIdentifier); + if (pEntity) + { + g_pGame->GetRenderWare()->SetRenderingClientEntity(pEntity, 0xFFFF, TYPE_MASK_OTHER); + return; + } + + // The pool lookup rounds to the containing slot, so make sure the identifier really points at a vehicle + const unsigned long ulVehicleInterface = ulIdentifier - HELI_SEARCHLIGHT_IDENTIFIER_OFFSET; + CClientEntity* pVehicle = g_pGame->GetPools()->GetClientEntity(reinterpret_cast(ulVehicleInterface)); + bool bExact = pVehicle && pVehicle->GetType() == CCLIENTVEHICLE && pVehicle->GetGameEntity() && + reinterpret_cast(pVehicle->GetGameEntity()->GetInterface()) == ulVehicleInterface; + if (bExact) + { + g_pGame->GetRenderWare()->SetRenderingClientEntity(pVehicle, static_cast(pVehicle)->GetModel(), TYPE_MASK_VEHICLE); + return; + } + } + + g_pGame->GetRenderWare()->SetRenderingClientEntity(nullptr, 0xFFFF, TYPE_MASK_WORLD); +} + void CClientGame::StaticTaskSimpleBeHitHandler(CPedSAInterface* pPedAttacker, ePedPieceTypes hitBodyPart, int hitBodySide, int weaponId) { g_pClientGame->TaskSimpleBeHitHandler(pPedAttacker, hitBodyPart, hitBodySide, weaponId); diff --git a/Client/mods/deathmatch/logic/CClientGame.h b/Client/mods/deathmatch/logic/CClientGame.h index cc978a6ada7..1663ac69581 100644 --- a/Client/mods/deathmatch/logic/CClientGame.h +++ b/Client/mods/deathmatch/logic/CClientGame.h @@ -612,6 +612,7 @@ class CClientGame static bool StaticWorldSoundHandler(const SWorldSoundEvent& event); static void StaticGameEntityRenderHandler(CEntitySAInterface* pEntity); static void StaticRadarBlipRenderHandler(int iBlipIndex); + static void StaticMarkerRenderHandler(unsigned long ulIdentifier); static void StaticTaskSimpleBeHitHandler(CPedSAInterface* pPedAttacker, ePedPieceTypes hitBodyPart, int hitBodySide, int weaponId); static void StaticFxSystemDestructionHandler(void* pFxSAInterface); static void StaticPedStepHandler(CPedSAInterface* pPed, bool bFoot); diff --git a/Client/mods/deathmatch/logic/CClientMarkerManager.cpp b/Client/mods/deathmatch/logic/CClientMarkerManager.cpp index 42b9059b276..b9baf3dadf4 100644 --- a/Client/mods/deathmatch/logic/CClientMarkerManager.cpp +++ b/Client/mods/deathmatch/logic/CClientMarkerManager.cpp @@ -49,6 +49,13 @@ void CClientMarkerManager::DeleteAll() // Clear the list m_Markers.clear(); + m_IdentifierMap.clear(); +} + +CClientEntity* CClientMarkerManager::GetEntityByIdentifier(unsigned long ulIdentifier) const +{ + auto iter = m_IdentifierMap.find(ulIdentifier); + return iter != m_IdentifierMap.end() ? iter->second : nullptr; } void CClientMarkerManager::DoPulse() diff --git a/Client/mods/deathmatch/logic/CClientMarkerManager.h b/Client/mods/deathmatch/logic/CClientMarkerManager.h index fd58c823ac3..6d072b8c074 100644 --- a/Client/mods/deathmatch/logic/CClientMarkerManager.h +++ b/Client/mods/deathmatch/logic/CClientMarkerManager.h @@ -12,6 +12,7 @@ #include "CClientMarker.h" #include +#include class CClientMarkerManager { @@ -25,6 +26,11 @@ class CClientMarkerManager void DeleteAll(); static bool IsMarkerModel(unsigned short usModel); + // Game marker and corona identifiers -> owning element (markers and searchlights) + CClientEntity* GetEntityByIdentifier(unsigned long ulIdentifier) const; + void RegisterIdentifier(unsigned long ulIdentifier, CClientEntity* pEntity) { m_IdentifierMap[ulIdentifier] = pEntity; } + void UnregisterIdentifier(unsigned long ulIdentifier) { m_IdentifierMap.erase(ulIdentifier); } + private: CClientMarkerManager(class CClientManager* pManager); ~CClientMarkerManager(); @@ -39,4 +45,6 @@ class CClientMarkerManager class CClientManager* m_pManager; CFastList m_Markers; bool m_bCanRemoveFromList; + + std::unordered_map m_IdentifierMap; }; diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp index 5d0ced42ac0..4d100ad1fdf 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp @@ -1318,6 +1318,7 @@ static bool IsShaderTargetElement(CClientEntity* pElement) case CCLIENTPROJECTILE: case CCLIENTSEARCHLIGHT: case CCLIENTRADARMARKER: + case CCLIENTMARKER: return true; default: return false; @@ -1328,7 +1329,7 @@ static void ReadShaderTargetElement(CScriptArgReader& argStream, CClientEntity*& { argStream.ReadUserData(pElement, nullptr); if (!argStream.HasErrors() && pElement && !IsShaderTargetElement(pElement)) - argStream.SetCustomError("targetElement must be a ped, player, vehicle, object, weapon, building, pickup, projectile, searchlight or blip", + argStream.SetCustomError("targetElement must be a ped, player, vehicle, object, weapon, building, pickup, projectile, searchlight, blip or marker", "Bad argument"); } diff --git a/Client/multiplayer_sa/CMultiplayerSA.h b/Client/multiplayer_sa/CMultiplayerSA.h index 5ce965d8fe8..0f1e887c741 100644 --- a/Client/multiplayer_sa/CMultiplayerSA.h +++ b/Client/multiplayer_sa/CMultiplayerSA.h @@ -149,6 +149,7 @@ class CMultiplayerSA : public CMultiplayer void SetGameRunNamedAnimDestructorHandler(GameRunNamedAnimDestructorHandler* pHandler); void SetGameEntityRenderHandler(GameEntityRenderHandler* pHandler); void SetRadarBlipRenderHandler(RadarBlipRenderHandler* pHandler); + void SetMarkerRenderHandler(MarkerRenderHandler* pHandler); void SetFxSystemDestructionHandler(FxSystemDestructionHandler* pHandler); void SetDrivebyAnimationHandler(DrivebyAnimationHandler* pHandler); void SetPedStepHandler(PedStepHandler* pHandler); diff --git a/Client/multiplayer_sa/CMultiplayerSA_Rendering.cpp b/Client/multiplayer_sa/CMultiplayerSA_Rendering.cpp index 63f76dfd5c4..bc6deb4c2c9 100644 --- a/Client/multiplayer_sa/CMultiplayerSA_Rendering.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA_Rendering.cpp @@ -13,6 +13,7 @@ extern CCoreInterface* g_pCore; GameEntityRenderHandler* pGameEntityRenderHandler = nullptr; RadarBlipRenderHandler* pRadarBlipRenderHandler = nullptr; +MarkerRenderHandler* pMarkerRenderHandler = nullptr; PreRenderSkyHandler* pPreRenderSkyHandlerHandler = nullptr; RenderHeliLightHandler* pRenderHeliLightHandler = nullptr; RenderEverythingBarRoadsHandler* pRenderEverythingBarRoadsHandler = nullptr; @@ -785,6 +786,126 @@ static void __declspec(naked) HOOK_CRadar_DrawCoordBlip() // clang-format on } +////////////////////////////////////////////////////////////////////////////////////////// +// +// C3dMarkers::Render +// +// Detect 3D marker rendering (cylinder, arrow, ring and checkpoint geometry) +// +////////////////////////////////////////////////////////////////////////////////////////// +void OnMY_MarkerRender_Pre(DWORD dwIdentifier) +{ + if (pMarkerRenderHandler) + pMarkerRenderHandler(dwIdentifier); +} + +void OnMY_MarkerRender_Post() +{ + if (pMarkerRenderHandler) + pMarkerRenderHandler(0); +} + +// Hook info +#define HOOKPOS_C3dMarkers_Render_Marker 0x7250B1 +#define FUNC_C3dMarker_Render 0x7223D0 +#define OFFSET_C3dMarker_Identifier 0x54 +static void __declspec(naked) HOOK_C3dMarkers_Render_Marker() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + pushad + push dword ptr [ecx+OFFSET_C3dMarker_Identifier] + call OnMY_MarkerRender_Pre + add esp, 4*1 + popad + + mov eax, FUNC_C3dMarker_Render + call eax + + pushad + call OnMY_MarkerRender_Post + popad + retn + } + // clang-format on +} + +////////////////////////////////////////////////////////////////////////////////////////// +// +// CCoronas::Render +// +// Detect corona rendering +// +////////////////////////////////////////////////////////////////////////////////////////// +#define HOOKPOS_CCoronas_Render_Corona 0x6FB2E6 +#define HOOKSIZE_CCoronas_Render_Corona 5 +#define FUNC_CSprite_RenderOneXLUSprite_Rotate_Aspect 0x70D490 +#define OFFSET_CRegisteredCorona_EdiToIdentifier 0x2C // edi points at the corona + 0x38, the identifier is at + 0x0C +DWORD RETURN_CCoronas_Render_Corona = 0x6FB2EB; +static void __declspec(naked) HOOK_CCoronas_Render_Corona() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + pushad + push dword ptr [edi-OFFSET_CRegisteredCorona_EdiToIdentifier] + call OnMY_MarkerRender_Pre + add esp, 4*1 + popad + + // Arguments are still on the stack + mov eax, FUNC_CSprite_RenderOneXLUSprite_Rotate_Aspect + call eax + + pushad + call OnMY_MarkerRender_Post + popad + jmp RETURN_CCoronas_Render_Corona + } + // clang-format on +} + +////////////////////////////////////////////////////////////////////////////////////////// +// +// CHeli::RenderAllHeliSearchLights +// +// Detect the game's own helicopter searchlight cone rendering (id = heli + 0xB, also the corona identifier) +// +////////////////////////////////////////////////////////////////////////////////////////// +#define HOOKPOS_CHeli_RenderAllHeliSearchLights_Cone 0x6C7CE8 +#define HOOKSIZE_CHeli_RenderAllHeliSearchLights_Cone 5 +#define FUNC_CHeli_SearchLightCone 0x6C58E0 +DWORD RETURN_CHeli_RenderAllHeliSearchLights_Cone = 0x6C7CED; +static void __declspec(naked) HOOK_CHeli_RenderAllHeliSearchLights_Cone() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + pushad + push dword ptr [esp+20h] + call OnMY_MarkerRender_Pre + add esp, 4*1 + popad + + // Arguments are still on the stack + mov eax, FUNC_CHeli_SearchLightCone + call eax + + pushad + call OnMY_MarkerRender_Post + popad + jmp RETURN_CHeli_RenderAllHeliSearchLights_Cone + } + // clang-format on +} + ////////////////////////////////////////////////////////////////////////////////////////// // // CMultiplayerSA::SetGameEntityRenderHandler @@ -796,6 +917,17 @@ void CMultiplayerSA::SetGameEntityRenderHandler(GameEntityRenderHandler* pHandle pGameEntityRenderHandler = pHandler; } +////////////////////////////////////////////////////////////////////////////////////////// +// +// CMultiplayerSA::SetMarkerRenderHandler +// +// +////////////////////////////////////////////////////////////////////////////////////////// +void CMultiplayerSA::SetMarkerRenderHandler(MarkerRenderHandler* pHandler) +{ + pMarkerRenderHandler = pHandler; +} + ////////////////////////////////////////////////////////////////////////////////////////// // // CMultiplayerSA::SetRadarBlipRenderHandler @@ -1027,4 +1159,7 @@ void CMultiplayerSA::InitHooks_Rendering() EZHookInstall(CRenderer_EverythingBarRoads); HookInstallCall(HOOKPOS_CRadar_DrawBlips_CoordBlip, (DWORD)HOOK_CRadar_DrawCoordBlip); HookInstallCall(HOOKPOS_CRadar_DrawBlips_CoordBlipWaypoint, (DWORD)HOOK_CRadar_DrawCoordBlip); + HookInstallCall(HOOKPOS_C3dMarkers_Render_Marker, (DWORD)HOOK_C3dMarkers_Render_Marker); + EZHookInstall(CCoronas_Render_Corona); + EZHookInstall(CHeli_RenderAllHeliSearchLights_Cone); } diff --git a/Client/sdk/multiplayer/CMultiplayer.h b/Client/sdk/multiplayer/CMultiplayer.h index eb1e031b414..f0abe71fc87 100644 --- a/Client/sdk/multiplayer/CMultiplayer.h +++ b/Client/sdk/multiplayer/CMultiplayer.h @@ -133,6 +133,7 @@ typedef void(GameModelRemoveHandler)(ushort usModelId); typedef void(GameRunNamedAnimDestructorHandler)(class CTaskSimpleRunNamedAnimSAInterface* pTask); typedef void(GameEntityRenderHandler)(CEntitySAInterface* pEntity); typedef void(RadarBlipRenderHandler)(int iBlipIndex); +typedef void(MarkerRenderHandler)(unsigned long ulIdentifier); typedef void(FxSystemDestructionHandler)(void* pFxSA); typedef AnimationId(DrivebyAnimationHandler)(AnimationId animGroup, AssocGroupId animId); typedef void(PedStepHandler)(CPedSAInterface* pPed, bool bFoot); @@ -260,6 +261,7 @@ class CMultiplayer virtual void SetGameRunNamedAnimDestructorHandler(GameRunNamedAnimDestructorHandler* pHandler) = 0; virtual void SetGameEntityRenderHandler(GameEntityRenderHandler* pHandler) = 0; virtual void SetRadarBlipRenderHandler(RadarBlipRenderHandler* pHandler) = 0; + virtual void SetMarkerRenderHandler(MarkerRenderHandler* pHandler) = 0; virtual void SetFxSystemDestructionHandler(FxSystemDestructionHandler* pHandler) = 0; virtual void SetDrivebyAnimationHandler(DrivebyAnimationHandler* pHandler) = 0; virtual void SetPedStepHandler(PedStepHandler* pHandler) = 0;