////////////////////////////////////////////////////////////////////// // This file is part of Remere's Map Editor ////////////////////////////////////////////////////////////////////// // Remere's Map Editor is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Remere's Map Editor is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . ////////////////////////////////////////////////////////////////////// #include "main.h" #include "live_client.h" #include "live_tab.h" #include "live_action.h" #include "editor.h" #include #include LiveClient::LiveClient() : LiveSocket(), readMessage(), queryNodeList(), currentOperation(), resolver(nullptr), socket(nullptr), editor(nullptr), stopped(false), isDrawingReady(false) { // Initialize buffer with minimum size to prevent "size 0" errors readMessage.buffer.resize(1024); readMessage.position = 0; // Log initialization to file only to help with debugging std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "client_init.log").ToStdString(), std::ios::app); if (logFile.is_open()) { wxDateTime now = wxDateTime::Now(); logFile << now.FormatISOCombined() << ": LiveClient initialized\n"; logFile.close(); } } LiveClient::~LiveClient() { // } bool LiveClient::connect(const std::string& address, uint16_t port) { NetworkConnection& connection = NetworkConnection::getInstance(); if (!connection.start()) { setLastError("The previous connection has not been terminated yet."); return false; } auto& service = connection.get_service(); if (!resolver) { resolver = std::make_shared(service); } if (!socket) { socket = std::make_shared(service); } // Set socket options for better error detection boost::system::error_code ec; socket->set_option(boost::asio::socket_base::keep_alive(true), ec); if (ec) { logMessage("Warning: Could not set keep_alive option: " + ec.message()); } // Set connection timeout options socket->set_option(boost::asio::socket_base::linger(true, 10), ec); if (ec) { logMessage("Warning: Could not set linger option: " + ec.message()); } // Log connection attempt logMessage(wxString::Format("Attempting to connect to %s:%d...", address, port)); // Try both IP address and hostname when dealing with localhost/0.0.0.0 bool isLocalAddress = (address == "localhost" || address == "127.0.0.1" || address == "0.0.0.0"); // If it's a local address, try both localhost and 127.0.0.1 if (isLocalAddress) { // Create a vector of addresses to try std::vector addressesToTry; // Always try the original address first addressesToTry.push_back(address); // Then add alternatives for local addresses if (address == "localhost") { addressesToTry.push_back("127.0.0.1"); } else if (address == "127.0.0.1") { addressesToTry.push_back("localhost"); } else if (address == "0.0.0.0") { addressesToTry.push_back("localhost"); addressesToTry.push_back("127.0.0.1"); } // Try each address for (const auto& addr : addressesToTry) { resolver->async_resolve( addr, std::to_string(port), [this, addr, addressesToTry, port](const boost::system::error_code& error, boost::asio::ip::tcp::resolver::results_type results) -> void { if (error) { wxString errorMsg = wxString::Format("Resolution error for %s: %s", addr, error.message()); logMessage(errorMsg); // If this is the last address in the list and we failed, log a final error if (addr == addressesToTry.back()) { logMessage("Failed to resolve any local address. Check your network configuration."); } } else { logMessage(wxString::Format("Host %s resolved. Connecting to endpoint...", addr)); tryConnect(results.begin()); } } ); } } else { // For non-local addresses, just try the given address resolver->async_resolve( address, std::to_string(port), [this](const boost::system::error_code& error, boost::asio::ip::tcp::resolver::results_type results) -> void { if (error) { wxString errorMsg = wxString::Format("Resolution error: %s", error.message()); logMessage(errorMsg); } else { logMessage(wxString::Format("Host resolved. Connecting to endpoint...")); tryConnect(results.begin()); } } ); } return true; } void LiveClient::tryConnect(boost::asio::ip::tcp::resolver::results_type::iterator endpoint_iterator) { if (stopped) { logMessage("Connection attempt aborted: Connection was stopped."); return; } // Check if we're at the end of the endpoints if (endpoint_iterator == boost::asio::ip::tcp::resolver::results_type::iterator()) { logMessage("Connection attempt failed: No more endpoints to try."); return; } logMessage("Joining server " + endpoint_iterator->host_name() + ":" + endpoint_iterator->service_name() + "..."); // In modern Boost.Asio, we need to connect to a single endpoint auto current_endpoint = *endpoint_iterator; auto next_endpoint = endpoint_iterator; ++next_endpoint; // Advance to get the next endpoint socket->async_connect(current_endpoint, [this, next_endpoint](boost::system::error_code error) -> void { if (!socket->is_open()) { logMessage(wxString::Format("Connection failed: Socket is not open. Trying next endpoint...")); tryConnect(next_endpoint); } else if (error) { wxString errorMsg = wxString::Format("Connection error: %s (code: %d)", error.message(), error.value()); logMessage(errorMsg); if (handleError(error)) { logMessage("Trying next endpoint..."); tryConnect(next_endpoint); } else { wxTheApp->CallAfter([this]() { logMessage("All connection attempts failed. Closing connection."); close(); g_gui.CloseLiveEditors(this); }); } } else { socket->set_option(boost::asio::ip::tcp::no_delay(true), error); if (error) { logMessage(wxString::Format("Warning: Could not set TCP no_delay option: %s", error.message())); wxTheApp->CallAfter([this]() { close(); }); return; } logMessage("Connection established successfully. Sending hello packet..."); sendHello(); receiveHeader(); } }); } void LiveClient::close() { if (resolver) { resolver->cancel(); } if (socket) { socket->close(); } if (log) { log->Message("Disconnected from server."); log->Disconnect(); log = nullptr; } // Reset drawing ready flag on disconnect isDrawingReady = false; stopped = true; } bool LiveClient::handleError(const boost::system::error_code& error) { // Handle common connection errors with more informative messages if (error == boost::asio::error::eof || error == boost::asio::error::connection_reset) { wxTheApp->CallAfter([this]() { log->Message(wxString() + getHostName() + ": disconnected."); close(); }); return true; } else if (error == boost::asio::error::connection_aborted) { logMessage("You have left the server."); return true; } else if (error == boost::asio::error::connection_refused) { // Connection refused - server might be using a different port logMessage("Connection refused. The server might be using a different port or not running."); return true; } else if (error == boost::asio::error::address_in_use) { // Address already in use - conflict with another instance logMessage("Network address already in use. Another instance might be using the same port."); return true; } else if (error == boost::asio::error::timed_out) { // Connection timeout logMessage("Connection attempt timed out. Server might be unreachable or blocked by firewall."); return true; } else if (error == boost::asio::error::network_unreachable || error == boost::asio::error::host_unreachable) { // Network or host unreachable logMessage("Network or host unreachable. Check your network connection."); return true; } else if (error) { // Handle other errors with the error code logMessage(wxString::Format("Connection error: %s (code: %d)", error.message(), error.value())); return true; } return false; } std::string LiveClient::getHostName() const { if (!socket) { return "not connected"; } return socket->remote_endpoint().address().to_string(); } void LiveClient::receiveHeader() { // Make sure buffer is properly initialized if (readMessage.buffer.size() < 4) { readMessage.buffer.resize(1024); } readMessage.position = 0; // Check if socket is valid before attempting to read if (!socket || !socket->is_open()) { logMessage("[Client]: Cannot receive header: Socket is not open"); return; } try { logMessage("[Client]: Waiting for incoming packet header..."); boost::asio::async_read(*socket, boost::asio::buffer(readMessage.buffer, 4), [this](const boost::system::error_code& error, size_t bytesReceived) -> void { if (error) { if (!handleError(error)) { wxString errorMsg = wxString::Format("[Client]: Network error: %s (code: %d)", error.message(), error.value()); logMessage(errorMsg); } } else if (bytesReceived < 4) { wxString errorMsg = wxString::Format("[Client]: Could not receive header [size: %zu], recovery needed", bytesReceived); logMessage(errorMsg); // Additional debug information about the connection logMessage(wxString::Format("[Client]: Connection details: Host=%s, Buffer size=%zu", getHostName(), readMessage.buffer.size())); // If we received some data but not enough, try again if (bytesReceived > 0) { logMessage("[Client]: Partial header received, attempting recovery..."); wxTheApp->CallAfter([this]() { receiveHeader(); }); } else { logMessage("[Client]: No data received, attempting direct reconnection..."); wxTheApp->CallAfter([this]() { // Try to reconnect if possible try { receiveHeader(); } catch (std::exception& e) { logMessage(wxString::Format("[Client]: Recovery failed: %s", e.what())); close(); } }); } } else { // Successfully received header, now receive the packet uint32_t packetSize = readMessage.read(); logMessage(wxString::Format("[Client]: Received header, packet size: %u bytes", packetSize)); // Check for zero packet size if (packetSize == 0) { logMessage("[Client]: Received zero-size packet, skipping and waiting for next header"); wxTheApp->CallAfter([this]() { receiveHeader(); }); } else { receive(packetSize); } } }); } catch (std::exception& e) { logMessage(wxString::Format("[Client]: Exception in receiveHeader: %s", e.what())); close(); } } void LiveClient::receive(uint32_t packetSize) { // Safety check for packet size if (packetSize > 1024 * 1024) { // 1MB sanity check logMessage(wxString::Format("[Client]: Suspiciously large packet size received: %u bytes, aborting", packetSize)); close(); return; } // Resize buffer to accommodate the incoming packet readMessage.buffer.resize(readMessage.position + packetSize); logMessage(wxString::Format("[Client]: Reading packet body (%u bytes)", packetSize)); boost::asio::async_read(*socket, boost::asio::buffer(&readMessage.buffer[readMessage.position], packetSize), [this, packetSize](const boost::system::error_code& error, size_t bytesReceived) -> void { if (error) { if (!handleError(error)) { wxString errorMsg = wxString::Format("[Client]: Network error reading packet: %s", error.message()); logMessage(errorMsg); } } else if (bytesReceived < packetSize) { wxString errorMsg = wxString::Format("[Client]: Incomplete packet received [got: %zu, expected: %u], attempting recovery", bytesReceived, packetSize); logMessage(errorMsg); // Log packet details for debugging logMessage(wxString::Format("[Client]: Packet details: Buffer size=%zu, Position=%zu", readMessage.buffer.size(), readMessage.position)); // Try to recover by reading the remaining bytes uint32_t remainingBytes = packetSize - bytesReceived; wxTheApp->CallAfter([this, remainingBytes, bytesReceived]() { logMessage(wxString::Format("[Client]: Attempting to receive remaining %u bytes...", remainingBytes)); boost::asio::async_read(*socket, boost::asio::buffer(&readMessage.buffer[readMessage.position + bytesReceived], remainingBytes), [this](const boost::system::error_code& innerError, size_t innerBytesReceived) { if (!innerError && innerBytesReceived > 0) { logMessage("[Client]: Successfully recovered partial packet"); wxTheApp->CallAfter([this]() { parsePacket(std::move(readMessage)); receiveHeader(); }); } else { logMessage("[Client]: Recovery failed, restarting from header"); wxTheApp->CallAfter([this]() { receiveHeader(); }); } } ); }); } else { // Successfully received the complete packet logMessage(wxString::Format("[Client]: Successfully received complete packet (%zu bytes)", bytesReceived)); wxTheApp->CallAfter([this]() { parsePacket(std::move(readMessage)); receiveHeader(); }); } }); } void LiveClient::send(NetworkMessage& message) { // Validate message size to avoid sending empty messages if (message.size == 0) { logMessage("[Client]: Attempted to send empty message, ignoring"); return; } // Write size to the first 4 bytes (header) memcpy(&message.buffer[0], &message.size, 4); // Log the message we're sending logMessage(wxString::Format("[Client]: Sending packet to server (size: %zu bytes)", message.size + 4)); try { boost::asio::async_write(*socket, boost::asio::buffer(message.buffer, message.size + 4), [this, msgSize = message.size](const boost::system::error_code& error, size_t bytesTransferred) -> void { if (error) { logMessage(wxString::Format("[Client]: Error sending packet to server: %s", error.message())); } else if (bytesTransferred != msgSize + 4) { logMessage(wxString::Format("[Client]: Incomplete packet sent to server [sent: %zu, expected: %zu]", bytesTransferred, msgSize + 4)); } else { logMessage(wxString::Format("[Client]: Successfully sent packet to server (%zu bytes)", bytesTransferred)); } } ); } catch (std::exception& e) { logMessage(wxString::Format("[Client]: Exception sending packet to server: %s", e.what())); } } void LiveClient::updateCursor(const Position& position) { LiveCursor cursor; cursor.id = 77; // Unimportant, server fixes it for us cursor.pos = position; cursor.color = wxColor( g_settings.getInteger(Config::CURSOR_RED), g_settings.getInteger(Config::CURSOR_GREEN), g_settings.getInteger(Config::CURSOR_BLUE), g_settings.getInteger(Config::CURSOR_ALPHA) ); NetworkMessage message; message.write(PACKET_CLIENT_UPDATE_CURSOR); writeCursor(message, cursor); // Send without logging cursor movements sendWithoutLogging(message); } // Add a new method for sending messages without logging void LiveClient::sendWithoutLogging(NetworkMessage& message) { // Validate message size to avoid sending empty messages if (message.size == 0) { return; } // Write size to the first 4 bytes (header) memcpy(&message.buffer[0], &message.size, 4); try { boost::asio::async_write(*socket, boost::asio::buffer(message.buffer, message.size + 4), [this, msgSize = message.size](const boost::system::error_code& error, size_t bytesTransferred) -> void { if (error) { logMessage(wxString::Format("[Client]: Error sending packet to server: %s", error.message())); } else if (bytesTransferred != msgSize + 4) { logMessage(wxString::Format("[Client]: Incomplete packet sent to server [sent: %zu, expected: %zu]", bytesTransferred, msgSize + 4)); } } ); } catch (std::exception& e) { logMessage(wxString::Format("[Client]: Exception sending packet to server: %s", e.what())); } } LiveLogTab* LiveClient::createLogWindow(wxWindow* parent) { MapTabbook* mtb = dynamic_cast(parent); ASSERT(mtb); log = newd LiveLogTab(mtb, this); log->Message("New Live mapping session started."); return log; } MapTab* LiveClient::createEditorWindow() { MapTabbook* mtb = dynamic_cast(g_gui.tabbook); ASSERT(mtb); MapTab* edit = newd MapTab(mtb, editor); edit->OnSwitchEditorMode(g_gui.IsSelectionMode() ? SELECTION_MODE : DRAWING_MODE); return edit; } void LiveClient::sendHello() { logMessage("[Client]: Preparing hello packet..."); NetworkMessage message; message.write(PACKET_HELLO_FROM_CLIENT); message.write(__RME_VERSION_ID__); message.write(__LIVE_NET_VERSION__); message.write(g_gui.GetCurrentVersionID()); message.write(nstr(name)); message.write(nstr(password)); // Calculate overall packet size for logging size_t packetSize = message.size + 4; // Including header size logMessage(wxString::Format("[Client]: Sending hello packet (size: %zu bytes, name: %s)", packetSize, name)); try { send(message); logMessage("[Client]: Hello packet sent successfully"); } catch (std::exception& e) { logMessage(wxString::Format("[Client]: Error sending hello packet: %s", e.what())); } } void LiveClient::sendNodeRequests() { if (queryNodeList.empty()) { return; } NetworkMessage message; message.write(PACKET_REQUEST_NODES); message.write(queryNodeList.size()); for (uint32_t node : queryNodeList) { message.write(node); } send(message); queryNodeList.clear(); } void LiveClient::sendChanges(DirtyList& dirtyList) { // Don't send changes if client is not ready for drawing operations if (!isDrawingReady) { logMessage("[Client]: Cannot send drawing changes, connection not fully established yet"); return; } ChangeList& changeList = dirtyList.GetChanges(); if (changeList.empty()) { return; } try { // Count tiles for logging int tileCount = 0; // Reset the writer and serialize changes mapWriter.reset(); for (Change* change : changeList) { switch (change->getType()) { case CHANGE_TILE: { const Position& position = static_cast(change->getData())->getPosition(); sendTile(mapWriter, editor->map.getTile(position), &position); tileCount++; break; } default: break; } } mapWriter.endNode(); // Create and send the message NetworkMessage message; message.write(PACKET_CHANGE_LIST); std::string data(reinterpret_cast(mapWriter.getMemory()), mapWriter.getSize()); message.write(data); logMessage(wxString::Format("[Client]: Sending %d tile changes to server (data size: %zu bytes)", tileCount, data.size())); send(message); } catch (std::exception& e) { logMessage(wxString::Format("[Client]: Error sending changes: %s", e.what())); } } void LiveClient::sendChat(const wxString& chatMessage) { // Don't send empty messages if (chatMessage.IsEmpty()) { return; } logMessage(wxString::Format("Sending chat message: %s", chatMessage)); NetworkMessage message; message.write(PACKET_CLIENT_TALK); message.write(nstr(chatMessage)); send(message); } void LiveClient::sendReady() { NetworkMessage message; message.write(PACKET_READY_CLIENT); send(message); } void LiveClient::queryNode(int32_t ndx, int32_t ndy, bool underground) { uint32_t nd = 0; nd |= ((ndx >> 2) << 18); nd |= ((ndy >> 2) << 4); nd |= (underground ? 1 : 0); queryNodeList.insert(nd); } void LiveClient::parsePacket(NetworkMessage message) { uint8_t packetType; try { while (message.position < message.buffer.size()) { // Log packet position for debugging size_t packetStart = message.position; // Check if we have at least 1 byte to read the packet type if (message.position + 1 > message.buffer.size()) { logMessage("[Client]: Warning - incomplete packet at end of buffer, ignoring"); break; } packetType = message.read(); logMessage(wxString::Format("[Client]: Parsing packet type 0x%02X at position %zu", packetType, packetStart)); try { switch (packetType) { case PACKET_HELLO_FROM_SERVER: parseHello(message); break; case PACKET_KICK: parseKick(message); break; case PACKET_ACCEPTED_CLIENT: parseClientAccepted(message); break; case PACKET_CHANGE_CLIENT_VERSION: parseChangeClientVersion(message); break; case PACKET_SERVER_TALK: parseServerTalk(message); break; case PACKET_NODE: parseNode(message); break; case PACKET_CURSOR_UPDATE: parseCursorUpdate(message); break; case PACKET_START_OPERATION: parseStartOperation(message); break; case PACKET_UPDATE_OPERATION: parseUpdateOperation(message); break; case PACKET_COLOR_UPDATE: parseColorUpdate(message); break; default: { logMessage(wxString::Format("[Client]: Unknown packet type 0x%02X received, disconnecting", packetType)); close(); return; } } } catch (std::exception& e) { logMessage(wxString::Format("[Client]: Error parsing packet type 0x%02X: %s", packetType, e.what())); // Continue to next packet if possible } } } catch (std::exception& e) { logMessage(wxString::Format("[Client]: Fatal error parsing packet: %s", e.what())); close(); } } void LiveClient::parseHello(NetworkMessage& message) { ASSERT(editor == nullptr); editor = newd Editor(g_gui.copybuffer, this); Map& map = editor->map; map.setName("Live Map - " + message.read()); map.setWidth(message.read()); map.setHeight(message.read()); createEditorWindow(); } void LiveClient::parseKick(NetworkMessage& message) { const std::string& kickMessage = message.read(); close(); g_gui.PopupDialog("Disconnected", wxstr(kickMessage), wxOK); } void LiveClient::parseClientAccepted(NetworkMessage& message) { try { // Write to a diagnostic file instead of UI logging to prevent crashes std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "client_status.log").ToStdString(), std::ios::app); if (logFile.is_open()) { wxDateTime now = wxDateTime::Now(); logFile << now.FormatISOCombined() << ": Client accepted, setting up cursor\n"; } // Initialize the host's cursor when we're accepted LiveCursor hostCursor; hostCursor.id = 0; // Host is always ID 0 hostCursor.color = wxColor(255, 0, 0); // Default red color for host hostCursor.pos = Position(); // Default position cursors[0] = hostCursor; // Add host cursor to our list if (logFile.is_open()) { logFile << "Host cursor initialized\n"; } // Set flag indicating we're fully connected and ready to draw - with extra safety wxTheApp->CallAfter([this]() { // Set the ready flag in a deferred way to ensure all initialization is complete if (!stopped) { isDrawingReady = true; // Write to diagnostic file std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "client_status.log").ToStdString(), std::ios::app); if (logFile.is_open()) { wxDateTime now = wxDateTime::Now(); logFile << now.FormatISOCombined() << ": Drawing ready flag set to true\n"; logFile.close(); } } }); if (logFile.is_open()) { logFile << "Ready flag setup queued\n"; logFile.close(); } sendReady(); } catch (const std::exception& e) { // Log error to file std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "client_error.log").ToStdString(), std::ios::app); if (logFile.is_open()) { wxDateTime now = wxDateTime::Now(); logFile << now.FormatISOCombined() << ": Error in parseClientAccepted: " << e.what() << "\n"; logFile.close(); } } } void LiveClient::parseChangeClientVersion(NetworkMessage& message) { ClientVersionID clientVersion = static_cast(message.read()); if (!g_gui.CloseAllEditors()) { close(); return; } wxString error; wxArrayString warnings; g_gui.LoadVersion(clientVersion, error, warnings); sendReady(); } void LiveClient::parseServerTalk(NetworkMessage& message) { const std::string& speaker = message.read(); const std::string& chatMessage = message.read(); log->Chat( wxstr(speaker), wxstr(chatMessage) ); } void LiveClient::parseNode(NetworkMessage& message) { try { uint32_t nodeid = message.read(); int32_t ndx = nodeid >> 18; int32_t ndy = (nodeid >> 4) & 0x3FFF; bool underground = (nodeid & 1) == 1; // Log node reception for debugging logMessage(wxString::Format("[Client]: Received node update [%d,%d,%s]", ndx, ndy, underground ? "underground" : "surface")); // Queue the node processing on the main thread to avoid threading issues wxTheApp->CallAfter([this, message = std::move(message), ndx, ndy, underground]() mutable { if (!editor) { logMessage("[Client]: Warning - received node update but no editor available"); return; } NetworkedAction* action = static_cast(editor->actionQueue->createAction(ACTION_REMOTE)); // Process the node data safely receiveNode(message, *editor, action, ndx, ndy, underground); // Only add the action if it contains changes if (action->size() > 0) { editor->actionQueue->addAction(action); g_gui.RefreshView(); g_gui.UpdateMinimap(); logMessage(wxString::Format("[Client]: Applying node update [%d,%d,%s]", ndx, ndy, underground ? "underground" : "surface")); logMessage("[Client]: Node update applied successfully."); } else { // Use proper action destruction } }); } catch (std::exception& e) { logMessage(wxString::Format("[Client]: Error parsing node packet: %s", e.what())); } } void LiveClient::parseCursorUpdate(NetworkMessage& message) { LiveCursor cursor = readCursor(message); // Store previous cursor data if it exists LiveCursor prevCursor; bool cursorExisted = false; auto it = cursors.find(cursor.id); if (it != cursors.end()) { prevCursor = it->second; cursorExisted = true; } // Update the cursor cursors[cursor.id] = cursor; // Only log when a new cursor appears or color changes, not for movement if (!cursorExisted) { logMessage(wxString::Format("[Client]: New cursor appeared for client ID %u", cursor.id)); } else if (prevCursor.color != cursor.color) { logMessage(wxString::Format("[Client]: Cursor color changed for client ID %u", cursor.id)); } // Update client list after receiving cursor updates if (log) { wxTheApp->CallAfter([this]() { std::unordered_map dummyPeers; log->UpdateClientList(dummyPeers); }); } g_gui.RefreshView(); } void LiveClient::parseStartOperation(NetworkMessage& message) { const std::string& operation = message.read(); currentOperation = wxstr(operation); g_gui.SetStatusText("Server Operation in Progress: " + currentOperation + "... (0%)"); } void LiveClient::parseUpdateOperation(NetworkMessage& message) { int32_t percent = message.read(); if (percent >= 100) { g_gui.SetStatusText("Server Operation Finished."); } else { g_gui.SetStatusText("Server Operation in Progress: " + currentOperation + "... (" + std::to_string(percent) + "%)"); } } void LiveClient::parseColorUpdate(NetworkMessage& message) { // Read client ID whose color changed uint32_t clientId = message.read(); // Read the color components uint8_t r = message.read(); uint8_t g = message.read(); uint8_t b = message.read(); uint8_t a = message.read(); wxColor newColor(r, g, b, a); logMessage(wxString::Format("[Client]: Received color update for client %u: RGB(%d,%d,%d)", clientId, r, g, b)); // Update the color in our local cursor list if (clientId == 0) { // Server's color (host) // Update the host's cursor in our list for (auto& cursorPair : cursors) { if (cursorPair.first == 0) { LiveCursor& cursor = cursorPair.second; cursor.color = newColor; break; } } } else { // Another client's color // Find this client ID in our cursor list and update it for (auto& cursorPair : cursors) { if (cursorPair.first == clientId) { LiveCursor& cursor = cursorPair.second; cursor.color = newColor; break; } } } // Refresh view to show updated cursor color g_gui.RefreshView(); // Also update the client list UI if the log tab exists if (log) { wxTheApp->CallAfter([this]() { // We don't have direct access to the peer list as a client, // so just tell the log tab to update based on cursor information std::unordered_map dummyPeers; log->UpdateClientList(dummyPeers); }); } } // Send a request to change user color void LiveClient::sendColorUpdate(uint32_t targetClientId, const wxColor& color) { logMessage(wxString::Format("[Client]: Sending color update request for client %u: RGB(%d,%d,%d)", targetClientId, color.Red(), color.Green(), color.Blue())); NetworkMessage message; message.write(PACKET_CLIENT_COLOR_UPDATE); message.write(targetClientId); message.write(color.Red()); message.write(color.Green()); message.write(color.Blue()); message.write(color.Alpha()); send(message); }