diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index d276bf8a..3c34882e 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -772,6 +772,9 @@ public: bool extended = false; }; const std::vector& getInstanceLockouts() const { return instanceLockouts_; } + + // Boss encounter unit tracking (SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT) + static constexpr uint32_t kMaxEncounterSlots = 5; // Returns boss unit guid for the given encounter slot (0 if none) uint64_t getEncounterUnitGuid(uint32_t slot) const { return (slot < kMaxEncounterSlots) ? encounterUnitGuids_[slot] : 0; @@ -1743,7 +1746,6 @@ private: std::vector instanceLockouts_; // Instance encounter boss units (slots 0-4 from SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT) - static constexpr uint32_t kMaxEncounterSlots = 5; std::array encounterUnitGuids_ = {}; // 0 = empty slot // LFG / Dungeon Finder state diff --git a/include/network/net_platform.hpp b/include/network/net_platform.hpp index 29eaf2c8..0cc38e1a 100644 --- a/include/network/net_platform.hpp +++ b/include/network/net_platform.hpp @@ -91,6 +91,20 @@ inline bool isWouldBlock(int err) { #endif } +// Returns true for errors that mean the peer closed the connection cleanly. +// On Windows, WSAENOTCONN / WSAECONNRESET / WSAESHUTDOWN can be returned by +// recv() when the server closes the connection, rather than returning 0. +inline bool isConnectionClosed(int err) { +#ifdef _WIN32 + return err == WSAENOTCONN || // socket not connected (server closed) + err == WSAECONNRESET || // connection reset by peer + err == WSAESHUTDOWN || // socket shut down + err == WSAECONNABORTED; // connection aborted +#else + return err == ENOTCONN || err == ECONNRESET; +#endif +} + inline bool isInProgress(int err) { #ifdef _WIN32 return err == WSAEWOULDBLOCK || err == WSAEALREADY; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index cd944b47..49d429b6 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -210,6 +210,7 @@ private: void renderMirrorTimers(game::GameHandler& gameHandler); void renderCombatText(game::GameHandler& gameHandler); void renderPartyFrames(game::GameHandler& gameHandler); + void renderBossFrames(game::GameHandler& gameHandler); void renderGroupInvitePopup(game::GameHandler& gameHandler); void renderDuelRequestPopup(game::GameHandler& gameHandler); void renderLootRollPopup(game::GameHandler& gameHandler); diff --git a/src/network/tcp_socket.cpp b/src/network/tcp_socket.cpp index 38a1cf6a..2dbf1b57 100644 --- a/src/network/tcp_socket.cpp +++ b/src/network/tcp_socket.cpp @@ -153,6 +153,11 @@ void TCPSocket::update() { if (net::isWouldBlock(err)) { break; } + if (net::isConnectionClosed(err)) { + // Peer closed the connection — treat the same as recv() returning 0 + sawClose = true; + break; + } LOG_ERROR("Receive failed: ", net::errorString(err)); disconnect(); diff --git a/src/network/world_socket.cpp b/src/network/world_socket.cpp index ab29a271..78c90c8e 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -128,6 +128,39 @@ bool WorldSocket::connect(const std::string& host, uint16_t port) { sockfd = INVALID_SOCK; return false; } + + // Non-blocking connect in progress — wait up to 10s for completion. + // On Windows, calling recv() before the connect completes returns + // WSAENOTCONN; we must poll writability before declaring connected. + fd_set writefds, errfds; + FD_ZERO(&writefds); + FD_ZERO(&errfds); + FD_SET(sockfd, &writefds); + FD_SET(sockfd, &errfds); + + struct timeval tv; + tv.tv_sec = 10; + tv.tv_usec = 0; + + int sel = ::select(static_cast(sockfd) + 1, nullptr, &writefds, &errfds, &tv); + if (sel <= 0) { + LOG_ERROR("World server connection timed out (", host, ":", port, ")"); + net::closeSocket(sockfd); + sockfd = INVALID_SOCK; + return false; + } + + // Verify the socket error code — writeable doesn't guarantee success on all platforms + int sockErr = 0; + socklen_t errLen = sizeof(sockErr); + getsockopt(sockfd, SOL_SOCKET, SO_ERROR, + reinterpret_cast(&sockErr), &errLen); + if (sockErr != 0) { + LOG_ERROR("Failed to connect to world server: ", net::errorString(sockErr)); + net::closeSocket(sockfd); + sockfd = INVALID_SOCK; + return false; + } } connected = true; @@ -369,6 +402,11 @@ void WorldSocket::update() { if (net::isWouldBlock(err)) { break; } + if (net::isConnectionClosed(err)) { + // Peer closed the connection — treat the same as recv() returning 0 + sawClose = true; + break; + } LOG_ERROR("Receive failed: ", net::errorString(err)); disconnect(); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 06e6b4b5..504b609e 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -403,6 +403,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { if (showNameplates_) renderNameplates(gameHandler); renderCombatText(gameHandler); renderPartyFrames(gameHandler); + renderBossFrames(gameHandler); renderGroupInvitePopup(gameHandler); renderDuelRequestPopup(gameHandler); renderLootRollPopup(gameHandler); @@ -4893,6 +4894,79 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { ImGui::PopStyleVar(); } +// ============================================================ +// Boss Encounter Frames +// ============================================================ + +void GameScreen::renderBossFrames(game::GameHandler& gameHandler) { + // Collect active boss unit slots + struct BossSlot { uint32_t slot; uint64_t guid; }; + std::vector active; + for (uint32_t s = 0; s < game::GameHandler::kMaxEncounterSlots; ++s) { + uint64_t g = gameHandler.getEncounterUnitGuid(s); + if (g != 0) active.push_back({s, g}); + } + if (active.empty()) return; + + const float frameW = 200.0f; + const float startX = ImGui::GetIO().DisplaySize.x - frameW - 10.0f; + float frameY = 120.0f; + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_AlwaysAutoResize; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.15f, 0.05f, 0.05f, 0.85f)); + + ImGui::SetNextWindowPos(ImVec2(startX, frameY), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(frameW, 0.0f), ImGuiCond_Always); + + if (ImGui::Begin("##BossFrames", nullptr, flags)) { + for (const auto& bs : active) { + ImGui::PushID(static_cast(bs.guid)); + + // Try to resolve name and health from entity manager + std::string name = "Boss"; + uint32_t hp = 0, maxHp = 0; + auto entity = gameHandler.getEntityManager().getEntity(bs.guid); + if (entity && (entity->getType() == game::ObjectType::UNIT || + entity->getType() == game::ObjectType::PLAYER)) { + auto unit = std::static_pointer_cast(entity); + const auto& n = unit->getName(); + if (!n.empty()) name = n; + hp = unit->getHealth(); + maxHp = unit->getMaxHealth(); + } + + // Clickable name to target + if (ImGui::Selectable(name.c_str(), gameHandler.getTargetGuid() == bs.guid)) { + gameHandler.setTarget(bs.guid); + } + + if (maxHp > 0) { + float pct = static_cast(hp) / static_cast(maxHp); + // Boss health bar in red shades + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, + pct > 0.5f ? ImVec4(0.8f, 0.2f, 0.2f, 1.0f) : + pct > 0.2f ? ImVec4(0.9f, 0.5f, 0.1f, 1.0f) : + ImVec4(1.0f, 0.8f, 0.1f, 1.0f)); + char label[32]; + std::snprintf(label, sizeof(label), "%u / %u", hp, maxHp); + ImGui::ProgressBar(pct, ImVec2(-1, 14), label); + ImGui::PopStyleColor(); + } + + ImGui::PopID(); + ImGui::Spacing(); + } + } + ImGui::End(); + + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); +} + // ============================================================ // Group Invite Popup (Phase 4) // ============================================================