| @@ -0,0 +1,719 @@ | |||
| 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_peer.h" | |
| 21 | + | #include "live_server.h" | |
| 22 | + | #include "live_tab.h" | |
| 23 | + | #include "live_action.h" | |
| 24 | + | ||
| 25 | + | #include "editor.h" | |
| 26 | + | ||
| 27 | + | LivePeer::LivePeer(LiveServer* server, boost::asio::ip::tcp::socket socket) : | |
| 28 | + | LiveSocket(), | |
| 29 | + | readMessage(), server(server), socket(std::move(socket)), color(), id(0), clientId(0), connected(false) { | |
| 30 | + | ASSERT(server != nullptr); | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | LivePeer::~LivePeer() { | |
| 34 | + | if (socket.is_open()) { | |
| 35 | + | socket.close(); | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | void LivePeer::close() { | |
| 40 | + | server->removeClient(id); | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | bool LivePeer::handleError(const boost::system::error_code& error) { | |
| 44 | + | if (error == boost::asio::error::eof || error == boost::asio::error::connection_reset) { | |
| 45 | + | logMessage(wxString() + getHostName() + ": disconnected."); | |
| 46 | + | close(); | |
| 47 | + | return true; | |
| 48 | + | } else if (error == boost::asio::error::connection_aborted) { | |
| 49 | + | logMessage(name + " have left the server."); | |
| 50 | + | return true; | |
| 51 | + | } | |
| 52 | + | return false; | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | std::string LivePeer::getHostName() const { | |
| 56 | + | return socket.remote_endpoint().address().to_string(); | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | void LivePeer::receiveHeader() { | |
| 60 | + | readMessage.position = 0; | |
| 61 | + | ||
| 62 | + | // Initialize buffer with minimum size to prevent "size 0" errors | |
| 63 | + | if (readMessage.buffer.size() < 4) { | |
| 64 | + | readMessage.buffer.resize(1024); | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | boost::asio::async_read(socket, boost::asio::buffer(readMessage.buffer, 4), [this](const boost::system::error_code& error, size_t bytesReceived) -> void { | |
| 68 | + | if (error) { | |
| 69 | + | if (!handleError(error)) { | |
| 70 | + | logMessage(wxString() + getHostName() + ": " + error.message()); | |
| 71 | + | } | |
| 72 | + | } else if (bytesReceived < 4) { | |
| 73 | + | logMessage(wxString::Format("[Client %s]: Could not receive header [size: %zu], attempting to recover...", | |
| 74 | + | getHostName(), bytesReceived)); | |
| 75 | + | ||
| 76 | + | // Try to recover by restarting the header receive process | |
| 77 | + | if (bytesReceived > 0) { | |
| 78 | + | wxTheApp->CallAfter([this]() { | |
| 79 | + | receiveHeader(); | |
| 80 | + | }); | |
| 81 | + | } else { | |
| 82 | + | // If we received 0 bytes, wait a bit and try again | |
| 83 | + | wxTheApp->CallAfter([this]() { | |
| 84 | + | logMessage("Attempting to reconnect header reader..."); | |
| 85 | + | receiveHeader(); | |
| 86 | + | }); | |
| 87 | + | } | |
| 88 | + | } else { | |
| 89 | + | // Successfully received header | |
| 90 | + | uint32_t packetSize = readMessage.read<uint32_t>(); | |
| 91 | + | logMessage(wxString::Format("[Client %s]: Received header, packet size: %u bytes", | |
| 92 | + | getHostName(), packetSize)); | |
| 93 | + | ||
| 94 | + | // Prevent empty packet errors | |
| 95 | + | if (packetSize == 0) { | |
| 96 | + | logMessage("[Client]: Empty packet received, skipping and waiting for next header"); | |
| 97 | + | wxTheApp->CallAfter([this]() { | |
| 98 | + | receiveHeader(); | |
| 99 | + | }); | |
| 100 | + | } else { | |
| 101 | + | receive(packetSize); | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | }); | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | void LivePeer::receive(uint32_t packetSize) { | |
| 108 | + | // Ensure buffer is large enough | |
| 109 | + | readMessage.buffer.resize(readMessage.position + packetSize); | |
| 110 | + | ||
| 111 | + | boost::asio::async_read(socket, boost::asio::buffer(&readMessage.buffer[readMessage.position], packetSize), | |
| 112 | + | [this, packetSize](const boost::system::error_code& error, size_t bytesReceived) -> void { | |
| 113 | + | if (error) { | |
| 114 | + | if (!handleError(error)) { | |
| 115 | + | logMessage(wxString::Format("[Client %s]: Network error: %s", | |
| 116 | + | getHostName(), error.message())); | |
| 117 | + | } | |
| 118 | + | } else if (bytesReceived < packetSize) { | |
| 119 | + | logMessage(wxString::Format("[Client %s]: Incomplete packet received [got: %zu, expected: %zu], attempting recovery...", | |
| 120 | + | getHostName(), bytesReceived, packetSize)); | |
| 121 | + | ||
| 122 | + | // Try to recover by restarting the read process | |
| 123 | + | if (bytesReceived > 0) { | |
| 124 | + | // We received some data, attempt to continue from where we left off | |
| 125 | + | uint32_t remainingBytes = packetSize - bytesReceived; | |
| 126 | + | wxTheApp->CallAfter([this, remainingBytes, bytesReceived]() { | |
| 127 | + | logMessage(wxString::Format("Attempting to receive remaining %u bytes...", remainingBytes)); | |
| 128 | + | // Continue reading the remaining bytes | |
| 129 | + | boost::asio::async_read(socket, | |
| 130 | + | boost::asio::buffer(&readMessage.buffer[readMessage.position + bytesReceived], remainingBytes), | |
| 131 | + | [this](const boost::system::error_code& innerError, size_t innerBytesReceived) { | |
| 132 | + | if (!innerError && innerBytesReceived > 0) { | |
| 133 | + | logMessage("Successfully recovered partial packet"); | |
| 134 | + | wxTheApp->CallAfter([this]() { | |
| 135 | + | if (connected) { | |
| 136 | + | parseEditorPacket(std::move(readMessage)); | |
| 137 | + | } else { | |
| 138 | + | parseLoginPacket(std::move(readMessage)); | |
| 139 | + | } | |
| 140 | + | receiveHeader(); | |
| 141 | + | }); | |
| 142 | + | } else { | |
| 143 | + | logMessage("Recovery failed, restarting header read"); | |
| 144 | + | wxTheApp->CallAfter([this]() { | |
| 145 | + | receiveHeader(); | |
| 146 | + | }); | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | ); | |
| 150 | + | }); | |
| 151 | + | } else { | |
| 152 | + | // If we received 0 bytes, just restart the header read | |
| 153 | + | wxTheApp->CallAfter([this]() { | |
| 154 | + | logMessage("No data received, restarting header read"); | |
| 155 | + | receiveHeader(); | |
| 156 | + | }); | |
| 157 | + | } | |
| 158 | + | } else { | |
| 159 | + | // Successfully received the complete packet | |
| 160 | + | logMessage(wxString::Format("[Client %s]: Successfully received complete packet (%zu bytes)", | |
| 161 | + | getHostName(), bytesReceived)); | |
| 162 | + | ||
| 163 | + | wxTheApp->CallAfter([this]() { | |
| 164 | + | if (connected) { | |
| 165 | + | parseEditorPacket(std::move(readMessage)); | |
| 166 | + | } else { | |
| 167 | + | parseLoginPacket(std::move(readMessage)); | |
| 168 | + | } | |
| 169 | + | receiveHeader(); | |
| 170 | + | }); | |
| 171 | + | } | |
| 172 | + | }); | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | void LivePeer::send(NetworkMessage& message) { | |
| 176 | + | // Validate message size to avoid sending empty messages | |
| 177 | + | if (message.size == 0) { | |
| 178 | + | logMessage("[Server]: Attempted to send empty message, ignoring"); | |
| 179 | + | return; | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | // Write size to the first 4 bytes (header) | |
| 183 | + | memcpy(&message.buffer[0], &message.size, 4); | |
| 184 | + | ||
| 185 | + | // Only log non-cursor packets to reduce excessive logging | |
| 186 | + | bool isCursorPacket = message.buffer.size() > 0 && message.buffer[4] == PACKET_CURSOR_UPDATE; | |
| 187 | + | if (!isCursorPacket) { | |
| 188 | + | logMessage(wxString::Format("[Server]: Sending packet to %s (size: %zu bytes, type: 0x%02X)", | |
| 189 | + | getHostName(), message.size + 4, message.buffer[4])); | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | try { | |
| 193 | + | boost::asio::async_write(socket, | |
| 194 | + | boost::asio::buffer(message.buffer, message.size + 4), | |
| 195 | + | [this, msgSize = message.size, isCursorPacket](const boost::system::error_code& error, size_t bytesTransferred) -> void { | |
| 196 | + | if (error) { | |
| 197 | + | logMessage(wxString::Format("[Server]: Error sending packet to %s: %s", | |
| 198 | + | getHostName(), error.message())); | |
| 199 | + | } else if (bytesTransferred != msgSize + 4) { | |
| 200 | + | logMessage(wxString::Format("[Server]: Incomplete packet sent to %s [sent: %zu, expected: %zu]", | |
| 201 | + | getHostName(), bytesTransferred, msgSize + 4)); | |
| 202 | + | } else if (!isCursorPacket) { | |
| 203 | + | // Only log successful sends for non-cursor packets | |
| 204 | + | logMessage(wxString::Format("[Server]: Successfully sent packet to %s (%zu bytes)", | |
| 205 | + | getHostName(), bytesTransferred)); | |
| 206 | + | } | |
| 207 | + | } | |
| 208 | + | ); | |
| 209 | + | } catch (std::exception& e) { | |
| 210 | + | logMessage(wxString::Format("[Server]: Exception sending packet to %s: %s", | |
| 211 | + | getHostName(), e.what())); | |
| 212 | + | } | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | void LivePeer::parseLoginPacket(NetworkMessage message) { | |
| 216 | + | logMessage(wxString::Format("[Server]: Parsing login packet from %s (buffer size: %zu, position: %zu)", | |
| 217 | + | getHostName(), message.buffer.size(), message.position)); | |
| 218 | + | ||
| 219 | + | if (message.buffer.size() <= message.position) { | |
| 220 | + | logMessage("[Server]: Empty packet received in login parser, disconnecting"); | |
| 221 | + | close(); | |
| 222 | + | return; | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | try { | |
| 226 | + | // Read just one packet type - login packets should only contain a single command | |
| 227 | + | uint8_t packetType = message.read<uint8_t>(); | |
| 228 | + | logMessage(wxString::Format("[Server]: Login packet type: 0x%02X", packetType)); | |
| 229 | + | ||
| 230 | + | switch (packetType) { | |
| 231 | + | case PACKET_HELLO_FROM_CLIENT: | |
| 232 | + | logMessage("[Server]: Received hello packet from client"); | |
| 233 | + | parseHello(message); | |
| 234 | + | break; | |
| 235 | + | case PACKET_READY_CLIENT: | |
| 236 | + | logMessage("[Server]: Received ready packet from client"); | |
| 237 | + | parseReady(message); | |
| 238 | + | break; | |
| 239 | + | default: { | |
| 240 | + | logMessage(wxString::Format("[Server]: Invalid login packet received (type: 0x%02X), connection severed", | |
| 241 | + | packetType)); | |
| 242 | + | close(); | |
| 243 | + | break; | |
| 244 | + | } | |
| 245 | + | } | |
| 246 | + | } catch (std::exception& e) { | |
| 247 | + | logMessage(wxString::Format("[Server]: Exception while parsing login packet: %s", e.what())); | |
| 248 | + | close(); | |
| 249 | + | } | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | void LivePeer::parseEditorPacket(NetworkMessage message) { | |
| 253 | + | uint8_t packetType; | |
| 254 | + | while (message.position < message.buffer.size()) { | |
| 255 | + | packetType = message.read<uint8_t>(); | |
| 256 | + | switch (packetType) { | |
| 257 | + | case PACKET_REQUEST_NODES: | |
| 258 | + | parseNodeRequest(message); | |
| 259 | + | break; | |
| 260 | + | case PACKET_CHANGE_LIST: | |
| 261 | + | parseReceiveChanges(message); | |
| 262 | + | break; | |
| 263 | + | case PACKET_ADD_HOUSE: | |
| 264 | + | parseAddHouse(message); | |
| 265 | + | break; | |
| 266 | + | case PACKET_EDIT_HOUSE: | |
| 267 | + | parseEditHouse(message); | |
| 268 | + | break; | |
| 269 | + | case PACKET_REMOVE_HOUSE: | |
| 270 | + | parseRemoveHouse(message); | |
| 271 | + | break; | |
| 272 | + | case PACKET_CLIENT_UPDATE_CURSOR: | |
| 273 | + | parseCursorUpdate(message); | |
| 274 | + | break; | |
| 275 | + | case PACKET_CLIENT_TALK: | |
| 276 | + | parseChatMessage(message); | |
| 277 | + | break; | |
| 278 | + | case PACKET_CLIENT_COLOR_UPDATE: | |
| 279 | + | parseClientColorUpdate(message); | |
| 280 | + | break; | |
| 281 | + | default: { | |
| 282 | + | log->Message("Invalid editor packet receieved, connection severed."); | |
| 283 | + | close(); | |
| 284 | + | break; | |
| 285 | + | } | |
| 286 | + | } | |
| 287 | + | } | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | void LivePeer::parseHello(NetworkMessage& message) { | |
| 291 | + | if (connected) { | |
| 292 | + | logMessage("[Server]: Client already connected but sent hello again, disconnecting"); | |
| 293 | + | close(); | |
| 294 | + | return; | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | // Calculate remaining data size | |
| 298 | + | size_t remainingBytes = message.buffer.size() - message.position; | |
| 299 | + | logMessage(wxString::Format("[Server]: Hello packet data - %zu bytes remaining to parse", remainingBytes)); | |
| 300 | + | ||
| 301 | + | // Minimum data needed: | |
| 302 | + | // 3x uint32_t = 12 bytes (version info) | |
| 303 | + | // 2x string = variable (name, password), but at least 2 bytes for length | |
| 304 | + | // Total minimum: 14 bytes | |
| 305 | + | if (remainingBytes < 14) { | |
| 306 | + | logMessage(wxString::Format("[Server]: Hello packet is too small (%zu bytes), disconnecting", remainingBytes)); | |
| 307 | + | close(); | |
| 308 | + | return; | |
| 309 | + | } | |
| 310 | + | ||
| 311 | + | try { | |
| 312 | + | // Log current position for debugging | |
| 313 | + | size_t startPos = message.position; | |
| 314 | + | logMessage(wxString::Format("[Server]: Reading packet from position %zu", startPos)); | |
| 315 | + | ||
| 316 | + | // Read and validate the RME version | |
| 317 | + | uint32_t rmeVersion = message.read<uint32_t>(); | |
| 318 | + | logMessage(wxString::Format("[Server]: Client RME version: %u (server: %u), new position: %zu", | |
| 319 | + | rmeVersion, __RME_VERSION_ID__, message.position)); | |
| 320 | + | ||
| 321 | + | if (rmeVersion != __RME_VERSION_ID__) { | |
| 322 | + | logMessage("[Server]: Client using incompatible RME version, rejecting"); | |
| 323 | + | NetworkMessage outMessage; | |
| 324 | + | outMessage.write<uint8_t>(PACKET_KICK); | |
| 325 | + | outMessage.write<std::string>("Wrong editor version."); | |
| 326 | + | ||
| 327 | + | send(outMessage); | |
| 328 | + | close(); | |
| 329 | + | return; | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | // Read and validate the network protocol version | |
| 333 | + | uint32_t netVersion = message.read<uint32_t>(); | |
| 334 | + | logMessage(wxString::Format("[Server]: Client net protocol version: %u (server: %u), new position: %zu", | |
| 335 | + | netVersion, __LIVE_NET_VERSION__, message.position)); | |
| 336 | + | ||
| 337 | + | if (netVersion != __LIVE_NET_VERSION__) { | |
| 338 | + | logMessage("[Server]: Client using incompatible net protocol version, rejecting"); | |
| 339 | + | NetworkMessage outMessage; | |
| 340 | + | outMessage.write<uint8_t>(PACKET_KICK); | |
| 341 | + | outMessage.write<std::string>("Wrong protocol version."); | |
| 342 | + | ||
| 343 | + | send(outMessage); | |
| 344 | + | close(); | |
| 345 | + | return; | |
| 346 | + | } | |
| 347 | + | ||
| 348 | + | // Read the client version | |
| 349 | + | uint32_t clientVersion = message.read<uint32_t>(); | |
| 350 | + | ||
| 351 | + | // Check if we have enough bytes for at least the string lengths | |
| 352 | + | if (message.position + 4 > message.buffer.size()) { | |
| 353 | + | logMessage("[Server]: Hello packet truncated after client version, disconnecting"); | |
| 354 | + | close(); | |
| 355 | + | return; | |
| 356 | + | } | |
| 357 | + | ||
| 358 | + | // Read nickname | |
| 359 | + | std::string nickname; | |
| 360 | + | try { | |
| 361 | + | nickname = message.read<std::string>(); | |
| 362 | + | logMessage(wxString::Format("[Server]: Read nickname: '%s', new position: %zu", nickname, message.position)); | |
| 363 | + | } catch (std::exception& e) { | |
| 364 | + | logMessage(wxString::Format("[Server]: Failed to read nickname: %s", e.what())); | |
| 365 | + | close(); | |
| 366 | + | return; | |
| 367 | + | } | |
| 368 | + | ||
| 369 | + | // Check if we have enough bytes for the password | |
| 370 | + | if (message.position >= message.buffer.size()) { | |
| 371 | + | logMessage("[Server]: Hello packet truncated after nickname, disconnecting"); | |
| 372 | + | close(); | |
| 373 | + | return; | |
| 374 | + | } | |
| 375 | + | ||
| 376 | + | // Read password | |
| 377 | + | std::string password; | |
| 378 | + | try { | |
| 379 | + | password = message.read<std::string>(); | |
| 380 | + | logMessage(wxString::Format("[Server]: Read password, length: %zu, new position: %zu", | |
| 381 | + | password.length(), message.position)); | |
| 382 | + | } catch (std::exception& e) { | |
| 383 | + | logMessage(wxString::Format("[Server]: Failed to read password: %s", e.what())); | |
| 384 | + | close(); | |
| 385 | + | return; | |
| 386 | + | } | |
| 387 | + | ||
| 388 | + | logMessage(wxString::Format("[Server]: Client login - Name: %s, Client version: %u", | |
| 389 | + | nickname, clientVersion)); | |
| 390 | + | ||
| 391 | + | // Check password | |
| 392 | + | if (server->getPassword() != wxString(password.c_str(), wxConvUTF8)) { | |
| 393 | + | logMessage(wxString::Format("[Server]: Client %s used wrong password, connection refused", nickname)); | |
| 394 | + | NetworkMessage outMessage; | |
| 395 | + | outMessage.write<uint8_t>(PACKET_KICK); | |
| 396 | + | outMessage.write<std::string>("Invalid password."); | |
| 397 | + | ||
| 398 | + | send(outMessage); | |
| 399 | + | close(); | |
| 400 | + | return; | |
| 401 | + | } | |
| 402 | + | ||
| 403 | + | // Set client name and notify | |
| 404 | + | name = wxString(nickname.c_str(), wxConvUTF8); | |
| 405 | + | log->Message(name + " (" + getHostName() + ") connected."); | |
| 406 | + | ||
| 407 | + | // Send appropriate response | |
| 408 | + | NetworkMessage outMessage; | |
| 409 | + | if (static_cast<ClientVersionID>(clientVersion) != g_gui.GetCurrentVersionID()) { | |
| 410 | + | logMessage(wxString::Format("[Server]: Client version mismatch, requesting client to change version from %u to %u", | |
| 411 | + | clientVersion, g_gui.GetCurrentVersionID())); | |
| 412 | + | ||
| 413 | + | outMessage.write<uint8_t>(PACKET_CHANGE_CLIENT_VERSION); | |
| 414 | + | outMessage.write<uint32_t>(g_gui.GetCurrentVersionID()); | |
| 415 | + | } else { | |
| 416 | + | logMessage("[Server]: Client version matches, accepting connection"); | |
| 417 | + | outMessage.write<uint8_t>(PACKET_ACCEPTED_CLIENT); | |
| 418 | + | } | |
| 419 | + | send(outMessage); | |
| 420 | + | logMessage("[Server]: Sent response to client hello"); | |
| 421 | + | ||
| 422 | + | } catch (std::exception& e) { | |
| 423 | + | logMessage(wxString::Format("[Server]: Error parsing hello packet: %s", e.what())); | |
| 424 | + | close(); | |
| 425 | + | } | |
| 426 | + | } | |
| 427 | + | ||
| 428 | + | void LivePeer::parseReady(NetworkMessage& message) { | |
| 429 | + | // Safety check - client already connected | |
| 430 | + | if (connected) { | |
| 431 | + | logMessage("[Server]: Client already connected but sent READY packet again, disconnecting"); | |
| 432 | + | close(); | |
| 433 | + | return; | |
| 434 | + | } | |
| 435 | + | ||
| 436 | + | try { | |
| 437 | + | // Mark client as connected | |
| 438 | + | connected = true; | |
| 439 | + | logMessage(wxString::Format("[Server]: Client %s entering READY state", getHostName())); | |
| 440 | + | ||
| 441 | + | // Find free client id | |
| 442 | + | clientId = server->getFreeClientId(); | |
| 443 | + | if (clientId == 0) { | |
| 444 | + | logMessage("[Server]: No free client IDs available, server is full"); | |
| 445 | + | NetworkMessage outMessage; | |
| 446 | + | outMessage.write<uint8_t>(PACKET_KICK); | |
| 447 | + | outMessage.write<std::string>("Server is full."); | |
| 448 | + | ||
| 449 | + | send(outMessage); | |
| 450 | + | close(); | |
| 451 | + | return; | |
| 452 | + | } | |
| 453 | + | ||
| 454 | + | // Assign a default color to the new client | |
| 455 | + | color = wxColor( | |
| 456 | + | 128 + rand() % 127, // R: 128-255 | |
| 457 | + | 128 + rand() % 127, // G: 128-255 | |
| 458 | + | 128 + rand() % 127, // B: 128-255 | |
| 459 | + | 255 // A: fully opaque | |
| 460 | + | ); | |
| 461 | + | ||
| 462 | + | // Update the client list in the UI | |
| 463 | + | server->updateClientList(); | |
| 464 | + | logMessage(wxString::Format("[Server]: Assigned client ID %u to %s", clientId, getHostName())); | |
| 465 | + | ||
| 466 | + | // Send HELLO_FROM_SERVER packet with map information | |
| 467 | + | try { | |
| 468 | + | NetworkMessage outMessage; | |
| 469 | + | outMessage.write<uint8_t>(PACKET_HELLO_FROM_SERVER); | |
| 470 | + | ||
| 471 | + | Map& map = server->getEditor()->map; | |
| 472 | + | outMessage.write<std::string>(map.getName()); | |
| 473 | + | outMessage.write<uint16_t>(map.getWidth()); | |
| 474 | + | outMessage.write<uint16_t>(map.getHeight()); | |
| 475 | + | ||
| 476 | + | send(outMessage); | |
| 477 | + | logMessage("[Server]: Sent HELLO packet with map information to client"); | |
| 478 | + | } catch (std::exception& e) { | |
| 479 | + | logMessage(wxString::Format("[Server]: Error sending HELLO packet: %s", e.what())); | |
| 480 | + | close(); | |
| 481 | + | return; | |
| 482 | + | } | |
| 483 | + | ||
| 484 | + | // Now send the host's cursor to the new client | |
| 485 | + | try { | |
| 486 | + | LiveCursor hostCursor; | |
| 487 | + | hostCursor.id = 0; | |
| 488 | + | hostCursor.color = server->getUsedColor(); | |
| 489 | + | hostCursor.pos = Position(); // Default position | |
| 490 | + | ||
| 491 | + | NetworkMessage cursorMessage; | |
| 492 | + | cursorMessage.write<uint8_t>(PACKET_CURSOR_UPDATE); | |
| 493 | + | server->writeCursorToMessage(cursorMessage, hostCursor); | |
| 494 | + | send(cursorMessage); | |
| 495 | + | logMessage("[Server]: Sent host cursor information to client"); | |
| 496 | + | } catch (std::exception& e) { | |
| 497 | + | logMessage(wxString::Format("[Server]: Error sending cursor information: %s", e.what())); | |
| 498 | + | // Non-fatal error, continue | |
| 499 | + | } | |
| 500 | + | ||
| 501 | + | // Send ACCEPTED_CLIENT packet to confirm client is ready for drawing operations | |
| 502 | + | try { | |
| 503 | + | NetworkMessage acceptedMessage; | |
| 504 | + | acceptedMessage.write<uint8_t>(PACKET_ACCEPTED_CLIENT); | |
| 505 | + | send(acceptedMessage); | |
| 506 | + | logMessage(wxString::Format("[Server]: Client %s (ID: %u) is now fully connected and ready", | |
| 507 | + | name, clientId)); | |
| 508 | + | } catch (std::exception& e) { | |
| 509 | + | logMessage(wxString::Format("[Server]: Error sending ACCEPTED packet: %s", e.what())); | |
| 510 | + | close(); | |
| 511 | + | return; | |
| 512 | + | } | |
| 513 | + | ||
| 514 | + | // Also send the colors of all existing clients to the new client | |
| 515 | + | try { | |
| 516 | + | for (const auto& clientPair : server->getClients()) { | |
| 517 | + | LivePeer* peer = clientPair.second; | |
| 518 | + | if (peer && peer->getClientId() != 0 && peer->getClientId() != clientId) { | |
| 519 | + | // Send this client's color | |
| 520 | + | NetworkMessage colorMessage; | |
| 521 | + | colorMessage.write<uint8_t>(PACKET_COLOR_UPDATE); | |
| 522 | + | colorMessage.write<uint32_t>(peer->getClientId()); | |
| 523 | + | colorMessage.write<uint8_t>(peer->getUsedColor().Red()); | |
| 524 | + | colorMessage.write<uint8_t>(peer->getUsedColor().Green()); | |
| 525 | + | colorMessage.write<uint8_t>(peer->getUsedColor().Blue()); | |
| 526 | + | colorMessage.write<uint8_t>(peer->getUsedColor().Alpha()); | |
| 527 | + | send(colorMessage); | |
| 528 | + | ||
| 529 | + | // Also send this client's cursor position | |
| 530 | + | LiveCursor peerCursor; | |
| 531 | + | peerCursor.id = peer->getClientId(); | |
| 532 | + | peerCursor.color = peer->getUsedColor(); | |
| 533 | + | peerCursor.pos = Position(); // Default position | |
| 534 | + | ||
| 535 | + | NetworkMessage peerCursorMessage; | |
| 536 | + | peerCursorMessage.write<uint8_t>(PACKET_CURSOR_UPDATE); | |
| 537 | + | server->writeCursorToMessage(peerCursorMessage, peerCursor); | |
| 538 | + | send(peerCursorMessage); | |
| 539 | + | } | |
| 540 | + | } | |
| 541 | + | logMessage("[Server]: Sent information about other clients to the new client"); | |
| 542 | + | } catch (std::exception& e) { | |
| 543 | + | logMessage(wxString::Format("[Server]: Error sending client information: %s", e.what())); | |
| 544 | + | // Non-fatal error, continue | |
| 545 | + | } | |
| 546 | + | } catch (std::exception& e) { | |
| 547 | + | // Handle any other unexpected errors during client initialization | |
| 548 | + | logMessage(wxString::Format("[Server]: Error initializing client: %s", e.what())); | |
| 549 | + | close(); | |
| 550 | + | } | |
| 551 | + | } | |
| 552 | + | ||
| 553 | + | void LivePeer::parseNodeRequest(NetworkMessage& message) { | |
| 554 | + | Map& map = server->getEditor()->map; | |
| 555 | + | for (uint32_t nodes = message.read<uint32_t>(); nodes != 0; --nodes) { | |
| 556 | + | uint32_t ind = message.read<uint32_t>(); | |
| 557 | + | ||
| 558 | + | int32_t ndx = ind >> 18; | |
| 559 | + | int32_t ndy = (ind >> 4) & 0x3FFF; | |
| 560 | + | bool underground = ind & 1; | |
| 561 | + | ||
| 562 | + | QTreeNode* node = map.createLeaf(ndx * 4, ndy * 4); | |
| 563 | + | if (node) { | |
| 564 | + | sendNode(clientId, node, ndx, ndy, underground ? 0xFF00 : 0x00FF); | |
| 565 | + | } | |
| 566 | + | } | |
| 567 | + | } | |
| 568 | + | ||
| 569 | + | void LivePeer::parseReceiveChanges(NetworkMessage& message) { | |
| 570 | + | try { | |
| 571 | + | // Read the change data | |
| 572 | + | const std::string& data = message.read<std::string>(); | |
| 573 | + | ||
| 574 | + | // Log the change reception | |
| 575 | + | logMessage(wxString::Format("[Server]: Received changes from client %s (data size: %zu bytes)", | |
| 576 | + | name, data.size())); | |
| 577 | + | ||
| 578 | + | // Process the changes on the main thread | |
| 579 | + | wxTheApp->CallAfter([this, data]() { | |
| 580 | + | if (!server || !server->getEditor()) { | |
| 581 | + | logMessage("[Server]: Error - cannot process changes, editor not available"); | |
| 582 | + | return; | |
| 583 | + | } | |
| 584 | + | ||
| 585 | + | Editor& editor = *server->getEditor(); | |
| 586 | + | ||
| 587 | + | try { | |
| 588 | + | // Create the action to hold the changes | |
| 589 | + | NetworkedAction* action = static_cast<NetworkedAction*>(editor.actionQueue->createAction(ACTION_REMOTE)); | |
| 590 | + | action->owner = clientId; | |
| 591 | + | ||
| 592 | + | // Parse the change data and create the tiles | |
| 593 | + | mapReader.assign(reinterpret_cast<const uint8_t*>(data.c_str() - 1), data.size()); | |
| 594 | + | BinaryNode* rootNode = mapReader.getRootNode(); | |
| 595 | + | BinaryNode* tileNode = rootNode->getChild(); | |
| 596 | + | ||
| 597 | + | bool anyChanges = false; | |
| 598 | + | ||
| 599 | + | if (tileNode) { | |
| 600 | + | do { | |
| 601 | + | Tile* tile = readTile(tileNode, editor, nullptr); | |
| 602 | + | if (tile) { | |
| 603 | + | action->addChange(newd Change(tile)); | |
| 604 | + | anyChanges = true; | |
| 605 | + | } | |
| 606 | + | } while (tileNode->advance()); | |
| 607 | + | } | |
| 608 | + | mapReader.close(); | |
| 609 | + | ||
| 610 | + | // Only add the action if we have changes | |
| 611 | + | if (anyChanges) { | |
| 612 | + | editor.actionQueue->addAction(action); | |
| 613 | + | g_gui.RefreshView(); | |
| 614 | + | g_gui.UpdateMinimap(); | |
| 615 | + | ||
| 616 | + | logMessage(wxString::Format("[Server]: Successfully processed changes from client %s", name)); | |
| 617 | + | } else { | |
| 618 | + | // Instead of deleting, just add the action as-is | |
| 619 | + | // An empty action won't do anything but will be safely managed by the queue | |
| 620 | + | editor.actionQueue->addAction(action); | |
| 621 | + | logMessage(wxString::Format("[Server]: No valid changes found in packet from client %s", name)); | |
| 622 | + | } | |
| 623 | + | } catch (std::exception& e) { | |
| 624 | + | logMessage(wxString::Format("[Server]: Error processing changes: %s", e.what())); | |
| 625 | + | } | |
| 626 | + | }); | |
| 627 | + | } catch (std::exception& e) { | |
| 628 | + | logMessage(wxString::Format("[Server]: Error parsing changes packet: %s", e.what())); | |
| 629 | + | } | |
| 630 | + | } | |
| 631 | + | ||
| 632 | + | void LivePeer::parseAddHouse(NetworkMessage& message) { | |
| 633 | + | } | |
| 634 | + | ||
| 635 | + | void LivePeer::parseEditHouse(NetworkMessage& message) { | |
| 636 | + | } | |
| 637 | + | ||
| 638 | + | void LivePeer::parseRemoveHouse(NetworkMessage& message) { | |
| 639 | + | } | |
| 640 | + | ||
| 641 | + | void LivePeer::parseCursorUpdate(NetworkMessage& message) { | |
| 642 | + | LiveCursor cursor = readCursor(message); | |
| 643 | + | cursor.id = clientId; | |
| 644 | + | ||
| 645 | + | // Only log and update client list if the color changes, not for movement | |
| 646 | + | if (cursor.color != color) { | |
| 647 | + | setUsedColor(cursor.color); | |
| 648 | + | server->updateClientList(); | |
| 649 | + | // Only log color changes, not cursor movements | |
| 650 | + | logMessage(wxString::Format("[Server]: Client %s changed cursor color", name)); | |
| 651 | + | } | |
| 652 | + | ||
| 653 | + | server->broadcastCursor(cursor); | |
| 654 | + | g_gui.RefreshView(); | |
| 655 | + | } | |
| 656 | + | ||
| 657 | + | void LivePeer::parseChatMessage(NetworkMessage& message) { | |
| 658 | + | const std::string& chatMessage = message.read<std::string>(); | |
| 659 | + | ||
| 660 | + | // Log the received message for debugging | |
| 661 | + | logMessage(wxString::Format("Chat message received from %s: %s", | |
| 662 | + | name, wxstr(chatMessage))); | |
| 663 | + | ||
| 664 | + | // Broadcast the chat message to all clients including the sender | |
| 665 | + | server->broadcastChat(name, wxstr(chatMessage)); | |
| 666 | + | } | |
| 667 | + | ||
| 668 | + | void LivePeer::parseClientColorUpdate(NetworkMessage& message) { | |
| 669 | + | // Read the target client ID | |
| 670 | + | uint32_t targetClientId = message.read<uint32_t>(); | |
| 671 | + | ||
| 672 | + | // Read the color components | |
| 673 | + | uint8_t r = message.read<uint8_t>(); | |
| 674 | + | uint8_t g = message.read<uint8_t>(); | |
| 675 | + | uint8_t b = message.read<uint8_t>(); | |
| 676 | + | uint8_t a = message.read<uint8_t>(); | |
| 677 | + | wxColor newColor(r, g, b, a); | |
| 678 | + | ||
| 679 | + | // Log the request | |
| 680 | + | logMessage(wxString::Format("[Server]: Client %s requested color change for client %u to RGB(%d,%d,%d)", | |
| 681 | + | name, targetClientId, r, g, b)); | |
| 682 | + | ||
| 683 | + | // If the client is changing their own color | |
| 684 | + | if (targetClientId == clientId) { | |
| 685 | + | // Update the client's color | |
| 686 | + | setUsedColor(newColor); | |
| 687 | + | ||
| 688 | + | // Log the change | |
| 689 | + | logMessage(wxString::Format("[Server]: Updated color for client %s (ID: %u)", | |
| 690 | + | name, clientId)); | |
| 691 | + | } | |
| 692 | + | ||
| 693 | + | // Broadcast the color change to all clients | |
| 694 | + | server->broadcastColorChange(targetClientId, newColor); | |
| 695 | + | ||
| 696 | + | // Update the client list to reflect the change in the UI | |
| 697 | + | server->updateClientList(); | |
| 698 | + | } | |
| 699 | + | ||
| 700 | + | void LivePeer::sendChat(const wxString& chatMessage) { | |
| 701 | + | // For a peer, sending a chat message means forwarding it to the server | |
| 702 | + | // which will then broadcast it to all clients | |
| 703 | + | NetworkMessage message; | |
| 704 | + | message.write<uint8_t>(PACKET_CLIENT_TALK); | |
| 705 | + | message.write<std::string>(nstr(chatMessage)); | |
| 706 | + | send(message); | |
| 707 | + | } | |
| 708 | + | ||
| 709 | + | void LivePeer::updateCursor(const Position& position) { | |
| 710 | + | // Forward cursor updates to the server | |
| 711 | + | LiveCursor cursor; | |
| 712 | + | cursor.id = clientId; | |
| 713 | + | cursor.pos = position; | |
| 714 | + | ||
| 715 | + | // Use client's color for the cursor | |
| 716 | + | cursor.color = color; | |
| 717 | + | ||
| 718 | + | server->broadcastCursor(cursor); | |
| 719 | + | } |
| @@ -0,0 +1,94 @@ | |||
| 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 | + | #ifndef _RME_LIVE_PEER_H_ | |
| 19 | + | #define _RME_LIVE_PEER_H_ | |
| 20 | + | ||
| 21 | + | #include "live_socket.h" | |
| 22 | + | #include "net_connection.h" | |
| 23 | + | ||
| 24 | + | class LiveServer; | |
| 25 | + | class LivePeer : public LiveSocket { | |
| 26 | + | public: | |
| 27 | + | LivePeer(LiveServer* server, boost::asio::ip::tcp::socket socket); | |
| 28 | + | ~LivePeer(); | |
| 29 | + | ||
| 30 | + | void close(); | |
| 31 | + | bool handleError(const boost::system::error_code& error); | |
| 32 | + | ||
| 33 | + | // | |
| 34 | + | uint32_t getId() const { | |
| 35 | + | return id; | |
| 36 | + | } | |
| 37 | + | uint32_t getClientId() const { | |
| 38 | + | return clientId; | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | std::string getHostName() const; | |
| 42 | + | ||
| 43 | + | wxColor getUsedColor() const { | |
| 44 | + | return color; | |
| 45 | + | } | |
| 46 | + | void setUsedColor(const wxColor& newColor) { | |
| 47 | + | color = newColor; | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // | |
| 51 | + | void receiveHeader() override; | |
| 52 | + | void receive(uint32_t packetSize) override; | |
| 53 | + | void send(NetworkMessage& message) override; | |
| 54 | + | void sendChat(const wxString& chatMessage) override; | |
| 55 | + | ||
| 56 | + | // | |
| 57 | + | void updateCursor(const Position& position) override; | |
| 58 | + | ||
| 59 | + | protected: | |
| 60 | + | void parseLoginPacket(NetworkMessage message); | |
| 61 | + | void parseEditorPacket(NetworkMessage message); | |
| 62 | + | ||
| 63 | + | // login packets | |
| 64 | + | void parseHello(NetworkMessage& message); | |
| 65 | + | void parseReady(NetworkMessage& message); | |
| 66 | + | ||
| 67 | + | // editor packets | |
| 68 | + | void parseNodeRequest(NetworkMessage& message); | |
| 69 | + | void parseReceiveChanges(NetworkMessage& message); | |
| 70 | + | void parseAddHouse(NetworkMessage& message); | |
| 71 | + | void parseEditHouse(NetworkMessage& message); | |
| 72 | + | void parseRemoveHouse(NetworkMessage& message); | |
| 73 | + | void parseCursorUpdate(NetworkMessage& message); | |
| 74 | + | void parseChatMessage(NetworkMessage& message); | |
| 75 | + | void parseClientColorUpdate(NetworkMessage& message); | |
| 76 | + | ||
| 77 | + | // | |
| 78 | + | NetworkMessage readMessage; | |
| 79 | + | ||
| 80 | + | LiveServer* server; | |
| 81 | + | boost::asio::ip::tcp::socket socket; | |
| 82 | + | ||
| 83 | + | wxColor color; | |
| 84 | + | ||
| 85 | + | uint32_t id; | |
| 86 | + | uint32_t clientId; | |
| 87 | + | ||
| 88 | + | bool connected; | |
| 89 | + | ||
| 90 | + | friend class LiveLogTab; | |
| 91 | + | friend class LiveServer; | |
| 92 | + | }; | |
| 93 | + | ||
| 94 | + | #endif |
| @@ -0,0 +1,610 @@ | |||
| 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_server.h" | |
| 21 | + | #include "live_peer.h" | |
| 22 | + | #include "live_tab.h" | |
| 23 | + | #include "live_action.h" | |
| 24 | + | ||
| 25 | + | #include "editor.h" | |
| 26 | + | ||
| 27 | + | #include <fstream> | |
| 28 | + | #include <wx/filename.h> | |
| 29 | + | ||
| 30 | + | LiveServer::LiveServer(Editor& editor) : | |
| 31 | + | LiveSocket(), | |
| 32 | + | clients(), acceptor(nullptr), socket(nullptr), editor(&editor), | |
| 33 | + | clientIds(0), port(0), stopped(false), drawingReady(false) { | |
| 34 | + | // Initialize with a safe color | |
| 35 | + | usedColor = wxColor(255, 0, 0); // Red for host | |
| 36 | + | ||
| 37 | + | // Log server initialization to file | |
| 38 | + | std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "server_init.log").ToStdString(), std::ios::app); | |
| 39 | + | if (logFile.is_open()) { | |
| 40 | + | wxDateTime now = wxDateTime::Now(); | |
| 41 | + | logFile << now.FormatISOCombined() << ": LiveServer initialized\n"; | |
| 42 | + | logFile.close(); | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // Set the drawing ready flag after a short delay to ensure all initialization is complete | |
| 46 | + | wxTheApp->CallAfter([this]() { | |
| 47 | + | drawingReady = true; | |
| 48 | + | ||
| 49 | + | // Log to file | |
| 50 | + | std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "server_status.log").ToStdString(), std::ios::app); | |
| 51 | + | if (logFile.is_open()) { | |
| 52 | + | wxDateTime now = wxDateTime::Now(); | |
| 53 | + | logFile << now.FormatISOCombined() << ": Server drawing ready flag set\n"; | |
| 54 | + | logFile.close(); | |
| 55 | + | } | |
| 56 | + | }); | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | LiveServer::~LiveServer() { | |
| 60 | + | // | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | bool LiveServer::bind() { | |
| 64 | + | // Ensure we're on the main thread for initialization | |
| 65 | + | if (!wxThread::IsMain()) { | |
| 66 | + | bool success = false; | |
| 67 | + | wxTheApp->CallAfter([this, &success]() { | |
| 68 | + | success = this->bind(); | |
| 69 | + | }); | |
| 70 | + | return success; | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | NetworkConnection& connection = NetworkConnection::getInstance(); | |
| 74 | + | if (!connection.start()) { | |
| 75 | + | setLastError("The previous connection has not been terminated yet."); | |
| 76 | + | return false; | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | auto& service = connection.get_service(); | |
| 80 | + | acceptor = std::make_shared<boost::asio::ip::tcp::acceptor>(service); | |
| 81 | + | ||
| 82 | + | // Try to bind to the specified port, if that fails, try the next port | |
| 83 | + | // This allows multiple instances to host simultaneously | |
| 84 | + | uint16_t originalPort = port; | |
| 85 | + | uint16_t maxPortRetries = 10; // Try up to 10 ports in sequence | |
| 86 | + | boost::system::error_code bindError; | |
| 87 | + | ||
| 88 | + | for (uint16_t retry = 0; retry < maxPortRetries; ++retry) { | |
| 89 | + | boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::tcp::v4(), port); | |
| 90 | + | ||
| 91 | + | // Close acceptor if it was previously opened | |
| 92 | + | if (acceptor->is_open()) { | |
| 93 | + | acceptor->close(); | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | // Try to open and bind | |
| 97 | + | acceptor->open(endpoint.protocol()); | |
| 98 | + | ||
| 99 | + | boost::system::error_code error; | |
| 100 | + | acceptor->set_option(boost::asio::ip::tcp::no_delay(true), error); | |
| 101 | + | if (error) { | |
| 102 | + | logMessage(wxString::Format("Warning: Could not set no_delay option: %s", error.message())); | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // Try binding to endpoint | |
| 106 | + | bindError.clear(); | |
| 107 | + | acceptor->bind(endpoint, bindError); | |
| 108 | + | ||
| 109 | + | if (!bindError) { | |
| 110 | + | // Binding successful | |
| 111 | + | if (port != originalPort) { | |
| 112 | + | // Log that we're using a different port | |
| 113 | + | logMessage(wxString::Format("Port %d was in use, using port %d instead", originalPort, port)); | |
| 114 | + | } | |
| 115 | + | break; | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | // If binding failed, try next port | |
| 119 | + | port++; | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | if (bindError) { | |
| 123 | + | setLastError("Error binding socket: " + bindError.message() + | |
| 124 | + | "\nTried ports " + std::to_string(originalPort) + " to " + | |
| 125 | + | std::to_string(originalPort + maxPortRetries - 1)); | |
| 126 | + | return false; | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | acceptor->listen(); | |
| 130 | + | acceptClient(); | |
| 131 | + | return true; | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | void LiveServer::close() { | |
| 135 | + | // Set stopped flag first to prevent any new operations | |
| 136 | + | stopped = true; | |
| 137 | + | ||
| 138 | + | // Also disable drawing operations | |
| 139 | + | drawingReady = false; | |
| 140 | + | ||
| 141 | + | // Log server shutdown to file | |
| 142 | + | std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "server_status.log").ToStdString(), std::ios::app); | |
| 143 | + | if (logFile.is_open()) { | |
| 144 | + | wxDateTime now = wxDateTime::Now(); | |
| 145 | + | logFile << now.FormatISOCombined() << ": Server shutting down\n"; | |
| 146 | + | logFile.close(); | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | // Then proceed with normal shutdown | |
| 150 | + | for (auto& clientEntry : clients) { | |
| 151 | + | delete clientEntry.second; | |
| 152 | + | } | |
| 153 | + | clients.clear(); | |
| 154 | + | ||
| 155 | + | if (log) { | |
| 156 | + | log->Message("Server was shutdown."); | |
| 157 | + | log->Disconnect(); | |
| 158 | + | log = nullptr; | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | if (acceptor) { | |
| 162 | + | acceptor->close(); | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | if (socket) { | |
| 166 | + | socket->close(); | |
| 167 | + | } | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | void LiveServer::acceptClient() { | |
| 171 | + | // Ensure we're on the main thread | |
| 172 | + | if (!wxThread::IsMain()) { | |
| 173 | + | wxTheApp->CallAfter([this]() { | |
| 174 | + | this->acceptClient(); | |
| 175 | + | }); | |
| 176 | + | return; | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | static uint32_t id = 0; | |
| 180 | + | if (stopped) { | |
| 181 | + | return; | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | if (!socket) { | |
| 185 | + | socket = std::make_shared<boost::asio::ip::tcp::socket>( | |
| 186 | + | NetworkConnection::getInstance().get_service() | |
| 187 | + | ); | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | acceptor->async_accept(*socket, [this](const boost::system::error_code& error) -> void { | |
| 191 | + | // Queue the client handling on the main thread | |
| 192 | + | wxTheApp->CallAfter([this, error]() { | |
| 193 | + | if (!error) { | |
| 194 | + | static uint32_t nextId = 0; | |
| 195 | + | LivePeer* peer = new LivePeer(this, std::move(*socket)); | |
| 196 | + | peer->log = log; | |
| 197 | + | peer->receiveHeader(); | |
| 198 | + | ||
| 199 | + | clients.insert(std::make_pair(nextId++, peer)); | |
| 200 | + | ||
| 201 | + | // Make sure the host's cursor exists | |
| 202 | + | if (cursors.find(0) == cursors.end()) { | |
| 203 | + | LiveCursor hostCursor; | |
| 204 | + | hostCursor.id = 0; | |
| 205 | + | hostCursor.color = usedColor; | |
| 206 | + | hostCursor.pos = Position(); | |
| 207 | + | cursors[0] = hostCursor; | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | updateClientList(); | |
| 211 | + | } | |
| 212 | + | // Queue next accept regardless of error | |
| 213 | + | acceptClient(); | |
| 214 | + | }); | |
| 215 | + | }); | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | void LiveServer::removeClient(uint32_t id) { | |
| 219 | + | // Ensure we're on the main thread | |
| 220 | + | if (!wxThread::IsMain()) { | |
| 221 | + | wxTheApp->CallAfter([this, id]() { | |
| 222 | + | this->removeClient(id); | |
| 223 | + | }); | |
| 224 | + | return; | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | auto it = clients.find(id); | |
| 228 | + | if (it == clients.end()) { | |
| 229 | + | return; | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | const uint32_t clientId = it->second->getClientId(); | |
| 233 | + | if (clientId != 0) { | |
| 234 | + | clientIds &= ~clientId; | |
| 235 | + | editor->map.clearVisible(clientIds); | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | clients.erase(it); | |
| 239 | + | updateClientList(); | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | void LiveServer::updateCursor(const Position& position) { | |
| 243 | + | // Ensure we're on the main thread | |
| 244 | + | if (!wxThread::IsMain()) { | |
| 245 | + | wxTheApp->CallAfter([this, position]() { | |
| 246 | + | this->updateCursor(position); | |
| 247 | + | }); | |
| 248 | + | return; | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | LiveCursor cursor; | |
| 252 | + | cursor.id = 0; | |
| 253 | + | cursor.pos = position; | |
| 254 | + | cursor.color = usedColor; | |
| 255 | + | ||
| 256 | + | this->cursors[cursor.id] = cursor; | |
| 257 | + | this->broadcastCursor(cursor); | |
| 258 | + | ||
| 259 | + | g_gui.RefreshView(); | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | void LiveServer::updateClientList() const { | |
| 263 | + | log->UpdateClientList(clients); | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | uint16_t LiveServer::getPort() const { | |
| 267 | + | return port; | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | bool LiveServer::setPort(int32_t newPort) { | |
| 271 | + | if (newPort < 1 || newPort > 65535) { | |
| 272 | + | setLastError("Port must be a number in the range 1-65535."); | |
| 273 | + | return false; | |
| 274 | + | } | |
| 275 | + | port = newPort; | |
| 276 | + | return true; | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | uint32_t LiveServer::getFreeClientId() { | |
| 280 | + | for (int32_t bit = 1; bit < (1 << 16); bit <<= 1) { | |
| 281 | + | if (!testFlags(clientIds, bit)) { | |
| 282 | + | clientIds |= bit; | |
| 283 | + | return bit; | |
| 284 | + | } | |
| 285 | + | } | |
| 286 | + | return 0; | |
| 287 | + | } | |
| 288 | + | ||
| 289 | + | std::string LiveServer::getHostName() const { | |
| 290 | + | if (acceptor) { | |
| 291 | + | auto endpoint = acceptor->local_endpoint(); | |
| 292 | + | return endpoint.address().to_string() + ":" + std::to_string(endpoint.port()); | |
| 293 | + | } | |
| 294 | + | return "localhost"; | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | void LiveServer::broadcastNodes(DirtyList& dirtyList) { | |
| 298 | + | // Skip if we're not ready for drawing operations | |
| 299 | + | if (!drawingReady || stopped) { | |
| 300 | + | // Log to file instead of UI | |
| 301 | + | std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "server_status.log").ToStdString(), std::ios::app); | |
| 302 | + | if (logFile.is_open()) { | |
| 303 | + | wxDateTime now = wxDateTime::Now(); | |
| 304 | + | logFile << now.FormatISOCombined() << ": Skipped broadcast, drawing not ready\n"; | |
| 305 | + | logFile.close(); | |
| 306 | + | } | |
| 307 | + | return; | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | // If there are no clients or no changes, there's nothing to do | |
| 311 | + | if (clients.empty() || dirtyList.Empty()) { | |
| 312 | + | return; | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | // Log to file rather than UI | |
| 316 | + | std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "server_ops.log").ToStdString(), std::ios::app); | |
| 317 | + | if (logFile.is_open()) { | |
| 318 | + | wxDateTime now = wxDateTime::Now(); | |
| 319 | + | logFile << now.FormatISOCombined() << ": Broadcasting changes to " << clients.size() << " clients\n"; | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | // Extract the change information to a struct we can capture in our lambda | |
| 323 | + | struct BroadcastData { | |
| 324 | + | uint32_t owner; | |
| 325 | + | std::vector<Change*> changes; // Changed from ChangeList to std::vector<Change*> | |
| 326 | + | std::vector<DirtyList::ValueType> positions; // Use the existing type from DirtyList | |
| 327 | + | }; | |
| 328 | + | ||
| 329 | + | std::shared_ptr<BroadcastData> broadcastData = std::make_shared<BroadcastData>(); | |
| 330 | + | broadcastData->owner = 0; // Server is always owner 0 | |
| 331 | + | ||
| 332 | + | try { | |
| 333 | + | // Get the changes from the dirty list, converting them to a format we can save and broadcast | |
| 334 | + | // Note: We need to create deep copies of the changes as the dirty list will be cleared | |
| 335 | + | ChangeList& changeList = dirtyList.GetChanges(); | |
| 336 | + | broadcastData->changes.reserve(changeList.size()); | |
| 337 | + | ||
| 338 | + | for (Change* change : changeList) { | |
| 339 | + | if (!change) continue; | |
| 340 | + | ||
| 341 | + | // Store a pointer - no unique_ptr here | |
| 342 | + | if (change->getType() == CHANGE_TILE) { | |
| 343 | + | // Create a deep copy of the change | |
| 344 | + | Tile* oldTile = static_cast<Tile*>(change->getData()); | |
| 345 | + | if (oldTile) { | |
| 346 | + | Tile* newTile = oldTile->deepCopy(editor->map); | |
| 347 | + | Change* newChange = new Change(newTile); | |
| 348 | + | broadcastData->changes.push_back(newChange); | |
| 349 | + | } | |
| 350 | + | } | |
| 351 | + | } | |
| 352 | + | ||
| 353 | + | // Get position list - need to copy the positions manually | |
| 354 | + | DirtyList::SetType& positionList = dirtyList.GetPosList(); | |
| 355 | + | for (const auto& pos : positionList) { | |
| 356 | + | broadcastData->positions.push_back(pos); | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | // Safe logging of the node count | |
| 360 | + | if (logFile.is_open()) { | |
| 361 | + | logFile << "Changes: " << broadcastData->changes.size() << ", Positions: " << broadcastData->positions.size() << "\n"; | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | // Use a safer approach with CallAfter | |
| 365 | + | wxTheApp->CallAfter([this, broadcastData]() { | |
| 366 | + | try { | |
| 367 | + | if (!editor || !drawingReady) return; | |
| 368 | + | ||
| 369 | + | // Handle host changes first | |
| 370 | + | if (broadcastData->owner == 0 && !broadcastData->changes.empty()) { | |
| 371 | + | NetworkedAction* action = static_cast<NetworkedAction*>(editor->actionQueue->createAction(ACTION_REMOTE)); | |
| 372 | + | if (action) { | |
| 373 | + | action->owner = broadcastData->owner; | |
| 374 | + | ||
| 375 | + | // Add the changes to the action - action takes ownership | |
| 376 | + | for (Change* change : broadcastData->changes) { | |
| 377 | + | if (change) { | |
| 378 | + | action->addChange(change); | |
| 379 | + | } | |
| 380 | + | } | |
| 381 | + | ||
| 382 | + | // Add the action to the queue | |
| 383 | + | editor->actionQueue->addAction(action, 0); | |
| 384 | + | } | |
| 385 | + | } | |
| 386 | + | ||
| 387 | + | // Now handle the network broadcasting | |
| 388 | + | // Create a vector of all the work we need to do | |
| 389 | + | struct BroadcastWork { | |
| 390 | + | LivePeer* peer; | |
| 391 | + | QTreeNode* node; | |
| 392 | + | int32_t ndx; | |
| 393 | + | int32_t ndy; | |
| 394 | + | uint32_t floors; | |
| 395 | + | uint32_t clientId; | |
| 396 | + | }; | |
| 397 | + | ||
| 398 | + | std::vector<BroadcastWork> workItems; | |
| 399 | + | ||
| 400 | + | // First gather all the work without doing any actual sending | |
| 401 | + | for (const auto& ind : broadcastData->positions) { | |
| 402 | + | int32_t ndx = ind.pos >> 18; | |
| 403 | + | int32_t ndy = (ind.pos >> 4) & 0x3FFF; | |
| 404 | + | uint32_t floors = ind.floors; | |
| 405 | + | ||
| 406 | + | QTreeNode* node = editor->map.getLeaf(ndx * 4, ndy * 4); | |
| 407 | + | if (!node) continue; | |
| 408 | + | ||
| 409 | + | for (auto& clientEntry : clients) { | |
| 410 | + | LivePeer* peer = clientEntry.second; | |
| 411 | + | if (!peer) continue; | |
| 412 | + | ||
| 413 | + | const uint32_t clientId = peer->getClientId(); | |
| 414 | + | ||
| 415 | + | if (node->isVisible(clientId, true) || node->isVisible(clientId, false)) { | |
| 416 | + | workItems.push_back({peer, node, ndx, ndy, floors, clientId}); | |
| 417 | + | } | |
| 418 | + | } | |
| 419 | + | } | |
| 420 | + | ||
| 421 | + | // If we have work to do, process it all in a single batch | |
| 422 | + | if (!workItems.empty()) { | |
| 423 | + | // Process work items in batches to avoid overwhelming the network | |
| 424 | + | const size_t batchSize = 100; // Process 100 work items at a time | |
| 425 | + | ||
| 426 | + | for (size_t startIdx = 0; startIdx < workItems.size(); startIdx += batchSize) { | |
| 427 | + | size_t endIdx = std::min(startIdx + batchSize, workItems.size()); | |
| 428 | + | ||
| 429 | + | for (size_t i = startIdx; i < endIdx; ++i) { | |
| 430 | + | const auto& work = workItems[i]; | |
| 431 | + | if (work.peer && work.node) { | |
| 432 | + | sendNode(work.clientId, work.node, work.ndx, work.ndy, work.floors); | |
| 433 | + | } | |
| 434 | + | } | |
| 435 | + | } | |
| 436 | + | ||
| 437 | + | // Log completion to file | |
| 438 | + | std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "server_ops.log").ToStdString(), std::ios::app); | |
| 439 | + | if (logFile.is_open()) { | |
| 440 | + | wxDateTime now = wxDateTime::Now(); | |
| 441 | + | logFile << now.FormatISOCombined() << ": Broadcast completed, sent " << workItems.size() << " node updates\n"; | |
| 442 | + | logFile.close(); | |
| 443 | + | } | |
| 444 | + | } | |
| 445 | + | } catch (std::exception& e) { | |
| 446 | + | // Log error to file | |
| 447 | + | std::ofstream logFile((GetAppDir() + wxFileName::GetPathSeparator() + "server_error.log").ToStdString(), std::ios::app); | |
| 448 | + | if (logFile.is_open()) { | |
| 449 | + | wxDateTime now = wxDateTime::Now(); | |
| 450 | + | logFile << now.FormatISOCombined() << ": Error broadcasting nodes: " << e.what() << "\n"; | |
| 451 | + | logFile.close(); | |
| 452 | + | } | |
| 453 | + | } | |
| 454 | + | }); | |
| 455 | + | ||
| 456 | + | if (logFile.is_open()) { | |
| 457 | + | logFile << "Broadcast queued to main thread\n"; | |
| 458 | + | logFile.close(); | |
| 459 | + | } | |
| 460 | + | } catch (std::exception& e) { | |
| 461 | + | // Log error to file | |
| 462 | + | if (logFile.is_open()) { | |
| 463 | + | logFile << "Error preparing broadcast: " << e.what() << "\n"; | |
| 464 | + | logFile.close(); | |
| 465 | + | } else { | |
| 466 | + | std::ofstream errorLog((GetAppDir() + wxFileName::GetPathSeparator() + "server_error.log").ToStdString(), std::ios::app); | |
| 467 | + | if (errorLog.is_open()) { | |
| 468 | + | wxDateTime now = wxDateTime::Now(); | |
| 469 | + | errorLog << now.FormatISOCombined() << ": Error preparing broadcast: " << e.what() << "\n"; | |
| 470 | + | errorLog.close(); | |
| 471 | + | } | |
| 472 | + | } | |
| 473 | + | } | |
| 474 | + | } | |
| 475 | + | ||
| 476 | + | void LiveServer::broadcastCursor(const LiveCursor& cursor) { | |
| 477 | + | // Ensure we're on the main thread | |
| 478 | + | if (!wxThread::IsMain()) { | |
| 479 | + | wxTheApp->CallAfter([this, cursor]() { | |
| 480 | + | this->broadcastCursor(cursor); | |
| 481 | + | }); | |
| 482 | + | return; | |
| 483 | + | } | |
| 484 | + | ||
| 485 | + | if (clients.empty()) { | |
| 486 | + | return; | |
| 487 | + | } | |
| 488 | + | ||
| 489 | + | // Update the cursor in our storage without logging each movement | |
| 490 | + | cursors[cursor.id] = cursor; | |
| 491 | + | ||
| 492 | + | NetworkMessage message; | |
| 493 | + | message.write<uint8_t>(PACKET_CURSOR_UPDATE); | |
| 494 | + | writeCursor(message, cursor); | |
| 495 | + | ||
| 496 | + | for (auto& clientEntry : clients) { | |
| 497 | + | LivePeer* peer = clientEntry.second; | |
| 498 | + | peer->send(message); | |
| 499 | + | } | |
| 500 | + | ||
| 501 | + | g_gui.RefreshView(); | |
| 502 | + | } | |
| 503 | + | ||
| 504 | + | void LiveServer::broadcastChat(const wxString& speaker, const wxString& chatMessage) { | |
| 505 | + | // Ensure we're on the main thread | |
| 506 | + | if (!wxThread::IsMain()) { | |
| 507 | + | wxTheApp->CallAfter([this, speaker, chatMessage]() { | |
| 508 | + | this->broadcastChat(speaker, chatMessage); | |
| 509 | + | }); | |
| 510 | + | return; | |
| 511 | + | } | |
| 512 | + | ||
| 513 | + | if (clients.empty()) { | |
| 514 | + | return; | |
| 515 | + | } | |
| 516 | + | ||
| 517 | + | wxString displayName = (speaker == "HOST") ? name : speaker; | |
| 518 | + | ||
| 519 | + | NetworkMessage message; | |
| 520 | + | message.write<uint8_t>(PACKET_SERVER_TALK); | |
| 521 | + | message.write<std::string>(nstr(displayName)); | |
| 522 | + | message.write<std::string>(nstr(chatMessage)); | |
| 523 | + | ||
| 524 | + | for (auto& clientEntry : clients) { | |
| 525 | + | clientEntry.second->send(message); | |
| 526 | + | } | |
| 527 | + | ||
| 528 | + | if (log) { | |
| 529 | + | log->Chat(displayName, chatMessage); | |
| 530 | + | } | |
| 531 | + | } | |
| 532 | + | ||
| 533 | + | void LiveServer::sendChat(const wxString& chatMessage) { | |
| 534 | + | // For server, sending a chat message means broadcasting it from HOST | |
| 535 | + | broadcastChat("HOST", chatMessage); | |
| 536 | + | } | |
| 537 | + | ||
| 538 | + | void LiveServer::startOperation(const wxString& operationMessage) { | |
| 539 | + | if (clients.empty()) { | |
| 540 | + | return; | |
| 541 | + | } | |
| 542 | + | ||
| 543 | + | NetworkMessage message; | |
| 544 | + | message.write<uint8_t>(PACKET_START_OPERATION); | |
| 545 | + | message.write<std::string>(nstr(operationMessage)); | |
| 546 | + | ||
| 547 | + | for (auto& clientEntry : clients) { | |
| 548 | + | clientEntry.second->send(message); | |
| 549 | + | } | |
| 550 | + | } | |
| 551 | + | ||
| 552 | + | void LiveServer::updateOperation(int32_t percent) { | |
| 553 | + | if (clients.empty()) { | |
| 554 | + | return; | |
| 555 | + | } | |
| 556 | + | ||
| 557 | + | NetworkMessage message; | |
| 558 | + | message.write<uint8_t>(PACKET_UPDATE_OPERATION); | |
| 559 | + | message.write<uint32_t>(percent); | |
| 560 | + | ||
| 561 | + | for (auto& clientEntry : clients) { | |
| 562 | + | clientEntry.second->send(message); | |
| 563 | + | } | |
| 564 | + | } | |
| 565 | + | ||
| 566 | + | LiveLogTab* LiveServer::createLogWindow(wxWindow* parent) { | |
| 567 | + | MapTabbook* mapTabBook = dynamic_cast<MapTabbook*>(parent); | |
| 568 | + | ASSERT(mapTabBook); | |
| 569 | + | ||
| 570 | + | log = newd LiveLogTab(mapTabBook, this); | |
| 571 | + | log->Message("New Live mapping session started."); | |
| 572 | + | log->Message("Hosted on server " + getHostName() + "."); | |
| 573 | + | ||
| 574 | + | updateClientList(); | |
| 575 | + | return log; | |
| 576 | + | } | |
| 577 | + | ||
| 578 | + | void LiveServer::broadcastColorChange(uint32_t clientId, const wxColor& color) { | |
| 579 | + | if (clients.empty()) { | |
| 580 | + | return; | |
| 581 | + | } | |
| 582 | + | ||
| 583 | + | // Prepare the color update packet | |
| 584 | + | NetworkMessage message; | |
| 585 | + | message.write<uint8_t>(PACKET_COLOR_UPDATE); | |
| 586 | + | message.write<uint32_t>(clientId); | |
| 587 | + | message.write<uint8_t>(color.Red()); | |
| 588 | + | message.write<uint8_t>(color.Green()); | |
| 589 | + | message.write<uint8_t>(color.Blue()); | |
| 590 | + | message.write<uint8_t>(color.Alpha()); | |
| 591 | + | ||
| 592 | + | // Log the color change for debugging | |
| 593 | + | logMessage(wxString::Format("[Server]: Broadcasting color change for client %u: RGB(%d,%d,%d)", | |
| 594 | + | clientId, color.Red(), color.Green(), color.Blue())); | |
| 595 | + | ||
| 596 | + | // Send to all clients | |
| 597 | + | for (auto& clientEntry : clients) { | |
| 598 | + | clientEntry.second->send(message); | |
| 599 | + | } | |
| 600 | + | ||
| 601 | + | // Update client list in all open log tabs | |
| 602 | + | updateClientList(); | |
| 603 | + | } | |
| 604 | + | ||
| 605 | + | void LiveServer::setUsedColor(const wxColor& color) { | |
| 606 | + | usedColor = color; | |
| 607 | + | ||
| 608 | + | // Broadcast the host's color change (host always has clientId 0) | |
| 609 | + | broadcastColorChange(0, color); | |
| 610 | + | } |
| @@ -0,0 +1,104 @@ | |||
| 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 | + | #ifndef _RME_LIVE_SERVER_H_ | |
| 19 | + | #define _RME_LIVE_SERVER_H_ | |
| 20 | + | ||
| 21 | + | #include "live_socket.h" | |
| 22 | + | #include "net_connection.h" | |
| 23 | + | #include "action.h" | |
| 24 | + | ||
| 25 | + | class LivePeer; | |
| 26 | + | class LiveLogTab; | |
| 27 | + | class QTreeNode; | |
| 28 | + | ||
| 29 | + | class LiveServer : public LiveSocket { | |
| 30 | + | public: | |
| 31 | + | LiveServer(Editor& editor); | |
| 32 | + | ~LiveServer(); | |
| 33 | + | ||
| 34 | + | // | |
| 35 | + | bool bind(); | |
| 36 | + | void close(); | |
| 37 | + | ||
| 38 | + | void acceptClient(); | |
| 39 | + | void removeClient(uint32_t id); | |
| 40 | + | ||
| 41 | + | // | |
| 42 | + | void receiveHeader() override { } | |
| 43 | + | void receive(uint32_t packetSize) override { } | |
| 44 | + | void send(NetworkMessage& message) override { } | |
| 45 | + | void sendChat(const wxString& chatMessage) override; | |
| 46 | + | ||
| 47 | + | // | |
| 48 | + | void updateCursor(const Position& position) override; | |
| 49 | + | void updateClientList() const; | |
| 50 | + | ||
| 51 | + | // | |
| 52 | + | LiveLogTab* createLogWindow(wxWindow* parent); | |
| 53 | + | ||
| 54 | + | // | |
| 55 | + | uint16_t getPort() const; | |
| 56 | + | bool setPort(int32_t newPort); | |
| 57 | + | ||
| 58 | + | Editor* getEditor() const { | |
| 59 | + | return editor; | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | uint32_t getFreeClientId(); | |
| 63 | + | std::string getHostName() const; | |
| 64 | + | ||
| 65 | + | // | |
| 66 | + | void broadcastNodes(DirtyList& dirtyList); | |
| 67 | + | void broadcastChat(const wxString& speaker, const wxString& chatMessage); | |
| 68 | + | void broadcastCursor(const LiveCursor& cursor); | |
| 69 | + | void broadcastColorChange(uint32_t clientId, const wxColor& color); | |
| 70 | + | ||
| 71 | + | void startOperation(const wxString& operationMessage); | |
| 72 | + | void updateOperation(int32_t percent); | |
| 73 | + | ||
| 74 | + | // Override socket type check | |
| 75 | + | bool IsServer() const override { return true; } | |
| 76 | + | ||
| 77 | + | void setUsedColor(const wxColor& color); | |
| 78 | + | wxColor getUsedColor() const { return usedColor; } | |
| 79 | + | ||
| 80 | + | const std::unordered_map<uint32_t, LivePeer*>& getClients() const { return clients; } | |
| 81 | + | ||
| 82 | + | // Helper method for writing cursor data to messages | |
| 83 | + | void writeCursorToMessage(NetworkMessage& message, const LiveCursor& cursor) { | |
| 84 | + | writeCursor(message, cursor); | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | protected: | |
| 88 | + | std::unordered_map<uint32_t, LivePeer*> clients; | |
| 89 | + | ||
| 90 | + | std::shared_ptr<boost::asio::ip::tcp::acceptor> acceptor; | |
| 91 | + | std::shared_ptr<boost::asio::ip::tcp::socket> socket; | |
| 92 | + | ||
| 93 | + | Editor* editor; | |
| 94 | + | ||
| 95 | + | uint32_t clientIds; | |
| 96 | + | uint16_t port; | |
| 97 | + | ||
| 98 | + | bool stopped; | |
| 99 | + | bool drawingReady; // Flag indicating server is ready for drawing operations | |
| 100 | + | ||
| 101 | + | wxColor usedColor; | |
| 102 | + | }; | |
| 103 | + | ||
| 104 | + | #endif |
| @@ -0,0 +1,434 @@ | |||
| 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 | + | #include "live_socket.h" | |
| 20 | + | #include "map_region.h" | |
| 21 | + | #include "iomap_otbm.h" | |
| 22 | + | #include "live_tab.h" | |
| 23 | + | #include "editor.h" | |
| 24 | + | ||
| 25 | + | LiveSocket::LiveSocket() : | |
| 26 | + | cursors(), mapReader(nullptr, 0), mapWriter(), | |
| 27 | + | mapVersion(MapVersion(MAP_OTBM_4, CLIENT_VERSION_NONE)), log(nullptr), | |
| 28 | + | name("User"), password("") { | |
| 29 | + | // | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | LiveSocket::~LiveSocket() { | |
| 33 | + | // | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | wxString LiveSocket::getName() const { | |
| 37 | + | return name; | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | bool LiveSocket::setName(const wxString& newName) { | |
| 41 | + | if (newName.empty()) { | |
| 42 | + | setLastError("Must provide a name."); | |
| 43 | + | return false; | |
| 44 | + | } else if (newName.length() > 32) { | |
| 45 | + | setLastError("Name is too long."); | |
| 46 | + | return false; | |
| 47 | + | } | |
| 48 | + | name = newName; | |
| 49 | + | return true; | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | wxString LiveSocket::getPassword() const { | |
| 53 | + | return password; | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | bool LiveSocket::setPassword(const wxString& newPassword) { | |
| 57 | + | if (newPassword.length() > 32) { | |
| 58 | + | setLastError("Password is too long."); | |
| 59 | + | return false; | |
| 60 | + | } | |
| 61 | + | password = newPassword; | |
| 62 | + | return true; | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | wxString LiveSocket::getLastError() const { | |
| 66 | + | return lastError; | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | void LiveSocket::setLastError(const wxString& error) { | |
| 70 | + | lastError = error; | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | std::string LiveSocket::getHostName() const { | |
| 74 | + | return "?"; | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | std::vector<LiveCursor> LiveSocket::getCursorList() const { | |
| 78 | + | std::vector<LiveCursor> cursorList; | |
| 79 | + | for (auto& cursorEntry : cursors) { | |
| 80 | + | cursorList.push_back(cursorEntry.second); | |
| 81 | + | } | |
| 82 | + | return cursorList; | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | void LiveSocket::logMessage(const wxString& message) { | |
| 86 | + | wxTheApp->CallAfter([this, message]() { | |
| 87 | + | if (log) { | |
| 88 | + | log->Message(message); | |
| 89 | + | } | |
| 90 | + | }); | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | void LiveSocket::receiveNode(NetworkMessage& message, Editor& editor, Action* action, int32_t ndx, int32_t ndy, bool underground) { | |
| 94 | + | QTreeNode* node = editor.map.getLeaf(ndx * 4, ndy * 4); | |
| 95 | + | if (!node) { | |
| 96 | + | log->Message("Warning: Received update for unknown tile (" + std::to_string(ndx * 4) + "/" + std::to_string(ndy * 4) + "/" + (underground ? "true" : "false") + ")"); | |
| 97 | + | return; | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | node->setRequested(underground, false); | |
| 101 | + | node->setVisible(underground, true); | |
| 102 | + | ||
| 103 | + | uint16_t floorBits = message.read<uint16_t>(); | |
| 104 | + | if (floorBits == 0) { | |
| 105 | + | return; | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | for (uint_fast8_t z = 0; z < MAP_LAYERS; ++z) { | |
| 109 | + | if (testFlags(floorBits, static_cast<uint64_t>(1) << z)) { | |
| 110 | + | receiveFloor(message, editor, action, ndx, ndy, z, node, node->getFloor(z)); | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | void LiveSocket::sendNode(uint32_t clientId, QTreeNode* node, int32_t ndx, int32_t ndy, uint32_t floorMask) { | |
| 116 | + | // Safety check | |
| 117 | + | if (!node) { | |
| 118 | + | logMessage(wxString::Format("Warning: Attempted to send null node at %d,%d", ndx * 4, ndy * 4)); | |
| 119 | + | return; | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | bool underground; | |
| 123 | + | if (floorMask & 0xFF00) { | |
| 124 | + | if (floorMask & 0x00FF) { | |
| 125 | + | underground = false; | |
| 126 | + | } else { | |
| 127 | + | underground = true; | |
| 128 | + | } | |
| 129 | + | } else { | |
| 130 | + | underground = false; | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | // Mark the node as visible to this client | |
| 134 | + | node->setVisible(clientId, underground, true); | |
| 135 | + | ||
| 136 | + | try { | |
| 137 | + | // Prepare the message | |
| 138 | + | NetworkMessage message; | |
| 139 | + | message.write<uint8_t>(PACKET_NODE); | |
| 140 | + | message.write<uint32_t>((ndx << 18) | (ndy << 4) | ((floorMask & 0xFF00) ? 1 : 0)); | |
| 141 | + | ||
| 142 | + | // Check floors | |
| 143 | + | Floor** floors = node->getFloors(); | |
| 144 | + | bool hasFloors = false; | |
| 145 | + | ||
| 146 | + | uint16_t sendMask = 0; | |
| 147 | + | for (uint32_t z = 0; z < MAP_LAYERS; ++z) { | |
| 148 | + | uint32_t bit = 1 << z; | |
| 149 | + | if (floors[z] && testFlags(floorMask, bit)) { | |
| 150 | + | sendMask |= bit; | |
| 151 | + | hasFloors = true; | |
| 152 | + | } | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | // Add the floor mask to the message | |
| 156 | + | message.write<uint16_t>(sendMask); | |
| 157 | + | ||
| 158 | + | // If we have floors to send, add them to the message | |
| 159 | + | if (hasFloors) { | |
| 160 | + | for (uint32_t z = 0; z < MAP_LAYERS; ++z) { | |
| 161 | + | if (testFlags(sendMask, static_cast<uint64_t>(1) << z)) { | |
| 162 | + | sendFloor(message, floors[z]); | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | // Send the message | |
| 168 | + | logMessage(wxString::Format("Sending node [%d,%d,%s] with floor mask 0x%04X", | |
| 169 | + | ndx, ndy, underground ? "underground" : "surface", sendMask)); | |
| 170 | + | send(message); | |
| 171 | + | } catch (std::exception& e) { | |
| 172 | + | logMessage(wxString::Format("Error sending node [%d,%d]: %s", ndx, ndy, e.what())); | |
| 173 | + | } | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | void LiveSocket::receiveFloor(NetworkMessage& message, Editor& editor, Action* action, int32_t ndx, int32_t ndy, int32_t z, QTreeNode* node, Floor* floor) { | |
| 177 | + | Map& map = editor.map; | |
| 178 | + | ||
| 179 | + | uint16_t tileBits = message.read<uint16_t>(); | |
| 180 | + | if (tileBits == 0) { | |
| 181 | + | for (uint_fast8_t x = 0; x < 4; ++x) { | |
| 182 | + | for (uint_fast8_t y = 0; y < 4; ++y) { | |
| 183 | + | action->addChange(new Change(map.allocator(node->createTile(ndx * 4 + x, ndy * 4 + y, z)))); | |
| 184 | + | } | |
| 185 | + | } | |
| 186 | + | return; | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | // -1 on address since we skip the first START_NODE when sending | |
| 190 | + | const std::string& data = message.read<std::string>(); | |
| 191 | + | mapReader.assign(reinterpret_cast<const uint8_t*>(data.c_str() - 1), data.size()); | |
| 192 | + | ||
| 193 | + | BinaryNode* rootNode = mapReader.getRootNode(); | |
| 194 | + | BinaryNode* tileNode = rootNode->getChild(); | |
| 195 | + | ||
| 196 | + | Position position(0, 0, z); | |
| 197 | + | for (uint_fast8_t x = 0; x < 4; ++x) { | |
| 198 | + | for (uint_fast8_t y = 0; y < 4; ++y) { | |
| 199 | + | position.x = (ndx * 4) + x; | |
| 200 | + | position.y = (ndy * 4) + y; | |
| 201 | + | ||
| 202 | + | if (testFlags(tileBits, static_cast<uint64_t>(1) << ((x * 4) + y))) { | |
| 203 | + | receiveTile(tileNode, editor, action, &position); | |
| 204 | + | tileNode->advance(); | |
| 205 | + | } else { | |
| 206 | + | action->addChange(new Change(map.allocator(node->createTile(position.x, position.y, z)))); | |
| 207 | + | } | |
| 208 | + | } | |
| 209 | + | } | |
| 210 | + | mapReader.close(); | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | void LiveSocket::sendFloor(NetworkMessage& message, Floor* floor) { | |
| 214 | + | uint16_t tileBits = 0; | |
| 215 | + | for (uint_fast8_t x = 0; x < 4; ++x) { | |
| 216 | + | for (uint_fast8_t y = 0; y < 4; ++y) { | |
| 217 | + | uint_fast8_t index = (x * 4) + y; | |
| 218 | + | ||
| 219 | + | Tile* tile = floor->locs[index].get(); | |
| 220 | + | if (tile && tile->size() > 0) { | |
| 221 | + | tileBits |= (1 << index); | |
| 222 | + | } | |
| 223 | + | } | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | message.write<uint16_t>(tileBits); | |
| 227 | + | if (tileBits == 0) { | |
| 228 | + | return; | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | mapWriter.reset(); | |
| 232 | + | for (uint_fast8_t x = 0; x < 4; ++x) { | |
| 233 | + | for (uint_fast8_t y = 0; y < 4; ++y) { | |
| 234 | + | uint_fast8_t index = (x * 4) + y; | |
| 235 | + | if (testFlags(tileBits, static_cast<uint64_t>(1) << index)) { | |
| 236 | + | sendTile(mapWriter, floor->locs[index].get(), nullptr); | |
| 237 | + | } | |
| 238 | + | } | |
| 239 | + | } | |
| 240 | + | mapWriter.endNode(); | |
| 241 | + | ||
| 242 | + | std::string stream( | |
| 243 | + | reinterpret_cast<char*>(mapWriter.getMemory()), | |
| 244 | + | mapWriter.getSize() | |
| 245 | + | ); | |
| 246 | + | message.write<std::string>(stream); | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | void LiveSocket::receiveTile(BinaryNode* node, Editor& editor, Action* action, const Position* position) { | |
| 250 | + | ASSERT(node != nullptr); | |
| 251 | + | ||
| 252 | + | Tile* tile = readTile(node, editor, position); | |
| 253 | + | if (tile) { | |
| 254 | + | action->addChange(newd Change(tile)); | |
| 255 | + | } | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | void LiveSocket::sendTile(MemoryNodeFileWriteHandle& writer, Tile* tile, const Position* position) { | |
| 259 | + | writer.addNode(tile->isHouseTile() ? OTBM_HOUSETILE : OTBM_TILE); | |
| 260 | + | if (position) { | |
| 261 | + | writer.addU16(position->x); | |
| 262 | + | writer.addU16(position->y); | |
| 263 | + | writer.addU8(position->z); | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | if (tile->isHouseTile()) { | |
| 267 | + | writer.addU32(tile->getHouseID()); | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | if (tile->getMapFlags()) { | |
| 271 | + | writer.addByte(OTBM_ATTR_TILE_FLAGS); | |
| 272 | + | writer.addU32(tile->getMapFlags()); | |
| 273 | + | if (tile->getMapFlags() & TILESTATE_ZONE_BRUSH) { | |
| 274 | + | for (const auto& zoneId : tile->getZoneIds()) { | |
| 275 | + | writer.addU16(zoneId); | |
| 276 | + | } | |
| 277 | + | writer.addU16(0); | |
| 278 | + | } | |
| 279 | + | } | |
| 280 | + | ||
| 281 | + | Item* ground = tile->ground; | |
| 282 | + | if (ground) { | |
| 283 | + | if (ground->isComplex()) { | |
| 284 | + | ground->serializeItemNode_OTBM(mapVersion, writer); | |
| 285 | + | } else { | |
| 286 | + | writer.addByte(OTBM_ATTR_ITEM); | |
| 287 | + | ground->serializeItemCompact_OTBM(mapVersion, writer); | |
| 288 | + | } | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | for (Item* item : tile->items) { | |
| 292 | + | item->serializeItemNode_OTBM(mapVersion, writer); | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | writer.endNode(); | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | Tile* LiveSocket::readTile(BinaryNode* node, Editor& editor, const Position* position) { | |
| 299 | + | ASSERT(node != nullptr); | |
| 300 | + | ||
| 301 | + | Map& map = editor.map; | |
| 302 | + | ||
| 303 | + | uint8_t tileType; | |
| 304 | + | node->getByte(tileType); | |
| 305 | + | ||
| 306 | + | if (tileType != OTBM_TILE && tileType != OTBM_HOUSETILE) { | |
| 307 | + | return nullptr; | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | Position pos; | |
| 311 | + | if (position) { | |
| 312 | + | pos = *position; | |
| 313 | + | } else { | |
| 314 | + | uint16_t x; | |
| 315 | + | node->getU16(x); | |
| 316 | + | pos.x = x; | |
| 317 | + | uint16_t y; | |
| 318 | + | node->getU16(y); | |
| 319 | + | pos.y = y; | |
| 320 | + | uint8_t z; | |
| 321 | + | node->getU8(z); | |
| 322 | + | pos.z = z; | |
| 323 | + | } | |
| 324 | + | ||
| 325 | + | Tile* tile = map.allocator( | |
| 326 | + | map.createTileL(pos) | |
| 327 | + | ); | |
| 328 | + | ||
| 329 | + | if (tileType == OTBM_HOUSETILE) { | |
| 330 | + | uint32_t houseId; | |
| 331 | + | if (!node->getU32(houseId)) { | |
| 332 | + | // warning("House tile without house data, discarding tile"); | |
| 333 | + | delete tile; | |
| 334 | + | return nullptr; | |
| 335 | + | } | |
| 336 | + | ||
| 337 | + | if (houseId) { | |
| 338 | + | House* house = map.houses.getHouse(houseId); | |
| 339 | + | if (house) { | |
| 340 | + | tile->setHouse(house); | |
| 341 | + | } | |
| 342 | + | } else { | |
| 343 | + | // warning("Invalid house id from tile %d:%d:%d", pos.x, pos.y, pos.z); | |
| 344 | + | } | |
| 345 | + | } | |
| 346 | + | ||
| 347 | + | uint8_t attribute; | |
| 348 | + | while (node->getU8(attribute)) { | |
| 349 | + | switch (attribute) { | |
| 350 | + | case OTBM_ATTR_TILE_FLAGS: { | |
| 351 | + | uint32_t flags = 0; | |
| 352 | + | if (!node->getU32(flags)) { | |
| 353 | + | // warning("Invalid tile flags of tile on %d:%d:%d", pos.x, pos.y, pos.z); | |
| 354 | + | } | |
| 355 | + | tile->setMapFlags(flags); | |
| 356 | + | if (flags & TILESTATE_ZONE_BRUSH) { | |
| 357 | + | uint16_t zoneId = 0; | |
| 358 | + | do { | |
| 359 | + | if (!node->getU16(zoneId)) { | |
| 360 | + | // warning("Invalid zone id of tile on %d:%d:%d", pos.x, pos.y, pos.z); | |
| 361 | + | } | |
| 362 | + | ||
| 363 | + | if (zoneId != 0) { | |
| 364 | + | tile->addZoneId(zoneId); | |
| 365 | + | } | |
| 366 | + | } while (zoneId != 0); | |
| 367 | + | } | |
| 368 | + | break; | |
| 369 | + | } | |
| 370 | + | case OTBM_ATTR_ITEM: { | |
| 371 | + | Item* item = Item::Create_OTBM(mapVersion, node); | |
| 372 | + | if (!item) { | |
| 373 | + | // warning("Invalid item at tile %d:%d:%d", pos.x, pos.y, pos.z); | |
| 374 | + | } | |
| 375 | + | tile->addItem(item); | |
| 376 | + | break; | |
| 377 | + | } | |
| 378 | + | default: | |
| 379 | + | // warning("Unknown tile attribute at %d:%d:%d", pos.x, pos.y, pos.z); | |
| 380 | + | break; | |
| 381 | + | } | |
| 382 | + | } | |
| 383 | + | ||
| 384 | + | // for(BinaryNode* itemNode = node->getChild(); itemNode; itemNode->advance()) { | |
| 385 | + | BinaryNode* itemNode = node->getChild(); | |
| 386 | + | if (itemNode) { | |
| 387 | + | do { | |
| 388 | + | uint8_t itemType; | |
| 389 | + | if (!itemNode->getByte(itemType)) { | |
| 390 | + | // warning("Unknown item type %d:%d:%d", pos.x, pos.y, pos.z); | |
| 391 | + | delete tile; | |
| 392 | + | return nullptr; | |
| 393 | + | } | |
| 394 | + | ||
| 395 | + | if (itemType == OTBM_ITEM) { | |
| 396 | + | Item* item = Item::Create_OTBM(mapVersion, itemNode); | |
| 397 | + | if (item) { | |
| 398 | + | if (!item->unserializeItemNode_OTBM(mapVersion, itemNode)) { | |
| 399 | + | // warning("Couldn't unserialize item attributes at %d:%d:%d", pos.x, pos.y, pos.z); | |
| 400 | + | } | |
| 401 | + | tile->addItem(item); | |
| 402 | + | } | |
| 403 | + | } else { | |
| 404 | + | // warning("Unknown type of tile child node"); | |
| 405 | + | } | |
| 406 | + | //} | |
| 407 | + | } while (itemNode->advance()); | |
| 408 | + | } | |
| 409 | + | ||
| 410 | + | return tile; | |
| 411 | + | } | |
| 412 | + | ||
| 413 | + | LiveCursor LiveSocket::readCursor(NetworkMessage& message) { | |
| 414 | + | LiveCursor cursor; | |
| 415 | + | cursor.id = message.read<uint32_t>(); | |
| 416 | + | ||
| 417 | + | uint8_t r = message.read<uint8_t>(); | |
| 418 | + | uint8_t g = message.read<uint8_t>(); | |
| 419 | + | uint8_t b = message.read<uint8_t>(); | |
| 420 | + | uint8_t a = message.read<uint8_t>(); | |
| 421 | + | cursor.color = wxColor(r, g, b, a); | |
| 422 | + | ||
| 423 | + | cursor.pos = message.read<Position>(); | |
| 424 | + | return cursor; | |
| 425 | + | } | |
| 426 | + | ||
| 427 | + | void LiveSocket::writeCursor(NetworkMessage& message, const LiveCursor& cursor) { | |
| 428 | + | message.write<uint32_t>(cursor.id); | |
| 429 | + | message.write<uint8_t>(cursor.color.Red()); | |
| 430 | + | message.write<uint8_t>(cursor.color.Green()); | |
| 431 | + | message.write<uint8_t>(cursor.color.Blue()); | |
| 432 | + | message.write<uint8_t>(cursor.color.Alpha()); | |
| 433 | + | message.write<Position>(cursor.pos); | |
| 434 | + | } |
| @@ -0,0 +1,121 @@ | |||
| 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 | + | #ifndef _RME_LIVE_SOCKET_H_ | |
| 19 | + | #define _RME_LIVE_SOCKET_H_ | |
| 20 | + | ||
| 21 | + | #include "position.h" | |
| 22 | + | #include "net_connection.h" | |
| 23 | + | #include "live_packets.h" | |
| 24 | + | #include "filehandle.h" | |
| 25 | + | #include "iomap.h" | |
| 26 | + | ||
| 27 | + | #include <memory> | |
| 28 | + | #include <unordered_map> | |
| 29 | + | ||
| 30 | + | class LiveLogTab; | |
| 31 | + | class Action; | |
| 32 | + | ||
| 33 | + | struct LiveCursor { | |
| 34 | + | uint32_t id; | |
| 35 | + | wxColor color; | |
| 36 | + | Position pos; | |
| 37 | + | }; | |
| 38 | + | ||
| 39 | + | class LiveSocket { | |
| 40 | + | public: | |
| 41 | + | LiveSocket(); | |
| 42 | + | virtual ~LiveSocket(); | |
| 43 | + | ||
| 44 | + | // | |
| 45 | + | wxString getName() const; | |
| 46 | + | bool setName(const wxString& newName); | |
| 47 | + | ||
| 48 | + | wxString getPassword() const; | |
| 49 | + | bool setPassword(const wxString& newPassword); | |
| 50 | + | ||
| 51 | + | wxString getLastError() const; | |
| 52 | + | void setLastError(const wxString& error); | |
| 53 | + | ||
| 54 | + | std::string getHostName() const; | |
| 55 | + | std::vector<LiveCursor> getCursorList() const; | |
| 56 | + | ||
| 57 | + | // Check socket type | |
| 58 | + | virtual bool IsServer() const { return false; } | |
| 59 | + | virtual bool IsClient() const { return false; } | |
| 60 | + | ||
| 61 | + | // | |
| 62 | + | void logMessage(const wxString& message); | |
| 63 | + | ||
| 64 | + | // | |
| 65 | + | virtual void receiveHeader() = 0; | |
| 66 | + | virtual void receive(uint32_t packetSize) = 0; | |
| 67 | + | virtual void send(NetworkMessage& message) = 0; | |
| 68 | + | virtual void sendChat(const wxString& chatMessage) = 0; | |
| 69 | + | ||
| 70 | + | // | |
| 71 | + | virtual void updateCursor(const Position& position) = 0; | |
| 72 | + | ||
| 73 | + | // Helper method to safely get app directory for logging | |
| 74 | + | static wxString GetAppDir() { | |
| 75 | + | wxString dir; | |
| 76 | + | try { | |
| 77 | + | dir = wxStandardPaths::Get().GetUserDataDir(); | |
| 78 | + | if (!wxDirExists(dir)) { | |
| 79 | + | wxMkdir(dir); | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | catch (...) { | |
| 83 | + | dir = "."; // Fallback to current directory | |
| 84 | + | } | |
| 85 | + | return dir; | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | protected: | |
| 89 | + | // receive / send methods | |
| 90 | + | void receiveNode(NetworkMessage& message, Editor& editor, Action* action, int32_t ndx, int32_t ndy, bool underground); | |
| 91 | + | void sendNode(uint32_t clientId, QTreeNode* node, int32_t ndx, int32_t ndy, uint32_t floorMask); | |
| 92 | + | ||
| 93 | + | void receiveFloor(NetworkMessage& message, Editor& editor, Action* action, int32_t ndx, int32_t ndy, int32_t z, QTreeNode* node, Floor* floor); | |
| 94 | + | void sendFloor(NetworkMessage& message, Floor* floor); | |
| 95 | + | ||
| 96 | + | void receiveTile(BinaryNode* node, Editor& editor, Action* action, const Position* position); | |
| 97 | + | void sendTile(MemoryNodeFileWriteHandle& writer, Tile* tile, const Position* position); | |
| 98 | + | ||
| 99 | + | // read / write types | |
| 100 | + | Tile* readTile(BinaryNode* node, Editor& editor, const Position* position); | |
| 101 | + | ||
| 102 | + | LiveCursor readCursor(NetworkMessage& message); | |
| 103 | + | void writeCursor(NetworkMessage& message, const LiveCursor& cursor); | |
| 104 | + | ||
| 105 | + | // | |
| 106 | + | std::unordered_map<uint32_t, LiveCursor> cursors; | |
| 107 | + | ||
| 108 | + | MemoryNodeFileReadHandle mapReader; | |
| 109 | + | MemoryNodeFileWriteHandle mapWriter; | |
| 110 | + | VirtualIOMap mapVersion; | |
| 111 | + | ||
| 112 | + | LiveLogTab* log; | |
| 113 | + | ||
| 114 | + | wxString name; | |
| 115 | + | wxString password; | |
| 116 | + | wxString lastError; | |
| 117 | + | ||
| 118 | + | friend class LiveLogTab; | |
| 119 | + | }; | |
| 120 | + | ||
| 121 | + | #endif |
| @@ -0,0 +1,505 @@ | |||
| 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 <wx/splitter.h> | |
| 21 | + | #include <wx/grid.h> | |
| 22 | + | #include <wx/colordlg.h> | |
| 23 | + | #include <fstream> | |
| 24 | + | #include <wx/filename.h> | |
| 25 | + | #include <wx/stdpaths.h> | |
| 26 | + | ||
| 27 | + | #include "live_tab.h" | |
| 28 | + | #include "live_socket.h" | |
| 29 | + | #include "live_peer.h" | |
| 30 | + | #include "live_server.h" | |
| 31 | + | #include "live_client.h" | |
| 32 | + | ||
| 33 | + | class myGrid : public wxGrid { | |
| 34 | + | public: | |
| 35 | + | myGrid(wxWindow* parent, wxWindowID id, wxPoint pos, wxSize size) : | |
| 36 | + | wxGrid(parent, id, pos, size) { } | |
| 37 | + | virtual ~myGrid() { } | |
| 38 | + | ||
| 39 | + | void DrawCellHighlight(wxDC& dc, const wxGridCellAttr* attr) { | |
| 40 | + | // wxGrid::DrawCellHighlight(dc, attr); | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | DECLARE_CLASS(myGrid); | |
| 44 | + | }; | |
| 45 | + | ||
| 46 | + | IMPLEMENT_CLASS(myGrid, wxGrid) | |
| 47 | + | ||
| 48 | + | // Menu IDs and control IDs | |
| 49 | + | enum { | |
| 50 | + | LIVE_LOG_COPY_SELECTED = 10001, | |
| 51 | + | LIVE_CHAT_INPUT = 10002 | |
| 52 | + | }; | |
| 53 | + | ||
| 54 | + | BEGIN_EVENT_TABLE(LiveLogTab, wxPanel) | |
| 55 | + | EVT_TEXT_ENTER(LIVE_CHAT_INPUT, LiveLogTab::OnChat) | |
| 56 | + | EVT_RIGHT_DOWN(LiveLogTab::OnLogRightClick) | |
| 57 | + | EVT_MENU(LIVE_LOG_COPY_SELECTED, LiveLogTab::OnCopySelectedLogText) | |
| 58 | + | EVT_BOOKCTRL_PAGE_CHANGED(wxID_ANY, LiveLogTab::OnPageChanged) | |
| 59 | + | EVT_GRID_CELL_LEFT_CLICK(LiveLogTab::OnGridCellLeftClick) | |
| 60 | + | END_EVENT_TABLE() | |
| 61 | + | ||
| 62 | + | LiveLogTab::LiveLogTab(MapTabbook* aui, LiveSocket* server) : | |
| 63 | + | EditorTab(), | |
| 64 | + | wxPanel(aui), | |
| 65 | + | aui(aui), | |
| 66 | + | socket(server) { | |
| 67 | + | wxSizer* topsizer = newd wxBoxSizer(wxVERTICAL); | |
| 68 | + | ||
| 69 | + | wxPanel* splitter = newd wxPanel(this); | |
| 70 | + | topsizer->Add(splitter, 1, wxEXPAND); | |
| 71 | + | ||
| 72 | + | // Setup left panel | |
| 73 | + | wxPanel* left_pane = newd wxPanel(splitter); | |
| 74 | + | wxSizer* left_sizer = newd wxBoxSizer(wxVERTICAL); | |
| 75 | + | ||
| 76 | + | wxFont time_font(*wxSWISS_FONT); | |
| 77 | + | ||
| 78 | + | // Create a notebook for tabs | |
| 79 | + | notebook = newd wxNotebook(left_pane, wxID_ANY); | |
| 80 | + | ||
| 81 | + | // Debug log tab | |
| 82 | + | debug_log = newd wxTextCtrl(notebook, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, | |
| 83 | + | wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH | wxTE_AUTO_URL); | |
| 84 | + | debug_log->SetFont(time_font); | |
| 85 | + | debug_log->Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(LiveLogTab::OnLogRightClick), nullptr, this); | |
| 86 | + | notebook->AddPage(debug_log, "Debug"); | |
| 87 | + | ||
| 88 | + | // Chat log tab | |
| 89 | + | chat_log = newd wxTextCtrl(notebook, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, | |
| 90 | + | wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH | wxTE_AUTO_URL); | |
| 91 | + | chat_log->SetFont(time_font); | |
| 92 | + | chat_log->Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(LiveLogTab::OnLogRightClick), nullptr, this); | |
| 93 | + | notebook->AddPage(chat_log, "Chat"); | |
| 94 | + | ||
| 95 | + | left_sizer->Add(notebook, 1, wxEXPAND); | |
| 96 | + | ||
| 97 | + | input = newd wxTextCtrl(left_pane, LIVE_CHAT_INPUT, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER); | |
| 98 | + | left_sizer->Add(input, 0, wxEXPAND); | |
| 99 | + | ||
| 100 | + | input->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(LiveLogTab::OnSelectChatbox), nullptr, this); | |
| 101 | + | input->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(LiveLogTab::OnDeselectChatbox), nullptr, this); | |
| 102 | + | input->Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(LiveLogTab::OnKeyDown), nullptr, this); | |
| 103 | + | ||
| 104 | + | left_pane->SetSizerAndFit(left_sizer); | |
| 105 | + | ||
| 106 | + | // Setup right panel | |
| 107 | + | user_list = newd myGrid(splitter, wxID_ANY, wxDefaultPosition, wxSize(280, 100)); | |
| 108 | + | user_list->CreateGrid(5, 3); | |
| 109 | + | user_list->DisableDragRowSize(); | |
| 110 | + | user_list->DisableDragColSize(); | |
| 111 | + | user_list->SetSelectionMode(wxGrid::wxGridSelectRows); | |
| 112 | + | user_list->SetRowLabelSize(0); | |
| 113 | + | ||
| 114 | + | user_list->SetColLabelValue(0, ""); | |
| 115 | + | user_list->SetColSize(0, 24); | |
| 116 | + | user_list->SetColLabelValue(1, "#"); | |
| 117 | + | user_list->SetColSize(1, 36); | |
| 118 | + | user_list->SetColLabelValue(2, "Name"); | |
| 119 | + | user_list->SetColSize(2, 200); | |
| 120 | + | ||
| 121 | + | // Finalize | |
| 122 | + | SetSizerAndFit(topsizer); | |
| 123 | + | ||
| 124 | + | wxSizer* split_sizer = newd wxBoxSizer(wxHORIZONTAL); | |
| 125 | + | split_sizer->Add(left_pane, wxSizerFlags(1).Expand()); | |
| 126 | + | split_sizer->Add(user_list, wxSizerFlags(0).Expand()); | |
| 127 | + | splitter->SetSizerAndFit(split_sizer); | |
| 128 | + | ||
| 129 | + | aui->AddTab(this, true); | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | LiveLogTab::~LiveLogTab() { | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | wxString LiveLogTab::GetTitle() const { | |
| 136 | + | if (socket) { | |
| 137 | + | return "Live Log - " + socket->getHostName(); | |
| 138 | + | } | |
| 139 | + | return "Live Log - Disconnected"; | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | void LiveLogTab::Disconnect() { | |
| 143 | + | socket->log = nullptr; | |
| 144 | + | input->SetWindowStyle(input->GetWindowStyle() | wxTE_READONLY); | |
| 145 | + | socket = nullptr; | |
| 146 | + | Refresh(); | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | wxString format00(wxDateTime::wxDateTime_t t) { | |
| 150 | + | wxString str; | |
| 151 | + | if (t < 10) { | |
| 152 | + | str << "0"; | |
| 153 | + | } | |
| 154 | + | str << t; | |
| 155 | + | return str; | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | wxString LiveLogTab::GetLogFilePath(const wxString& logType) { | |
| 159 | + | // Create a logs directory in the user's documents folder | |
| 160 | + | wxString docsDir = wxStandardPaths::Get().GetDocumentsDir(); | |
| 161 | + | wxString logsDir = docsDir + wxFileName::GetPathSeparator() + "RME_Logs"; | |
| 162 | + | ||
| 163 | + | // Create the directory if it doesn't exist | |
| 164 | + | if (!wxDirExists(logsDir)) { | |
| 165 | + | wxMkdir(logsDir); | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | // Create a unique filename based on session timestamp | |
| 169 | + | static wxString sessionTimestamp; | |
| 170 | + | if (sessionTimestamp.IsEmpty()) { | |
| 171 | + | wxDateTime now = wxDateTime::Now(); | |
| 172 | + | sessionTimestamp = now.Format("%Y%m%d_%H%M%S"); | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | // Return the full path | |
| 176 | + | return logsDir + wxFileName::GetPathSeparator() + "rmelog_" + logType + "_" + sessionTimestamp + ".txt"; | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | void LiveLogTab::Message(const wxString& str) { | |
| 180 | + | // Simply log to file - no UI interaction, no CallAfter | |
| 181 | + | wxDateTime t = wxDateTime::Now(); | |
| 182 | + | wxString time, message; | |
| 183 | + | time << format00(t.GetHour()) << ":" | |
| 184 | + | << format00(t.GetMinute()) << ":" | |
| 185 | + | << format00(t.GetSecond()); | |
| 186 | + | ||
| 187 | + | // Format the message with timestamp | |
| 188 | + | message << time << " - " << str << "\n"; | |
| 189 | + | ||
| 190 | + | // Write to log file only | |
| 191 | + | static wxString logFilePath; | |
| 192 | + | if (logFilePath.IsEmpty()) { | |
| 193 | + | logFilePath = GetLogFilePath("debug"); | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | try { | |
| 197 | + | std::ofstream logFile(logFilePath.ToStdString(), std::ios::app); | |
| 198 | + | if (logFile.is_open()) { | |
| 199 | + | logFile << message; | |
| 200 | + | logFile.close(); | |
| 201 | + | } | |
| 202 | + | } | |
| 203 | + | catch (...) { | |
| 204 | + | // Silent failure - don't propagate exceptions | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | void LiveLogTab::Chat(const wxString& speaker, const wxString& str) { | |
| 209 | + | // Format the chat message | |
| 210 | + | wxDateTime t = wxDateTime::Now(); | |
| 211 | + | wxString time, message; | |
| 212 | + | time << format00(t.GetHour()) << ":" | |
| 213 | + | << format00(t.GetMinute()) << ":" | |
| 214 | + | << format00(t.GetSecond()); | |
| 215 | + | ||
| 216 | + | // Format the chat message with timestamp and speaker | |
| 217 | + | message << time << " [" << speaker << "]: " << str << "\n"; | |
| 218 | + | ||
| 219 | + | // Write to log file first - this is guaranteed not to crash | |
| 220 | + | static wxString logFilePath; | |
| 221 | + | if (logFilePath.IsEmpty()) { | |
| 222 | + | logFilePath = GetLogFilePath("chat"); | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | try { | |
| 226 | + | // Log to file | |
| 227 | + | std::ofstream logFile(logFilePath.ToStdString(), std::ios::app); | |
| 228 | + | if (logFile.is_open()) { | |
| 229 | + | logFile << message; | |
| 230 | + | logFile.close(); | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | // Only try to update UI if we're the main thread to avoid CallAfter issues | |
| 234 | + | if (wxThread::IsMain() && chat_log && chat_log->IsShownOnScreen()) { | |
| 235 | + | // Direct update without CallAfter | |
| 236 | + | chat_log->AppendText(message); | |
| 237 | + | } | |
| 238 | + | } | |
| 239 | + | catch (...) { | |
| 240 | + | // Silent failure - don't propagate exceptions | |
| 241 | + | } | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | void LiveLogTab::OnChat(wxCommandEvent& evt) { | |
| 245 | + | wxString message = input->GetValue(); | |
| 246 | + | if (message.IsEmpty()) { | |
| 247 | + | return; | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | if (socket) { | |
| 251 | + | // Send the chat message | |
| 252 | + | if (socket->getName() == "HOST") { | |
| 253 | + | // If this is the host, the message appears with HOST prefix | |
| 254 | + | socket->sendChat(message); | |
| 255 | + | // Display the message locally too | |
| 256 | + | Chat("HOST", message); | |
| 257 | + | } else { | |
| 258 | + | socket->sendChat(message); | |
| 259 | + | } | |
| 260 | + | ||
| 261 | + | // Clear the input field | |
| 262 | + | input->Clear(); | |
| 263 | + | } | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | void LiveLogTab::OnKeyDown(wxKeyEvent& evt) { | |
| 267 | + | if (evt.GetKeyCode() == WXK_RETURN && !evt.ShiftDown()) { | |
| 268 | + | // Send the message when Enter is pressed (without Shift) | |
| 269 | + | wxString message = input->GetValue(); | |
| 270 | + | if (!message.IsEmpty() && socket) { | |
| 271 | + | if (socket->getName() == "HOST") { | |
| 272 | + | socket->sendChat(message); | |
| 273 | + | Chat("HOST", message); | |
| 274 | + | } else { | |
| 275 | + | socket->sendChat(message); | |
| 276 | + | } | |
| 277 | + | input->Clear(); | |
| 278 | + | } | |
| 279 | + | } else { | |
| 280 | + | evt.Skip(); | |
| 281 | + | } | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | void LiveLogTab::OnResizeChat(wxSizeEvent& evt) { | |
| 285 | + | // No need to adjust anything for wxTextCtrl | |
| 286 | + | evt.Skip(); | |
| 287 | + | } | |
| 288 | + | ||
| 289 | + | void LiveLogTab::OnResizeClientList(wxSizeEvent& evt) { | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | void LiveLogTab::OnSelectChatbox(wxFocusEvent& evt) { | |
| 293 | + | g_gui.DisableHotkeys(); | |
| 294 | + | } | |
| 295 | + | ||
| 296 | + | void LiveLogTab::OnDeselectChatbox(wxFocusEvent& evt) { | |
| 297 | + | g_gui.EnableHotkeys(); | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | void LiveLogTab::OnLogRightClick(wxMouseEvent& evt) { | |
| 301 | + | wxMenu menu; | |
| 302 | + | menu.Append(LIVE_LOG_COPY_SELECTED, "Copy Selected Text"); | |
| 303 | + | PopupMenu(&menu); | |
| 304 | + | } | |
| 305 | + | ||
| 306 | + | void LiveLogTab::OnCopySelectedLogText(wxCommandEvent& evt) { | |
| 307 | + | if(wxTheClipboard->Open()) { | |
| 308 | + | // Determine which log has focus and copy from that one | |
| 309 | + | wxTextCtrl* activeLog = nullptr; | |
| 310 | + | ||
| 311 | + | if (wxWindow::FindFocus() == debug_log) { | |
| 312 | + | activeLog = debug_log; | |
| 313 | + | } else if (wxWindow::FindFocus() == chat_log) { | |
| 314 | + | activeLog = chat_log; | |
| 315 | + | } else { | |
| 316 | + | // Default to currently visible page | |
| 317 | + | int selection = notebook->GetSelection(); | |
| 318 | + | activeLog = (selection == 0) ? debug_log : chat_log; | |
| 319 | + | } | |
| 320 | + | ||
| 321 | + | wxTheClipboard->SetData(new wxTextDataObject(activeLog->GetStringSelection())); | |
| 322 | + | wxTheClipboard->Close(); | |
| 323 | + | } | |
| 324 | + | } | |
| 325 | + | ||
| 326 | + | void LiveLogTab::UpdateClientList(const std::unordered_map<uint32_t, LivePeer*>& updatedClients) { | |
| 327 | + | // Skip during critical operations to prevent crashes | |
| 328 | + | static bool insideOperation = false; | |
| 329 | + | if (insideOperation) return; | |
| 330 | + | ||
| 331 | + | // Guard against reentrancy which might cause crashes | |
| 332 | + | insideOperation = true; | |
| 333 | + | ||
| 334 | + | // Use CallAfter to ensure UI operations happen on the main thread | |
| 335 | + | // This prevents crashes during critical drawing operations | |
| 336 | + | wxTheApp->CallAfter([this, updatedClients]() { | |
| 337 | + | // Only proceed if grid is properly initialized and visible | |
| 338 | + | if (!user_list || !user_list->IsShownOnScreen()) { | |
| 339 | + | insideOperation = false; | |
| 340 | + | return; | |
| 341 | + | } | |
| 342 | + | ||
| 343 | + | try { | |
| 344 | + | // Delete old rows | |
| 345 | + | if (user_list->GetNumberRows() > 0) { | |
| 346 | + | user_list->DeleteRows(0, user_list->GetNumberRows()); | |
| 347 | + | } | |
| 348 | + | ||
| 349 | + | clients = updatedClients; | |
| 350 | + | ||
| 351 | + | // For server, use the provided client list | |
| 352 | + | if (socket && socket->IsServer()) { | |
| 353 | + | // Make sure we have data to display | |
| 354 | + | if (clients.empty()) { | |
| 355 | + | // At least show the host | |
| 356 | + | user_list->AppendRows(1); | |
| 357 | + | user_list->SetCellBackgroundColour(0, 0, dynamic_cast<LiveServer*>(socket)->getUsedColor()); | |
| 358 | + | user_list->SetCellValue(0, 1, "Host"); | |
| 359 | + | user_list->SetCellValue(0, 2, "HOST"); | |
| 360 | + | } else { | |
| 361 | + | user_list->AppendRows(clients.size()); | |
| 362 | + | ||
| 363 | + | int32_t i = 0; | |
| 364 | + | for (auto& clientEntry : clients) { | |
| 365 | + | LivePeer* peer = clientEntry.second; | |
| 366 | + | if (peer) { | |
| 367 | + | user_list->SetCellBackgroundColour(i, 0, peer->getUsedColor()); | |
| 368 | + | user_list->SetCellValue(i, 1, i2ws((peer->getClientId() >> 1) + 1)); | |
| 369 | + | user_list->SetCellValue(i, 2, peer->getName()); | |
| 370 | + | ++i; | |
| 371 | + | } | |
| 372 | + | } | |
| 373 | + | } | |
| 374 | + | } | |
| 375 | + | // For client, use cursor information | |
| 376 | + | else if (socket && socket->IsClient()) { | |
| 377 | + | // Get cursor list from the socket | |
| 378 | + | std::vector<LiveCursor> cursors = socket->getCursorList(); | |
| 379 | + | ||
| 380 | + | if (cursors.empty()) { | |
| 381 | + | // If no cursors, at least show the host | |
| 382 | + | user_list->AppendRows(1); | |
| 383 | + | user_list->SetCellBackgroundColour(0, 0, wxColor(255, 0, 0)); // Default red for host | |
| 384 | + | user_list->SetCellValue(0, 1, "Host"); | |
| 385 | + | user_list->SetCellValue(0, 2, "HOST"); | |
| 386 | + | } else { | |
| 387 | + | // Add a row for each cursor | |
| 388 | + | user_list->AppendRows(cursors.size()); | |
| 389 | + | ||
| 390 | + | int32_t i = 0; | |
| 391 | + | for (const auto& cursor : cursors) { | |
| 392 | + | user_list->SetCellBackgroundColour(i, 0, cursor.color); | |
| 393 | + | ||
| 394 | + | // In display, clientId 0 is the host, others are +1 and shifted | |
| 395 | + | wxString displayId; | |
| 396 | + | wxString displayName; | |
| 397 | + | ||
| 398 | + | if (cursor.id == 0) { | |
| 399 | + | displayId = "Host"; | |
| 400 | + | displayName = "HOST"; | |
| 401 | + | } else { | |
| 402 | + | displayId = i2ws((cursor.id >> 1) + 1); | |
| 403 | + | displayName = wxString::Format("Client %s", displayId); | |
| 404 | + | } | |
| 405 | + | ||
| 406 | + | user_list->SetCellValue(i, 1, displayId); | |
| 407 | + | user_list->SetCellValue(i, 2, displayName); | |
| 408 | + | ++i; | |
| 409 | + | } | |
| 410 | + | } | |
| 411 | + | } | |
| 412 | + | ||
| 413 | + | // Refresh the grid after changes | |
| 414 | + | user_list->AutoSize(); | |
| 415 | + | user_list->Refresh(); | |
| 416 | + | } | |
| 417 | + | catch (const std::exception& e) { | |
| 418 | + | wxLogError("Error updating client list: %s", e.what()); | |
| 419 | + | } | |
| 420 | + | ||
| 421 | + | insideOperation = false; | |
| 422 | + | }); | |
| 423 | + | } | |
| 424 | + | ||
| 425 | + | void LiveLogTab::OnPageChanged(wxBookCtrlEvent& evt) { | |
| 426 | + | // If switching to chat tab, set focus to the input box | |
| 427 | + | if (evt.GetSelection() == 1) { // Chat tab index | |
| 428 | + | input->SetFocus(); | |
| 429 | + | } | |
| 430 | + | evt.Skip(); | |
| 431 | + | } | |
| 432 | + | ||
| 433 | + | void LiveLogTab::OnGridCellLeftClick(wxGridEvent& evt) { | |
| 434 | + | int row = evt.GetRow(); | |
| 435 | + | int col = evt.GetCol(); | |
| 436 | + | ||
| 437 | + | // Column 0 is the color column | |
| 438 | + | if (col == 0) { | |
| 439 | + | // Get the current color from the background | |
| 440 | + | wxColor currentColor = user_list->GetCellBackgroundColour(row, 0); | |
| 441 | + | ||
| 442 | + | // Show color picker dialog | |
| 443 | + | wxColourData colorData; | |
| 444 | + | colorData.SetColour(currentColor); | |
| 445 | + | ||
| 446 | + | wxColourDialog dialog(this, &colorData); | |
| 447 | + | dialog.SetTitle("Choose Color"); | |
| 448 | + | ||
| 449 | + | if (dialog.ShowModal() == wxID_OK) { | |
| 450 | + | wxColour newColor = dialog.GetColourData().GetColour(); | |
| 451 | + | ChangeUserColor(row, newColor); | |
| 452 | + | } | |
| 453 | + | } | |
| 454 | + | ||
| 455 | + | evt.Skip(); | |
| 456 | + | } | |
| 457 | + | ||
| 458 | + | void LiveLogTab::ChangeUserColor(int row, const wxColor& color) { | |
| 459 | + | // Make sure we have a valid client selected | |
| 460 | + | if (row < 0 || row >= user_list->GetNumberRows()) { | |
| 461 | + | return; | |
| 462 | + | } | |
| 463 | + | ||
| 464 | + | // Update the color in the UI grid | |
| 465 | + | user_list->SetCellBackgroundColour(row, 0, color); | |
| 466 | + | user_list->Refresh(); | |
| 467 | + | ||
| 468 | + | // Get the client ID from the row | |
| 469 | + | wxString clientIdStr = user_list->GetCellValue(row, 1); | |
| 470 | + | long displayId = 0; | |
| 471 | + | uint32_t clientId = 0; | |
| 472 | + | ||
| 473 | + | if (clientIdStr.ToLong(&displayId)) { | |
| 474 | + | // Convert display ID to actual client ID | |
| 475 | + | clientId = (displayId - 1) << 1; | |
| 476 | + | } | |
| 477 | + | ||
| 478 | + | // If we're the host, directly update and broadcast | |
| 479 | + | auto server = dynamic_cast<LiveServer*>(socket); | |
| 480 | + | if (server) { | |
| 481 | + | if (row == 0) { | |
| 482 | + | // Update the host's color | |
| 483 | + | server->setUsedColor(color); | |
| 484 | + | } else { | |
| 485 | + | // Update a client's color by broadcasting | |
| 486 | + | server->broadcastColorChange(clientId, color); | |
| 487 | + | ||
| 488 | + | // Also update the local representation | |
| 489 | + | for (const auto& pair : clients) { | |
| 490 | + | LivePeer* peer = pair.second; | |
| 491 | + | if (peer && peer->getClientId() == clientId) { | |
| 492 | + | peer->setUsedColor(color); | |
| 493 | + | break; | |
| 494 | + | } | |
| 495 | + | } | |
| 496 | + | } | |
| 497 | + | } else if (socket && socket->IsClient()) { | |
| 498 | + | // We're a client - send a color update request to the server | |
| 499 | + | LiveClient* client = static_cast<LiveClient*>(socket); | |
| 500 | + | ||
| 501 | + | // Row 0 is always the host with client ID 0 | |
| 502 | + | uint32_t targetClientId = (row == 0) ? 0 : clientId; | |
| 503 | + | client->sendColorUpdate(targetClientId, color); | |
| 504 | + | } | |
| 505 | + | } |
| @@ -0,0 +1,88 @@ | |||
| 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 | + | #ifndef _RME_LIVE_TAB_H_ | |
| 19 | + | #define _RME_LIVE_TAB_H_ | |
| 20 | + | ||
| 21 | + | #include "main.h" | |
| 22 | + | ||
| 23 | + | #include "editor_tabs.h" | |
| 24 | + | #include "application.h" | |
| 25 | + | #include "live_server.h" | |
| 26 | + | ||
| 27 | + | class wxGrid; | |
| 28 | + | class wxNotebook; | |
| 29 | + | ||
| 30 | + | class MapTabbook; | |
| 31 | + | class LiveSocket; | |
| 32 | + | class LiveServer; | |
| 33 | + | ||
| 34 | + | class LiveLogTab : public EditorTab, public wxPanel { | |
| 35 | + | public: | |
| 36 | + | LiveLogTab(MapTabbook* aui, LiveSocket* socket); | |
| 37 | + | ~LiveLogTab(); | |
| 38 | + | ||
| 39 | + | void Message(const wxString& str); | |
| 40 | + | void Chat(const wxString& speaker, const wxString& str); | |
| 41 | + | ||
| 42 | + | virtual wxWindow* GetWindow() const { | |
| 43 | + | return (wxPanel*)this; | |
| 44 | + | } | |
| 45 | + | virtual wxString GetTitle() const; | |
| 46 | + | ||
| 47 | + | bool IsConnected() const { | |
| 48 | + | return socket != nullptr; | |
| 49 | + | } | |
| 50 | + | void Disconnect(); | |
| 51 | + | ||
| 52 | + | LiveSocket* GetSocket() { | |
| 53 | + | return socket; | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | void UpdateClientList(const std::unordered_map<uint32_t, LivePeer*>& updatedClients); | |
| 57 | + | ||
| 58 | + | void OnSelectChatbox(wxFocusEvent& evt); | |
| 59 | + | void OnDeselectChatbox(wxFocusEvent& evt); | |
| 60 | + | ||
| 61 | + | void OnChat(wxCommandEvent& evt); | |
| 62 | + | void OnKeyDown(wxKeyEvent& evt); | |
| 63 | + | void OnResizeChat(wxSizeEvent& evt); | |
| 64 | + | void OnResizeClientList(wxSizeEvent& evt); | |
| 65 | + | void OnCopySelectedLogText(wxCommandEvent& evt); | |
| 66 | + | void OnLogRightClick(wxMouseEvent& evt); | |
| 67 | + | void OnPageChanged(wxBookCtrlEvent& evt); | |
| 68 | + | void OnGridCellLeftClick(wxGridEvent& evt); | |
| 69 | + | void ChangeUserColor(int row, const wxColor& color); | |
| 70 | + | ||
| 71 | + | // Method to get the log file path | |
| 72 | + | wxString GetLogFilePath(const wxString& logType); | |
| 73 | + | ||
| 74 | + | protected: | |
| 75 | + | MapTabbook* aui; | |
| 76 | + | LiveSocket* socket; | |
| 77 | + | wxNotebook* notebook; | |
| 78 | + | wxTextCtrl* debug_log; // For debug messages | |
| 79 | + | wxTextCtrl* chat_log; // For player chat | |
| 80 | + | wxTextCtrl* input; | |
| 81 | + | wxGrid* user_list; | |
| 82 | + | ||
| 83 | + | std::unordered_map<uint32_t, LivePeer*> clients; | |
| 84 | + | ||
| 85 | + | DECLARE_EVENT_TABLE(); | |
| 86 | + | }; | |
| 87 | + | ||
| 88 | + | #endif |
| @@ -0,0 +1,37 @@ | |||
| 1 | + | ||
| 2 | + | #ifndef RME_LOD_MANAGER_H | |
| 3 | + | #define RME_LOD_MANAGER_H | |
| 4 | + | ||
| 5 | + | class LODManager { | |
| 6 | + | public: | |
| 7 | + | enum LODLevel { | |
| 8 | + | FULL_DETAIL = 0, // Zoom 1-3 | |
| 9 | + | MEDIUM_DETAIL = 1, // Zoom 4-7 | |
| 10 | + | GROUND_ONLY = 2 // Zoom 8+ | |
| 11 | + | }; | |
| 12 | + | ||
| 13 | + | LODManager() : current_level(FULL_DETAIL) {} | |
| 14 | + | ||
| 15 | + | LODLevel getLevelForZoom(double zoom) { | |
| 16 | + | if(zoom >= 8.0) return GROUND_ONLY; | |
| 17 | + | if(zoom >= 4.0) return MEDIUM_DETAIL; | |
| 18 | + | return FULL_DETAIL; | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | void updateRenderSettings(double zoom) { | |
| 22 | + | current_level = getLevelForZoom(zoom); | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | bool isGroundOnly() const { | |
| 26 | + | return current_level == GROUND_ONLY; | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | bool isMediumDetail() const { | |
| 30 | + | return current_level == MEDIUM_DETAIL; | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | private: | |
| 34 | + | LODLevel current_level; | |
| 35 | + | }; | |
| 36 | + | ||
| 37 | + | #endif // RME_LOD_MANAGER_H |
| @@ -0,0 +1,144 @@ | |||
| 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 | + | #ifndef RME_MAIN_H_ | |
| 19 | + | #define RME_MAIN_H_ | |
| 20 | + | ||
| 21 | + | #ifdef _WIN32 | |
| 22 | + | #define WIN32_LEAN_AND_MEAN | |
| 23 | + | #ifdef _WIN32_WINNT | |
| 24 | + | #undef _WIN32_WINNT | |
| 25 | + | #endif | |
| 26 | + | #define _WIN32_WINNT 0x0501 | |
| 27 | + | #endif | |
| 28 | + | ||
| 29 | + | #ifdef DEBUG_MEM | |
| 30 | + | ||
| 31 | + | #define _CRTDBG_MAP_ALLOC | |
| 32 | + | ||
| 33 | + | #include <stdlib.h> | |
| 34 | + | #include <crtdbg.h> | |
| 35 | + | ||
| 36 | + | #pragma warning(disable : 4291) | |
| 37 | + | _Ret_bytecap_(_Size) inline void* __CRTDECL operator new(size_t _Size, const char* file, int line) { | |
| 38 | + | return ::operator new(_Size, _NORMAL_BLOCK, file, line); | |
| 39 | + | } | |
| 40 | + | _Ret_bytecap_(_Size) inline void* __CRTDECL operator new[](size_t _Size, const char* file, int line) { | |
| 41 | + | return ::operator new[](_Size, _NORMAL_BLOCK, file, line); | |
| 42 | + | } | |
| 43 | + | #define newd new (__FILE__, __LINE__) | |
| 44 | + | ||
| 45 | + | #else | |
| 46 | + | ||
| 47 | + | #define newd new | |
| 48 | + | ||
| 49 | + | #endif | |
| 50 | + | ||
| 51 | + | // Boost libraries | |
| 52 | + | #include <boost/utility.hpp> | |
| 53 | + | #include <boost/range/adaptor/reversed.hpp> | |
| 54 | + | #include <boost/asio.hpp> | |
| 55 | + | ||
| 56 | + | #include <wx/defs.h> | |
| 57 | + | #include "definitions.h" | |
| 58 | + | ||
| 59 | + | #include <wx/wxprec.h> | |
| 60 | + | #ifndef WX_PRECOMP | |
| 61 | + | #include <wx/wx.h> | |
| 62 | + | #endif | |
| 63 | + | #include <wx/thread.h> | |
| 64 | + | #include <wx/utils.h> | |
| 65 | + | #include <wx/progdlg.h> | |
| 66 | + | #include <wx/glcanvas.h> | |
| 67 | + | #include <wx/debugrpt.h> | |
| 68 | + | #include <wx/minifram.h> | |
| 69 | + | #include <wx/gbsizer.h> | |
| 70 | + | #include <wx/choicebk.h> | |
| 71 | + | #include <wx/tglbtn.h> | |
| 72 | + | #include <wx/dcbuffer.h> | |
| 73 | + | #include <wx/aui/aui.h> | |
| 74 | + | #include <wx/cmdline.h> | |
| 75 | + | #include <wx/filename.h> | |
| 76 | + | #include <wx/filepicker.h> | |
| 77 | + | #include <wx/arrstr.h> | |
| 78 | + | #include <wx/slider.h> | |
| 79 | + | #include <wx/spinctrl.h> | |
| 80 | + | #include <wx/wxhtml.h> | |
| 81 | + | #include <wx/vlbox.h> | |
| 82 | + | #include <wx/stdpaths.h> | |
| 83 | + | #include <wx/url.h> | |
| 84 | + | #include <wx/ipc.h> | |
| 85 | + | #include <wx/grid.h> | |
| 86 | + | #include <wx/clipbrd.h> | |
| 87 | + | ||
| 88 | + | // PugiXML | |
| 89 | + | #include "ext/pugixml.hpp" | |
| 90 | + | ||
| 91 | + | // Libarchive, for OTGZ | |
| 92 | + | #ifdef OTGZ_SUPPORT | |
| 93 | + | #include <archive.h> | |
| 94 | + | #include <archive_entry.h> | |
| 95 | + | #endif | |
| 96 | + | ||
| 97 | + | // This has annoyed me one time too many | |
| 98 | + | #define wxANY_ID (wxID_ANY) | |
| 99 | + | ||
| 100 | + | #include <assert.h> | |
| 101 | + | #define _MSG(msg) !bool(msg) | |
| 102 | + | #ifdef __DEBUG__ | |
| 103 | + | #define ASSERT assert | |
| 104 | + | #else | |
| 105 | + | #define ASSERT(...) | |
| 106 | + | #endif | |
| 107 | + | ||
| 108 | + | // The complete STL ?, well, almost ;) | |
| 109 | + | #include <math.h> | |
| 110 | + | #include <list> | |
| 111 | + | #include <vector> | |
| 112 | + | #include <map> | |
| 113 | + | #include <string> | |
| 114 | + | #include <istream> | |
| 115 | + | #include <ostream> | |
| 116 | + | #include <sstream> | |
| 117 | + | #include <iostream> | |
| 118 | + | #include <iomanip> | |
| 119 | + | #include <algorithm> | |
| 120 | + | #include <limits> | |
| 121 | + | #include <set> | |
| 122 | + | #include <queue> | |
| 123 | + | #include <stdexcept> | |
| 124 | + | #include <time.h> | |
| 125 | + | #include <fstream> | |
| 126 | + | ||
| 127 | + | typedef std::vector<std::string> StringVector; | |
| 128 | + | typedef wxFileName FileName; | |
| 129 | + | ||
| 130 | + | #include "json.h" | |
| 131 | + | ||
| 132 | + | #include "con_vector.h" | |
| 133 | + | #include "common.h" | |
| 134 | + | #include "threads.h" | |
| 135 | + | ||
| 136 | + | #include "rme_forward_declarations.h" | |
| 137 | + | ||
| 138 | + | #if wxCHECK_VERSION(3, 1, 0) | |
| 139 | + | #define FROM_DIP(widget, size) widget->FromDIP(size) | |
| 140 | + | #else | |
| 141 | + | #define FROM_DIP(widget, size) size | |
| 142 | + | #endif | |
| 143 | + | ||
| 144 | + | #endif |