diff --git a/Client/mods/deathmatch/StdInc.h b/Client/mods/deathmatch/StdInc.h index b676ad0f469..c9ea30d8e12 100644 --- a/Client/mods/deathmatch/StdInc.h +++ b/Client/mods/deathmatch/StdInc.h @@ -84,6 +84,7 @@ #include #include #include +#include #include #include #include @@ -143,6 +144,7 @@ #include #include #include +#include #include #include #include diff --git a/Client/mods/deathmatch/logic/CClientEntity.h b/Client/mods/deathmatch/logic/CClientEntity.h index 4ddd8baefb2..8c49b71947a 100644 --- a/Client/mods/deathmatch/logic/CClientEntity.h +++ b/Client/mods/deathmatch/logic/CClientEntity.h @@ -81,6 +81,7 @@ enum eClientEntityType CCLIENTUNKNOWN, CCLIENTIMG, CCLIENTBUILDING, + CCLIENTWATERCANNON, }; class CEntity; @@ -146,6 +147,7 @@ enum eCClientEntityClassTypes CLASS_CClientSearchLight, CLASS_CClientIMG, CLASS_CClientBuilding, + CLASS_CClientWaterCannon, }; class CClientEntity : public CClientEntityBase diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 14cfe869285..94505355680 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -3714,9 +3714,9 @@ bool CClientGame::StaticObjectBreakHandler(CObjectSAInterface* pObjectInterface, return g_pClientGame->ObjectBreakHandler(pObjectInterface, pAttackerInterface); } -bool CClientGame::StaticWaterCannonHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed) +bool CClientGame::StaticWaterCannonHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed, void* pWaterCannonInterface) { - return g_pClientGame->WaterCannonHitHandler(pCannonVehicle, pHitPed); + return g_pClientGame->WaterCannonHitHandler(pCannonVehicle, pHitPed, pWaterCannonInterface); } bool CClientGame::StaticVehicleFellThroughMapHandler(CVehicleSAInterface* pVehicle) @@ -4938,8 +4938,35 @@ bool CClientGame::ObjectBreakHandler(CObjectSAInterface* pObjectInterface, CEnti return true; } -bool CClientGame::WaterCannonHitHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed) +bool CClientGame::WaterCannonHitHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed, void* pWaterCannonInterface) { + // A script-owned standalone cannon (createWaterCannon) has no vehicle to fire + // onClientPedHitByWaterCannon/onClientPlayerHitByWaterCannon against; fire the same two events + // from the cannon element itself instead, so scripts get exactly the same notification (and the + // same chance to cancel the native knockdown) a real vehicle cannon gives them + if (!pCannonVehicle) + { + CClientWaterCannon* pWaterCannon = m_pManager->GetWaterCannonManager()->GetByNativeHandle(pWaterCannonInterface); + if (!pWaterCannon || !pWaterCannon->IsKnockdownEnabled()) + return false; + + CPools* pPools = g_pGame->GetPools(); + CClientPed* pClientPed = nullptr; + SClientEntity* pPedEntity = pPools->GetPed(reinterpret_cast(pHitPed)); + if (pPedEntity) + pClientPed = reinterpret_cast(pPedEntity->pClientEntity); + + CLuaArguments Arguments; + if (pClientPed) + Arguments.PushElement(pClientPed); + else + Arguments.PushNil(); + + if (pClientPed && !IS_PLAYER(pClientPed)) + return pWaterCannon->CallEvent("onClientPedHitByWaterCannon", Arguments, true); + return pWaterCannon->CallEvent("onClientPlayerHitByWaterCannon", Arguments, true); + } + if (pCannonVehicle && pHitPed) { CPools* pPools = g_pGame->GetPools(); diff --git a/Client/mods/deathmatch/logic/CClientGame.h b/Client/mods/deathmatch/logic/CClientGame.h index cf8048eb67c..341871bef18 100644 --- a/Client/mods/deathmatch/logic/CClientGame.h +++ b/Client/mods/deathmatch/logic/CClientGame.h @@ -601,7 +601,7 @@ class CClientGame static bool StaticHeliKillHandler(CVehicleSAInterface* pHeli, CEntitySAInterface* pHitInterface); static bool StaticObjectDamageHandler(CObjectSAInterface* pObjectInterface, float fLoss, CEntitySAInterface* pAttackerInterface); static bool StaticObjectBreakHandler(CObjectSAInterface* pObjectInterface, CEntitySAInterface* pAttackerInterface); - static bool StaticWaterCannonHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed); + static bool StaticWaterCannonHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed, void* pWaterCannonInterface); static bool StaticVehicleFellThroughMapHandler(CVehicleSAInterface* pVehicle); static void StaticGameObjectDestructHandler(CEntitySAInterface* pObject); static void StaticGameVehicleDestructHandler(CEntitySAInterface* pVehicle); @@ -648,7 +648,7 @@ class CClientGame bool HeliKillHandler(CVehicleSAInterface* pHeli, CEntitySAInterface* pHitInterface); bool ObjectDamageHandler(CObjectSAInterface* pObjectInterface, float fLoss, CEntitySAInterface* pAttackerInterface); bool ObjectBreakHandler(CObjectSAInterface* pObjectInterface, CEntitySAInterface* pAttackerInterface); - bool WaterCannonHitHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed); + bool WaterCannonHitHandler(CVehicleSAInterface* pCannonVehicle, CPedSAInterface* pHitPed, void* pWaterCannonInterface); bool VehicleFellThroughMapHandler(CVehicleSAInterface* pVehicle); void GameObjectDestructHandler(CEntitySAInterface* pObject); void GameVehicleDestructHandler(CEntitySAInterface* pVehicle); diff --git a/Client/mods/deathmatch/logic/CClientManager.cpp b/Client/mods/deathmatch/logic/CClientManager.cpp index bf79bf9ec93..78681d28930 100644 --- a/Client/mods/deathmatch/logic/CClientManager.cpp +++ b/Client/mods/deathmatch/logic/CClientManager.cpp @@ -55,6 +55,7 @@ CClientManager::CClientManager() m_pPacketRecorder = new CClientPacketRecorder(this); m_pImgManager = new CClientIMGManager(this); m_pBuildingManager = new CClientBuildingManager(this); + m_pWaterCannonManager = new CClientWaterCannonManager(this); m_bBeingDeleted = false; m_bGameUnloadedFlag = false; @@ -181,6 +182,9 @@ CClientManager::~CClientManager() delete m_pBuildingManager; m_pBuildingManager = nullptr; + + delete m_pWaterCannonManager; + m_pWaterCannonManager = nullptr; } // @@ -218,6 +222,7 @@ void CClientManager::DoPulse(bool bDoStandardPulses, bool bDoVehicleManagerPulse m_pColManager->DoPulse(); m_pGUIManager->DoPulse(); m_pWeaponManager->DoPulse(); + m_pWaterCannonManager->DoPulse(); } else { diff --git a/Client/mods/deathmatch/logic/CClientManager.h b/Client/mods/deathmatch/logic/CClientManager.h index 26f93f0015b..ce372662ecf 100644 --- a/Client/mods/deathmatch/logic/CClientManager.h +++ b/Client/mods/deathmatch/logic/CClientManager.h @@ -44,6 +44,7 @@ class CClientManager; #include "CClientModelManager.h" #include "CClientIMGManager.h" #include "CClientBuildingManager.h" +#include "CClientWaterCannonManager.h" class CClientProjectileManager; class CClientExplosionManager; @@ -96,6 +97,7 @@ class CClientManager CClientWeaponManager* GetWeaponManager() { return m_pWeaponManager; } CClientEffectManager* GetEffectManager() { return m_pEffectManager; } CClientPointLightsManager* GetPointLightsManager() { return m_pPointLightsManager; } + CClientWaterCannonManager* GetWaterCannonManager() { return m_pWaterCannonManager; } CClientIMGManager* GetIMGManager() { return m_pImgManager; } CClientBuildingManager* GetBuildingManager() const noexcept { return m_pBuildingManager; } @@ -150,6 +152,7 @@ class CClientManager CClientWeaponManager* m_pWeaponManager; CClientEffectManager* m_pEffectManager; CClientPointLightsManager* m_pPointLightsManager; + CClientWaterCannonManager* m_pWaterCannonManager; CClientModelManager* m_pModelManager; CClientIMGManager* m_pImgManager; CClientPacketRecorder* m_pPacketRecorder; diff --git a/Client/mods/deathmatch/logic/CClientWaterCannon.cpp b/Client/mods/deathmatch/logic/CClientWaterCannon.cpp new file mode 100644 index 00000000000..32d96519b7e --- /dev/null +++ b/Client/mods/deathmatch/logic/CClientWaterCannon.cpp @@ -0,0 +1,89 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * (Shared logic for modifications) + * LICENSE: See LICENSE in the top level directory + * FILE: mods/shared_logic/CClientWaterCannon.cpp + * PURPOSE: Water cannon entity class + * + *****************************************************************************/ + +#include + +CClientWaterCannon::CClientWaterCannon(CClientManager* pManager, ElementID ID) : ClassInit(this), CClientEntity(ID) +{ + m_pWaterCannonManager = pManager->GetWaterCannonManager(); + + m_vecDirection = CVector(0.0f, 1.0f, 0.0f); + m_fForce = 1.0f; + m_bEnabled = true; + m_bKnockdownEnabled = true; + m_bHasCustomColor = false; + m_Color = SColor(0xFFC8C8FF); // the native jet's light blue (R200 G200 B255), opaque + m_pNativeCannon = g_pMultiplayer->CreateCustomWaterCannon(); + + SetTypeName("water-cannon"); + + m_pWaterCannonManager->AddToList(this); +} + +CClientWaterCannon::~CClientWaterCannon() +{ + Unlink(); + g_pMultiplayer->DestroyCustomWaterCannon(m_pNativeCannon); +} + +void CClientWaterCannon::Unlink() +{ + m_pWaterCannonManager->RemoveFromList(this); +} + +void CClientWaterCannon::SetColor(const SColor& color) +{ + m_Color = color; + m_bHasCustomColor = true; + if (m_pNativeCannon) + g_pMultiplayer->SetCustomWaterCannonColor(m_pNativeCannon, color.R, color.G, color.B, color.A); +} + +void CClientWaterCannon::ResetColor() +{ + m_bHasCustomColor = false; + m_Color = SColor(0xFFC8C8FF); + if (m_pNativeCannon) + g_pMultiplayer->ResetCustomWaterCannonColor(m_pNativeCannon); +} + +void CClientWaterCannon::DoPulse() +{ + // Update our position/rotation if we're attached, and let anything attached to us follow ours + DoAttaching(); + + if (!m_bEnabled || !m_pNativeCannon) + return; + + // m_vecDirection is always normalized (see SetDirection), so this is purely "orientation + // times force", not the direction's own magnitude bleeding into the velocity. + const CVector vecVelocity = m_vecDirection * m_fForce; + g_pMultiplayer->UpdateCustomWaterCannon(m_pNativeCannon, m_vecPosition, vecVelocity); +} + +// Same heading/pitch-from-direction convention the native water cannons use +// (CAutomobile::FireTruckControl/TankControl), so a model attached to us via attachElements +// aims consistently with how the game's own nozzles look. atan2f's second argument here is +// never negative, so pitch always comes back between -90 and 90 degrees for a normalized +// direction, which is exactly the range SetRotationRadians below expects back. +void CClientWaterCannon::GetRotationRadians(CVector& vecOutRadians) const +{ + const float fHorizontalMagnitude = sqrtf(m_vecDirection.fX * m_vecDirection.fX + m_vecDirection.fY * m_vecDirection.fY); + const float fHeading = atan2f(-m_vecDirection.fX, m_vecDirection.fY); + const float fPitch = atan2f(m_vecDirection.fZ, fHorizontalMagnitude); + vecOutRadians = CVector(fPitch, 0.0f, fHeading); +} + +void CClientWaterCannon::SetRotationRadians(const CVector& vecRadians) +{ + const float fPitch = vecRadians.fX; + const float fHeading = vecRadians.fZ; + SetDirection(CVector(-sinf(fHeading) * cosf(fPitch), cosf(fHeading) * cosf(fPitch), sinf(fPitch))); +} diff --git a/Client/mods/deathmatch/logic/CClientWaterCannon.h b/Client/mods/deathmatch/logic/CClientWaterCannon.h new file mode 100644 index 00000000000..62d29dfc8bc --- /dev/null +++ b/Client/mods/deathmatch/logic/CClientWaterCannon.h @@ -0,0 +1,91 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * (Shared logic for modifications) + * LICENSE: See LICENSE in the top level directory + * FILE: mods/shared_logic/CClientWaterCannon.h + * PURPOSE: Water cannon entity class header + * + *****************************************************************************/ + +#pragma once + +#include "CClientEntity.h" + +class CClientWaterCannonManager; + +// A standalone water jet using the native CWaterCannons system (the same one the Firetruck and +// SWAT tank use), but not tied to any real vehicle. See CClientWaterCannon.cpp for how the native +// side treats the "owner" id as fully opaque, which is what makes that safe. +class CClientWaterCannon final : public CClientEntity +{ + DECLARE_CLASS(CClientWaterCannon, CClientEntity) + friend class CClientWaterCannonManager; + +public: + CClientWaterCannon(CClientManager* pManager, ElementID ID); + ~CClientWaterCannon(); + + void Unlink(); + + eClientEntityType GetType() const { return CCLIENTWATERCANNON; }; + + void GetPosition(CVector& vecPosition) const { vecPosition = m_vecPosition; }; + void SetPosition(const CVector& vecPosition) { m_vecPosition = vecPosition; }; + + // The generic element rotation, kept in sync with the direction below so that scripts can + // attach a model to us with the standard attachElements and have it aim correctly; see + // CClientWaterCannon.cpp for the angle convention shared with the direction accessors. + void GetRotationRadians(CVector& vecOutRadians) const; + void SetRotationRadians(const CVector& vecRadians); + + // Always stored normalized, so it's a pure orientation; SetForce's value is the only thing + // that controls the velocity's actual magnitude. + const CVector& GetDirection() const { return m_vecDirection; }; + void SetDirection(CVector vecDirection) + { + vecDirection.Normalize(); + m_vecDirection = vecDirection; + }; + + float GetForce() const { return m_fForce; }; + void SetForce(float fForce) { m_fForce = fForce; }; + + bool IsEnabled() const { return m_bEnabled; }; + void SetEnabled(bool bEnabled) { m_bEnabled = bEnabled; }; + + // Whether getting sprayed by this cannon knocks a ped down, the same reaction a real vehicle- + // owned cannon gives; on by default to match that vanilla behaviour + bool IsKnockdownEnabled() const { return m_bKnockdownEnabled; }; + void SetKnockdownEnabled(bool bEnabled) { m_bKnockdownEnabled = bEnabled; }; + + // A custom jet colour; when unset the native fading light-blue is used + bool HasCustomColor() const { return m_bHasCustomColor; }; + const SColor& GetColor() const { return m_Color; }; + void SetColor(const SColor& color); + void ResetColor(); + + // False if the shared custom-cannon pool (CMultiplayer::CreateCustomWaterCannon, capped + // separately from the vehicle-owned one) was full at construction time. + bool HasNativeCannon() const { return m_pNativeCannon != nullptr; }; + void* GetNativeHandle() const { return m_pNativeCannon; }; + +protected: + void DoPulse(); + +private: + CClientWaterCannonManager* m_pWaterCannonManager; + + CVector m_vecPosition; + CVector m_vecDirection; + float m_fForce; + bool m_bEnabled; + bool m_bKnockdownEnabled; + + bool m_bHasCustomColor; + SColor m_Color; + + // Opaque handle from CMultiplayer::CreateCustomWaterCannon; our own native CWaterCannon + // instance, kept apart from the vehicle-owned ones (see CClientWaterCannon.cpp). + void* m_pNativeCannon; +}; diff --git a/Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp b/Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp new file mode 100644 index 00000000000..3a68c24c2a7 --- /dev/null +++ b/Client/mods/deathmatch/logic/CClientWaterCannonManager.cpp @@ -0,0 +1,79 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * (Shared logic for modifications) + * LICENSE: See LICENSE in the top level directory + * FILE: mods/shared_logic/CClientWaterCannonManager.cpp + * PURPOSE: Water cannon entity manager class + * + *****************************************************************************/ + +#include + +CClientWaterCannonManager::CClientWaterCannonManager(CClientManager* pManager) +{ + m_pManager = pManager; + m_bDontRemoveFromList = false; +} + +CClientWaterCannonManager::~CClientWaterCannonManager() +{ + DeleteAll(); +} + +CClientWaterCannon* CClientWaterCannonManager::Create(ElementID ID) +{ + if (Get(ID)) + return nullptr; + + CClientWaterCannon* pCannon = new CClientWaterCannon(m_pManager, ID); + if (!pCannon->HasNativeCannon()) + { + // The custom-cannon pool (separate from the vehicle-owned one, but still capped) is full + delete pCannon; + return nullptr; + } + + return pCannon; +} + +void CClientWaterCannonManager::DeleteAll() +{ + m_bDontRemoveFromList = true; + for (CClientWaterCannon* pCannon : m_List) + delete pCannon; + m_bDontRemoveFromList = false; + + m_List.clear(); +} + +CClientWaterCannon* CClientWaterCannonManager::Get(ElementID ID) +{ + CClientEntity* pEntity = CElementIDs::GetElement(ID); + if (pEntity && pEntity->GetType() == CCLIENTWATERCANNON) + return static_cast(pEntity); + + return nullptr; +} + +void CClientWaterCannonManager::DoPulse() +{ + for (CClientWaterCannon* pCannon : m_List) + pCannon->DoPulse(); +} + +CClientWaterCannon* CClientWaterCannonManager::GetByNativeHandle(void* pWaterCannonInterface) const +{ + for (CClientWaterCannon* pCannon : m_List) + { + if (pCannon->GetNativeHandle() == pWaterCannonInterface) + return pCannon; + } + return nullptr; +} + +void CClientWaterCannonManager::RemoveFromList(CClientWaterCannon* pCannon) +{ + if (!m_bDontRemoveFromList && !m_List.empty()) + m_List.remove(pCannon); +} diff --git a/Client/mods/deathmatch/logic/CClientWaterCannonManager.h b/Client/mods/deathmatch/logic/CClientWaterCannonManager.h new file mode 100644 index 00000000000..5e0fd1dc2f3 --- /dev/null +++ b/Client/mods/deathmatch/logic/CClientWaterCannonManager.h @@ -0,0 +1,49 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * (Shared logic for modifications) + * LICENSE: See LICENSE in the top level directory + * FILE: mods/shared_logic/CClientWaterCannonManager.h + * PURPOSE: Water cannon entity manager class header + * + *****************************************************************************/ + +#pragma once + +#include "CClientWaterCannon.h" +#include + +class CClientWaterCannonManager +{ + friend class CClientManager; + friend class CClientWaterCannon; + +public: + CClientWaterCannonManager(CClientManager* pManager); + ~CClientWaterCannonManager(); + + CClientWaterCannon* Create(ElementID ID); + + void DeleteAll(); + + std::list::const_iterator IterBegin() { return m_List.begin(); }; + std::list::const_iterator IterEnd() { return m_List.end(); }; + + static CClientWaterCannon* Get(ElementID ID); + + void DoPulse(); + + // Traces a hit back to the CClientWaterCannon that owns the raw native CWaterCannon instance + // (CClientWaterCannon::GetNativeHandle) it happened on; null for a handle this manager doesn't + // recognise (a real vehicle-owned cannon's native instance, most likely). + CClientWaterCannon* GetByNativeHandle(void* pWaterCannonInterface) const; + +private: + void AddToList(CClientWaterCannon* pCannon) { m_List.push_back(pCannon); }; + void RemoveFromList(CClientWaterCannon* pCannon); + + CClientManager* m_pManager; + + std::list m_List; + bool m_bDontRemoveFromList; +}; diff --git a/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp b/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp index 9cefd79ce91..8bc5b556a89 100644 --- a/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp +++ b/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp @@ -7758,6 +7758,17 @@ CClientWeapon* CStaticFunctionDefinitions::CreateWeapon(CResource& Resource, eWe return pWeapon; } +CClientWaterCannon* CStaticFunctionDefinitions::CreateWaterCannon(CResource& Resource, const CVector& vecPosition) +{ + CClientWaterCannon* pCannon = m_pManager->GetWaterCannonManager()->Create(INVALID_ELEMENT_ID); + if (!pCannon) + return nullptr; + + pCannon->SetPosition(vecPosition); + pCannon->SetParent(Resource.GetResourceDynamicEntity()); + return pCannon; +} + bool CStaticFunctionDefinitions::FireWeapon(CClientWeapon* pWeapon) { if (pWeapon) diff --git a/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.h b/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.h index d1e019758e9..75a1d6c796f 100644 --- a/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.h +++ b/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.h @@ -717,6 +717,9 @@ class CStaticFunctionDefinitions static bool GetWeaponFlags(CClientWeapon* pWeapon, SLineOfSightFlags& flags); static bool GetWeaponFlags(CClientWeapon* pWeapon, eWeaponFlags flag, bool& bData); + // Water cannon funcs + static CClientWaterCannon* CreateWaterCannon(CResource& Resource, const CVector& vecPosition); + static bool SetWeaponFiringRate(CClientWeapon* pWeapon, int iFiringRate); static bool ResetWeaponFiringRate(CClientWeapon* pWeapon); static bool GetWeaponFiringRate(CClientWeapon* pWeapon, int& iFiringRate); diff --git a/Client/mods/deathmatch/logic/lua/CLuaMain.cpp b/Client/mods/deathmatch/logic/lua/CLuaMain.cpp index 91f26561267..6f0d4d6e1c6 100644 --- a/Client/mods/deathmatch/logic/lua/CLuaMain.cpp +++ b/Client/mods/deathmatch/logic/lua/CLuaMain.cpp @@ -127,6 +127,7 @@ void CLuaMain::InitClasses(lua_State* luaVM) CLuaVehicleDefs::AddClass(luaVM); CLuaWaterDefs::AddClass(luaVM); CLuaWeaponDefs::AddClass(luaVM); + CLuaWaterCannonDefs::AddClass(luaVM); CLuaBuildingDefs::AddClass(luaVM); CLuaShared::AddClasses(luaVM); diff --git a/Client/mods/deathmatch/logic/lua/CLuaManager.cpp b/Client/mods/deathmatch/logic/lua/CLuaManager.cpp index e97f802e612..01c7ba661b5 100644 --- a/Client/mods/deathmatch/logic/lua/CLuaManager.cpp +++ b/Client/mods/deathmatch/logic/lua/CLuaManager.cpp @@ -279,6 +279,7 @@ void CLuaManager::LoadCFunctions() CLuaVehicleDefs::LoadFunctions(); CLuaWaterDefs::LoadFunctions(); CLuaWeaponDefs::LoadFunctions(); + CLuaWaterCannonDefs::LoadFunctions(); CLuaWorldDefs::LoadFunctions(); CLuaXMLDefs::LoadFunctions(); CLuaClientDefs::LoadFunctions(); diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp new file mode 100644 index 00000000000..c289e9394c7 --- /dev/null +++ b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.cpp @@ -0,0 +1,143 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto + * LICENSE: See LICENSE in the top level directory + * FILE: mods/shared_logic/luadefs/CLuaWaterCannonDefs.cpp + * PURPOSE: Lua definitions class + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#include "StdInc.h" + +void CLuaWaterCannonDefs::LoadFunctions() +{ + constexpr static const std::pair functions[]{ + {"createWaterCannon", ArgumentParser}, + {"setWaterCannonDirection", ArgumentParser}, + {"getWaterCannonDirection", ArgumentParser}, + {"setWaterCannonForce", ArgumentParser}, + {"getWaterCannonForce", ArgumentParser}, + {"setWaterCannonEnabled", ArgumentParser}, + {"isWaterCannonEnabled", ArgumentParser}, + {"setWaterCannonKnockdownEnabled", ArgumentParser}, + {"isWaterCannonKnockdownEnabled", ArgumentParser}, + {"setWaterCannonColor", ArgumentParser}, + {"getWaterCannonColor", ArgumentParser}, + {"resetWaterCannonColor", ArgumentParser}, + }; + + for (const auto& [name, func] : functions) + CLuaCFunctions::AddFunction(name, func); +} + +void CLuaWaterCannonDefs::AddClass(lua_State* luaVM) +{ + lua_newclass(luaVM); + + lua_classfunction(luaVM, "create", "createWaterCannon"); + + lua_classfunction(luaVM, "setDirection", "setWaterCannonDirection"); + lua_classfunction(luaVM, "setForce", "setWaterCannonForce"); + lua_classfunction(luaVM, "setEnabled", "setWaterCannonEnabled"); + lua_classfunction(luaVM, "setKnockdownEnabled", "setWaterCannonKnockdownEnabled"); + lua_classfunction(luaVM, "setColor", "setWaterCannonColor"); + lua_classfunction(luaVM, "resetColor", "resetWaterCannonColor"); + + lua_classfunction(luaVM, "getDirection", "getWaterCannonDirection"); + lua_classfunction(luaVM, "getForce", "getWaterCannonForce"); + lua_classfunction(luaVM, "isEnabled", "isWaterCannonEnabled"); + lua_classfunction(luaVM, "isKnockdownEnabled", "isWaterCannonKnockdownEnabled"); + lua_classfunction(luaVM, "getColor", "getWaterCannonColor"); + + lua_classvariable(luaVM, "direction", "setWaterCannonDirection", "getWaterCannonDirection"); + lua_classvariable(luaVM, "force", "setWaterCannonForce", "getWaterCannonForce"); + lua_classvariable(luaVM, "enabled", "setWaterCannonEnabled", "isWaterCannonEnabled"); + lua_classvariable(luaVM, "knockdownEnabled", "setWaterCannonKnockdownEnabled", "isWaterCannonKnockdownEnabled"); + + lua_registerclass(luaVM, "WaterCannon", "Element"); +} + +std::variant CLuaWaterCannonDefs::CreateWaterCannon(lua_State* luaVM, CVector vecPosition, std::optional color, + std::optional bKnockdownEnabled) +{ + CResource& resource = lua_getownerresource(luaVM); + CClientWaterCannon* pCannon = CStaticFunctionDefinitions::CreateWaterCannon(resource, vecPosition); + if (!pCannon) + return false; + + if (color.has_value()) + pCannon->SetColor(color.value()); + + if (bKnockdownEnabled.has_value()) + pCannon->SetKnockdownEnabled(bKnockdownEnabled.value()); + + if (CElementGroup* elementGroup = resource.GetElementGroup()) + elementGroup->Add(pCannon); + + return pCannon; +} + +bool CLuaWaterCannonDefs::SetWaterCannonDirection(CClientWaterCannon* pCannon, CVector vecDirection) +{ + pCannon->SetDirection(vecDirection); + return true; +} + +CVector CLuaWaterCannonDefs::GetWaterCannonDirection(CClientWaterCannon* pCannon) +{ + return pCannon->GetDirection(); +} + +bool CLuaWaterCannonDefs::SetWaterCannonForce(CClientWaterCannon* pCannon, float fForce) +{ + pCannon->SetForce(fForce); + return true; +} + +float CLuaWaterCannonDefs::GetWaterCannonForce(CClientWaterCannon* pCannon) +{ + return pCannon->GetForce(); +} + +bool CLuaWaterCannonDefs::SetWaterCannonEnabled(CClientWaterCannon* pCannon, bool bEnabled) +{ + pCannon->SetEnabled(bEnabled); + return true; +} + +bool CLuaWaterCannonDefs::IsWaterCannonEnabled(CClientWaterCannon* pCannon) +{ + return pCannon->IsEnabled(); +} + +bool CLuaWaterCannonDefs::SetWaterCannonKnockdownEnabled(CClientWaterCannon* pCannon, bool bEnabled) +{ + pCannon->SetKnockdownEnabled(bEnabled); + return true; +} + +bool CLuaWaterCannonDefs::IsWaterCannonKnockdownEnabled(CClientWaterCannon* pCannon) +{ + return pCannon->IsKnockdownEnabled(); +} + +bool CLuaWaterCannonDefs::SetWaterCannonColor(CClientWaterCannon* pCannon, unsigned char ucRed, unsigned char ucGreen, unsigned char ucBlue, + std::optional ucAlpha) +{ + pCannon->SetColor(SColorRGBA(ucRed, ucGreen, ucBlue, ucAlpha.value_or(255))); + return true; +} + +CLuaMultiReturn CLuaWaterCannonDefs::GetWaterCannonColor(CClientWaterCannon* pCannon) +{ + const SColor color = pCannon->GetColor(); + return {color.R, color.G, color.B, color.A}; +} + +bool CLuaWaterCannonDefs::ResetWaterCannonColor(CClientWaterCannon* pCannon) +{ + pCannon->ResetColor(); + return true; +} diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h new file mode 100644 index 00000000000..0775ec574c2 --- /dev/null +++ b/Client/mods/deathmatch/logic/luadefs/CLuaWaterCannonDefs.h @@ -0,0 +1,41 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto + * LICENSE: See LICENSE in the top level directory + * FILE: mods/shared_logic/luadefs/CLuaWaterCannonDefs.h + * PURPOSE: Lua definitions class + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#pragma once +#include "CLuaDefs.h" +#include + +class CLuaWaterCannonDefs : public CLuaDefs +{ +public: + static void LoadFunctions(); + static void AddClass(lua_State* luaVM); + + static std::variant CreateWaterCannon(lua_State* luaVM, CVector vecPosition, std::optional color, + std::optional bKnockdownEnabled); + + static bool SetWaterCannonDirection(CClientWaterCannon* pCannon, CVector vecDirection); + static CVector GetWaterCannonDirection(CClientWaterCannon* pCannon); + + static bool SetWaterCannonForce(CClientWaterCannon* pCannon, float fForce); + static float GetWaterCannonForce(CClientWaterCannon* pCannon); + + static bool SetWaterCannonEnabled(CClientWaterCannon* pCannon, bool bEnabled); + static bool IsWaterCannonEnabled(CClientWaterCannon* pCannon); + + static bool SetWaterCannonKnockdownEnabled(CClientWaterCannon* pCannon, bool bEnabled); + static bool IsWaterCannonKnockdownEnabled(CClientWaterCannon* pCannon); + + static bool SetWaterCannonColor(CClientWaterCannon* pCannon, unsigned char ucRed, unsigned char ucGreen, unsigned char ucBlue, + std::optional ucAlpha); + static CLuaMultiReturn GetWaterCannonColor(CClientWaterCannon* pCannon); + static bool ResetWaterCannonColor(CClientWaterCannon* pCannon); +}; diff --git a/Client/multiplayer_sa/CMultiplayerSA.cpp b/Client/multiplayer_sa/CMultiplayerSA.cpp index af42702f3f8..80defd9a273 100644 --- a/Client/multiplayer_sa/CMultiplayerSA.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA.cpp @@ -1586,6 +1586,7 @@ void CMultiplayerSA::InitHooks() InitHooks_VehicleDamage(); InitHooks_VehicleLights(); InitHooks_VehicleWeapons(); + InitHooks_WaterCannons(); InitHooks_Streaming(); InitHooks_FrameRateFixes(); diff --git a/Client/multiplayer_sa/CMultiplayerSA.h b/Client/multiplayer_sa/CMultiplayerSA.h index 9f85b307909..67a581bfbf0 100644 --- a/Client/multiplayer_sa/CMultiplayerSA.h +++ b/Client/multiplayer_sa/CMultiplayerSA.h @@ -77,6 +77,7 @@ class CMultiplayerSA : public CMultiplayer void InitHooks_VehicleLights(); void InitHooks_VehicleDamage(); void InitHooks_VehicleWeapons(); + void InitHooks_WaterCannons(); void InitHooks_Direct3D(); void InitHooks_FixLineOfSightArgs(); void InitHooks_Streaming(); @@ -108,51 +109,56 @@ class CMultiplayerSA : public CMultiplayer void DisableCloseRangeDamage(bool bDisable); void DisableBadDrivebyHitboxes(bool bDisable) { m_bBadDrivebyHitboxesDisabled = bDisable; } - bool GetExplosionsDisabled(); - void DisableExplosions(bool bDisabled); - void SetExplosionHandler(ExplosionHandler* pExplosionHandler); - void SetDamageHandler(DamageHandler* pDamageHandler); - void SetDeathHandler(DeathHandler* pDeathHandler); - void SetProjectileHandler(ProjectileHandler* pProjectileHandler); - void SetProjectileStopHandler(ProjectileStopHandler* pProjectileHandler); - void SetFireHandler(FireHandler* pFireHandler); - void SetBreakTowLinkHandler(BreakTowLinkHandler* pBreakTowLinkHandler); - void SetProcessCamHandler(ProcessCamHandler* pProcessCamHandler); - void SetChokingHandler(ChokingHandler* pChokingHandler); - void SetPreWeatherUpdateHandler(PreWeatherUpdateHandler* pHandler); - void SetPreWorldProcessHandler(PreWorldProcessHandler* pHandler); - void SetPostWorldProcessHandler(PostWorldProcessHandler* pHandler); - void SetPostWorldProcessPedsAfterPreRenderHandler(PostWorldProcessPedsAfterPreRenderHandler* pHandler); - void SetIdleHandler(IdleHandler* pHandler); - void SetPreFxRenderHandler(PreFxRenderHandler* pHandler); - void SetPostColorFilterRenderHandler(PostColorFilterRenderHandler* pHandler) override; - void SetPreHudRenderHandler(PreHudRenderHandler* pHandler); - void DisableCallsToCAnimBlendNode(bool bDisableCalls); - void SetCAnimBlendAssocDestructorHandler(CAnimBlendAssocDestructorHandler* pHandler); - void SetAddAnimationHandler(AddAnimationHandler* pHandler); - void SetAddAnimationAndSyncHandler(AddAnimationAndSyncHandler* pHandler); - void SetAssocGroupCopyAnimationHandler(AssocGroupCopyAnimationHandler* pHandler); - void SetBlendAnimationHierarchyHandler(BlendAnimationHierarchyHandler* pHandler); - void SetProcessCollisionHandler(ProcessCollisionHandler* pHandler); - void SetVehicleCollisionHandler(VehicleCollisionHandler* pHandler); - void SetVehicleDamageHandler(VehicleDamageHandler* pHandler); - void SetHeliKillHandler(HeliKillHandler* pHandler); - void SetObjectDamageHandler(ObjectDamageHandler* pHandler); - void SetObjectBreakHandler(ObjectBreakHandler* pHandler); - void SetWaterCannonHitHandler(WaterCannonHitHandler* pHandler); - void SetVehicleFellThroughMapHandler(VehicleFellThroughMapHandler* pHandler); - void SetGameObjectDestructHandler(GameObjectDestructHandler* pHandler); - void SetGameVehicleDestructHandler(GameVehicleDestructHandler* pHandler); - void SetGamePlayerDestructHandler(GamePlayerDestructHandler* pHandler); - void SetGameProjectileDestructHandler(GameProjectileDestructHandler* pHandler); - void SetGameModelRemoveHandler(GameModelRemoveHandler* pHandler); - void SetGameRunNamedAnimDestructorHandler(GameRunNamedAnimDestructorHandler* pHandler); - void SetGameEntityRenderHandler(GameEntityRenderHandler* pHandler); - void SetFxSystemDestructionHandler(FxSystemDestructionHandler* pHandler); - void SetDrivebyAnimationHandler(DrivebyAnimationHandler* pHandler); - void SetPedStepHandler(PedStepHandler* pHandler); - void SetVehicleWeaponHitHandler(VehicleWeaponHitHandler* pHandler) override; - void SetAudioZoneRadioSwitchHandler(AudioZoneRadioSwitchHandler* pHandler); + bool GetExplosionsDisabled(); + void DisableExplosions(bool bDisabled); + void SetExplosionHandler(ExplosionHandler* pExplosionHandler); + void SetDamageHandler(DamageHandler* pDamageHandler); + void SetDeathHandler(DeathHandler* pDeathHandler); + void SetProjectileHandler(ProjectileHandler* pProjectileHandler); + void SetProjectileStopHandler(ProjectileStopHandler* pProjectileHandler); + void SetFireHandler(FireHandler* pFireHandler); + void SetBreakTowLinkHandler(BreakTowLinkHandler* pBreakTowLinkHandler); + void SetProcessCamHandler(ProcessCamHandler* pProcessCamHandler); + void SetChokingHandler(ChokingHandler* pChokingHandler); + void SetPreWeatherUpdateHandler(PreWeatherUpdateHandler* pHandler); + void SetPreWorldProcessHandler(PreWorldProcessHandler* pHandler); + void SetPostWorldProcessHandler(PostWorldProcessHandler* pHandler); + void SetPostWorldProcessPedsAfterPreRenderHandler(PostWorldProcessPedsAfterPreRenderHandler* pHandler); + void SetIdleHandler(IdleHandler* pHandler); + void SetPreFxRenderHandler(PreFxRenderHandler* pHandler); + void SetPostColorFilterRenderHandler(PostColorFilterRenderHandler* pHandler) override; + void SetPreHudRenderHandler(PreHudRenderHandler* pHandler); + void DisableCallsToCAnimBlendNode(bool bDisableCalls); + void SetCAnimBlendAssocDestructorHandler(CAnimBlendAssocDestructorHandler* pHandler); + void SetAddAnimationHandler(AddAnimationHandler* pHandler); + void SetAddAnimationAndSyncHandler(AddAnimationAndSyncHandler* pHandler); + void SetAssocGroupCopyAnimationHandler(AssocGroupCopyAnimationHandler* pHandler); + void SetBlendAnimationHierarchyHandler(BlendAnimationHierarchyHandler* pHandler); + void SetProcessCollisionHandler(ProcessCollisionHandler* pHandler); + void SetVehicleCollisionHandler(VehicleCollisionHandler* pHandler); + void SetVehicleDamageHandler(VehicleDamageHandler* pHandler); + void SetHeliKillHandler(HeliKillHandler* pHandler); + void SetObjectDamageHandler(ObjectDamageHandler* pHandler); + void SetObjectBreakHandler(ObjectBreakHandler* pHandler); + void SetWaterCannonHitHandler(WaterCannonHitHandler* pHandler); + void* CreateCustomWaterCannon() override; + void DestroyCustomWaterCannon(void* pCannon) override; + void UpdateCustomWaterCannon(void* pCannon, const CVector& vecStart, const CVector& vecVelocity) override; + void SetCustomWaterCannonColor(void* pCannon, unsigned char ucRed, unsigned char ucGreen, unsigned char ucBlue, unsigned char ucAlpha) override; + void ResetCustomWaterCannonColor(void* pCannon) override; + void SetVehicleFellThroughMapHandler(VehicleFellThroughMapHandler* pHandler); + void SetGameObjectDestructHandler(GameObjectDestructHandler* pHandler); + void SetGameVehicleDestructHandler(GameVehicleDestructHandler* pHandler); + void SetGamePlayerDestructHandler(GamePlayerDestructHandler* pHandler); + void SetGameProjectileDestructHandler(GameProjectileDestructHandler* pHandler); + void SetGameModelRemoveHandler(GameModelRemoveHandler* pHandler); + void SetGameRunNamedAnimDestructorHandler(GameRunNamedAnimDestructorHandler* pHandler); + void SetGameEntityRenderHandler(GameEntityRenderHandler* pHandler); + void SetFxSystemDestructionHandler(FxSystemDestructionHandler* pHandler); + void SetDrivebyAnimationHandler(DrivebyAnimationHandler* pHandler); + void SetPedStepHandler(PedStepHandler* pHandler); + void SetVehicleWeaponHitHandler(VehicleWeaponHitHandler* pHandler) override; + void SetAudioZoneRadioSwitchHandler(AudioZoneRadioSwitchHandler* pHandler); void AllowMouseMovement(bool bAllow); void DoSoundHacksOnLostFocus(bool bLostFocus); diff --git a/Client/multiplayer_sa/CMultiplayerSA_1.3.cpp b/Client/multiplayer_sa/CMultiplayerSA_1.3.cpp index 2cc722219e4..c4b017ba1bd 100644 --- a/Client/multiplayer_sa/CMultiplayerSA_1.3.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA_1.3.cpp @@ -159,13 +159,14 @@ void CMultiplayerSA::SetWaterCannonHitHandler(WaterCannonHitHandler* pHandler) CPedSAInterface* pPedHitByWaterCannonInterface = NULL; CVehicleSAInterface* pVehicleWithTheCannonMounted = NULL; +void* pWaterCannonHitBy = NULL; bool TriggerTheEvent() { // Is our handler alive if (m_pWaterCannonHitHandler) { // Return our handlers return - return !m_pWaterCannonHitHandler(pVehicleWithTheCannonMounted, pPedHitByWaterCannonInterface); + return !m_pWaterCannonHitHandler(pVehicleWithTheCannonMounted, pPedHitByWaterCannonInterface, pWaterCannonHitBy); } return false; } @@ -184,6 +185,7 @@ static void __declspec(naked) HOOK_CEventHitByWaterCannon() mov eax, [edx] mov pPedHitByWaterCannonInterface, esi mov pVehicleWithTheCannonMounted, eax + mov pWaterCannonHitBy, edx } // clang-format on if (TriggerTheEvent()) diff --git a/Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp b/Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp new file mode 100644 index 00000000000..9ad7032f8a3 --- /dev/null +++ b/Client/multiplayer_sa/CMultiplayerSA_WaterCannons.cpp @@ -0,0 +1,308 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto + * LICENSE: See LICENSE in the top level directory + * FILE: multiplayer_sa/CMultiplayerSA_WaterCannons.cpp + * PURPOSE: Script-owned water cannons, kept apart from the vehicle-owned ones + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ +#include "StdInc.h" +#include +#include +#include +#include + +// Native CWaterCannon (per-instance) methods. The Firetruck/SWAT tank's own water cannons live in +// a single fixed-size, byte-limited array (CWaterCannons::aCannons, see CGameSA.h) that every +// vehicle shares; a script spawning many custom cannons into that same array could starve a real +// vehicle of a slot. Calling these directly on our own, separately allocated instances instead +// keeps the two pools from ever competing for space. +#define FUNC_CWaterCannon_Constructor 0x728B10 +#define FUNC_CWaterCannon_Destructor 0x728B30 +#define FUNC_CWaterCannon_Init 0x728B40 +#define FUNC_CWaterCannon_UpdateNewInput 0x728C20 +#define FUNC_CWaterCannon_UpdateOncePerFrame 0x72A280 +#define FUNC_CWaterCannon_Render 0x728DA0 +#define SIZE_CWaterCannon 0x3CC + +// The jet's audio entity is embedded at this offset; its per-frame Service is what the stock +// CWaterCannons::Update calls for the vehicle-owned cannons, and Initialise wires its owner entity +#define OFFSET_CWaterCannon_Audio 0x32C +#define FUNC_CAEWaterCannonAudioEntity_Initialise 0x503060 +#define FUNC_CAEWaterCannonAudioEntity_Service 0x5030D0 + +// The jet-sound gate in Service is "section in use AND owner vehicle (cannon+0) != 0". Our +// vehicle-less cannons have no owner, so this je would mute them; NOP it to drop just the owner +// half of the gate. Firetruck cannons always have an owner, so their behaviour is unchanged. +#define PATCH_CWaterCannon_SoundGateOwnerCheck 0x503127 + +// Not a native constraint, since these live entirely outside the vehicles' own fixed-size array; +// just a sanity ceiling so a runaway script can't grow this list without bound. Update_OncePerFrame +// runs a world-wide ped scan for every entry every few frames, so this is also a soft cost limit. +static constexpr size_t MAX_CUSTOM_WATER_CANNONS = 256; + +struct SWaterCannonColor +{ + unsigned char r; + unsigned char g; + unsigned char b; + unsigned char a; +}; + +static std::vector customWaterCannons; +static std::map customWaterCannonColors; + +// Which of our cannons is being rendered right now, so the shared native Render colour code knows +// whose colour to use; null while the game renders the vehicle-owned cannons, which keep the +// native colour. Set around each of our own Render calls, never fished out of registers +static void* g_pRenderingCustomCannon = nullptr; + +void* CMultiplayerSA::CreateCustomWaterCannon() +{ + if (customWaterCannons.size() >= MAX_CUSTOM_WATER_CANNONS) + return nullptr; + + void* pCannon = malloc(SIZE_CWaterCannon); + memset(pCannon, 0, SIZE_CWaterCannon); + ((void(__thiscall*)(void*))FUNC_CWaterCannon_Constructor)(pCannon); + ((void(__thiscall*)(void*))FUNC_CWaterCannon_Init)(pCannon); + + // Point the embedded audio entity at this cannon and initialise it, so its per-frame Service + // has a valid owner to read from and passes its sound-bank checks; a vehicle-less cannon whose + // audio was never wired up this way stays silent + ((void(__thiscall*)(void*, void*))FUNC_CAEWaterCannonAudioEntity_Initialise)(static_cast(pCannon) + OFFSET_CWaterCannon_Audio, pCannon); + + customWaterCannons.push_back(pCannon); + return pCannon; +} + +void CMultiplayerSA::DestroyCustomWaterCannon(void* pCannon) +{ + if (!pCannon) + return; + + const auto iter = std::find(customWaterCannons.begin(), customWaterCannons.end(), pCannon); + if (iter == customWaterCannons.end()) + return; + + customWaterCannons.erase(iter); + customWaterCannonColors.erase(pCannon); + + ((void(__thiscall*)(void*))FUNC_CWaterCannon_Destructor)(pCannon); + free(pCannon); +} + +void CMultiplayerSA::UpdateCustomWaterCannon(void* pCannon, const CVector& vecStart, const CVector& vecVelocity) +{ + if (!pCannon) + return; + + ((void(__thiscall*)(void*, const CVector*, const CVector*))FUNC_CWaterCannon_UpdateNewInput)(pCannon, &vecStart, &vecVelocity); +} + +void CMultiplayerSA::SetCustomWaterCannonColor(void* pCannon, unsigned char ucRed, unsigned char ucGreen, unsigned char ucBlue, unsigned char ucAlpha) +{ + if (!pCannon) + return; + + customWaterCannonColors[pCannon] = {ucRed, ucGreen, ucBlue, ucAlpha}; +} + +void CMultiplayerSA::ResetCustomWaterCannonColor(void* pCannon) +{ + if (!pCannon) + return; + + customWaterCannonColors.erase(pCannon); +} + +static void UpdateAllCustomWaterCannons() +{ + std::int16_t index = 0; + for (void* pCannon : customWaterCannons) + { + // Service the audio BEFORE the per-frame update, the same order CWaterCannons::Update uses. + // UpdateOncePerFrame advances m_nSectionsCount onto a freshly cleared section, so servicing + // after it would see that section unused and restart the jet sound every frame (a machine + // gun stutter). + ((void(__thiscall*)(void*))FUNC_CAEWaterCannonAudioEntity_Service)(static_cast(pCannon) + OFFSET_CWaterCannon_Audio); + + ((void(__thiscall*)(void*, std::int16_t))FUNC_CWaterCannon_UpdateOncePerFrame)(pCannon, index++); + } +} + +static void RenderAllCustomWaterCannons() +{ + for (void* pCannon : customWaterCannons) + { + g_pRenderingCustomCannon = pCannon; + ((void(__thiscall*)(void*))FUNC_CWaterCannon_Render)(pCannon); + } + g_pRenderingCustomCannon = nullptr; +} + +////////////////////////////////////////////////////////////////////////////////////////// +// The single call site for CWaterCannons::Update, so our own custom cannons age, animate and +// extinguish fires alongside the vehicle-owned ones every frame, without touching their array. +////////////////////////////////////////////////////////////////////////////////////////// +// >>> 0x53C086 | E8 35 63 1E 00 | call 0x0072A3C0 +// 0x53C08B | E8 10 70 03 00 | call 0x005720A0 +static const DWORD FUNC_CWaterCannons_Update = 0x0072A3C0; +#define HOOKPOS_CWaterCannons_UpdateCustomDispatch 0x53C086 +#define HOOKSIZE_CWaterCannons_UpdateCustomDispatch 5 +static const DWORD CONTINUE_CWaterCannons_UpdateCustomDispatch = 0x53C08B; + +static void __declspec(naked) HOOK_CWaterCannons_UpdateCustomDispatch() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + // this call sits in a flat chain of independent, argument-less subsystem update calls; + // none of them read a register left behind by the previous one, so nothing needs saving + call UpdateAllCustomWaterCannons + + call FUNC_CWaterCannons_Update + jmp CONTINUE_CWaterCannons_UpdateCustomDispatch + } + // clang-format on +} + +////////////////////////////////////////////////////////////////////////////////////////// +// The single call site for CWaterCannons::Render, same reasoning as the Update dispatch above. +////////////////////////////////////////////////////////////////////////////////////////// +// >>> 0x53E1A5 | E8 86 D7 1F 00 | call 0x00729B30 +// 0x53E1AA | E8 B1 95 12 00 | call 0x006E7760 +static const DWORD FUNC_CWaterCannons_Render = 0x00729B30; +#define HOOKPOS_CWaterCannons_RenderCustomDispatch 0x53E1A5 +#define HOOKSIZE_CWaterCannons_RenderCustomDispatch 5 +static const DWORD CONTINUE_CWaterCannons_RenderCustomDispatch = 0x53E1AA; + +static void __declspec(naked) HOOK_CWaterCannons_RenderCustomDispatch() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + call RenderAllCustomWaterCannons + + call FUNC_CWaterCannons_Render + jmp CONTINUE_CWaterCannons_RenderCustomDispatch + } + // clang-format on +} + +////////////////////////////////////////////////////////////////////////////////////////// +// CWaterCannon::Render packs a hardcoded RGBA (R=200 G=200 B=255, alpha fades along the jet) +// into every vertex; override it per instance so setWaterCannonColor works. The block runs for +// every cannon, and eax holds the jet's fade alpha (0..64) on entry. The cannon being rendered +// is taken from g_pRenderingCustomCannon (set around our own Render calls), so a null one (a +// vehicle-owned cannon) or one without a custom colour keeps the native light blue. +////////////////////////////////////////////////////////////////////////////////////////// +// >>> 0x7290FF | C1 E0 18 | shl eax, 0x18 +// 0x729102 | 8B C8 | mov ecx, eax +// 0x729104 | 81 C9 FF C8 C8 00 | or ecx, 0x00C8C8FF (11 bytes total, rejoin at 0x72910A) +static DWORD __cdecl ComputeCustomWaterCannonColor(int iFadeAlpha) +{ + const DWORD dwFade = static_cast(iFadeAlpha & 0xFF) << 24; + + if (!g_pRenderingCustomCannon) + return dwFade | 0x00C8C8FF; // native default (R200 G200 B255) + + const auto iter = customWaterCannonColors.find(g_pRenderingCustomCannon); + if (iter == customWaterCannonColors.end()) + return dwFade | 0x00C8C8FF; + + // Keep the jet's own fade, scaled by the chosen alpha, and swap in the chosen RGB + const SWaterCannonColor& color = iter->second; + const DWORD dwAlpha = static_cast((iFadeAlpha * color.a / 255) & 0xFF) << 24; + return dwAlpha | (static_cast(color.r) << 16) | (static_cast(color.g) << 8) | static_cast(color.b); +} + +#define HOOKPOS_CWaterCannon_RenderColor 0x7290FF +#define HOOKSIZE_CWaterCannon_RenderColor 11 +static const DWORD RETURN_CWaterCannon_RenderColor = 0x72910A; + +static void __declspec(naked) HOOK_CWaterCannon_RenderColor() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + push edx // the lookup call clobbers edx, which is live past the rejoin + push eax // arg: fade alpha + call ComputeCustomWaterCannonColor + add esp, 4 + pop edx + mov ecx, eax // the vertex store loop at the rejoin expects the colour in ecx + mov eax, RETURN_CWaterCannon_RenderColor + jmp eax + } + // clang-format on +} + +////////////////////////////////////////////////////////////////////////////////////////// +// PushPeds only knocks down a ped standing within sqrt(5) (~2.24) units of one of the jet's own +// recorded droplet points, measured from the ped's feet - fine for a real vehicle cannon, since its +// arc has always sagged to near ground level by the time it reaches a target at a normal spray +// angle/range. A script can aim a custom cannon level with someone's chest instead, where the beam +// still visibly cuts off and splashes against them (Render's own hit test is a full-body raycast, +// not a feet-anchored point check), but PushPeds's tighter test can miss every single droplet. +// Widen the radius only when the cannon has no vehicle owner, so a real Firetruck/SWAT tank's own +// spray is untouched; a stale/dangling owner pointer from a destroyed vehicle can't misfire this, +// since PushPeds only ever runs against sections that are still genuinely in flight, and a vehicle +// cannon's owner field is only ever cleared once none of its sections are left. +////////////////////////////////////////////////////////////////////////////////////////// +// >>> 0x72987D | D8 1D 80 8C 85 00 | fcomp dword ptr [0x00858C80] +// 0x729883 | DF E0 | fnstsw ax +static constexpr float CUSTOM_WATER_CANNON_HIT_RADIUS_SQUARED = 1.0f; // radius 1.0, vs the native sqrt(5) (~2.24) + +#define HOOKPOS_CWaterCannon_PushPeds_HitRadius 0x72987D +#define HOOKSIZE_CWaterCannon_PushPeds_HitRadius 6 +static constexpr std::uintptr_t CONTINUE_CWaterCannon_PushPeds_HitRadius = 0x729883; + +static void __declspec(naked) HOOK_CWaterCannon_PushPeds_HitRadius() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + // [esp+0x1C] holds the cannon's own "this" pointer for the whole function (written once in + // the prologue); EDI, which held it earlier, has already been repurposed as a per-point + // record pointer by this point in the loop, so it can't be used here + mov ebx, [esp+0x1C] + cmp dword ptr [ebx], 0 + jne useOriginal + + fcomp CUSTOM_WATER_CANNON_HIT_RADIUS_SQUARED + jmp continue_ + + useOriginal: + fcomp ds:[0x858C80] // Original comparison, byte-identical to the stolen instruction + + continue_: + jmp CONTINUE_CWaterCannon_PushPeds_HitRadius + } + // clang-format on +} + +////////////////////////////////////////////////////////////////////////////////////////// +// +// CMultiplayerSA::InitHooks_WaterCannons +// +////////////////////////////////////////////////////////////////////////////////////////// +void CMultiplayerSA::InitHooks_WaterCannons() +{ + EZHookInstall(CWaterCannons_UpdateCustomDispatch); + EZHookInstall(CWaterCannons_RenderCustomDispatch); + EZHookInstall(CWaterCannon_RenderColor); + EZHookInstall(CWaterCannon_PushPeds_HitRadius); + + MemSet(reinterpret_cast(PATCH_CWaterCannon_SoundGateOwnerCheck), 0x90, 2); +} diff --git a/Client/sdk/multiplayer/CMultiplayer.h b/Client/sdk/multiplayer/CMultiplayer.h index 28d2e074088..b8b4d6dffa9 100644 --- a/Client/sdk/multiplayer/CMultiplayer.h +++ b/Client/sdk/multiplayer/CMultiplayer.h @@ -123,7 +123,7 @@ typedef bool(VehicleDamageHandler)(CEntitySAInterface* pVehicle, float fLoss, CE typedef bool(HeliKillHandler)(class CVehicleSAInterface* pVehicle, class CEntitySAInterface* pHitInterface); typedef bool(ObjectDamageHandler)(class CObjectSAInterface* pObject, float fLoss, class CEntitySAInterface* pAttacker); typedef bool(ObjectBreakHandler)(class CObjectSAInterface* pObject, class CEntitySAInterface* pAttacker); -typedef bool(WaterCannonHitHandler)(class CVehicleSAInterface* pCannonVehicle, class CPedSAInterface* pHitPed); +typedef bool(WaterCannonHitHandler)(class CVehicleSAInterface* pCannonVehicle, class CPedSAInterface* pHitPed, void* pWaterCannonInterface); typedef bool(VehicleFellThroughMapHandler)(class CVehicleSAInterface* pVehicle); typedef void(GameObjectDestructHandler)(CEntitySAInterface* pObject); typedef void(GameVehicleDestructHandler)(CEntitySAInterface* pVehicle); @@ -218,38 +218,47 @@ class CMultiplayer virtual void DisableCloseRangeDamage(bool bDisable) = 0; virtual void DisableBadDrivebyHitboxes(bool bDisable) = 0; - virtual bool GetExplosionsDisabled() = 0; - virtual void DisableExplosions(bool bDisabled) = 0; - virtual void SetExplosionHandler(ExplosionHandler* pExplosionHandler) = 0; - virtual void SetBreakTowLinkHandler(BreakTowLinkHandler* pBreakTowLinkHandler) = 0; - virtual void SetDamageHandler(DamageHandler* pDamageHandler) = 0; - virtual void SetDeathHandler(DeathHandler* pDeathHandler) = 0; - virtual void SetFireHandler(FireHandler* pFireHandler) = 0; - virtual void SetProcessCamHandler(ProcessCamHandler* pProcessCamHandler) = 0; - virtual void SetChokingHandler(ChokingHandler* pChokingHandler) = 0; - virtual void SetProjectileHandler(ProjectileHandler* pProjectileHandler) = 0; - virtual void SetProjectileStopHandler(ProjectileStopHandler* pProjectileHandler) = 0; - virtual void SetPreWeatherUpdateHandler(PreWeatherUpdateHandler* pHandler) = 0; - virtual void SetPreWorldProcessHandler(PreWorldProcessHandler* pHandler) = 0; - virtual void SetPostWorldProcessHandler(PostWorldProcessHandler* pHandler) = 0; - virtual void SetPostWorldProcessPedsAfterPreRenderHandler(PostWorldProcessPedsAfterPreRenderHandler* pHandler) = 0; - virtual void SetIdleHandler(IdleHandler* pHandler) = 0; - virtual void SetPreFxRenderHandler(PreFxRenderHandler* pHandler) = 0; - virtual void SetPostColorFilterRenderHandler(PostColorFilterRenderHandler* pHandler) = 0; - virtual void SetPreHudRenderHandler(PreHudRenderHandler* pHandler) = 0; - virtual void DisableCallsToCAnimBlendNode(bool bDisableCalls) = 0; - virtual void SetCAnimBlendAssocDestructorHandler(CAnimBlendAssocDestructorHandler* pHandler) = 0; - virtual void SetAddAnimationHandler(AddAnimationHandler* pHandler) = 0; - virtual void SetAddAnimationAndSyncHandler(AddAnimationAndSyncHandler* pHandler) = 0; - virtual void SetAssocGroupCopyAnimationHandler(AssocGroupCopyAnimationHandler* pHandler) = 0; - virtual void SetBlendAnimationHierarchyHandler(BlendAnimationHierarchyHandler* pHandler) = 0; - virtual void SetProcessCollisionHandler(ProcessCollisionHandler* pHandler) = 0; - virtual void SetVehicleCollisionHandler(VehicleCollisionHandler* pHandler) = 0; - virtual void SetVehicleDamageHandler(VehicleDamageHandler* pHandler) = 0; - virtual void SetHeliKillHandler(HeliKillHandler* pHandler) = 0; - virtual void SetObjectDamageHandler(ObjectDamageHandler* pHandler) = 0; - virtual void SetObjectBreakHandler(ObjectBreakHandler* pHandler) = 0; - virtual void SetWaterCannonHitHandler(WaterCannonHitHandler* pHandler) = 0; + virtual bool GetExplosionsDisabled() = 0; + virtual void DisableExplosions(bool bDisabled) = 0; + virtual void SetExplosionHandler(ExplosionHandler* pExplosionHandler) = 0; + virtual void SetBreakTowLinkHandler(BreakTowLinkHandler* pBreakTowLinkHandler) = 0; + virtual void SetDamageHandler(DamageHandler* pDamageHandler) = 0; + virtual void SetDeathHandler(DeathHandler* pDeathHandler) = 0; + virtual void SetFireHandler(FireHandler* pFireHandler) = 0; + virtual void SetProcessCamHandler(ProcessCamHandler* pProcessCamHandler) = 0; + virtual void SetChokingHandler(ChokingHandler* pChokingHandler) = 0; + virtual void SetProjectileHandler(ProjectileHandler* pProjectileHandler) = 0; + virtual void SetProjectileStopHandler(ProjectileStopHandler* pProjectileHandler) = 0; + virtual void SetPreWeatherUpdateHandler(PreWeatherUpdateHandler* pHandler) = 0; + virtual void SetPreWorldProcessHandler(PreWorldProcessHandler* pHandler) = 0; + virtual void SetPostWorldProcessHandler(PostWorldProcessHandler* pHandler) = 0; + virtual void SetPostWorldProcessPedsAfterPreRenderHandler(PostWorldProcessPedsAfterPreRenderHandler* pHandler) = 0; + virtual void SetIdleHandler(IdleHandler* pHandler) = 0; + virtual void SetPreFxRenderHandler(PreFxRenderHandler* pHandler) = 0; + virtual void SetPostColorFilterRenderHandler(PostColorFilterRenderHandler* pHandler) = 0; + virtual void SetPreHudRenderHandler(PreHudRenderHandler* pHandler) = 0; + virtual void DisableCallsToCAnimBlendNode(bool bDisableCalls) = 0; + virtual void SetCAnimBlendAssocDestructorHandler(CAnimBlendAssocDestructorHandler* pHandler) = 0; + virtual void SetAddAnimationHandler(AddAnimationHandler* pHandler) = 0; + virtual void SetAddAnimationAndSyncHandler(AddAnimationAndSyncHandler* pHandler) = 0; + virtual void SetAssocGroupCopyAnimationHandler(AssocGroupCopyAnimationHandler* pHandler) = 0; + virtual void SetBlendAnimationHierarchyHandler(BlendAnimationHierarchyHandler* pHandler) = 0; + virtual void SetProcessCollisionHandler(ProcessCollisionHandler* pHandler) = 0; + virtual void SetVehicleCollisionHandler(VehicleCollisionHandler* pHandler) = 0; + virtual void SetVehicleDamageHandler(VehicleDamageHandler* pHandler) = 0; + virtual void SetHeliKillHandler(HeliKillHandler* pHandler) = 0; + virtual void SetObjectDamageHandler(ObjectDamageHandler* pHandler) = 0; + virtual void SetObjectBreakHandler(ObjectBreakHandler* pHandler) = 0; + virtual void SetWaterCannonHitHandler(WaterCannonHitHandler* pHandler) = 0; + + // A script-owned water cannon, driven by its own list of native CWaterCannon instances kept + // entirely apart from the vehicle-owned ones (the Firetruck/SWAT tank's own aCannons array), + // so scripts spawning a lot of them can never starve a real vehicle's water cannon of a slot. + virtual void* CreateCustomWaterCannon() = 0; + virtual void DestroyCustomWaterCannon(void* pCannon) = 0; + virtual void UpdateCustomWaterCannon(void* pCannon, const CVector& vecStart, const CVector& vecVelocity) = 0; + virtual void SetCustomWaterCannonColor(void* pCannon, unsigned char ucRed, unsigned char ucGreen, unsigned char ucBlue, unsigned char ucAlpha) = 0; + virtual void ResetCustomWaterCannonColor(void* pCannon) = 0; virtual void SetVehicleFellThroughMapHandler(VehicleFellThroughMapHandler* pHandler) = 0; virtual void SetGameObjectDestructHandler(GameObjectDestructHandler* pHandler) = 0; virtual void SetGameVehicleDestructHandler(GameVehicleDestructHandler* pHandler) = 0;