Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Code/client/Games/Skyrim/Actor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
12 changes: 12 additions & 0 deletions Code/client/Games/Skyrim/Magic/MagicTarget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
}

Expand Down
4 changes: 4 additions & 0 deletions Code/client/Services/CombatService.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
75 changes: 75 additions & 0 deletions Code/client/Services/Generic/CombatService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
#include <Forms/TESObjectWEAP.h>
#include <Forms/TESAmmo.h>
#include <Games/ActorExtension.h>
#include <Actor.h>

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)
Expand All @@ -30,6 +37,7 @@ CombatService::CombatService(World& aWorld, TransportService& aTransport, entt::
void CombatService::OnUpdate(const UpdateEvent& acEvent) const noexcept
{
RunTargetUpdates(static_cast<float>(acEvent.Delta));
RunSummonTargetUpdates(static_cast<float>(acEvent.Delta));
}

void CombatService::OnLocalComponentRemoved(entt::registry& aRegistry, entt::entity aEntity) const noexcept
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<Actor>(TESForm::GetById(acEvent.HitterId));
Actor* pHittee = Cast<Actor>(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<FormIdComponent, LocalComponent>();
for (const auto entity : view)
{
Actor* pSummon = Cast<Actor>(TESForm::GetById(view.get<FormIdComponent>(entity).Id));
if (!pSummon || !pSummon->IsPlayerSummon() || pSummon->IsDead())
continue;

const auto* pCombatComponent = m_world.try_get<CombatComponent>(entity);
const bool cIsAttackingEnemy = pCombatComponent && pCombatComponent->TargetFormId == pEnemy->formID && pSummon->IsInCombat();

m_world.emplace_or_replace<CombatComponent>(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<entt::entity> toRemove{};

auto view = m_world.view<FormIdComponent, CombatComponent>();
for (const auto entity : view)
{
auto& combatComponent = view.get<CombatComponent>(entity);
combatComponent.Timer -= acDelta;

Actor* pEnemy = Cast<Actor>(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<Actor>(TESForm::GetById(view.get<FormIdComponent>(entity).Id)))
pSummon->StopCombat();

toRemove.push_back(entity);
}

for (const auto entity : toRemove)
m_world.remove<CombatComponent>(entity);
}

void CombatService::RunTargetUpdates(const float acDelta) const noexcept
{
#if 0
Expand Down