diff --git a/Code/client/Games/Skyrim/Actor.cpp b/Code/client/Games/Skyrim/Actor.cpp index 9e1933e86..604c64899 100644 --- a/Code/client/Games/Skyrim/Actor.cpp +++ b/Code/client/Games/Skyrim/Actor.cpp @@ -976,7 +976,8 @@ bool TP_MAKE_THISCALL(HookDamageActor, Actor, float aDamage, Actor* apHitter, bo { if (!World::Get().GetServerSettings().PvpEnabled) { - if (apHitter && apHitter->GetExtension()->IsRemotePlayer()) + // A remote player's summon counts too, its copy swings at whatever the owner's client tells it to. + if (apHitter && (apHitter->GetExtension()->IsRemotePlayer() || (apHitter->GetExtension()->IsRemote() && apHitter->IsPlayerSummon()))) return false; } diff --git a/Code/client/Games/Skyrim/Magic/MagicTarget.cpp b/Code/client/Games/Skyrim/Magic/MagicTarget.cpp index fe6a5ac5b..6ee5a0149 100644 --- a/Code/client/Games/Skyrim/Magic/MagicTarget.cpp +++ b/Code/client/Games/Skyrim/Magic/MagicTarget.cpp @@ -118,6 +118,10 @@ bool TP_MAKE_THISCALL(HookAddTarget, MagicTarget, MagicTarget::AddTargetData& ar if (!arData.pCaster) return false; + // The spell doesn't land here, but it does make the two players enemies. + if (arData.pSpell->iHostileCount > 0 && arData.pCaster->GetExtension()->IsLocalPlayer()) + World::Get().GetRunner().Trigger(HitEvent(arData.pCaster->formID, pTargetActor->formID)); + if (!arData.pSpell->IsHealingSpell() && !arData.pSpell->IsBuffSpell()) return false; @@ -150,6 +154,14 @@ bool TP_MAKE_THISCALL(HookAddTarget, MagicTarget, MagicTarget::AddTargetData& ar // Heal and buff spells are already synced by the caster. if (arData.pSpell->IsHealingSpell() || arData.pSpell->IsBuffSpell()) return false; + + if (arData.pSpell->iHostileCount > 0) + World::Get().GetRunner().Trigger(HitEvent(arData.pCaster->formID, pTargetActor->formID)); + } + else if (pCasterExtension->IsRemote() && arData.pCaster->IsPlayerSummon() && arData.pSpell->iHostileCount > 0) + { + if (!World::Get().GetServerSettings().PvpEnabled) + return false; } } diff --git a/Code/client/Services/CombatService.h b/Code/client/Services/CombatService.h index 4e02e701c..75f372cc4 100644 --- a/Code/client/Services/CombatService.h +++ b/Code/client/Services/CombatService.h @@ -26,6 +26,10 @@ struct CombatService void RunTargetUpdates(const float acDelta) const noexcept; + // Local summons attack a remote player that is fighting the local player, for as long as the fight lasts. + void SendSummonsAtEnemyPlayer(const HitEvent& acEvent) const noexcept; + void RunSummonTargetUpdates(const float acDelta) const noexcept; + private: World& m_world; TransportService& m_transport; diff --git a/Code/client/Services/Generic/CombatService.cpp b/Code/client/Services/Generic/CombatService.cpp index df07fd327..4ac9ac56b 100644 --- a/Code/client/Services/Generic/CombatService.cpp +++ b/Code/client/Services/Generic/CombatService.cpp @@ -15,6 +15,13 @@ #include #include #include +#include + +namespace +{ +// How long after the last hit between two players the local summons keep attacking the other player. +constexpr float kSummonAggroSeconds = 30.f; +} CombatService::CombatService(World& aWorld, TransportService& aTransport, entt::dispatcher& aDispatcher) : m_world(aWorld) @@ -30,6 +37,7 @@ CombatService::CombatService(World& aWorld, TransportService& aTransport, entt:: void CombatService::OnUpdate(const UpdateEvent& acEvent) const noexcept { RunTargetUpdates(static_cast(acEvent.Delta)); + RunSummonTargetUpdates(static_cast(acEvent.Delta)); } void CombatService::OnLocalComponentRemoved(entt::registry& aRegistry, entt::entity aEntity) const noexcept @@ -162,6 +170,8 @@ void CombatService::OnNotifyProjectileLaunch(const NotifyProjectileLaunch& acMes void CombatService::OnHitEvent(const HitEvent& acEvent) const noexcept { + SendSummonsAtEnemyPlayer(acEvent); + #if 0 if (!m_transport.IsConnected()) return; @@ -199,6 +209,71 @@ void CombatService::OnHitEvent(const HitEvent& acEvent) const noexcept #endif } +void CombatService::SendSummonsAtEnemyPlayer(const HitEvent& acEvent) const noexcept +{ + if (!m_world.GetServerSettings().PvpEnabled) + return; + + Actor* pHitter = Cast(TESForm::GetById(acEvent.HitterId)); + Actor* pHittee = Cast(TESForm::GetById(acEvent.HitteeId)); + if (!pHitter || !pHittee) + return; + + Actor* pEnemy = nullptr; + if (pHitter->GetExtension()->IsRemotePlayer() && pHittee->GetExtension()->IsLocalPlayer()) + pEnemy = pHitter; + else if (pHitter->GetExtension()->IsLocalPlayer() && pHittee->GetExtension()->IsRemotePlayer()) + pEnemy = pHittee; + + if (!pEnemy || pEnemy->IsDead()) + return; + + auto view = m_world.view(); + for (const auto entity : view) + { + Actor* pSummon = Cast(TESForm::GetById(view.get(entity).Id)); + if (!pSummon || !pSummon->IsPlayerSummon() || pSummon->IsDead()) + continue; + + const auto* pCombatComponent = m_world.try_get(entity); + const bool cIsAttackingEnemy = pCombatComponent && pCombatComponent->TargetFormId == pEnemy->formID && pSummon->IsInCombat(); + + m_world.emplace_or_replace(entity, pEnemy->formID).Timer = kSummonAggroSeconds; + + // Commanded actors are only hostile to whoever is fighting their commander according to the target's own + // combat group, and a remote copy doesn't have one, so the target has to be forced. + if (!cIsAttackingEnemy) + pSummon->StartCombat(pEnemy); + } +} + +void CombatService::RunSummonTargetUpdates(const float acDelta) const noexcept +{ + const bool cIsPvpEnabled = m_world.GetServerSettings().PvpEnabled; + + Vector toRemove{}; + + auto view = m_world.view(); + for (const auto entity : view) + { + auto& combatComponent = view.get(entity); + combatComponent.Timer -= acDelta; + + Actor* pEnemy = Cast(TESForm::GetById(combatComponent.TargetFormId)); + if (cIsPvpEnabled && combatComponent.Timer > 0.f && pEnemy && !pEnemy->IsDead()) + continue; + + // Combat also ends against anyone else here, but the summon picks actual enemies right back up. + if (Actor* pSummon = Cast(TESForm::GetById(view.get(entity).Id))) + pSummon->StopCombat(); + + toRemove.push_back(entity); + } + + for (const auto entity : toRemove) + m_world.remove(entity); +} + void CombatService::RunTargetUpdates(const float acDelta) const noexcept { #if 0