diff --git a/Data/expansions/classic/opcodes.json b/Data/expansions/classic/opcodes.json index d2ab7d63..fb0a72eb 100644 --- a/Data/expansions/classic/opcodes.json +++ b/Data/expansions/classic/opcodes.json @@ -25,6 +25,7 @@ "SMSG_CHAR_CREATE": "0x03A", "SMSG_CHAR_ENUM": "0x03B", "SMSG_CHAR_DELETE": "0x03C", + "SMSG_CHARACTER_LOGIN_FAILED": "0x041", "SMSG_PONG": "0x1DD", "SMSG_LOGIN_VERIFY_WORLD": "0x236", "SMSG_LOGIN_SETTIMESPEED": "0x042", diff --git a/Data/expansions/tbc/opcodes.json b/Data/expansions/tbc/opcodes.json index 44e50cc6..b9565a0b 100644 --- a/Data/expansions/tbc/opcodes.json +++ b/Data/expansions/tbc/opcodes.json @@ -25,6 +25,7 @@ "SMSG_CHAR_CREATE": "0x03A", "SMSG_CHAR_ENUM": "0x03B", "SMSG_CHAR_DELETE": "0x03C", + "SMSG_CHARACTER_LOGIN_FAILED": "0x041", "SMSG_PONG": "0x1DD", "SMSG_LOGIN_VERIFY_WORLD": "0x236", "SMSG_LOGIN_SETTIMESPEED": "0x042", diff --git a/Data/expansions/turtle/opcodes.json b/Data/expansions/turtle/opcodes.json index 55965275..8dbd776c 100644 --- a/Data/expansions/turtle/opcodes.json +++ b/Data/expansions/turtle/opcodes.json @@ -25,6 +25,7 @@ "SMSG_CHAR_CREATE": "0x03A", "SMSG_CHAR_ENUM": "0x03B", "SMSG_CHAR_DELETE": "0x03C", + "SMSG_CHARACTER_LOGIN_FAILED": "0x041", "SMSG_PONG": "0x1DD", "SMSG_LOGIN_VERIFY_WORLD": "0x236", "SMSG_LOGIN_SETTIMESPEED": "0x042", diff --git a/Data/expansions/wotlk/opcodes.json b/Data/expansions/wotlk/opcodes.json index 38a3a58b..38ac1ac8 100644 --- a/Data/expansions/wotlk/opcodes.json +++ b/Data/expansions/wotlk/opcodes.json @@ -25,6 +25,7 @@ "SMSG_CHAR_CREATE": "0x03A", "SMSG_CHAR_ENUM": "0x03B", "SMSG_CHAR_DELETE": "0x03C", + "SMSG_CHARACTER_LOGIN_FAILED": "0x041", "SMSG_PONG": "0x1DD", "SMSG_LOGIN_VERIFY_WORLD": "0x236", "SMSG_LOGIN_SETTIMESPEED": "0x042", diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index c480877e..89c518a6 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -175,6 +175,9 @@ public: void setCharDeleteCallback(CharDeleteCallback cb) { charDeleteCallback_ = std::move(cb); } uint8_t getLastCharDeleteResult() const { return lastCharDeleteResult_; } + using CharLoginFailCallback = std::function; + void setCharLoginFailCallback(CharLoginFailCallback cb) { charLoginFailCallback_ = std::move(cb); } + /** * Select and log in with a character * @param characterGuid GUID of character to log in with @@ -906,6 +909,11 @@ private: */ void handleCharEnum(network::Packet& packet); + /** + * Handle SMSG_CHARACTER_LOGIN_FAILED from server + */ + void handleCharLoginFailed(network::Packet& packet); + /** * Handle SMSG_LOGIN_VERIFY_WORLD from server */ @@ -1469,6 +1477,7 @@ private: WorldConnectFailureCallback onFailure; CharCreateCallback charCreateCallback_; CharDeleteCallback charDeleteCallback_; + CharLoginFailCallback charLoginFailCallback_; uint8_t lastCharDeleteResult_ = 0xFF; bool pendingCharCreateResult_ = false; bool pendingCharCreateSuccess_ = false; diff --git a/include/game/opcode_table.hpp b/include/game/opcode_table.hpp index b68986af..bc089018 100644 --- a/include/game/opcode_table.hpp +++ b/include/game/opcode_table.hpp @@ -47,6 +47,7 @@ enum class LogicalOpcode : uint16_t { SMSG_CHAR_CREATE, SMSG_CHAR_ENUM, SMSG_CHAR_DELETE, + SMSG_CHARACTER_LOGIN_FAILED, SMSG_PONG, SMSG_LOGIN_VERIFY_WORLD, SMSG_LOGIN_SETTIMESPEED, diff --git a/include/ui/character_screen.hpp b/include/ui/character_screen.hpp index 384464e0..aae4d6a0 100644 --- a/include/ui/character_screen.hpp +++ b/include/ui/character_screen.hpp @@ -58,6 +58,7 @@ public: restoredLastCharacter = false; newlyCreatedCharacterName.clear(); statusMessage.clear(); + statusIsError = false; deleteConfirmStage = 0; previewInitialized_ = false; previewGuid_ = 0; @@ -80,7 +81,7 @@ public: /** * Update status message */ - void setStatus(const std::string& message); + void setStatus(const std::string& message, bool isError = false); /** * Select character by name (used after character creation) @@ -97,6 +98,7 @@ private: // Status std::string statusMessage; + bool statusIsError = false; // Callbacks std::function onCharacterSelected; diff --git a/src/core/application.cpp b/src/core/application.cpp index 1e395b86..33cd621c 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1006,6 +1006,13 @@ void Application::setupUICallbacks() { } }); + // Character login failure callback + gameHandler->setCharLoginFailCallback([this](const std::string& reason) { + LOG_WARNING("Character login failed: ", reason); + setState(AppState::CHARACTER_SELECTION); + uiManager->getCharacterScreen().setStatus("Login failed: " + reason, true); + }); + // World entry callback (online mode) - load terrain when entering world gameHandler->setWorldEntryCallback([this](uint32_t mapId, float x, float y, float z) { LOG_INFO("Online world entry: mapId=", mapId, " pos=(", x, ", ", y, ", ", z, ")"); @@ -1784,7 +1791,7 @@ void Application::setupUICallbacks() { } else { uint8_t code = gameHandler ? gameHandler->getLastCharDeleteResult() : 0xFF; uiManager->getCharacterScreen().setStatus( - "Delete failed (code " + std::to_string(static_cast(code)) + ")."); + "Delete failed (code " + std::to_string(static_cast(code)) + ").", true); } }); } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 53144e65..dd59bdbc 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -699,6 +699,10 @@ void GameHandler::handlePacket(network::Packet& packet) { } break; + case Opcode::SMSG_CHARACTER_LOGIN_FAILED: + handleCharLoginFailed(packet); + break; + case Opcode::SMSG_LOGIN_VERIFY_WORLD: if (state == WorldState::ENTERING_WORLD || state == WorldState::IN_WORLD) { handleLoginVerifyWorld(packet); @@ -1889,6 +1893,32 @@ const Character* GameHandler::getFirstCharacter() const { +void GameHandler::handleCharLoginFailed(network::Packet& packet) { + uint8_t reason = packet.readUInt8(); + + static const char* reasonNames[] = { + "Login failed", // 0 + "World server is down", // 1 + "Duplicate character", // 2 (session still active) + "No instance servers", // 3 + "Login disabled", // 4 + "Character not found", // 5 + "Locked for transfer", // 6 + "Locked by billing", // 7 + "Using remote", // 8 + }; + const char* msg = (reason < 9) ? reasonNames[reason] : "Unknown reason"; + + LOG_ERROR("SMSG_CHARACTER_LOGIN_FAILED: reason=", (int)reason, " (", msg, ")"); + + // Allow the player to re-select a character + setState(WorldState::CHAR_LIST_RECEIVED); + + if (charLoginFailCallback_) { + charLoginFailCallback_(msg); + } +} + void GameHandler::selectCharacter(uint64_t characterGuid) { if (state != WorldState::CHAR_LIST_RECEIVED) { LOG_WARNING("Cannot select character in state: ", (int)state); diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp index 2e7fda54..f85a23bc 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -48,6 +48,7 @@ static const OpcodeNameEntry kOpcodeNames[] = { {"SMSG_CHAR_CREATE", LogicalOpcode::SMSG_CHAR_CREATE}, {"SMSG_CHAR_ENUM", LogicalOpcode::SMSG_CHAR_ENUM}, {"SMSG_CHAR_DELETE", LogicalOpcode::SMSG_CHAR_DELETE}, + {"SMSG_CHARACTER_LOGIN_FAILED", LogicalOpcode::SMSG_CHARACTER_LOGIN_FAILED}, {"SMSG_PONG", LogicalOpcode::SMSG_PONG}, {"SMSG_LOGIN_VERIFY_WORLD", LogicalOpcode::SMSG_LOGIN_VERIFY_WORLD}, {"SMSG_LOGIN_SETTIMESPEED", LogicalOpcode::SMSG_LOGIN_SETTIMESPEED}, @@ -386,6 +387,7 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::SMSG_CHAR_CREATE, 0x03A}, {LogicalOpcode::SMSG_CHAR_ENUM, 0x03B}, {LogicalOpcode::SMSG_CHAR_DELETE, 0x03C}, + {LogicalOpcode::SMSG_CHARACTER_LOGIN_FAILED, 0x041}, {LogicalOpcode::SMSG_PONG, 0x1DD}, {LogicalOpcode::SMSG_LOGIN_VERIFY_WORLD, 0x236}, {LogicalOpcode::SMSG_LOGIN_SETTIMESPEED, 0x042}, diff --git a/src/ui/character_screen.cpp b/src/ui/character_screen.cpp index 5872b880..64e24716 100644 --- a/src/ui/character_screen.cpp +++ b/src/ui/character_screen.cpp @@ -156,7 +156,8 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { // Status message if (!statusMessage.empty()) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f)); + ImVec4 color = statusIsError ? ImVec4(1.0f, 0.3f, 0.3f, 1.0f) : ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_Text, color); ImGui::TextWrapped("%s", statusMessage.c_str()); ImGui::PopStyleColor(); ImGui::Spacing(); @@ -454,8 +455,9 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { ImGui::End(); } -void CharacterScreen::setStatus(const std::string& message) { +void CharacterScreen::setStatus(const std::string& message, bool isError) { statusMessage = message; + statusIsError = isError; } void CharacterScreen::selectCharacterByName(const std::string& name) {