#include #include #include #include #include #include #include #include #include #include #include #include #include CombatService::CombatService(World& aWorld, TransportService& aTransport, entt::dispatcher& aDispatcher) : m_world(aWorld) , m_transport(aTransport) { m_updateConnection = aDispatcher.sink().connect<&CombatService::OnUpdate>(this); m_hitConnection = aDispatcher.sink().connect<&CombatService::OnHitEvent>(this); m_localComponentRemoved = m_world.on_destroy().connect<&CombatService::OnLocalComponentRemoved>(this); m_projectileLaunchedConnection = aDispatcher.sink().connect<&CombatService::OnProjectileLaunchedEvent>(this); m_projectileLaunchConnection = aDispatcher.sink().connect<&CombatService::OnNotifyProjectileLaunch>(this); } void CombatService::OnUpdate(const UpdateEvent& acEvent) const noexcept { RunTargetUpdates(static_cast(acEvent.Delta)); } void CombatService::OnLocalComponentRemoved(entt::registry& aRegistry, entt::entity aEntity) const noexcept { m_world.remove(aEntity); } void CombatService::OnProjectileLaunchedEvent(const ProjectileLaunchedEvent& acEvent) const noexcept { ModSystem& modSystem = m_world.Get().GetModSystem(); uint32_t shooterFormId = acEvent.ShooterID; auto view = m_world.view(); const auto shooterEntityIt = std::find_if(std::begin(view), std::end(view), [shooterFormId, view](entt::entity entity) { return view.get(entity).Id == shooterFormId; }); if (shooterEntityIt == std::end(view)) return; LocalComponent& localComponent = view.get(*shooterEntityIt); ProjectileLaunchRequest request{}; request.OriginX = acEvent.Origin.x; request.OriginY = acEvent.Origin.y; request.OriginZ = acEvent.Origin.z; modSystem.GetServerModId(acEvent.ProjectileBaseID, request.ProjectileBaseID); modSystem.GetServerModId(acEvent.WeaponID, request.WeaponID); modSystem.GetServerModId(acEvent.AmmoID, request.AmmoID); request.ShooterID = localComponent.Id; request.ZAngle = acEvent.ZAngle; request.XAngle = acEvent.XAngle; request.YAngle = acEvent.YAngle; modSystem.GetServerModId(acEvent.ParentCellID, request.ParentCellID); modSystem.GetServerModId(acEvent.SpellID, request.SpellID); request.CastingSource = acEvent.CastingSource; request.Area = acEvent.Area; request.Power = acEvent.Power; request.Scale = acEvent.Scale; request.AlwaysHit = acEvent.AlwaysHit; request.NoDamageOutsideCombat = acEvent.NoDamageOutsideCombat; request.AutoAim = acEvent.AutoAim; request.DeferInitialization = acEvent.DeferInitialization; request.ForceConeOfFire = acEvent.ForceConeOfFire; request.UnkBool1 = acEvent.UnkBool1; request.UnkBool2 = acEvent.UnkBool2; m_transport.Send(request); } void CombatService::OnNotifyProjectileLaunch(const NotifyProjectileLaunch& acMessage) const noexcept { ModSystem& modSystem = World::Get().GetModSystem(); auto remoteView = m_world.view(); const auto remoteIt = std::find_if(std::begin(remoteView), std::end(remoteView), [remoteView, Id = acMessage.ShooterID](auto entity) { return remoteView.get(entity).Id == Id; }); if (remoteIt == std::end(remoteView)) { spdlog::warn("Shooter with remote id {:X} not found.", acMessage.ShooterID); return; } FormIdComponent formIdComponent = remoteView.get(*remoteIt); Projectile::LaunchData launchData{}; // Projectile::Launch relies on pParentCell being valid. // TODO: it's possible that more of these values must have a value, should probably check for that in the game code. const uint32_t cParentCellId = modSystem.GetGameId(acMessage.ParentCellID); launchData.pParentCell = Cast(TESForm::GetById(cParentCellId)); if (!launchData.pParentCell) { spdlog::warn("Cannot launch projectile, invalid parent cell: {:X}", cParentCellId); return; } launchData.pShooter = Cast(TESForm::GetById(formIdComponent.Id)); launchData.Origin.x = acMessage.OriginX; launchData.Origin.y = acMessage.OriginY; launchData.Origin.z = acMessage.OriginZ; const uint32_t cProjectileBaseId = modSystem.GetGameId(acMessage.ProjectileBaseID); launchData.pProjectileBase = TESForm::GetById(cProjectileBaseId); const uint32_t cFromWeaponId = modSystem.GetGameId(acMessage.WeaponID); launchData.pFromWeapon = Cast(TESForm::GetById(cFromWeaponId)); const uint32_t cFromAmmoId = modSystem.GetGameId(acMessage.AmmoID); launchData.pFromAmmo = Cast(TESForm::GetById(cFromAmmoId)); launchData.fZAngle = acMessage.ZAngle; launchData.fXAngle = acMessage.XAngle; launchData.fYAngle = acMessage.YAngle; const uint32_t cSpellId = modSystem.GetGameId(acMessage.SpellID); launchData.pSpell = Cast(TESForm::GetById(cSpellId)); launchData.eCastingSource = (MagicSystem::CastingSource)acMessage.CastingSource; launchData.iArea = acMessage.Area; launchData.fPower = acMessage.Power; launchData.fScale = acMessage.Scale; launchData.bAlwaysHit = acMessage.AlwaysHit; launchData.bNoDamageOutsideCombat = acMessage.NoDamageOutsideCombat; launchData.bAutoAim = acMessage.AutoAim; launchData.bForceConeOfFire = acMessage.ForceConeOfFire; // always use origin, or it'll recalculate it and it desyncs launchData.bUseOrigin = true; launchData.bUnkBool1 = acMessage.UnkBool1; launchData.bUnkBool2 = acMessage.UnkBool2; BSPointerHandle result; Projectile::Launch(&result, launchData); } void CombatService::OnHitEvent(const HitEvent& acEvent) const noexcept { #if 0 if (!m_transport.IsConnected()) return; // The targeting system does not apply to players, and only local actors should be considered. auto* pHittee = Cast(TESForm::GetById(acEvent.HitteeId)); if (!pHittee || pHittee->GetExtension()->IsPlayer() || pHittee->GetExtension()->IsRemote()) return; // NPCs should not affect targeting. auto* pHitter = Cast(TESForm::GetById(acEvent.HitterId)); if (!pHitter || !pHitter->GetExtension()->IsPlayer()) return; // If the target is not in combat, don't start combat, let the game take care of that first. if (!pHittee->IsInCombat()) return; auto view = m_world.view(entt::exclude); const auto hitteeIt = std::find_if(std::begin(view), std::end(view), [id = acEvent.HitteeId, view](entt::entity entity) { return view.get(entity).Id == id; }); if (hitteeIt == std::end(view)) { spdlog::warn(__FUNCTION__ ": hittee form id component not found, form id: {:X}", acEvent.HitterId); return; } if (m_world.any_of(*hitteeIt)) return; m_world.emplace_or_replace(*hitteeIt, acEvent.HitterId); pHittee->SetCombatTargetEx(pHitter); #endif } void CombatService::RunTargetUpdates(const float acDelta) const noexcept { #if 0 static std::chrono::steady_clock::time_point lastSendTimePoint; constexpr auto cDelayBetweenUpdates = 200ms; const auto now = std::chrono::steady_clock::now(); if (now - lastSendTimePoint < cDelayBetweenUpdates) return; lastSendTimePoint = now; const auto view = m_world.view(); Vector toRemove{}; for (const auto entity : view) { auto& combatComponent = view.get(entity); combatComponent.Timer = combatComponent.Timer - acDelta; if (combatComponent.Timer <= 0.f) { toRemove.push_back(entity); continue; } auto* pTarget = Cast(TESForm::GetById(combatComponent.TargetFormId)); if (!pTarget) { spdlog::warn(__FUNCTION__ ": combat target not found, form id {:X}", combatComponent.TargetFormId); toRemove.push_back(entity); continue; } const auto& formIdComponent = view.get(entity); auto* pActor = Cast(TESForm::GetById(formIdComponent.Id)); if (!pActor) { spdlog::error(__FUNCTION__ ": actor not found, form id {:X}", formIdComponent.Id); toRemove.push_back(entity); continue; } } for (const auto entity : toRemove) m_world.remove(entity); #endif }