#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 #include #include using TiltedPhoques::OverlayRenderHandler; using TiltedPhoques::OverlayRenderHandlerD3D11; struct D3D11RenderProvider final : OverlayApp::RenderProvider, OverlayRenderHandlerD3D11::Renderer { explicit D3D11RenderProvider(RenderSystemD3D11* apRenderSystem) : m_pRenderSystem(apRenderSystem) { } OverlayRenderHandler* Create() override { auto* pHandler = new OverlayRenderHandlerD3D11(this); pHandler->SetVisible(true); return pHandler; } [[nodiscard]] HWND GetWindow() override { return m_pRenderSystem->GetWindow(); } [[nodiscard]] IDXGISwapChain* GetSwapChain() const noexcept override { return m_pRenderSystem->GetSwapChain(); } [[nodiscard]] ID3D11Device* GetDevice() const noexcept override { return m_pRenderSystem->GetDevice(); } [[nodiscard]] ID3D11DeviceContext* GetDeviceContext() const noexcept override { return m_pRenderSystem->GetDeviceContext(); } private: RenderSystemD3D11* m_pRenderSystem; }; String GetCellName(const GameId& aWorldSpaceId, const GameId& aCellId) noexcept { auto& modSystem = World::Get().GetModSystem(); String cellName = "UNKNOWN"; if (aWorldSpaceId) { const uint32_t worldSpaceId = modSystem.GetGameId(aWorldSpaceId); TESWorldSpace* pWorldSpace = Cast(TESForm::GetById(worldSpaceId)); if (pWorldSpace) cellName = pWorldSpace->GetName(); } else { const uint32_t cellId = modSystem.GetGameId(aCellId); TESObjectCELL* pCell = Cast(TESForm::GetById(cellId)); if (pCell) cellName = pCell->GetName(); } return cellName; } float CalculateHealthPercentage(Actor* apActor) noexcept { const float maxHealth = apActor->GetActorPermanentValue(ActorValueInfo::kHealth); const float tempModHealth = apActor->healthModifiers.temporaryModifier; if (maxHealth == 0.f) return 0.f; const float health = apActor->GetActorValue(ActorValueInfo::kHealth); float percentage = health / (maxHealth + tempModHealth) * 100.f; if (percentage < 0.f) percentage = 0.f; return percentage; } OverlayService::OverlayService(World& aWorld, TransportService& transport, entt::dispatcher& aDispatcher) : m_world(aWorld) , m_transport(transport) { m_updateConnection = aDispatcher.sink().connect<&OverlayService::OnUpdate>(this); m_connectedConnection = aDispatcher.sink().connect<&OverlayService::OnConnectedEvent>(this); m_disconnectedConnection = aDispatcher.sink().connect<&OverlayService::OnDisconnectedEvent>(this); m_connectionErrorConnection = aDispatcher.sink().connect<&OverlayService::OnConnectionError>(this); m_chatMessageConnection = aDispatcher.sink().connect<&OverlayService::OnChatMessageReceived>(this); m_playerJoinedConnection = aDispatcher.sink().connect<&OverlayService::OnPlayerJoined>(this); m_playerLeftConnection = aDispatcher.sink().connect<&OverlayService::OnPlayerLeft>(this); m_playerDialogueConnection = aDispatcher.sink().connect<&OverlayService::OnPlayerDialogue>(this); m_playerAddedConnection = m_world.on_destroy().connect<&OverlayService::OnWaitingFor3DRemoved>(this); m_playerRemovedConnection = m_world.on_destroy().connect<&OverlayService::OnPlayerComponentRemoved>(this); m_playerLevelConnection = aDispatcher.sink().connect<&OverlayService::OnPlayerLevel>(this); m_cellChangedConnection = aDispatcher.sink().connect<&OverlayService::OnPlayerCellChanged>(this); m_teleportConnection = aDispatcher.sink().connect<&OverlayService::OnNotifyTeleport>(this); m_playerHealthConnection = aDispatcher.sink().connect<&OverlayService::OnNotifyPlayerHealthUpdate>(this); m_partyJoinedConnection = aDispatcher.sink().connect<&OverlayService::OnPartyJoinedEvent>(this); m_partyLeftConnection = aDispatcher.sink().connect<&OverlayService::OnPartyLeftEvent>(this); } OverlayService::~OverlayService() noexcept { } void OverlayService::Create(RenderSystemD3D11* apRenderSystem) noexcept { m_pProvider = TiltedPhoques::MakeUnique(apRenderSystem); m_pOverlay = new OverlayApp(m_pProvider.get(), new ::OverlayClient(m_transport, m_pProvider->Create())); if (!m_pOverlay->Initialize()) { spdlog::error("Overlay could not be initialized"); if (int32_t exitCode = CefGetExitCode()) { spdlog::critical("CEF failed to initialize, exit code {}. See 'cef_types.h' for description", exitCode); } } m_pOverlay->GetClient()->Create(); } void OverlayService::Render() noexcept { auto pPlayer = PlayerCharacter::Get(); bool inGame = pPlayer && pPlayer->GetNiNode(); if (inGame && !m_inGame) SetInGame(true); else if (!inGame && m_inGame) SetInGame(false); m_pOverlay->GetClient()->Render(); } void OverlayService::Reset() const noexcept { m_pOverlay->GetClient()->Reset(); } void OverlayService::Reload() noexcept { SetInGame(false); SetActive(false); GetOverlayApp()->GetClient()->GetBrowser()->Reload(); Initialize(); SetInGame(true); m_pOverlay->ExecuteAsync("enterGame"); SetActive(true); } void OverlayService::Initialize() noexcept { m_pOverlay->ExecuteAsync("init"); } void OverlayService::SetActive(bool aActive) noexcept { if (!m_inGame) return; if (m_active == aActive) return; m_active = aActive; m_pOverlay->ExecuteAsync(m_active ? "activate" : "deactivate"); } bool OverlayService::GetActive() const noexcept { return m_active; } void OverlayService::SetInGame(bool aInGame) noexcept { if (m_inGame == aInGame) return; m_inGame = aInGame; if (m_inGame) { SetVersion(BUILD_COMMIT); m_pOverlay->ExecuteAsync("enterGame"); } else { m_pOverlay->ExecuteAsync("exitGame"); // TODO: this does nothing, since m_inGame is false SetActive(false); } } bool OverlayService::GetInGame() const noexcept { return m_inGame; } void OverlayService::SetVersion(const std::string& acVersion) { if (!m_pOverlay) return; auto pArguments = CefListValue::Create(); pArguments->SetString(0, acVersion); m_pOverlay->ExecuteAsync("setVersion", pArguments); } void OverlayService::SendSystemMessage(const std::string& acMessage) { if (!m_pOverlay) return; auto pArguments = CefListValue::Create(); pArguments->SetInt(0, kSystemMessage); pArguments->SetString(1, acMessage); m_pOverlay->ExecuteAsync("message", pArguments); } void OverlayService::SetPlayerHealthPercentage(uint32_t aFormId) const noexcept { Actor* pActor = Cast(TESForm::GetById(aFormId)); if (!pActor) { spdlog::error("{}: cannot find actor for form id {:X}", __FUNCTION__, aFormId); return; } float percentage = CalculateHealthPercentage(pActor); auto view = m_world.view(); auto entityIt = std::find_if(view.begin(), view.end(), [view, aFormId](auto aEntity) { return view.get(aEntity).Id == aFormId; }); if (entityIt == view.end()) { spdlog::error("{}: cannot find player entity for form id {:X}", __FUNCTION__, aFormId); return; } const auto& playerComponent = view.get(*entityIt); auto pArguments = CefListValue::Create(); pArguments->SetInt(0, playerComponent.Id); pArguments->SetDouble(1, static_cast(percentage)); m_pOverlay->ExecuteAsync("setHealth", pArguments); } void OverlayService::OnUpdate(const UpdateEvent&) noexcept { RunDebugDataUpdates(); RunPlayerHealthUpdates(); } void OverlayService::OnConnectedEvent(const ConnectedEvent& acEvent) noexcept { m_pOverlay->ExecuteAsync("connect"); auto pArguments = CefListValue::Create(); pArguments->SetInt(0, acEvent.PlayerId); m_pOverlay->ExecuteAsync("setLocalPlayerId", pArguments); } void OverlayService::OnDisconnectedEvent(const DisconnectedEvent&) noexcept { m_pOverlay->ExecuteAsync("disconnect"); } void OverlayService::OnWaitingFor3DRemoved(entt::registry& aRegistry, entt::entity aEntity) const noexcept { const auto* pPlayerComponent = m_world.try_get(aEntity); if (!pPlayerComponent) return; const auto& formIdComponent = m_world.get(aEntity); Actor* pActor = Cast(TESForm::GetById(formIdComponent.Id)); if (!pActor) { spdlog::error("{}: cannot find actor for form id {:X}", __FUNCTION__, formIdComponent.Id); return; } float percentage = CalculateHealthPercentage(pActor); auto pArguments = CefListValue::Create(); pArguments->SetInt(0, pPlayerComponent->Id); pArguments->SetInt(1, static_cast(percentage)); m_pOverlay->ExecuteAsync("setPlayer3dLoaded", pArguments); } void OverlayService::OnPlayerComponentRemoved(entt::registry& aRegistry, entt::entity aEntity) const noexcept { const auto& playerComponent = m_world.get(aEntity); auto pArguments = CefListValue::Create(); pArguments->SetInt(0, playerComponent.Id); m_pOverlay->ExecuteAsync("setPlayer3dUnloaded", pArguments); } void OverlayService::OnChatMessageReceived(const NotifyChatMessageBroadcast& acMessage) noexcept { if (!m_pOverlay) return; auto pArguments = CefListValue::Create(); pArguments->SetInt(0, (int)acMessage.MessageType); pArguments->SetString(1, acMessage.ChatMessage.c_str()); pArguments->SetString(2, acMessage.PlayerName.c_str()); m_pOverlay->ExecuteAsync("message", pArguments); } void OverlayService::OnPlayerDialogue(const NotifyPlayerDialogue& acMessage) noexcept { if (!m_pOverlay) return; auto pArguments = CefListValue::Create(); pArguments->SetInt(0, kPlayerDialogue); pArguments->SetString(1, acMessage.Text.c_str()); pArguments->SetString(2, acMessage.Name.c_str()); m_pOverlay->ExecuteAsync("message", pArguments); } void OverlayService::OnConnectionError(const ConnectionErrorEvent& acConnectedEvent) const noexcept { auto pArgs = CefListValue::Create(); pArgs->SetString(0, acConnectedEvent.ErrorDetail.c_str()); m_pOverlay->ExecuteAsync("triggerError", pArgs); } void OverlayService::OnPlayerJoined(const NotifyPlayerJoined& acMessage) noexcept { auto pArguments = CefListValue::Create(); pArguments->SetInt(0, acMessage.PlayerId); pArguments->SetString(1, acMessage.Username.c_str()); pArguments->SetInt(2, acMessage.Level); String cellName = GetCellName(acMessage.WorldSpaceId, acMessage.CellId); pArguments->SetString(3, cellName.c_str()); m_pOverlay->ExecuteAsync("playerConnected", pArguments); } void OverlayService::OnPlayerLeft(const NotifyPlayerLeft& acMessage) noexcept { auto pArguments = CefListValue::Create(); pArguments->SetInt(0, acMessage.PlayerId); pArguments->SetString(1, acMessage.Username.c_str()); m_pOverlay->ExecuteAsync("playerDisconnected", pArguments); } void OverlayService::OnPlayerLevel(const NotifyPlayerLevel& acMessage) noexcept { auto pArguments = CefListValue::Create(); pArguments->SetInt(0, acMessage.PlayerId); pArguments->SetInt(1, acMessage.NewLevel); m_pOverlay->ExecuteAsync("setLevel", pArguments); } void OverlayService::OnPlayerCellChanged(const NotifyPlayerCellChanged& acMessage) const noexcept { auto pArguments = CefListValue::Create(); pArguments->SetInt(0, acMessage.PlayerId); String cellName = GetCellName(acMessage.WorldSpaceId, acMessage.CellId); pArguments->SetString(1, cellName.c_str()); m_pOverlay->ExecuteAsync("setCell", pArguments); } void OverlayService::OnNotifyTeleport(const NotifyTeleport& acMessage) noexcept { auto& modSystem = m_world.GetModSystem(); const uint32_t cellId = modSystem.GetGameId(acMessage.CellId); TESObjectCELL* pCell = Cast(TESForm::GetById(cellId)); if (!pCell) { const uint32_t worldSpaceId = modSystem.GetGameId(acMessage.WorldSpaceId); TESWorldSpace* pWorldSpace = Cast(TESForm::GetById(worldSpaceId)); if (pWorldSpace) { GridCellCoords coordinates = GridCellCoords::CalculateGridCellCoords(acMessage.Position); pCell = pWorldSpace->LoadCell(coordinates.X, coordinates.Y); } if (!pCell) { spdlog::error("Failed to fetch cell to teleport to."); m_world.GetOverlayService().SendSystemMessage("Teleporting to player failed."); return; } } PlayerCharacter::Get()->MoveTo(pCell, acMessage.Position); } void OverlayService::OnNotifyPlayerHealthUpdate(const NotifyPlayerHealthUpdate& acMessage) noexcept { const float percentage = acMessage.Percentage >= 0.f ? acMessage.Percentage : 0.f; auto pArguments = CefListValue::Create(); pArguments->SetInt(0, acMessage.PlayerId); pArguments->SetDouble(1, static_cast(percentage)); m_pOverlay->ExecuteAsync("setHealth", pArguments); } void OverlayService::OnPartyJoinedEvent(const PartyJoinedEvent& acEvent) noexcept { if (acEvent.IsLeader) m_world.GetOverlayService().GetOverlayApp()->ExecuteAsync("partyCreated"); } void OverlayService::OnPartyLeftEvent(const PartyLeftEvent& acEvent) noexcept { m_world.GetOverlayService().GetOverlayApp()->ExecuteAsync("partyLeft"); } void OverlayService::RunDebugDataUpdates() noexcept { 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; auto internalStats = m_transport.GetStatistics(); auto steamStats = m_transport.GetConnectionStatus(); auto pArguments = CefListValue::Create(); pArguments->SetInt(0, steamStats.m_flOutPacketsPerSec); pArguments->SetInt(1, steamStats.m_flInPacketsPerSec); pArguments->SetInt(2, steamStats.m_nPing); pArguments->SetInt(3, 0); pArguments->SetInt(4, internalStats.SentBytes); pArguments->SetInt(5, internalStats.RecvBytes); m_pOverlay->ExecuteAsync("debugData", pArguments); } // TODO(cosideci): this whole thing is a really hacky solution to // health sync code being somewhat broken for players. void OverlayService::RunPlayerHealthUpdates() noexcept { if (!m_transport.IsConnected() || !m_world.GetPartyService().IsInParty()) return; static std::chrono::steady_clock::time_point lastSendTimePoint; constexpr auto cDelayBetweenUpdates = 500ms; const auto now = std::chrono::steady_clock::now(); if (now - lastSendTimePoint < cDelayBetweenUpdates) return; lastSendTimePoint = now; static float s_previousPercentage = -1.f; const float newPercentage = CalculateHealthPercentage(PlayerCharacter::Get()); if (newPercentage == s_previousPercentage) return; s_previousPercentage = newPercentage; RequestPlayerHealthUpdate request{}; request.Percentage = newPercentage; m_transport.Send(request); }