#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include PlayerService::PlayerService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept : m_world(aWorld) , m_dispatcher(aDispatcher) , m_transport(aTransport) { m_updateConnection = m_dispatcher.sink().connect<&PlayerService::OnUpdate>(this); m_connectedConnection = m_dispatcher.sink().connect<&PlayerService::OnConnected>(this); m_disconnectedConnection = m_dispatcher.sink().connect<&PlayerService::OnDisconnected>(this); m_settingsConnection = m_dispatcher.sink().connect<&PlayerService::OnServerSettingsReceived>(this); m_notifyRespawnConnection = m_dispatcher.sink().connect<&PlayerService::OnNotifyPlayerRespawn>(this); m_gridCellChangeConnection = m_dispatcher.sink().connect<&PlayerService::OnGridCellChangeEvent>(this); m_cellChangeConnection = m_dispatcher.sink().connect<&PlayerService::OnCellChangeEvent>(this); m_playerDialogueConnection = m_dispatcher.sink().connect<&PlayerService::OnPlayerDialogueEvent>(this); m_playerLevelConnection = m_dispatcher.sink().connect<&PlayerService::OnPlayerLevelEvent>(this); m_partyJoinedConnection = aDispatcher.sink().connect<&PlayerService::OnPartyJoinedEvent>(this); m_partyLeftConnection = aDispatcher.sink().connect<&PlayerService::OnPartyLeftEvent>(this); } void PlayerService::OnUpdate(const UpdateEvent& acEvent) noexcept { RunRespawnUpdates(acEvent.Delta); RunPostDeathUpdates(acEvent.Delta); RunDifficultyUpdates(); RunLevelUpdates(); RunBeastFormDetection(); } void PlayerService::OnConnected(const ConnectedEvent& acEvent) noexcept { // TODO: SkyrimTogether.esm TESGlobal* pKillMove = Cast(TESForm::GetById(0x100F19)); pKillMove->f = 0.f; TESGlobal* pWorldEncountersEnabled = Cast(TESForm::GetById(0xB8EC1)); pWorldEncountersEnabled->f = 0.f; } void PlayerService::OnDisconnected(const DisconnectedEvent& acEvent) noexcept { PlayerCharacter::Get()->SetDifficulty(m_previousDifficulty); m_serverDifficulty = m_previousDifficulty = 6; ToggleDeathSystem(false); TESGlobal* pKillMove = Cast(TESForm::GetById(0x100F19)); pKillMove->f = 1.f; // Restore to the default value (150 in skyrim, 175 in fallout 4) float* greetDistance = Settings::GetGreetDistance(); *greetDistance = 150.f; TESGlobal* pWorldEncountersEnabled = Cast(TESForm::GetById(0xB8EC1)); pWorldEncountersEnabled->f = 1.f; } void PlayerService::OnServerSettingsReceived(const ServerSettings& acSettings) noexcept { m_previousDifficulty = *Settings::GetDifficulty(); PlayerCharacter::Get()->SetDifficulty(acSettings.Difficulty); m_serverDifficulty = acSettings.Difficulty; if (!acSettings.GreetingsEnabled) { float* greetDistance = Settings::GetGreetDistance(); *greetDistance = 0.f; } ToggleDeathSystem(acSettings.DeathSystemEnabled); } void PlayerService::OnNotifyPlayerRespawn(const NotifyPlayerRespawn& acMessage) const noexcept { PlayerCharacter::Get()->PayGold(acMessage.GoldLost); std::string message = fmt::format("You died and lost {} gold.", acMessage.GoldLost); Utils::ShowHudMessage(String(message)); } void PlayerService::OnGridCellChangeEvent(const GridCellChangeEvent& acEvent) const noexcept { uint32_t baseId = 0; uint32_t modId = 0; if (m_world.GetModSystem().GetServerModId(acEvent.WorldSpaceId, modId, baseId)) { ShiftGridCellRequest request; request.WorldSpaceId = GameId(modId, baseId); request.PlayerCell = acEvent.PlayerCell; request.CenterCoords = acEvent.CenterCoords; request.Cells = acEvent.Cells; m_transport.Send(request); } } void PlayerService::OnCellChangeEvent(const CellChangeEvent& acEvent) const noexcept { if (acEvent.WorldSpaceId) { EnterExteriorCellRequest message; message.CellId = acEvent.CellId; message.WorldSpaceId = acEvent.WorldSpaceId; message.CurrentCoords = acEvent.CurrentCoords; m_transport.Send(message); } else { EnterInteriorCellRequest message; message.CellId = acEvent.CellId; m_transport.Send(message); } } void PlayerService::OnPlayerDialogueEvent(const PlayerDialogueEvent& acEvent) const noexcept { if (!m_transport.IsConnected()) return; const auto& partyService = m_world.GetPartyService(); if (!partyService.IsInParty()) return; PlayerDialogueRequest request{}; request.Text = acEvent.Text; m_transport.Send(request); } void PlayerService::OnPlayerLevelEvent(const PlayerLevelEvent& acEvent) const noexcept { if (!m_transport.IsConnected()) return; PlayerLevelRequest request{}; request.NewLevel = PlayerCharacter::Get()->GetLevel(); m_transport.Send(request); } void PlayerService::OnPartyJoinedEvent(const PartyJoinedEvent& acEvent) noexcept { // TODO: this can be done a bit prettier if (acEvent.IsLeader) { TESGlobal* pWorldEncountersEnabled = Cast(TESForm::GetById(0xB8EC1)); pWorldEncountersEnabled->f = 1.f; } } void PlayerService::OnPartyLeftEvent(const PartyLeftEvent& acEvent) noexcept { // TODO: this can be done a bit prettier if (World::Get().GetTransport().IsConnected()) { TESGlobal* pWorldEncountersEnabled = Cast(TESForm::GetById(0xB8EC1)); pWorldEncountersEnabled->f = 0.f; } } void PlayerService::RunRespawnUpdates(const double acDeltaTime) noexcept { if (!m_isDeathSystemEnabled) return; static bool s_startTimer = false; PlayerCharacter* pPlayer = PlayerCharacter::Get(); if (!pPlayer->actorState.IsBleedingOut()) { m_cachedMainSpellId = pPlayer->magicItems[0] ? pPlayer->magicItems[0]->formID : 0; m_cachedSecondarySpellId = pPlayer->magicItems[1] ? pPlayer->magicItems[1]->formID : 0; m_cachedPowerId = pPlayer->equippedShout ? pPlayer->equippedShout->formID : 0; s_startTimer = false; return; } if (!s_startTimer) { s_startTimer = true; m_respawnTimer = 5.0; FadeOutGame(true, true, 3.0f, true, 2.0f); // If a player dies not by its health reaching 0, getting it up from its bleedout state isn't possible // just by setting its health back to max. Therefore, put it to 0. if (pPlayer->GetActorValue(ActorValueInfo::kHealth) > 0.f) pPlayer->ForceActorValue(ActorValueOwner::ForceMode::DAMAGE, ActorValueInfo::kHealth, 0); pPlayer->PayCrimeGoldToAllFactions(); } m_respawnTimer -= acDeltaTime; if (m_respawnTimer <= 0.0) { pPlayer->RespawnPlayer(); m_knockdownTimer = 1.5; m_knockdownStart = true; m_transport.Send(PlayerRespawnRequest()); s_startTimer = false; auto* pEquipManager = EquipManager::Get(); TESForm* pSpell = TESForm::GetById(m_cachedMainSpellId); if (pSpell) pEquipManager->EquipSpell(pPlayer, pSpell, 0); pSpell = TESForm::GetById(m_cachedSecondarySpellId); if (pSpell) pEquipManager->EquipSpell(pPlayer, pSpell, 1); pSpell = TESForm::GetById(m_cachedPowerId); if (pSpell) pEquipManager->EquipShout(pPlayer, pSpell); } } // Doesn't seem to respawn quite yet void PlayerService::RunPostDeathUpdates(const double acDeltaTime) noexcept { if (!m_isDeathSystemEnabled) return; // If a player dies in ragdoll, it gets stuck. // This code ragdolls the player again upon respawning. // It also makes the player invincible for 5 seconds. if (m_knockdownStart) { m_knockdownTimer -= acDeltaTime; if (m_knockdownTimer <= 0.0) { PlayerCharacter::SetGodMode(true); m_godmodeStart = true; m_godmodeTimer = 10.0; PlayerCharacter* pPlayer = PlayerCharacter::Get(); pPlayer->currentProcess->KnockExplosion(pPlayer, &pPlayer->position, 0.f); FadeOutGame(false, true, 0.5f, true, 2.f); m_knockdownStart = false; } } if (m_godmodeStart) { m_godmodeTimer -= acDeltaTime; if (m_godmodeTimer <= 0.0) { PlayerCharacter::SetGodMode(false); m_godmodeStart = false; } } } void PlayerService::RunDifficultyUpdates() const noexcept { if (!m_transport.IsConnected()) return; PlayerCharacter::Get()->SetDifficulty(m_serverDifficulty); } void PlayerService::RunLevelUpdates() const noexcept { // The LevelUp hook is kinda weird, so ehh, just check periodically, doesn't really cost anything. static std::chrono::steady_clock::time_point lastSendTimePoint; constexpr auto cDelayBetweenUpdates = 1000ms; const auto now = std::chrono::steady_clock::now(); if (now - lastSendTimePoint < cDelayBetweenUpdates) return; lastSendTimePoint = now; static uint16_t oldLevel = PlayerCharacter::Get()->GetLevel(); uint16_t newLevel = PlayerCharacter::Get()->GetLevel(); if (newLevel != oldLevel) { PlayerLevelRequest request{}; request.NewLevel = newLevel; m_transport.Send(request); oldLevel = newLevel; } } void PlayerService::RunBeastFormDetection() const noexcept { static uint32_t lastRaceFormID = 0; static std::chrono::steady_clock::time_point lastSendTimePoint; constexpr auto cDelayBetweenUpdates = 250ms; const auto now = std::chrono::steady_clock::now(); if (now - lastSendTimePoint < cDelayBetweenUpdates) return; lastSendTimePoint = now; PlayerCharacter* pPlayer = PlayerCharacter::Get(); if (!pPlayer->race) return; if (pPlayer->race->formID == lastRaceFormID) return; if (pPlayer->race->formID == 0x200283A || pPlayer->race->formID == 0xCDD84) m_world.GetDispatcher().trigger(BeastFormChangeEvent()); lastRaceFormID = pPlayer->race->formID; } void PlayerService::ToggleDeathSystem(bool aSet) noexcept { m_isDeathSystemEnabled = aSet; PlayerCharacter::Get()->SetPlayerRespawnMode(aSet); }