1//////////////////////////////////////////////////////////////////////
2// This file is part of Remere's Map Editor
3//////////////////////////////////////////////////////////////////////
4// Remere's Map Editor is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Remere's Map Editor is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16//////////////////////////////////////////////////////////////////////
17
18#include "main.h"
19
20#include "live_client.h"
21#include "live_tab.h"
22#include "live_action.h"
23#include "editor.h"
24
25#include <wx/event.h>
26#include <fstream>
27
28LiveClient::LiveClient() :
29 LiveSocket(),
30 readMessage(), queryNodeList(), currentOperation(),
31 resolver(nullptr), socket(nullptr), editor(nullptr), stopped(false), isDrawingReady(false) {
32 // Initialize buffer with minimum size to prevent "size 0" errors
33 readMessage.buffer.resize(1024);
34 readMessage.position = 0;
35
36 // Log initialization to file only to help with debugging
37 std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "client_init.log").ToStdString(), std::ios::app);
38 if (logFile.is_open()) {
39 wxDateTime now = wxDateTime::Now();
40 logFile << now.FormatISOCombined() << ": LiveClient initialized\n";
41 logFile.close();
42 }
43}
44
45LiveClient::~LiveClient() {
46 //
47}
48
49bool LiveClient::connect(const std::string& address, uint16_t port) {
50 NetworkConnection& connection = NetworkConnection::getInstance();
51 if (!connection.start()) {
52 setLastError("The previous connection has not been terminated yet.");
53 return false;
54 }
55
56 auto& service = connection.get_service();
57 if (!resolver) {
58 resolver = std::make_shared<boost::asio::ip::tcp::resolver>(service);
59 }
60
61 if (!socket) {
62 socket = std::make_shared<boost::asio::ip::tcp::socket>(service);
63 }
64
65 // Set socket options for better error detection
66 boost::system::error_code ec;
67 socket->set_option(boost::asio::socket_base::keep_alive(true), ec);
68 if (ec) {
69 logMessage("Warning: Could not set keep_alive option: " + ec.message());
70 }
71
72 // Set connection timeout options
73 socket->set_option(boost::asio::socket_base::linger(true, 10), ec);
74 if (ec) {
75 logMessage("Warning: Could not set linger option: " + ec.message());
76 }
77
78 // Log connection attempt
79 logMessage(wxString::Format("Attempting to connect to %s:%d...", address, port));
80
81 // Try both IP address and hostname when dealing with localhost/0.0.0.0
82 bool isLocalAddress = (address == "localhost" || address == "127.0.0.1" || address == "0.0.0.0");
83
84 // If it's a local address, try both localhost and 127.0.0.1
85 if (isLocalAddress) {
86 // Create a vector of addresses to try
87 std::vector<std::string> addressesToTry;
88
89 // Always try the original address first
90 addressesToTry.push_back(address);
91
92 // Then add alternatives for local addresses
93 if (address == "localhost") {
94 addressesToTry.push_back("127.0.0.1");
95 } else if (address == "127.0.0.1") {
96 addressesToTry.push_back("localhost");
97 } else if (address == "0.0.0.0") {
98 addressesToTry.push_back("localhost");
99 addressesToTry.push_back("127.0.0.1");
100 }
101
102 // Try each address
103 for (const auto& addr : addressesToTry) {
104 resolver->async_resolve(
105 addr,
106 std::to_string(port),
107 [this, addr, addressesToTry, port](const boost::system::error_code& error,
108 boost::asio::ip::tcp::resolver::results_type results) -> void {
109 if (error) {
110 wxString errorMsg = wxString::Format("Resolution error for %s: %s", addr, error.message());
111 logMessage(errorMsg);
112
113 // If this is the last address in the list and we failed, log a final error
114 if (addr == addressesToTry.back()) {
115 logMessage("Failed to resolve any local address. Check your network configuration.");
116 }
117 } else {
118 logMessage(wxString::Format("Host %s resolved. Connecting to endpoint...", addr));
119 tryConnect(results.begin());
120 }
121 }
122 );
123 }
124 } else {
125 // For non-local addresses, just try the given address
126 resolver->async_resolve(
127 address,
128 std::to_string(port),
129 [this](const boost::system::error_code& error, boost::asio::ip::tcp::resolver::results_type results) -> void {
130 if (error) {
131 wxString errorMsg = wxString::Format("Resolution error: %s", error.message());
132 logMessage(errorMsg);
133 } else {
134 logMessage(wxString::Format("Host resolved. Connecting to endpoint..."));
135 tryConnect(results.begin());
136 }
137 }
138 );
139 }
140
141 return true;
142}
143
144void LiveClient::tryConnect(boost::asio::ip::tcp::resolver::results_type::iterator endpoint_iterator) {
145 if (stopped) {
146 logMessage("Connection attempt aborted: Connection was stopped.");
147 return;
148 }
149
150 // Check if we're at the end of the endpoints
151 if (endpoint_iterator == boost::asio::ip::tcp::resolver::results_type::iterator()) {
152 logMessage("Connection attempt failed: No more endpoints to try.");
153 return;
154 }
155
156 logMessage("Joining server " + endpoint_iterator->host_name() + ":" + endpoint_iterator->service_name() + "...");
157
158 // In modern Boost.Asio, we need to connect to a single endpoint
159 auto current_endpoint = *endpoint_iterator;
160 auto next_endpoint = endpoint_iterator;
161 ++next_endpoint; // Advance to get the next endpoint
162
163 socket->async_connect(current_endpoint,
164 [this, next_endpoint](boost::system::error_code error) -> void {
165 if (!socket->is_open()) {
166 logMessage(wxString::Format("Connection failed: Socket is not open. Trying next endpoint..."));
167 tryConnect(next_endpoint);
168 } else if (error) {
169 wxString errorMsg = wxString::Format("Connection error: %s (code: %d)", error.message(), error.value());
170 logMessage(errorMsg);
171
172 if (handleError(error)) {
173 logMessage("Trying next endpoint...");
174 tryConnect(next_endpoint);
175 } else {
176 wxTheApp->CallAfter([this]() {
177 logMessage("All connection attempts failed. Closing connection.");
178 close();
179 g_gui.CloseLiveEditors(this);
180 });
181 }
182 } else {
183 socket->set_option(boost::asio::ip::tcp::no_delay(true), error);
184 if (error) {
185 logMessage(wxString::Format("Warning: Could not set TCP no_delay option: %s", error.message()));
186 wxTheApp->CallAfter([this]() {
187 close();
188 });
189 return;
190 }
191
192 logMessage("Connection established successfully. Sending hello packet...");
193 sendHello();
194 receiveHeader();
195 }
196 });
197}
198
199void LiveClient::close() {
200 if (resolver) {
201 resolver->cancel();
202 }
203
204 if (socket) {
205 socket->close();
206 }
207
208 if (log) {
209 log->Message("Disconnected from server.");
210 log->Disconnect();
211 log = nullptr;
212 }
213
214 // Reset drawing ready flag on disconnect
215 isDrawingReady = false;
216 stopped = true;
217}
218
219bool LiveClient::handleError(const boost::system::error_code& error) {
220 // Handle common connection errors with more informative messages
221 if (error == boost::asio::error::eof || error == boost::asio::error::connection_reset) {
222 wxTheApp->CallAfter([this]() {
223 log->Message(wxString() + getHostName() + ": disconnected.");
224 close();
225 });
226 return true;
227 } else if (error == boost::asio::error::connection_aborted) {
228 logMessage("You have left the server.");
229 return true;
230 } else if (error == boost::asio::error::connection_refused) {
231 // Connection refused - server might be using a different port
232 logMessage("Connection refused. The server might be using a different port or not running.");
233 return true;
234 } else if (error == boost::asio::error::address_in_use) {
235 // Address already in use - conflict with another instance
236 logMessage("Network address already in use. Another instance might be using the same port.");
237 return true;
238 } else if (error == boost::asio::error::timed_out) {
239 // Connection timeout
240 logMessage("Connection attempt timed out. Server might be unreachable or blocked by firewall.");
241 return true;
242 } else if (error == boost::asio::error::network_unreachable ||
243 error == boost::asio::error::host_unreachable) {
244 // Network or host unreachable
245 logMessage("Network or host unreachable. Check your network connection.");
246 return true;
247 } else if (error) {
248 // Handle other errors with the error code
249 logMessage(wxString::Format("Connection error: %s (code: %d)",
250 error.message(), error.value()));
251 return true;
252 }
253 return false;
254}
255
256std::string LiveClient::getHostName() const {
257 if (!socket) {
258 return "not connected";
259 }
260 return socket->remote_endpoint().address().to_string();
261}
262
263void LiveClient::receiveHeader() {
264 // Make sure buffer is properly initialized
265 if (readMessage.buffer.size() < 4) {
266 readMessage.buffer.resize(1024);
267 }
268
269 readMessage.position = 0;
270
271 // Check if socket is valid before attempting to read
272 if (!socket || !socket->is_open()) {
273 logMessage("[Client]: Cannot receive header: Socket is not open");
274 return;
275 }
276
277 try {
278 logMessage("[Client]: Waiting for incoming packet header...");
279
280 boost::asio::async_read(*socket, boost::asio::buffer(readMessage.buffer, 4),
281 [this](const boost::system::error_code& error, size_t bytesReceived) -> void {
282 if (error) {
283 if (!handleError(error)) {
284 wxString errorMsg = wxString::Format("[Client]: Network error: %s (code: %d)",
285 error.message(), error.value());
286 logMessage(errorMsg);
287 }
288 } else if (bytesReceived < 4) {
289 wxString errorMsg = wxString::Format("[Client]: Could not receive header [size: %zu], recovery needed",
290 bytesReceived);
291 logMessage(errorMsg);
292
293 // Additional debug information about the connection
294 logMessage(wxString::Format("[Client]: Connection details: Host=%s, Buffer size=%zu",
295 getHostName(), readMessage.buffer.size()));
296
297 // If we received some data but not enough, try again
298 if (bytesReceived > 0) {
299 logMessage("[Client]: Partial header received, attempting recovery...");
300 wxTheApp->CallAfter([this]() {
301 receiveHeader();
302 });
303 } else {
304 logMessage("[Client]: No data received, attempting direct reconnection...");
305 wxTheApp->CallAfter([this]() {
306 // Try to reconnect if possible
307 try {
308 receiveHeader();
309 } catch (std::exception& e) {
310 logMessage(wxString::Format("[Client]: Recovery failed: %s", e.what()));
311 close();
312 }
313 });
314 }
315 } else {
316 // Successfully received header, now receive the packet
317 uint32_t packetSize = readMessage.read<uint32_t>();
318 logMessage(wxString::Format("[Client]: Received header, packet size: %u bytes", packetSize));
319
320 // Check for zero packet size
321 if (packetSize == 0) {
322 logMessage("[Client]: Received zero-size packet, skipping and waiting for next header");
323 wxTheApp->CallAfter([this]() {
324 receiveHeader();
325 });
326 } else {
327 receive(packetSize);
328 }
329 }
330 });
331 } catch (std::exception& e) {
332 logMessage(wxString::Format("[Client]: Exception in receiveHeader: %s", e.what()));
333 close();
334 }
335}
336
337void LiveClient::receive(uint32_t packetSize) {
338 // Safety check for packet size
339 if (packetSize > 1024 * 1024) { // 1MB sanity check
340 logMessage(wxString::Format("[Client]: Suspiciously large packet size received: %u bytes, aborting", packetSize));
341 close();
342 return;
343 }
344
345 // Resize buffer to accommodate the incoming packet
346 readMessage.buffer.resize(readMessage.position + packetSize);
347
348 logMessage(wxString::Format("[Client]: Reading packet body (%u bytes)", packetSize));
349
350 boost::asio::async_read(*socket, boost::asio::buffer(&readMessage.buffer[readMessage.position], packetSize),
351 [this, packetSize](const boost::system::error_code& error, size_t bytesReceived) -> void {
352 if (error) {
353 if (!handleError(error)) {
354 wxString errorMsg = wxString::Format("[Client]: Network error reading packet: %s", error.message());
355 logMessage(errorMsg);
356 }
357 } else if (bytesReceived < packetSize) {
358 wxString errorMsg = wxString::Format("[Client]: Incomplete packet received [got: %zu, expected: %u], attempting recovery",
359 bytesReceived, packetSize);
360 logMessage(errorMsg);
361
362 // Log packet details for debugging
363 logMessage(wxString::Format("[Client]: Packet details: Buffer size=%zu, Position=%zu",
364 readMessage.buffer.size(), readMessage.position));
365
366 // Try to recover by reading the remaining bytes
367 uint32_t remainingBytes = packetSize - bytesReceived;
368 wxTheApp->CallAfter([this, remainingBytes, bytesReceived]() {
369 logMessage(wxString::Format("[Client]: Attempting to receive remaining %u bytes...", remainingBytes));
370
371 boost::asio::async_read(*socket,
372 boost::asio::buffer(&readMessage.buffer[readMessage.position + bytesReceived], remainingBytes),
373 [this](const boost::system::error_code& innerError, size_t innerBytesReceived) {
374 if (!innerError && innerBytesReceived > 0) {
375 logMessage("[Client]: Successfully recovered partial packet");
376 wxTheApp->CallAfter([this]() {
377 parsePacket(std::move(readMessage));
378 receiveHeader();
379 });
380 } else {
381 logMessage("[Client]: Recovery failed, restarting from header");
382 wxTheApp->CallAfter([this]() {
383 receiveHeader();
384 });
385 }
386 }
387 );
388 });
389 } else {
390 // Successfully received the complete packet
391 logMessage(wxString::Format("[Client]: Successfully received complete packet (%zu bytes)", bytesReceived));
392
393 wxTheApp->CallAfter([this]() {
394 parsePacket(std::move(readMessage));
395 receiveHeader();
396 });
397 }
398 });
399}
400
401void LiveClient::send(NetworkMessage& message) {
402 // Validate message size to avoid sending empty messages
403 if (message.size == 0) {
404 logMessage("[Client]: Attempted to send empty message, ignoring");
405 return;
406 }
407
408 // Write size to the first 4 bytes (header)
409 memcpy(&message.buffer[0], &message.size, 4);
410
411 // Log the message we're sending
412 logMessage(wxString::Format("[Client]: Sending packet to server (size: %zu bytes)",
413 message.size + 4));
414
415 try {
416 boost::asio::async_write(*socket,
417 boost::asio::buffer(message.buffer, message.size + 4),
418 [this, msgSize = message.size](const boost::system::error_code& error, size_t bytesTransferred) -> void {
419 if (error) {
420 logMessage(wxString::Format("[Client]: Error sending packet to server: %s",
421 error.message()));
422 } else if (bytesTransferred != msgSize + 4) {
423 logMessage(wxString::Format("[Client]: Incomplete packet sent to server [sent: %zu, expected: %zu]",
424 bytesTransferred, msgSize + 4));
425 } else {
426 logMessage(wxString::Format("[Client]: Successfully sent packet to server (%zu bytes)",
427 bytesTransferred));
428 }
429 }
430 );
431 } catch (std::exception& e) {
432 logMessage(wxString::Format("[Client]: Exception sending packet to server: %s", e.what()));
433 }
434}
435
436void LiveClient::updateCursor(const Position& position) {
437 LiveCursor cursor;
438 cursor.id = 77; // Unimportant, server fixes it for us
439 cursor.pos = position;
440 cursor.color = wxColor(
441 g_settings.getInteger(Config::CURSOR_RED),
442 g_settings.getInteger(Config::CURSOR_GREEN),
443 g_settings.getInteger(Config::CURSOR_BLUE),
444 g_settings.getInteger(Config::CURSOR_ALPHA)
445 );
446
447 NetworkMessage message;
448 message.write<uint8_t>(PACKET_CLIENT_UPDATE_CURSOR);
449 writeCursor(message, cursor);
450
451 // Send without logging cursor movements
452 sendWithoutLogging(message);
453}
454
455// Add a new method for sending messages without logging
456void LiveClient::sendWithoutLogging(NetworkMessage& message) {
457 // Validate message size to avoid sending empty messages
458 if (message.size == 0) {
459 return;
460 }
461
462 // Write size to the first 4 bytes (header)
463 memcpy(&message.buffer[0], &message.size, 4);
464
465 try {
466 boost::asio::async_write(*socket,
467 boost::asio::buffer(message.buffer, message.size + 4),
468 [this, msgSize = message.size](const boost::system::error_code& error, size_t bytesTransferred) -> void {
469 if (error) {
470 logMessage(wxString::Format("[Client]: Error sending packet to server: %s",
471 error.message()));
472 } else if (bytesTransferred != msgSize + 4) {
473 logMessage(wxString::Format("[Client]: Incomplete packet sent to server [sent: %zu, expected: %zu]",
474 bytesTransferred, msgSize + 4));
475 }
476 }
477 );
478 } catch (std::exception& e) {
479 logMessage(wxString::Format("[Client]: Exception sending packet to server: %s", e.what()));
480 }
481}
482
483LiveLogTab* LiveClient::createLogWindow(wxWindow* parent) {
484 MapTabbook* mtb = dynamic_cast<MapTabbook*>(parent);
485 ASSERT(mtb);
486
487 log = newd LiveLogTab(mtb, this);
488 log->Message("New Live mapping session started.");
489
490 return log;
491}
492
493MapTab* LiveClient::createEditorWindow() {
494 MapTabbook* mtb = dynamic_cast<MapTabbook*>(g_gui.tabbook);
495 ASSERT(mtb);
496
497 MapTab* edit = newd MapTab(mtb, editor);
498 edit->OnSwitchEditorMode(g_gui.IsSelectionMode() ? SELECTION_MODE : DRAWING_MODE);
499
500 return edit;
501}
502
503void LiveClient::sendHello() {
504 logMessage("[Client]: Preparing hello packet...");
505
506 NetworkMessage message;
507 message.write<uint8_t>(PACKET_HELLO_FROM_CLIENT);
508 message.write<uint32_t>(__RME_VERSION_ID__);
509 message.write<uint32_t>(__LIVE_NET_VERSION__);
510 message.write<uint32_t>(g_gui.GetCurrentVersionID());
511 message.write<std::string>(nstr(name));
512 message.write<std::string>(nstr(password));
513
514 // Calculate overall packet size for logging
515 size_t packetSize = message.size + 4; // Including header size
516
517 logMessage(wxString::Format("[Client]: Sending hello packet (size: %zu bytes, name: %s)",
518 packetSize, name));
519
520 try {
521 send(message);
522 logMessage("[Client]: Hello packet sent successfully");
523 } catch (std::exception& e) {
524 logMessage(wxString::Format("[Client]: Error sending hello packet: %s", e.what()));
525 }
526}
527
528void LiveClient::sendNodeRequests() {
529 if (queryNodeList.empty()) {
530 return;
531 }
532
533 NetworkMessage message;
534 message.write<uint8_t>(PACKET_REQUEST_NODES);
535
536 message.write<uint32_t>(queryNodeList.size());
537 for (uint32_t node : queryNodeList) {
538 message.write<uint32_t>(node);
539 }
540
541 send(message);
542 queryNodeList.clear();
543}
544
545void LiveClient::sendChanges(DirtyList& dirtyList) {
546 // Don't send changes if client is not ready for drawing operations
547 if (!isDrawingReady) {
548 logMessage("[Client]: Cannot send drawing changes, connection not fully established yet");
549 return;
550 }
551
552 ChangeList& changeList = dirtyList.GetChanges();
553 if (changeList.empty()) {
554 return;
555 }
556
557 try {
558 // Count tiles for logging
559 int tileCount = 0;
560
561 // Reset the writer and serialize changes
562 mapWriter.reset();
563
564 for (Change* change : changeList) {
565 switch (change->getType()) {
566 case CHANGE_TILE: {
567 const Position& position = static_cast<Tile*>(change->getData())->getPosition();
568 sendTile(mapWriter, editor->map.getTile(position), &position);
569 tileCount++;
570 break;
571 }
572 default:
573 break;
574 }
575 }
576 mapWriter.endNode();
577
578 // Create and send the message
579 NetworkMessage message;
580 message.write<uint8_t>(PACKET_CHANGE_LIST);
581
582 std::string data(reinterpret_cast<const char*>(mapWriter.getMemory()), mapWriter.getSize());
583 message.write<std::string>(data);
584
585 logMessage(wxString::Format("[Client]: Sending %d tile changes to server (data size: %zu bytes)",
586 tileCount, data.size()));
587
588 send(message);
589 } catch (std::exception& e) {
590 logMessage(wxString::Format("[Client]: Error sending changes: %s", e.what()));
591 }
592}
593
594void LiveClient::sendChat(const wxString& chatMessage) {
595 // Don't send empty messages
596 if (chatMessage.IsEmpty()) {
597 return;
598 }
599
600 logMessage(wxString::Format("Sending chat message: %s", chatMessage));
601
602 NetworkMessage message;
603 message.write<uint8_t>(PACKET_CLIENT_TALK);
604 message.write<std::string>(nstr(chatMessage));
605 send(message);
606}
607
608void LiveClient::sendReady() {
609 NetworkMessage message;
610 message.write<uint8_t>(PACKET_READY_CLIENT);
611 send(message);
612}
613
614void LiveClient::queryNode(int32_t ndx, int32_t ndy, bool underground) {
615 uint32_t nd = 0;
616 nd |= ((ndx >> 2) << 18);
617 nd |= ((ndy >> 2) << 4);
618 nd |= (underground ? 1 : 0);
619 queryNodeList.insert(nd);
620}
621
622void LiveClient::parsePacket(NetworkMessage message) {
623 uint8_t packetType;
624
625 try {
626 while (message.position < message.buffer.size()) {
627 // Log packet position for debugging
628 size_t packetStart = message.position;
629
630 // Check if we have at least 1 byte to read the packet type
631 if (message.position + 1 > message.buffer.size()) {
632 logMessage("[Client]: Warning - incomplete packet at end of buffer, ignoring");
633 break;
634 }
635
636 packetType = message.read<uint8_t>();
637 logMessage(wxString::Format("[Client]: Parsing packet type 0x%02X at position %zu",
638 packetType, packetStart));
639
640 try {
641 switch (packetType) {
642 case PACKET_HELLO_FROM_SERVER:
643 parseHello(message);
644 break;
645 case PACKET_KICK:
646 parseKick(message);
647 break;
648 case PACKET_ACCEPTED_CLIENT:
649 parseClientAccepted(message);
650 break;
651 case PACKET_CHANGE_CLIENT_VERSION:
652 parseChangeClientVersion(message);
653 break;
654 case PACKET_SERVER_TALK:
655 parseServerTalk(message);
656 break;
657 case PACKET_NODE:
658 parseNode(message);
659 break;
660 case PACKET_CURSOR_UPDATE:
661 parseCursorUpdate(message);
662 break;
663 case PACKET_START_OPERATION:
664 parseStartOperation(message);
665 break;
666 case PACKET_UPDATE_OPERATION:
667 parseUpdateOperation(message);
668 break;
669 case PACKET_COLOR_UPDATE:
670 parseColorUpdate(message);
671 break;
672 default: {
673 logMessage(wxString::Format("[Client]: Unknown packet type 0x%02X received, disconnecting", packetType));
674 close();
675 return;
676 }
677 }
678 } catch (std::exception& e) {
679 logMessage(wxString::Format("[Client]: Error parsing packet type 0x%02X: %s",
680 packetType, e.what()));
681 // Continue to next packet if possible
682 }
683 }
684 } catch (std::exception& e) {
685 logMessage(wxString::Format("[Client]: Fatal error parsing packet: %s", e.what()));
686 close();
687 }
688}
689
690void LiveClient::parseHello(NetworkMessage& message) {
691 ASSERT(editor == nullptr);
692 editor = newd Editor(g_gui.copybuffer, this);
693
694 Map& map = editor->map;
695 map.setName("Live Map - " + message.read<std::string>());
696 map.setWidth(message.read<uint16_t>());
697 map.setHeight(message.read<uint16_t>());
698
699 createEditorWindow();
700}
701
702void LiveClient::parseKick(NetworkMessage& message) {
703 const std::string& kickMessage = message.read<std::string>();
704 close();
705
706 g_gui.PopupDialog("Disconnected", wxstr(kickMessage), wxOK);
707}
708
709void LiveClient::parseClientAccepted(NetworkMessage& message) {
710 try {
711 // Write to a diagnostic file instead of UI logging to prevent crashes
712 std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "client_status.log").ToStdString(), std::ios::app);
713 if (logFile.is_open()) {
714 wxDateTime now = wxDateTime::Now();
715 logFile << now.FormatISOCombined() << ": Client accepted, setting up cursor\n";
716 }
717
718 // Initialize the host's cursor when we're accepted
719 LiveCursor hostCursor;
720 hostCursor.id = 0; // Host is always ID 0
721 hostCursor.color = wxColor(255, 0, 0); // Default red color for host
722 hostCursor.pos = Position(); // Default position
723 cursors[0] = hostCursor; // Add host cursor to our list
724
725 if (logFile.is_open()) {
726 logFile << "Host cursor initialized\n";
727 }
728
729 // Set flag indicating we're fully connected and ready to draw - with extra safety
730 wxTheApp->CallAfter([this]() {
731 // Set the ready flag in a deferred way to ensure all initialization is complete
732 if (!stopped) {
733 isDrawingReady = true;
734
735 // Write to diagnostic file
736 std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "client_status.log").ToStdString(), std::ios::app);
737 if (logFile.is_open()) {
738 wxDateTime now = wxDateTime::Now();
739 logFile << now.FormatISOCombined() << ": Drawing ready flag set to true\n";
740 logFile.close();
741 }
742 }
743 });
744
745 if (logFile.is_open()) {
746 logFile << "Ready flag setup queued\n";
747 logFile.close();
748 }
749
750 sendReady();
751 }
752 catch (const std::exception& e) {
753 // Log error to file
754 std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "client_error.log").ToStdString(), std::ios::app);
755 if (logFile.is_open()) {
756 wxDateTime now = wxDateTime::Now();
757 logFile << now.FormatISOCombined() << ": Error in parseClientAccepted: " << e.what() << "\n";
758 logFile.close();
759 }
760 }
761}
762
763void LiveClient::parseChangeClientVersion(NetworkMessage& message) {
764 ClientVersionID clientVersion = static_cast<ClientVersionID>(message.read<uint32_t>());
765 if (!g_gui.CloseAllEditors()) {
766 close();
767 return;
768 }
769
770 wxString error;
771 wxArrayString warnings;
772 g_gui.LoadVersion(clientVersion, error, warnings);
773
774 sendReady();
775}
776
777void LiveClient::parseServerTalk(NetworkMessage& message) {
778 const std::string& speaker = message.read<std::string>();
779 const std::string& chatMessage = message.read<std::string>();
780 log->Chat(
781 wxstr(speaker),
782 wxstr(chatMessage)
783 );
784}
785
786void LiveClient::parseNode(NetworkMessage& message) {
787 try {
788 uint32_t nodeid = message.read<uint32_t>();
789 int32_t ndx = nodeid >> 18;
790 int32_t ndy = (nodeid >> 4) & 0x3FFF;
791 bool underground = (nodeid & 1) == 1;
792
793 // Log node reception for debugging
794 logMessage(wxString::Format("[Client]: Received node update [%d,%d,%s]",
795 ndx, ndy, underground ? "underground" : "surface"));
796
797 // Queue the node processing on the main thread to avoid threading issues
798 wxTheApp->CallAfter([this, message = std::move(message), ndx, ndy, underground]() mutable {
799 if (!editor) {
800 logMessage("[Client]: Warning - received node update but no editor available");
801 return;
802 }
803
804 NetworkedAction* action = static_cast<NetworkedAction*>(editor->actionQueue->createAction(ACTION_REMOTE));
805
806 // Process the node data safely
807 receiveNode(message, *editor, action, ndx, ndy, underground);
808
809 // Only add the action if it contains changes
810 if (action->size() > 0) {
811 editor->actionQueue->addAction(action);
812 g_gui.RefreshView();
813 g_gui.UpdateMinimap();
814
815 logMessage(wxString::Format("[Client]: Applying node update [%d,%d,%s]",
816 ndx, ndy, underground ? "underground" : "surface"));
817 logMessage("[Client]: Node update applied successfully.");
818 } else {
819 // Use proper action destruction
820
821 }
822 });
823 } catch (std::exception& e) {
824 logMessage(wxString::Format("[Client]: Error parsing node packet: %s", e.what()));
825 }
826}
827
828void LiveClient::parseCursorUpdate(NetworkMessage& message) {
829 LiveCursor cursor = readCursor(message);
830
831 // Store previous cursor data if it exists
832 LiveCursor prevCursor;
833 bool cursorExisted = false;
834 auto it = cursors.find(cursor.id);
835 if (it != cursors.end()) {
836 prevCursor = it->second;
837 cursorExisted = true;
838 }
839
840 // Update the cursor
841 cursors[cursor.id] = cursor;
842
843 // Only log when a new cursor appears or color changes, not for movement
844 if (!cursorExisted) {
845 logMessage(wxString::Format("[Client]: New cursor appeared for client ID %u", cursor.id));
846 } else if (prevCursor.color != cursor.color) {
847 logMessage(wxString::Format("[Client]: Cursor color changed for client ID %u", cursor.id));
848 }
849
850 // Update client list after receiving cursor updates
851 if (log) {
852 wxTheApp->CallAfter([this]() {
853 std::unordered_map<uint32_t, LivePeer*> dummyPeers;
854 log->UpdateClientList(dummyPeers);
855 });
856 }
857
858 g_gui.RefreshView();
859}
860
861void LiveClient::parseStartOperation(NetworkMessage& message) {
862 const std::string& operation = message.read<std::string>();
863
864 currentOperation = wxstr(operation);
865 g_gui.SetStatusText("Server Operation in Progress: " + currentOperation + "... (0%)");
866}
867
868void LiveClient::parseUpdateOperation(NetworkMessage& message) {
869 int32_t percent = message.read<uint32_t>();
870 if (percent >= 100) {
871 g_gui.SetStatusText("Server Operation Finished.");
872 } else {
873 g_gui.SetStatusText("Server Operation in Progress: " + currentOperation + "... (" + std::to_string(percent) + "%)");
874 }
875}
876
877void LiveClient::parseColorUpdate(NetworkMessage& message) {
878 // Read client ID whose color changed
879 uint32_t clientId = message.read<uint32_t>();
880
881 // Read the color components
882 uint8_t r = message.read<uint8_t>();
883 uint8_t g = message.read<uint8_t>();
884 uint8_t b = message.read<uint8_t>();
885 uint8_t a = message.read<uint8_t>();
886 wxColor newColor(r, g, b, a);
887
888 logMessage(wxString::Format("[Client]: Received color update for client %u: RGB(%d,%d,%d)",
889 clientId, r, g, b));
890
891 // Update the color in our local cursor list
892 if (clientId == 0) {
893 // Server's color (host)
894 // Update the host's cursor in our list
895 for (auto& cursorPair : cursors) {
896 if (cursorPair.first == 0) {
897 LiveCursor& cursor = cursorPair.second;
898 cursor.color = newColor;
899 break;
900 }
901 }
902 } else {
903 // Another client's color
904 // Find this client ID in our cursor list and update it
905 for (auto& cursorPair : cursors) {
906 if (cursorPair.first == clientId) {
907 LiveCursor& cursor = cursorPair.second;
908 cursor.color = newColor;
909 break;
910 }
911 }
912 }
913
914 // Refresh view to show updated cursor color
915 g_gui.RefreshView();
916
917 // Also update the client list UI if the log tab exists
918 if (log) {
919 wxTheApp->CallAfter([this]() {
920 // We don't have direct access to the peer list as a client,
921 // so just tell the log tab to update based on cursor information
922 std::unordered_map<uint32_t, LivePeer*> dummyPeers;
923 log->UpdateClientList(dummyPeers);
924 });
925 }
926}
927
928// Send a request to change user color
929void LiveClient::sendColorUpdate(uint32_t targetClientId, const wxColor& color) {
930 logMessage(wxString::Format("[Client]: Sending color update request for client %u: RGB(%d,%d,%d)",
931 targetClientId, color.Red(), color.Green(), color.Blue()));
932
933 NetworkMessage message;
934 message.write<uint8_t>(PACKET_CLIENT_COLOR_UPDATE);
935 message.write<uint32_t>(targetClientId);
936 message.write<uint8_t>(color.Red());
937 message.write<uint8_t>(color.Green());
938 message.write<uint8_t>(color.Blue());
939 message.write<uint8_t>(color.Alpha());
940
941 send(message);
942}
943