diff --git a/code/components/citizen-resources-core/include/ResourceEventComponent.h b/code/components/citizen-resources-core/include/ResourceEventComponent.h index 36f5cef877..df3832d604 100644 --- a/code/components/citizen-resources-core/include/ResourceEventComponent.h +++ b/code/components/citizen-resources-core/include/ResourceEventComponent.h @@ -118,6 +118,13 @@ class RESOURCES_CORE_EXPORT ResourceEventManagerComponent : public fwRefCountabl // void AddResourceHandledEvent(const std::string& resourceName, const std::string& eventName); + // + // An event to observe which events resources subscribe to, so that expensive event sources + // can cache whether producing an event is worth it. + // Arguments: eventName, resourceName + // + fwEvent OnResourceHandledEvent; + // // An event to handle event execution externally. // Arguments: eventName, eventPayload, eventSource, eventCanceled diff --git a/code/components/citizen-resources-core/src/ResourceEventComponent.cpp b/code/components/citizen-resources-core/src/ResourceEventComponent.cpp index b789e62f26..f36aa92198 100644 --- a/code/components/citizen-resources-core/src/ResourceEventComponent.cpp +++ b/code/components/citizen-resources-core/src/ResourceEventComponent.cpp @@ -177,8 +177,11 @@ void ResourceEventManagerComponent::AddResourceHandledEvent(const std::string& r } m_eventResources.emplace(eventName, resourceName); + + OnResourceHandledEvent(eventName, resourceName); } + bool ResourceEventManagerComponent::TriggerEvent(const std::string& eventName, const std::string& eventPayload, const std::string& eventSource /* = std::string() */, ResourceEventComponent* filter /* = nullptr*/) { // add a value to signify event cancelation diff --git a/code/components/citizen-server-impl/include/state/ServerGameState.h b/code/components/citizen-server-impl/include/state/ServerGameState.h index 51f0316dbc..5e037826d3 100644 --- a/code/components/citizen-server-impl/include/state/ServerGameState.h +++ b/code/components/citizen-server-impl/include/state/ServerGameState.h @@ -1569,6 +1569,8 @@ class ServerGameState : public ServerGameStatePublic, public fx::IAttached blockedEvents; std::shared_mutex blockedEventsMutex; @@ -1588,6 +1590,16 @@ class ServerGameState : public ServerGameStatePublic, public fx::IAttached m_pedEventFlags{ 0 }; + #ifdef USE_ASYNC_SCL_POSTING std::unique_ptr m_tg; #endif diff --git a/code/components/citizen-server-impl/src/state/ServerGameState.cpp b/code/components/citizen-server-impl/src/state/ServerGameState.cpp index 3052fc9c35..4a88819010 100644 --- a/code/components/citizen-server-impl/src/state/ServerGameState.cpp +++ b/code/components/citizen-server-impl/src/state/ServerGameState.cpp @@ -3518,9 +3518,23 @@ bool ServerGameState::ProcessClonePacket(const fx::ClientSharedPtr& client, rl:: { if (parsingType == 2) { + auto healthNode = (entity->type == sync::NetObjEntityType::Ped || entity->type == sync::NetObjEntityType::Player) + ? syncTree->GetPedHealth() + : nullptr; + + // maxHealth is only 0 until the node was parsed for the first time + bool hadHealth = healthNode && healthNode->maxHealth != 0; + int oldHealth = hadHealth ? healthNode->health : 0; + int oldArmour = hadHealth ? healthNode->armour : 0; + syncTree->ParseSync(state); entity->hasSynced = true; + + if (hadHealth) + { + HandlePedHealthUpdate(entity, healthNode, oldHealth, oldArmour); + } } else if (parsingType == 1) { @@ -3708,6 +3722,83 @@ bool ServerGameState::ProcessClonePacket(const fx::ClientSharedPtr& client, rl:: return true; } +void ServerGameState::HandlePedHealthUpdate(const fx::sync::SyncEntityPtr& entity, fx::sync::CPedHealthNodeData* healthNode, int oldHealth, int oldArmour) +{ + int health = healthNode->health; + int armour = healthNode->armour; + + if (health == oldHealth && armour == oldArmour) + { + return; + } + + bool died = oldHealth > 0 && health <= 0; + + uint32_t flags = m_pedEventFlags.load(std::memory_order_relaxed); + bool wantsHealthChanged = (flags & PedEventHealthChanged) != 0; + bool wantsDeath = died && (flags & PedEventDeath) != 0; + + // don't pay for events nobody is listening to + if (!wantsHealthChanged && !wantsDeath) + { + return; + } + + // the node may be parsed again before the callback runs, so pass values, not the node + uint32_t weaponHash = healthNode->causeOfDeath; + int sourceOfDamage = healthNode->sourceOfDamage; + + gscomms_execute_callback_on_main_thread([this, entity, wantsHealthChanged, wantsDeath, oldHealth, health, oldArmour, armour, sourceOfDamage, weaponHash]() + { + auto evComponent = m_instance->GetComponent()->GetComponent(); + auto ped = MakeScriptHandle(entity); + + uint32_t attacker = 0; + + if (sourceOfDamage != 0) + { + if (auto attackerEntity = GetEntity(0, sourceOfDamage)) + { + attacker = MakeScriptHandle(attackerEntity); + } + } + + if (wantsHealthChanged) + { + /*NETEV onPedHealthChanged SERVER + /#* + * Triggered when the health or armour of a ped changed. + * + * @param ped - The handle of the ped whose health or armour changed. + * @param oldHealth - The health the ped had before the change. + * @param health - The health the ped has after the change. + * @param oldArmour - The armour the ped had before the change. + * @param armour - The armour the ped has after the change. + * @param attacker - The handle of the entity that caused the damage, or 0 if unknown. + * @param weaponHash - The hash of the weapon that caused the damage, or 0 if unknown. + #/ + declare function onPedHealthChanged(ped: number, oldHealth: number, health: number, oldArmour: number, armour: number, attacker: number, weaponHash: number): void; + */ + evComponent->TriggerEvent2("onPedHealthChanged", {}, ped, oldHealth, health, oldArmour, armour, attacker, weaponHash); + } + + if (wantsDeath) + { + /*NETEV onPedDeath SERVER + /#* + * Triggered when a ped died. + * + * @param ped - The handle of the ped that died. + * @param attacker - The handle of the entity that killed the ped, or 0 if unknown. + * @param weaponHash - The hash of the weapon that killed the ped, or 0 if unknown. + #/ + declare function onPedDeath(ped: number, attacker: number, weaponHash: number): void; + */ + evComponent->TriggerEvent2("onPedDeath", {}, ped, attacker, weaponHash); + } + }); +} + bool ServerGameState::ValidateEntity(EntityLockdownMode entityLockdownMode, const fx::sync::SyncEntityPtr& entity) { // can't validate an entity without sync tree @@ -4571,6 +4662,29 @@ void ServerGameState::AttachToObject(fx::ServerInstanceBase* instance) { m_instance = instance; + instance->GetComponent()->GetComponent()->OnResourceHandledEvent.Connect([this](const std::string& eventName, const std::string&) + { + uint32_t flags = 0; + + if (eventName == "onPedHealthChanged") + { + flags = PedEventHealthChanged; + } + else if (eventName == "onPedDeath") + { + flags = PedEventDeath; + } + else if (eventName == "*") + { + flags = PedEventHealthChanged | PedEventDeath; + } + + if (flags) + { + m_pedEventFlags.fetch_or(flags, std::memory_order_relaxed); + } + }); + m_lockdownModeVar = instance->AddVariable("sv_entityLockdown", ConVar_None, m_entityLockdownMode, &m_entityLockdownMode); m_stateBagStrictModeVar = instance->AddVariable("sv_stateBagStrictMode", ConVar_None, m_stateBagStrictMode, &m_stateBagStrictMode);