RunicVTT Open Source Virtual Tabletop for TTRPG using P2P
Loading...
Searching...
No Matches
NetworkManager.cpp
Go to the documentation of this file.
1#include "NetworkManager.h"
2#include "NetworkUtilities.h"
3#include "SignalingServer.h"
4#include "SignalingClient.h"
5#include "BoardManager.h"
6#include "GameTableManager.h"
7#include "Message.h"
8#include "UPnPManager.h"
9#include "Serializer.h"
10#include "DebugConsole.h"
11#include "Logger.h"
12#include <unordered_set>
13
14NetworkManager::NetworkManager(flecs::world ecs, std::shared_ptr<IdentityManager> identity_manager) :
15 ecs(ecs), identity_manager(identity_manager), peer_role(Role::NONE)
16{
19 rtc::InitLogger(rtc::LogLevel::Verbose);
21}
22
23void NetworkManager::setup(std::weak_ptr<BoardManager> board_manager, std::weak_ptr<GameTableManager> gametable_manager)
24{
25 this->board_manager = board_manager;
26 this->gametable_manager = gametable_manager;
27
28 signalingClient = std::make_shared<SignalingClient>(weak_from_this());
29 signalingServer = std::make_shared<SignalingServer>(weak_from_this());
30
32 [this]() -> std::string
33 {
35 Logger::instance().log("localtunnel", Logger::Level::Debug, "Starting with runic-" + ip + ":" + std::to_string(port));
36 return NetworkUtilities::startLocalTunnel("runic-" + ip, port);
37 },
38 [this]()
39 {
41 Logger::instance().log("localtunnel", Logger::Level::Info, "Stop requested");
42 });
43 DebugConsole::setIdentityLogger([this]() -> std::string
44 { return debugIdentitySnapshot(); });
45}
46
52/*
53void NetworkManager::startServer(ConnectionType mode, unsigned short port, bool tryUpnp)
54{
55 peer_role = Role::GAMEMASTER;
56
57 // Ensure server exists (your setup() likely already does this)
58 if (!signalingServer)
59 {
60 signalingServer = std::make_shared<SignalingServer>(shared_from_this());
61 }
62 if (!signalingClient)
63 {
64 signalingClient = std::make_shared<SignalingClient>(shared_from_this());
65 }
66
67 // Start WS server
68 signalingServer->start(port);
69 setPort(port);
70
71 const std::string localIp = getLocalIPAddress();
72 const std::string externalIp = getExternalIPAddress();
73
74 switch (mode)
75 {
76 case ConnectionType::LOCALTUNNEL:
77 {
78 const auto ltUrl = NetworkUtilities::startLocalTunnel("runic-" + localIp, static_cast<int>(port));
79 if (ltUrl.empty())
80 {
81 break;
82 }
83 signalingClient->connectUrl(ltUrl);
84
85 break;
86 }
87 case ConnectionType::LOCAL:
88 {
89 signalingClient->connect("127.0.0.1", port);
90 break;
91 }
92 case ConnectionType::EXTERNAL:
93 {
94 if (tryUpnp)
95 {
96 try
97 {
98 if (!UPnPManager::addPortMapping(localIp, port, port, "TCP", "RunicVTT"))
99 {
100 bool open = true;
101 pushStatusToast("Signalling Server UPnP Failled - Check Moldem configuration!!", ImGuiToaster::Level::Error, 4);
102 }
103 }
104 catch (...)
105 {
106 pushStatusToast("Signalling Server UPnP Failled - Check Moldem configuration!!", ImGuiToaster::Level::Error, 4);
107 }
108 }
109 signalingClient->connect("127.0.0.1", port);
110 break;
111 }
112 case ConnectionType::CUSTOM:
113 {
114 //signalingClient->connect(getCustomHost(), port);
115 signalingClient->connect("127.0.0.1", port);
116 break;
117 }
118 }
119 pushStatusToast("Signalling Server Started!!", ImGuiToaster::Level::Good, 4);
120 //startRawDrainWorker();
121}
122*/
123void NetworkManager::startServer(ConnectionType mode, unsigned short port, bool tryUpnp)
124{
126
127 if (!signalingServer)
128 signalingServer = std::make_shared<SignalingServer>(shared_from_this());
129 if (!signalingClient)
130 signalingClient = std::make_shared<SignalingClient>(shared_from_this());
131
132 // Bind WS to all interfaces (overlay/LAN/etc.)
133 signalingServer->start(port);
134 setPort(port);
135
136 const std::string localIp = getLocalIPAddress();
137
138 // Mode-specific side effects (no client connect here)
139 switch (mode)
140 {
142 {
143 // Expose to the world; GM still connects locally below
144 try {
145 (void)NetworkUtilities::startLocalTunnel("runic-" + localIp, static_cast<int>(port));
146 } catch (...) {
147 pushStatusToast("LocalTunnel failed to start.", ImGuiToaster::Level::Error, 4);
148 }
149 break;
150 }
151
153 {
154 if (tryUpnp)
155 {
156 try
157 {
158 (void)UPnPManager::addPortMapping(localIp, port, port, "TCP", "RunicVTT");
159 }
160 catch (...)
161 {
162 pushStatusToast("UPnP port mapping failed — check router config.", ImGuiToaster::Level::Error, 4);
163 }
164 }
165 // Players will connect via public IP:port; GM still connects locally below
166 break;
167 }
168
171 default:
172 // No special side-effect needed; GM still connects locally below
173 break;
174 }
175
176 // GM always connects locally to the server it just started.
177 signalingClient->connect("127.0.0.1", port);
178
179 pushStatusToast("Signalling Server Started!!", ImGuiToaster::Level::Good, 4);
180 // startRawDrainWorker(); // if/when you want it
181}
182
183//RECONNECT
184//bool NetworkManager::reconnectPeer(const std::string& peerId)
185//{
186// // 0) Check we know this peer
187// auto it = peers.find(peerId);
188// if (it == peers.end())
189// return false;
190//
191// Logger::instance().log("main", Logger::Level::Warn, "[NM] Reconnect requested for peer " + peerId);
192//
193// // 1) Quiesce & close the old link (don’t let callbacks call back into NM anymore)
194// if (it->second)
195// {
196// try
197// {
198// //it->second->quiesce(); // (no-op if you don’t have it; just stops callbacks)
199// it->second->close(); // your safe-close (swallow exceptions inside)
200// }
201// catch (...)
202// {
203// // swallow
204// }
205// }
206//
207// // 2) Replace with a fresh link object (same peerId)
208// auto fresh = replaceWithFreshLink_(peerId);
209// if (!fresh)
210// {
211// Logger::instance().log("main", Logger::Level::Error, "[NM] Reconnect failed: could not create PeerLink");
212// return false;
213// }
214// peers[peerId] = fresh; //maybe use swap?
215//
216// // 3) If you are the side that normally *offers* (e.g. GM), create DCs now then create & send offer
217// if (peer_role == Role::GAMEMASTER)
218// {
219// createChannelsIfOfferer_(fresh); // create game/chat/notes DCs; attach handlers
220// createOfferAndSend_(peerId, fresh); // hook to your signaling send
221// }
222// else
223// {
224// // Player side: wait for offer from the server/GM.
225// // Make sure PeerConnection has onDataChannel callback attached inside PeerLink setup.
226// Logger::instance().log("main", Logger::Level::Info, "[NM] Waiting for remote offer from " + peerId);
227// }
228//
229// return true;
230//}
231
232// --- helpers ---------------------------------------------------------
233
234//std::shared_ptr<PeerLink> NetworkManager::replaceWithFreshLink_(const std::string& peerId)
235//{
236// // Build a new PeerLink (this constructs a new rtc::PeerConnection and wires PC-level callbacks)
237// auto link = std::make_shared<PeerLink>(peerId, weak_from_this());
238//
239// // If your PeerLink exposes a “pc” and “onDataChannel” wiring internally, you’re done.
240// // Otherwise ensure PeerLink sets pc->onDataChannel to call link->attachChannelHandlers().
241//
242// return link;
243//}
244//
245//void NetworkManager::createChannelsIfOfferer_(const std::shared_ptr<PeerLink>& link)
246//{
247// // Offerer creates DCs *before* creating the SDP offer so they’re negotiated and will open automatically.
248// // You already have attachChannelHandlers on PeerLink – reuse it.
249//
250// auto pc = link->getPeerConnection(); // whatever getter you have
251// if (!pc)
252// return;
253//
254// // Create “game” channel
255// auto dcGame = pc->createDataChannel(msg::dc::name::Game);
256// link->attachChannelHandlers(dcGame, msg::dc::name::Game);
257//
258// // Create “chat” channel
259// auto dcChat = pc->createDataChannel(msg::dc::name::Chat);
260// link->attachChannelHandlers(dcChat, msg::dc::name::Chat);
261//
262// // Create “notes” channel (if you use it)
263// auto dcNotes = pc->createDataChannel(msg::dc::name::Notes);
264// link->attachChannelHandlers(dcNotes, msg::dc::name::Notes);
265//}
266//
267//void NetworkManager::createOfferAndSend_(const std::string& peerId, const std::shared_ptr<PeerLink>& link)
268//{
269// auto pc = link->getPeerConnection();
270// if (!pc)
271// return;
272//
273// // Hook local description/candidates → send via your signaling client
274// //pc->onLocalDescription([this, peerId](const rtc::Description& desc)
275// // {
276// // Logger::instance().log("main", Logger::Level::Debug, "[NM] Sending offer to " + peerId);
277// // auto offer = msg::makeOffer(myClientId_, peerId, desc, myUsername_);
278// // if (signalingClient)
279// // signalingClient->send(offer.dump()); // adapt to your API
280// // });
281//
282// //pc->onLocalCandidate([this, peerId](const rtc::Candidate& cand)
283// // { auto candidate = msg::makeCandidate(myClientId_,peerId,);
284// // if (signalingClient)
285// // signalingClient->send(peerId, cand);
286// // });
287//
288// // Trigger offer creation
289// try
290// {
291// pc->setLocalDescription(); // libdatachannel: generates an offer and fires onLocalDescription
292// }
293// catch (const std::exception& e)
294// {
295// Logger::instance().log("main", Logger::Level::Error,
296// std::string("[NM] setLocalDescription(offer) failed: ") + e.what());
297// }
298//}
299
300// Keep your old method (back-compat). Default it to LocalTunnel behavior.
301void NetworkManager::startServer(std::string internal_ip_address, unsigned short port)
302{
303 // Old default used LocalTunnel; keep that behavior
304 startServer(ConnectionType::LOCALTUNNEL, port, /*tryUpnp=*/false);
305}
306
308{
309 if (signalingServer.get() != nullptr)
310 {
312 signalingServer->stop();
313 }
314 //stopRawDrainWorker();
316}
317
319{
320 for (auto& [peerId, link] : peers)
321 {
322 if (link && link->isConnected())
323 return true;
324 }
325 return false;
326}
328{
329 return signalingServer && signalingServer->isRunning(); // if you expose isRunning()
330}
332{
333 return !peers.empty();
334}
336{
337 int n = 0;
338 for (auto& [id, link] : peers)
339 if (link && link->isConnected())
340 ++n;
341 return n;
342}
343
344bool NetworkManager::connectToPeer(const std::string& connectionString)
345{
346 std::string server;
347 unsigned short port = 0;
348 std::string password;
349 parseConnectionString(connectionString, server, port, password);
350
351 if (!password.empty())
352 setNetworkPassword(password.c_str());
354
355 if (hasUrlScheme(server))
356 {
357 return signalingClient->connectUrl(server); // NEW method (see below)
358 }
359 return signalingClient->connect(server, port); // your old method
360}
361
362//INFORMATION AND PARSE OPERATIONS
364{
365 if (local_ip_address == "")
366 {
368 local_ip_address = local_ip;
369 return local_ip;
370 }
371 return local_ip_address;
372}
373
375{
376 if (external_ip_address == "")
377 {
378 //auto external_ip = UPnPManager::getExternalIPv4Address();
379 std::string external_ip = NetworkUtilities::httpGet(L"loca.lt", L"/mytunnelpassword");
380 external_ip_address = external_ip;
381 return external_ip;
382 }
383 return external_ip_address;
384}
385
387{
388 const auto port = getPort();
389 const auto pwd = getNetworkPassword();
390
391 if (type == ConnectionType::LOCAL)
392 { // LAN (192.168.x.y)
393 const auto ip = getLocalIPAddress();
394 return "runic:" + ip + ":" + std::to_string(port) + "?" + pwd;
395 }
396 else if (type == ConnectionType::EXTERNAL)
397 { // public IP
398 const auto ip = getExternalIPAddress();
399 return "runic:" + ip + ":" + std::to_string(port) + "?" + pwd;
400 }
401 else if (type == ConnectionType::LOCALTUNNEL)
402 {
403 const auto url = getLocalTunnelURL(); // e.g., https://sub.loca.lt
404 return url + "?" + pwd; // no port needed (LT handles it)
405 }
406 else if (type == ConnectionType::CUSTOM)
407 { // public IP
408 const auto customHost = getCustomHost();
409 return "runic:" + customHost + ":" + std::to_string(port) + "?" + pwd;
410 }
411 return {};
412}
413
418
419void NetworkManager::parseConnectionString(std::string connection_string, std::string& server, unsigned short& port, std::string& password)
420{
421 server.clear();
422 password.clear();
423 port = 0;
424
425 const std::string prefix = "runic:";
426 if (connection_string.rfind(prefix, 0) == 0)
427 connection_string.erase(0, prefix.size());
428
429 // split pass
430 std::string left = connection_string;
431 if (auto q = connection_string.find('?'); q != std::string::npos)
432 {
433 left = connection_string.substr(0, q);
434 password = connection_string.substr(q + 1);
435 }
436
437 if (hasUrlScheme(left))
438 { // URL path
439 server = left;
440 return;
441 }
442
443 // host[:port]
444 if (auto colon = left.find(':'); colon != std::string::npos)
445 {
446 server = left.substr(0, colon);
447 try
448 {
449 port = static_cast<unsigned short>(std::stoi(left.substr(colon + 1)));
450 }
451 catch (...)
452 {
453 port = 0;
454 }
455 }
456 else
457 {
458 server = left; // no port
459 }
460}
461
462void NetworkManager::setNetworkPassword(const char* password)
463{
464 strncpy(network_password, password, sizeof(network_password) - 1);
465 network_password[sizeof(network_password) - 1] = '\0';
466}
467
468//SINGLE POPUP RENDER FOR PORT FOWARD HELPER (MOVE TO WIKI)
470{
471 if (*p_open)
472 {
473 ImGui::OpenPopup("Port Forwarding Failed");
474 }
475
476 if (ImGui::BeginPopupModal("Port Forwarding Failed", p_open, ImGuiWindowFlags_AlwaysAutoResize))
477 {
478 ImGui::Text("Automatic port forwarding failed. This is likely because your router does not\n"
479 "support UPnP or it is disabled. You can still host by choosing one of the\n"
480 "following three options:");
481
482 ImGui::Separator();
483
484 if (ImGui::BeginTabBar("PortOptions"))
485 {
486
487 // Tab 1: Enable UPnP
488 if (ImGui::BeginTabItem("Enable UPnP"))
489 {
490 ImGui::Text("This is the easiest solution and will allow automatic port forwarding to work.");
491 ImGui::Spacing();
492 ImGui::BulletText("Access your router's administration page. This is usually done by\n"
493 "typing its IP address (e.g., 192.168.1.1) in your browser.");
494 ImGui::BulletText("Log in with your router's credentials.");
495 ImGui::BulletText("Look for a setting named 'UPnP' or 'Universal Plug and Play' and enable it.");
496 ImGui::BulletText("Save the settings and restart your router if necessary.");
497 ImGui::BulletText("Try hosting again. The application should now automatically forward the port.");
498 ImGui::EndTabItem();
499 }
500
501 // Tab 2: Manual Port Forwarding
502 if (ImGui::BeginTabItem("Manual Port Forwarding"))
503 {
504 ImGui::Text("This method always works but requires you to configure your router manually.");
505 ImGui::Spacing();
506 ImGui::BulletText("Find your local IP address. Open Command Prompt and type 'ipconfig' to find it\n"
507 "(e.g., 192.168.1.100).");
508 ImGui::BulletText("Access your router's administration page.");
509 ImGui::BulletText("Look for a setting named 'Port Forwarding', 'Virtual Server', or 'NAT'.");
510 ImGui::BulletText("Create a new rule with the following details:");
511 ImGui::Indent();
512 ImGui::BulletText("Protocol: TCP");
513 ImGui::BulletText("Internal Port: [Your application's port, e.g., 8080]");
514 ImGui::BulletText("External Port: [The same port]");
515 ImGui::BulletText("Internal IP: [Your local IP from step 1]");
516 ImGui::Unindent();
517 ImGui::BulletText("Save the rule and try hosting again.");
518 ImGui::EndTabItem();
519 }
520
521 // Tab 3: Use a VPN (Hamachi)
522 if (ImGui::BeginTabItem("Use a VPN (Hamachi)"))
523 {
524 ImGui::Text("A VPN creates a virtual local network, bypassing the need for port forwarding.");
525 ImGui::Spacing();
526 ImGui::BulletText("Download and install a VPN client like Hamachi.");
527 ImGui::BulletText("Create a new network within the VPN client.");
528 ImGui::BulletText("Share the network name and password with your friends.");
529 ImGui::BulletText("Instruct your friends to join your network.");
530 ImGui::BulletText("On the host screen, you should be able to see your VPN IP address.\n"
531 "Use this IP to host your session.");
532 ImGui::EndTabItem();
533 }
534
535 ImGui::EndTabBar();
536 }
537
538 if (ImGui::Button("Close"))
539 {
540 *p_open = false;
541 ImGui::CloseCurrentPopup();
542 }
543
544 ImGui::EndPopup();
545 }
546}
547
548//PEER DISCONNECT OPERATIONS------------------------------------------------------------------
549
550// Optional: remove peers that are no longer usable (Closed/Failed or nullptr)
552{
553 std::vector<std::string> toErase;
554 std::vector<std::shared_ptr<PeerLink>> toClose;
555 toErase.reserve(peers.size());
556 toClose.reserve(peers.size());
557
558 // 1) Decide who to remove; DO NOT mutate the map yet.
559 for (auto const& [pid, link] : peers)
560 {
561 const bool shouldRemove = (!link) || link->isClosedOrFailed();
562 if (shouldRemove)
563 {
564 toErase.push_back(pid);
565 if (link)
566 toClose.emplace_back(link);
567 }
568 }
569
570 // 2) Erase all selected entries from the map (no destructors called yet for links we retained).
571 for (auto const& pid : toErase)
572 peers.erase(pid);
573
574 // 3) Close outside the map to avoid re-entrancy/races.
575 for (auto& link : toClose)
576 {
577 try
578 {
579 link->close(); // should be idempotent and NOT call back into NM
580 }
581 catch (const std::exception& e)
582 {
583 Logger::instance().log("main", Logger::Level::Error, std::string("[removeDisconnectedPeers] ") + e.what());
584 //pushStatusToast("Peer close error", ImGuiToaster::Level::Error, 4.0f);
585 }
586 catch (...)
587 {
588 Logger::instance().log("main", Logger::Level::Error, "[removeDisconnectedPeers] unknown exception");
589 //pushStatusToast("Peer close error (unknown)", ImGuiToaster::Level::Error, 4.0f);
590 }
591 }
592
593 return toErase.size();
594}
595
596bool NetworkManager::removePeer(std::string peerId)
597{
598 // 1) Find, move the link out, erase entry first.
599 std::shared_ptr<PeerLink> link;
600 if (auto it = peers.find(peerId); it != peers.end())
601 {
602 link = std::move(it->second);
603 peers.erase(it);
604 }
605 else
606 {
607 return false;
608 }
609
610 // 2) Close outside the map.
611 if (link)
612 {
613 try
614 {
615 link->close(); // safe: detach callbacks first, idempotent
616 }
617 catch (const std::exception& e)
618 {
619 Logger::instance().log("main", Logger::Level::Error, std::string("[removePeer] ") + e.what());
620 //pushStatusToast("Peer close error", ImGuiToaster::Level::Error, 4.0f);
621 }
622 catch (...)
623 {
624 Logger::instance().log("main", Logger::Level::Error, "[removePeer] unknown exception");
625 //pushStatusToast("Peer close error (unknown)", ImGuiToaster::Level::Error, 4.0f);
626 }
627 }
628 return true;
629}
630
631// PEER INSERT METHOD AND LOCAL CALLBACKS --------------------------------------------------------------------
632std::shared_ptr<PeerLink> NetworkManager::ensurePeerLink(const std::string& peerId)
633{
634 if (auto it = peers.find(peerId); it != peers.end())
635 return it->second;
636 auto link = std::make_shared<PeerLink>(peerId, weak_from_this());
637 peers.emplace(peerId, link);
638 return link;
639}
640
641// NetworkManager.cpp (relevant part)
642
643void NetworkManager::onPeerLocalDescription(const std::string& peerId, const rtc::Description& desc)
644{
645 nlohmann::json j;
646 const std::string sdp = std::string(desc);
647
648 // Always pull identity from IdentityManager
649 const std::string username = getMyUsername(); // forwards to IdentityManager
650 const std::string uniqueId = getMyUniqueId(); // forwards to IdentityManager
651
652 if (desc.type() == rtc::Description::Type::Offer)
653 {
654 // server will overwrite "from", so "" is fine
655 j = msg::makeOffer(/*from*/ "", /*to*/ peerId, sdp, username, uniqueId);
656 }
657 else if (desc.type() == rtc::Description::Type::Answer)
658 {
659 j = msg::makeAnswer(/*from*/ "", /*to*/ peerId, sdp, username, uniqueId);
660 }
661 else
662 {
663 return;
664 }
665
666 if (signalingClient)
667 signalingClient->send(j.dump());
668}
669
670void NetworkManager::onPeerLocalCandidate(const std::string& peerId, const rtc::Candidate& cand)
671{
672 auto j = msg::makeCandidate("", peerId, cand.candidate());
673 if (signalingClient)
674 signalingClient->send(j.dump());
675}
676
677//NECESSARY NETWORK OPERATIONS -------------------------------------------------------------------------------------------
679{
680 const uint64_t now = nowMs();
681 const uint64_t kGraceMs = 10'000; // disconnect grace period
682
683 static std::unordered_map<std::string, uint64_t> firstDiscPeerAt;
684 static std::unordered_map<std::string, uint64_t> firstDiscWsAt;
685
686 // ---- PeerLink cleanup ----
687 for (auto it = peers.begin(); it != peers.end();)
688 {
689 const std::string& pid = it->first;
690 auto& link = it->second;
691
692 const bool connected = (link && link->isConnected());
693 if (connected)
694 {
695 firstDiscPeerAt.erase(pid);
696 ++it;
697 continue;
698 }
699
700 uint64_t& t0 = firstDiscPeerAt[pid];
701 if (t0 == 0)
702 t0 = now;
703
704 if (now - t0 >= kGraceMs)
705 {
706 try
707 {
708 if (link)
709 link->close();
710 }
711 catch (...)
712 {
713 }
714 it = peers.erase(it);
715 firstDiscPeerAt.erase(pid);
716 }
717 else
718 {
719 ++it;
720 }
721 }
722
723 // ---- WebSocket cleanup (server side) ----
724 auto wsClients = signalingServer->authClients();
725 for (auto it = wsClients.begin(); it != wsClients.end();)
726 {
727 const std::string& cid = it->first;
728 auto& ws = it->second;
729
730 const bool open = (ws && ws->isOpen()); // if your API is different, change this check
731 if (open)
732 {
733 firstDiscWsAt.erase(cid);
734 ++it;
735 continue;
736 }
737
738 uint64_t& t0 = firstDiscWsAt[cid];
739 if (t0 == 0)
740 t0 = now;
741
742 if (now - t0 >= kGraceMs)
743 {
744 try
745 {
747 }
748 catch (...)
749 {
750 }
751 it = wsClients.erase(it);
752 firstDiscWsAt.erase(cid);
753 }
754 else
755 {
756 ++it;
757 }
758 }
759}
760
761void NetworkManager::upsertPeerIdentityWithUnique(const std::string& peerId,
762 const std::string& uniqueId,
763 const std::string& username)
764{
765 identity_manager->bindPeer(/*peerId=*/peerId,
766 /*uniqueId=*/uniqueId,
767 /*username=*/username);
768
769 if (auto it = peers.find(peerId); it != peers.end() && it->second)
770 it->second->setDisplayName(username);
771}
772
773std::string NetworkManager::displayNameForPeer(const std::string& peerId) const
774{
775 if (!identity_manager)
776 return peerId;
777 auto u = identity_manager->usernameForPeer(peerId);
778 return u.value_or(peerId);
779}
780
782{
783 // Move out to avoid mutation during destruction
784 std::unordered_map<std::string, std::shared_ptr<PeerLink>> moved = std::move(peers);
785 peers.clear();
786
787 for (auto& [pid, link] : moved)
788 {
789 if (!link)
790 continue;
791 try
792 {
793 link->close();
794 }
795 catch (const std::exception& e)
796 {
797 Logger::instance().log("main", Logger::Level::Error, std::string("[disconnectFromPeers] ") + e.what());
798 pushStatusToast("Peer disconnect error", ImGuiToaster::Level::Error, 5.0f);
799 }
800 catch (...)
801 {
802 Logger::instance().log("main", Logger::Level::Error, "[disconnectFromPeers] unknown");
803 pushStatusToast("Peer disconnect error (unknown)", ImGuiToaster::Level::Error, 5.0f);
804 }
805 }
806 moved.clear();
807
808 if (signalingClient)
809 {
810 signalingClient->close();
811 }
812
814 return true;
815}
816
818{
819 if (signalingServer)
820 {
821 signalingServer->broadcastShutdown();
822 }
823
824 std::unordered_map<std::string, std::shared_ptr<PeerLink>> moved = std::move(peers);
825 peers.clear();
826
827 for (auto& [pid, link] : moved)
828 {
829 if (!link)
830 continue;
831 try
832 {
833 link->close();
834 }
835 catch (const std::exception& e)
836 {
837 Logger::instance().log("main", Logger::Level::Error, std::string("[disconnectAllPeers] ") + e.what());
838 pushStatusToast("Peer disconnect error", ImGuiToaster::Level::Error, 5.0f);
839 }
840 catch (...)
841 {
842 Logger::instance().log("main", Logger::Level::Error, "[disconnectAllPeers] unknown");
843 pushStatusToast("Peer disconnect error (unknown)", ImGuiToaster::Level::Error, 5.0f);
844 }
845 }
846 moved.clear();
847
848 if (signalingServer)
849 {
850 signalingServer->disconnectAllClients();
851 try
852 {
853 signalingServer->stop();
854 }
855 catch (...)
856 {
857 }
858 }
859
860 if (signalingClient)
861 {
862 signalingClient->close();
863 }
864
866 return true;
867}
868
869void NetworkManager::broadcastPeerDisconnect(const std::string& targetId)
870{
871 if (!signalingClient)
872 return; // GM might be loopbacked as a client; else send via server
873 auto j = msg::makePeerDisconnect(targetId, /*broadcast*/ true);
874 signalingClient->send(j.dump());
875}
876
877std::vector<std::string> NetworkManager::getConnectedPeerIds() const
878{
879 std::vector<std::string> ids;
880 ids.reserve(peers.size());
881 for (auto& [pid, link] : peers)
882 {
883 if (link && link->isConnected())
884 {
885 ids.push_back(pid);
886 }
887 }
888 return ids;
889}
890
891std::vector<std::string> NetworkManager::getConnectedUsernames() const
892{
893 std::vector<std::string> user_names;
894 user_names.reserve(peers.size());
895 for (auto& [pid, link] : peers)
896 {
897 if (link && link->isConnected())
898 {
899 user_names.push_back(displayNameForPeer(pid));
900 }
901 }
902 return user_names;
903}
904
905// NetworkManager.cpp
907{
908 std::ostringstream os;
909 os << "[Identity Snapshot]\n";
910
911 os << "self.peerId=" << (getMyPeerId().empty() ? "(none)" : getMyPeerId()) << "\n";
913 {
914 os << "self.uniqueId=" << identity_manager->myUniqueId()
915 << " username=\"" << identity_manager->myUsername() << "\"\n";
916 }
917 // GM (if tracked)
918 if (!getGMId().empty())
919 {
920 os << "gm.uniqueId=" << getGMId()
921 << " username=\"" << (identity_manager ? identity_manager->usernameForUnique(getGMId()) : std::string{}) << "\"\n";
922 }
923
924 os << "\n[Peers]\n";
925 for (const auto& [peerId, link] : peers)
926 {
927 os << "- peerId=" << peerId;
928
929 std::string uid, uname;
931 {
932 if (auto u = identity_manager->uniqueForPeer(peerId))
933 uid = *u;
934 if (!uid.empty())
935 uname = identity_manager->usernameForUnique(uid);
936 }
937
938 if (!uid.empty())
939 os << " uniqueId=" << uid;
940 if (!uname.empty())
941 os << " username=\"" << uname << "\"";
942
943 if (link)
944 os << " link.display=\"" << link->displayName() << "\"";
945
946 os << "\n";
947 }
948
949 // Optionally: dump address-book keys too
951 {
952 os << "\n[Peer→Unique map]\n";
953 // If you don’t expose an accessor, add one that returns a copy or iterate via a friend.
954 for (const auto& pid : getConnectedPeerIds())
955 {
956 if (auto u = identity_manager->uniqueForPeer(pid))
957 os << " " << pid << " -> " << *u << "\n";
958 }
959 }
960
961 return os.str();
962}
963void NetworkManager::clearDragState(uint64_t markerId)
964{
965 drag_.erase(markerId);
966}
967
968// ----------- GAME MESSAGE BROADCASTERS -------------------------------------------------------------------------------- -----------
969
970void NetworkManager::broadcastGameTable(const flecs::entity& gameTable)
971{ //{check}--USE THIS METHOD
972 auto ids = getConnectedPeerIds();
973 if (!ids.empty())
974 {
975 sendGameTable(gameTable, ids);
976 }
977}
978
979void NetworkManager::broadcastBoard(const flecs::entity& board)
980{ //{check}--USE THIS METHOD
981 auto ids = getConnectedPeerIds();
982 if (!ids.empty())
983 {
984 sendBoard(board, ids);
985 }
986}
987
988void NetworkManager::broadcastMarker(uint64_t boardId, const flecs::entity& marker)
989{ //{check}--USE THIS METHOD
990 auto ids = getConnectedPeerIds();
991 if (!ids.empty())
992 {
993 sendMarker(boardId, marker, ids);
994 }
995}
996
997void NetworkManager::broadcastFog(uint64_t boardId, const flecs::entity& fog)
998{ //{check}--USE THIS METHOD
999 auto ids = getConnectedPeerIds();
1000 if (!ids.empty())
1001 {
1002 sendFog(boardId, fog, ids);
1003 }
1004}
1005
1006// ----------------------------
1007// Broadcast wrappers
1008// ----------------------------
1009
1010void NetworkManager::broadcastMarkerDelete(uint64_t boardId, const flecs::entity& marker)
1011{
1012 auto ids = getConnectedPeerIds();
1013 if (!ids.empty())
1014 sendMarkerDelete(boardId, marker, ids);
1015}
1016
1017void NetworkManager::broadcastFogUpdate(uint64_t boardId, const flecs::entity& fog)
1018{
1019 auto ids = getConnectedPeerIds();
1020 if (!ids.empty())
1021 sendFogUpdate(boardId, fog, ids);
1022}
1023
1024void NetworkManager::broadcastFogDelete(uint64_t boardId, const flecs::entity& fog)
1025{
1026 auto ids = getConnectedPeerIds();
1027 if (!ids.empty())
1028 sendFogDelete(boardId, fog, ids);
1029}
1030
1031// NetworkManager.cpp
1032
1033void NetworkManager::buildUserNameUpdate(std::vector<uint8_t>& out,
1034 uint64_t tableId,
1035 const std::string& userUniqueId,
1036 const std::string& oldUsername,
1037 const std::string& newUsername,
1038 bool reboundFlag) const
1039{
1040 out.clear();
1041 Serializer::serializeUInt64(out, tableId);
1042 Serializer::serializeString(out, userUniqueId); // <— uniqueId
1043 Serializer::serializeString(out, oldUsername);
1044 Serializer::serializeString(out, newUsername);
1045 Serializer::serializeUInt8(out, reboundFlag ? 1 : 0);
1046}
1047
1048void NetworkManager::broadcastUserNameUpdate(const std::vector<uint8_t>& payload)
1049{
1050 for (auto& [pid, link] : peers)
1051 {
1052 if (!link)
1053 continue;
1054 sendUserNameUpdateTo(pid, payload);
1055 }
1056}
1057
1058void NetworkManager::sendUserNameUpdateTo(const std::string& peerId,
1059 const std::vector<uint8_t>& payload)
1060{
1061 auto it = peers.find(peerId);
1062 if (it == peers.end() || !it->second)
1063 return;
1064
1065 std::vector<uint8_t> frame;
1066 frame.reserve(1 + payload.size());
1067 frame.push_back((uint8_t)msg::DCType::UserNameUpdate);
1068 frame.insert(frame.end(), payload.begin(), payload.end());
1069
1070 it->second->sendOn(msg::dc::name::Game, frame);
1071}
1072
1073// NetworkManager.cpp
1075{
1076 const std::string text = j.dump(); // UTF-8 JSON
1077 bool any = false;
1078 for (auto& [pid, link] : peers)
1079 {
1080 if (link->sendOn(msg::dc::name::Chat, std::string_view(text)))
1081 any = true;
1082 }
1083 return any;
1084}
1085bool NetworkManager::sendChatJsonTo(const std::string& peerId, const msg::Json& j)
1086{
1087 auto it = peers.find(peerId);
1088 if (it == peers.end() || !it->second)
1089 return false;
1090 auto& link = it->second;
1091 if (!link->isConnected())
1092 return false;
1093 return link->sendOn(msg::dc::name::Chat, std::string_view(j.dump()));
1094}
1095bool NetworkManager::sendChatJsonTo(const std::set<std::string>& targets, const msg::Json& j)
1096{
1097 const std::string text = j.dump();
1098 bool any = false;
1099 for (auto& pid : targets)
1100 {
1101 auto it = peers.find(pid);
1102 if (it == peers.end() || !it->second)
1103 continue;
1104 auto& link = it->second;
1105 if (!link->isConnected())
1106 continue;
1107 if (link->sendOn(msg::dc::name::Chat, std::string_view(text)))
1108 any = true;
1109 }
1110 return any;
1111}
1112
1113void NetworkManager::decodeRawGameBuffer(const std::string& fromPeer, const std::vector<uint8_t>& b)
1114{
1115 decodingFromPeer_ = fromPeer;
1116 size_t off = 0;
1117 while (off < b.size())
1118 {
1119 if (!ensureRemaining(b, off, 1))
1120 break;
1121
1122 auto type = static_cast<msg::DCType>(b[off]);
1123 off += 1;
1124
1125 Logger::instance().log("localtunnel", Logger::Level::Info, msg::DCtypeString(type) + " Received!!");
1126 switch (type)
1127 {
1130 Logger::instance().log("localtunnel", Logger::Level::Info, "Snapshot_GameTable Handled!!");
1131 break;
1132
1134 handleBoardMeta(b, off); // fills imagesRx_
1135 Logger::instance().log("localtunnel", Logger::Level::Info, "Snapshot_Board Handled!!");
1136 break;
1137
1139 handleMarkerMeta(b, off); // fills imagesRx_
1140 //Logger::instance().log("localtunnel", Logger::Level::Info, "MarkerCreate Handled!!");
1141 break;
1142
1144 handleFogCreate(b, off);
1145 //Logger::instance().log("localtunnel", Logger::Level::Info, "FogCreate Handled!!");
1146 break;
1147
1149 handleImageChunk(b, off);
1150 Logger::instance().log("localtunnel", Logger::Level::Info, "ImageChunk Handled!!");
1151 break;
1152
1154 handleCommitBoard(b, off);
1155 Logger::instance().log("localtunnel", Logger::Level::Info, "CommitBoard Handled!!");
1156 break;
1157
1159 handleCommitMarker(b, off);
1160 Logger::instance().log("localtunnel", Logger::Level::Info, "CommitMarker Handled!!");
1161 break;
1162
1164 handleMarkerUpdate(b, off);
1165 Logger::instance().log("localtunnel", Logger::Level::Info, "MarkerUpdate Handled!!");
1166 break;
1167
1169 handleMarkerMoveState(b, off);
1170 Logger::instance().log("localtunnel", Logger::Level::Info, "MarkerUpdate Handled!!");
1171 break;
1172
1174 handleFogUpdate(b, off);
1175 Logger::instance().log("localtunnel", Logger::Level::Info, "FogUpdate Handled!!");
1176 break;
1177
1179 handleMarkerDelete(b, off);
1180 Logger::instance().log("localtunnel", Logger::Level::Info, "MarkerDelete Handled!!");
1181 break;
1182
1184 handleFogDelete(b, off);
1185 Logger::instance().log("localtunnel", Logger::Level::Info, "MarkerDelete FogDelete!!");
1186 break;
1187
1189 handleGridUpdate(b, off);
1190 Logger::instance().log("localtunnel", Logger::Level::Info, "GridUpdate Handled!!");
1191 break;
1192
1194 {
1195 handleUserNameUpdate(b, off);
1196 break;
1197 }
1198 default:
1199 Logger::instance().log("localtunnel", Logger::Level::Warn, "Unkown Message Type not Handled!!");
1200 break;
1201 }
1202 }
1203 decodingFromPeer_.clear();
1204}
1205
1206// MARKER MOVE OPERATIONS -----------------------------------------------------------------------------------------------------------------------------------------
1207void NetworkManager::decodeRawMarkerMoveBuffer(const std::string& fromPeer, const std::vector<uint8_t>& b)
1208{
1209 decodingFromPeer_ = fromPeer;
1210 size_t off = 0;
1211 while (off < b.size())
1212 {
1213 if (!ensureRemaining(b, off, 1))
1214 break;
1215
1216 auto type = static_cast<msg::DCType>(b[off]);
1217 off += 1;
1218 Logger::instance().log("localtunnel", Logger::Level::Info, msg::DCtypeString(type) + " Received!! MarkerMove");
1219 if (type != msg::DCType::MarkerMove)
1220 {
1221 // If the sender packed something else on this DC, bail
1222 return;
1223 }
1224
1225 handleMarkerMove(b, off); // parses one frame and updates coalescer
1226 Logger::instance().log("localtunnel", Logger::Level::Info, "MarkerMove Handled!!");
1227 }
1228 decodingFromPeer_.clear();
1229}
1230
1231void NetworkManager::handleMarkerMove(const std::vector<uint8_t>& raw, size_t& off)
1232{
1233 if (!ensureRemaining(raw, off, 8 + 8 + 4 + 4 + 8 + 8)) // 8 bytes Position (2x int32)
1234 return;
1235
1236 const auto& b = reinterpret_cast<const std::vector<unsigned char>&>(raw);
1237
1240 m.boardId = Serializer::deserializeUInt64(b, off);
1241 m.markerId = Serializer::deserializeUInt64(b, off);
1242 m.dragEpoch = Serializer::deserializeUInt32(b, off);
1243 m.seq = Serializer::deserializeUInt32(b, off);
1244 m.ts = Serializer::deserializeUInt64(b, off);
1246 m.pos = p;
1247 m.mov = Moving{true};
1248
1249 m.fromPeerId = decodingFromPeer_;
1250
1251 inboundGame_.push(std::move(m));
1252}
1253
1254// MarkerUpdate
1255void NetworkManager::handleMarkerUpdate(const std::vector<uint8_t>& b, size_t& off)
1256{
1259
1260 m.boardId = Serializer::deserializeUInt64(b, off);
1261 m.markerId = Serializer::deserializeUInt64(b, off);
1262 m.size = Serializer::deserializeSize(b, off);
1263 m.vis = Serializer::deserializeVisibility(b, off);
1264 m.markerComp = Serializer::deserializeMarkerComponent(b, off);
1265
1266 m.fromPeerId = decodingFromPeer_;
1267 inboundGame_.push(std::move(m));
1268}
1269void NetworkManager::handleMarkerMoveState(const std::vector<uint8_t>& raw, size_t& off)
1270{
1271 // Expect (after type):
1272 // [boardId:u64][markerId:u64][epoch:u32][seq:u32][ts:u64][Moving][(Position if final)]
1273 if (!ensureRemaining(raw, off, 8 + 8 + 4 + 4 + 8 + 1))
1274 return;
1275
1276 const auto& b = reinterpret_cast<const std::vector<unsigned char>&>(raw);
1277
1280 m.boardId = Serializer::deserializeUInt64(b, off);
1281 m.markerId = Serializer::deserializeUInt64(b, off);
1282 m.dragEpoch = Serializer::deserializeUInt32(b, off);
1283 m.seq = Serializer::deserializeUInt32(b, off);
1284 m.ts = Serializer::deserializeUInt64(b, off);
1286 m.mov = mv;
1287
1288 if (!mv.isDragging)
1289 {
1290 if (!ensureRemaining(raw, off, 8))
1291 return; // Position (2x int)
1293 m.pos = p;
1294 }
1295 m.fromPeerId = decodingFromPeer_;
1296 inboundGame_.push(std::move(m));
1297}
1298
1299std::vector<unsigned char> NetworkManager::buildMarkerMoveFrame(uint64_t boardId, const flecs::entity& marker, uint32_t seq)
1300{
1301 std::vector<unsigned char> out;
1302 const auto* id = marker.get<Identifier>();
1303 const auto* pos = marker.get<Position>();
1304 if (!id || !pos)
1305 return out;
1306
1307 // epoch from drag_ state
1308 auto& s = drag_[id->id];
1309 if (s.locallyProposedEpoch == 0 && s.epoch == 0)
1310 {
1311 s.locallyProposedEpoch = s.epoch + 1;
1312 s.epoch = s.locallyProposedEpoch;
1313 }
1314 const uint32_t epoch = (s.locallyProposedEpoch ? s.locallyProposedEpoch : s.epoch);
1315 const uint64_t ts = nowMs();
1316
1317 Serializer::serializeUInt8(out, static_cast<uint8_t>(msg::DCType::MarkerMove));
1318 Serializer::serializeUInt64(out, boardId);
1319 Serializer::serializeUInt64(out, id->id);
1320 Serializer::serializeUInt32(out, epoch);
1324 return out;
1325}
1326std::vector<unsigned char> NetworkManager::buildMarkerMoveStateFrame(uint64_t boardId, const flecs::entity& marker)
1327{
1328 std::vector<unsigned char> out;
1329 const auto* id = marker.get<Identifier>();
1330 const auto* mv = marker.get<Moving>();
1331 if (!id || !mv)
1332 return out;
1333
1334 auto& s = drag_[id->id];
1335 if (s.locallyProposedEpoch == 0 && s.epoch == 0)
1336 {
1337 s.locallyProposedEpoch = s.epoch + 1;
1338 s.epoch = s.locallyProposedEpoch;
1339 }
1340 const uint32_t epoch = (s.locallyProposedEpoch ? s.locallyProposedEpoch : s.epoch);
1341 const uint32_t seq = ++s.localSeq;
1342 const uint64_t ts = nowMs();
1343 s.lastTxMs = ts;
1344
1345 Serializer::serializeUInt8(out, static_cast<uint8_t>(msg::DCType::MarkerMoveState));
1346 Serializer::serializeUInt64(out, boardId);
1347 Serializer::serializeUInt64(out, id->id);
1348 Serializer::serializeUInt32(out, epoch);
1351 Serializer::serializeMoving(out, mv); // isDragging
1352
1353 if (!mv->isDragging && marker.has<Position>())
1354 {
1355 const auto* pos = marker.get<Position>(); // final pos at end
1357 }
1358 return out;
1359}
1360
1361// ---- MARKER UPDATE/DELETE ----
1362std::vector<unsigned char> NetworkManager::buildMarkerUpdateFrame(uint64_t boardId, const flecs::entity& marker)
1363{
1364 std::vector<unsigned char> b;
1365 Serializer::serializeUInt8(b, static_cast<uint8_t>(msg::DCType::MarkerUpdate));
1366 Serializer::serializeUInt64(b, boardId);
1367
1368 const auto id = marker.get<Identifier>()->id;
1369 const auto siz = *marker.get<Size>();
1370 const auto vis = *marker.get<Visibility>();
1371 const auto mc = *marker.get<MarkerComponent>();
1372
1377 return b;
1378}
1379
1380void NetworkManager::broadcastMarkerUpdate(uint64_t boardId, const flecs::entity& marker)
1381{
1382 auto ids = getConnectedPeerIds();
1383 if (!ids.empty())
1384 sendMarkerUpdate(boardId, marker, ids);
1385}
1386
1387void NetworkManager::broadcastMarkerMove(uint64_t boardId, const flecs::entity& marker)
1388{
1389 auto ids = getConnectedPeerIds();
1390 if (!ids.empty())
1391 sendMarkerMove(boardId, marker, ids);
1392}
1393void NetworkManager::sendMarkerMove(uint64_t boardId, const flecs::entity& marker, const std::vector<std::string>& toPeerIds)
1394{
1395 if (!marker.is_valid() || !marker.has<Identifier>() || !marker.has<Position>())
1396 return;
1397
1398 const uint64_t markerId = marker.get<Identifier>()->id;
1399
1400 // per-drag local seq in DragState
1401 auto& s = drag_[markerId];
1402 if (s.locallyProposedEpoch == 0 && s.epoch == 0)
1403 {
1404 s.locallyProposedEpoch = s.epoch + 1;
1405 s.epoch = s.locallyProposedEpoch;
1406 }
1407 const uint32_t seq = ++s.localSeq;
1408 s.lastTxMs = nowMs();
1409
1410 auto frame = buildMarkerMoveFrame(boardId, marker, seq);
1411 if (frame.empty())
1412 return;
1413
1414 for (auto& pid : toPeerIds)
1415 {
1416 if (auto it = peers.find(pid); it != peers.end() && it->second)
1417 it->second->sendMarkerMove(frame);
1418 }
1419}
1420
1421void NetworkManager::broadcastMarkerMoveState(uint64_t boardId, const flecs::entity& marker)
1422{
1423 auto ids = getConnectedPeerIds();
1424 if (!ids.empty())
1425 sendMarkerMoveState(boardId, marker, ids);
1426}
1427
1428void NetworkManager::sendMarkerMoveState(uint64_t boardId, const flecs::entity& marker, const std::vector<std::string>& toPeerIds)
1429{
1430 if (!marker.is_valid() || !marker.has<Identifier>() || !marker.has<Moving>())
1431 return;
1432
1433 auto frame = buildMarkerMoveStateFrame(boardId, marker);
1434 if (frame.empty())
1435 return;
1436
1437 // Reliable: use your game channel
1438 broadcastGameFrame(frame, toPeerIds);
1439}
1440
1441//bool NetworkManager::amIDragging(uint64_t markerId) const
1442//{
1443// auto it = drag_.find(markerId);
1444// if (it == drag_.end())
1445// return false;
1446// const auto& s = it->second;
1447// return !s.closed && s.ownerPeerId == getMyPeerId();
1448//}
1449
1450bool NetworkManager::amIDragging(uint64_t markerId) const
1451{
1452 auto it = drag_.find(markerId);
1453 if (it == drag_.end())
1454 return false;
1455 const auto& s = it->second;
1456
1457 // If we started the drag locally, trust the local flag.
1458 if (!s.closed && s.locallyDragging)
1459 return true;
1460
1461 // Otherwise fall back to peer-id ownership (remote handoff cases).
1462 return !s.closed && s.ownerPeerId == getMyPeerId();
1463}
1464
1465// markDraggingLocal — called by BoardManager on start/end
1466void NetworkManager::markDraggingLocal(uint64_t markerId, bool dragging)
1467{
1468 auto& s = drag_[markerId];
1469 if (dragging)
1470 {
1471 s.locallyDragging = true;
1472 s.locallyProposedEpoch = (s.closed ? (s.epoch + 1) : s.epoch);
1473 s.localSeq = 0;
1474 s.ownerPeerId = getMyPeerId();
1475 s.epochOpenedMs = nowMs();
1476 s.closed = false;
1477 if (s.locallyProposedEpoch > s.epoch)
1478 s.epoch = s.locallyProposedEpoch;
1479 }
1480 else
1481 {
1482 s.locallyDragging = false; // we close epoch on final frame or forceCloseDrag
1483 }
1484}
1485
1486bool NetworkManager::isMarkerBeingDragged(uint64_t markerId) const
1487{
1488 auto it = drag_.find(markerId);
1489 return (it != drag_.end() && !it->second.closed);
1490}
1491
1492void NetworkManager::forceCloseDrag(uint64_t markerId)
1493{
1494 auto it = drag_.find(markerId);
1495 if (it == drag_.end())
1496 return;
1497 auto& s = it->second;
1498 s.closed = true;
1499 s.locallyDragging = false;
1500 s.localSeq = 0;
1501}
1502
1503void NetworkManager::sendMarkerUpdate(uint64_t boardId, const flecs::entity& marker,
1504 const std::vector<std::string>& toPeerIds)
1505{
1506 if (!marker.is_valid() || !marker.has<Identifier>())
1507 return;
1508
1509 auto frame = buildMarkerUpdateFrame(boardId, marker);
1510 broadcastGameFrame(frame, toPeerIds);
1511}
1512
1514{
1515 if (!m.markerId || !m.dragEpoch)
1516 return false;
1517 auto& s = drag_[*m.markerId];
1518
1519 // Epoch
1520 if (*m.dragEpoch < s.epoch)
1521 return false;
1522 if (*m.dragEpoch > s.epoch)
1523 {
1524 s.epoch = *m.dragEpoch;
1525 s.closed = false;
1526 s.lastSeq = 0;
1527 s.ownerPeerId = m.fromPeerId;
1528 }
1529 else
1530 {
1531 if (s.closed)
1532 return false;
1533 if (!s.ownerPeerId.empty() && s.ownerPeerId != m.fromPeerId)
1534 {
1535 if (!tieBreakWins(m.fromPeerId, s.ownerPeerId))
1536 return false;
1537 s.ownerPeerId = m.fromPeerId;
1538 if (s.locallyDragging)
1539 {
1540 s.locallyDragging = false;
1541 s.localSeq = 0;
1542 }
1543 }
1544 else
1545 {
1546 s.ownerPeerId = m.fromPeerId; // first owner for this epoch
1547 }
1548 }
1549
1550 // Seq
1551 if (m.seq && *m.seq <= s.lastSeq)
1552 return false;
1553 if (m.seq)
1554 s.lastSeq = *m.seq;
1555
1556 // Local echo suppression while we drag
1557 if (s.locallyDragging)
1558 return false;
1559
1560 return true;
1561}
1562
1564{
1565 if (!m.markerId || !m.dragEpoch || !m.mov || !m.mov->isDragging)
1566 return false;
1567 auto& s = drag_[*m.markerId];
1568
1569 if (*m.dragEpoch < s.epoch)
1570 return false;
1571 if (*m.dragEpoch > s.epoch)
1572 {
1573 s.epoch = *m.dragEpoch;
1574 s.closed = false;
1575 s.lastSeq = 0;
1576 s.ownerPeerId = m.fromPeerId;
1577 }
1578 else
1579 {
1580 if (s.closed)
1581 return false;
1582 if (!s.ownerPeerId.empty() && s.ownerPeerId != m.fromPeerId)
1583 {
1584 if (!tieBreakWins(m.fromPeerId, s.ownerPeerId))
1585 return false;
1586 s.ownerPeerId = m.fromPeerId;
1587 if (s.locallyDragging)
1588 {
1589 s.locallyDragging = false;
1590 s.localSeq = 0;
1591 }
1592 }
1593 else
1594 {
1595 s.ownerPeerId = m.fromPeerId;
1596 }
1597 }
1598
1599 if (m.seq && *m.seq <= s.lastSeq)
1600 return false;
1601 if (m.seq)
1602 s.lastSeq = *m.seq;
1603
1604 return true;
1605}
1606
1608{
1609 if (!m.markerId || !m.dragEpoch || !m.mov || m.mov->isDragging)
1610 return false;
1611 auto& s = drag_[*m.markerId];
1612
1613 if (*m.dragEpoch < s.epoch)
1614 return false;
1615 if (*m.dragEpoch > s.epoch)
1616 {
1617 s.epoch = *m.dragEpoch;
1618 s.closed = false;
1619 s.lastSeq = 0;
1620 s.ownerPeerId = m.fromPeerId;
1621 }
1622 else
1623 {
1624 if (s.closed)
1625 return false;
1626 if (!s.ownerPeerId.empty() && s.ownerPeerId != m.fromPeerId)
1627 {
1628 if (!tieBreakWins(m.fromPeerId, s.ownerPeerId))
1629 return false;
1630 s.ownerPeerId = m.fromPeerId;
1631 if (s.locallyDragging)
1632 {
1633 s.locallyDragging = false;
1634 s.localSeq = 0;
1635 }
1636 }
1637 else
1638 {
1639 s.ownerPeerId = m.fromPeerId;
1640 }
1641 if (m.seq && *m.seq < s.lastSeq)
1642 return false;
1643 if (m.seq && *m.seq >= s.lastSeq)
1644 s.lastSeq = *m.seq;
1645 }
1646
1647 // finalize epoch
1648 s.closed = true;
1649 s.locallyDragging = false;
1650 s.localSeq = 0;
1651
1652 return true;
1653}
1654// END MARKER OPERATIONS -----------------------------------------------------------------------------------------------------------------------------------------
1655
1656void NetworkManager::sendMarkerDelete(uint64_t boardId, const flecs::entity& marker,
1657 const std::vector<std::string>& toPeerIds)
1658{
1659 if (!marker.is_valid() || !marker.has<Identifier>())
1660 return;
1661
1662 const uint64_t mid = marker.get<Identifier>()->id;
1663 auto frame = buildMarkerDeleteFrame(boardId, mid);
1664 broadcastGameFrame(frame, toPeerIds);
1665}
1666
1667void NetworkManager::sendFogUpdate(uint64_t boardId, const flecs::entity& fog,
1668 const std::vector<std::string>& toPeerIds)
1669{
1670 if (!fog.is_valid() || !fog.has<Identifier>())
1671 return;
1672
1673 auto frame = buildFogUpdateFrame(boardId, fog);
1674 broadcastGameFrame(frame, toPeerIds);
1675}
1676
1677void NetworkManager::sendFogDelete(uint64_t boardId, const flecs::entity& fog,
1678 const std::vector<std::string>& toPeerIds)
1679{
1680 if (!fog.is_valid() || !fog.has<Identifier>())
1681 return;
1682
1683 const uint64_t fid = fog.get<Identifier>()->id;
1684 auto frame = buildFogDeleteFrame(boardId, fid);
1685 broadcastGameFrame(frame, toPeerIds);
1686}
1687
1688// ---------- MESSAGE SENDERS --------------------------------------------------------------------------------
1689void NetworkManager::sendGameTable(const flecs::entity& gameTable, const std::vector<std::string>& toPeerIds)
1690{
1691 auto gtId = gameTable.get<Identifier>()->id;
1692 auto gt = *gameTable.get<GameTable>();
1693 auto frame = buildSnapshotGameTableFrame(gtId, gt.gameTableName);
1694 broadcastGameFrame(frame, toPeerIds);
1695}
1696
1697void NetworkManager::sendBoard(const flecs::entity& board, const std::vector<std::string>& toPeerIds)
1698{
1699 // read board image (from TextureComponent.image_path)
1700 auto tex = board.get<TextureComponent>();
1701 auto image_path = tex->image_path;
1702 Logger::instance().log("localtunnel", Logger::Level::Info, "Board Texture Path: " + image_path);
1703 auto is_file_only = PathManager::isFilenameOnly(tex->image_path);
1704 auto is_path_like = PathManager::isPathLike(tex->image_path);
1705 if (is_file_only || !is_path_like)
1706 {
1707 Logger::instance().log("localtunnel", Logger::Level::Info, "Board Texture Path is FILE ONLY");
1708 auto map_folder = PathManager::getMapsPath();
1709 auto image_path_ = map_folder / tex->image_path;
1710 image_path = image_path_.string();
1711 }
1712 Logger::instance().log("localtunnel", Logger::Level::Info, "Board Texture Path AGAIN: " + image_path);
1713 std::vector<unsigned char> img = tex ? readFileBytes(image_path) : std::vector<unsigned char>{};
1714
1715 // 1) meta
1716 auto meta = buildSnapshotBoardFrame(board, static_cast<uint64_t>(img.size()));
1717 broadcastGameFrame(meta, toPeerIds);
1718
1720 uint64_t bid = board.get<Identifier>()->id;
1721 sendImageChunks(msg::ImageOwnerKind::Board, bid, img, toPeerIds);
1722
1723 // 3) commit
1724 auto commit = buildCommitBoardFrame(bid);
1725 broadcastGameFrame(commit, toPeerIds);
1726
1727 board.children([&](flecs::entity child)
1728 {
1729 if (child.has<MarkerComponent>()) {
1730 uint64_t bid = board.get<Identifier>()->id;
1731 sendMarker(bid, child, toPeerIds);
1732 Logger::instance().log("localtunnel", Logger::Level::Info, "SentMarker");
1733 }
1734 else if (child.has<FogOfWar>()) {
1735 uint64_t bid = board.get<Identifier>()->id;
1736 sendFog(bid, child, toPeerIds);
1737 Logger::instance().log("localtunnel", Logger::Level::Info, "SentFog");
1738 } });
1739}
1740
1741void NetworkManager::sendMarker(uint64_t boardId, const flecs::entity& marker, const std::vector<std::string>& toPeerIds)
1742{
1743 const TextureComponent* tex = marker.get<TextureComponent>();
1744 auto image_path = tex->image_path;
1745 Logger::instance().log("localtunnel", Logger::Level::Info, "Marker Path: " + image_path);
1746 auto is_file_only = PathManager::isFilenameOnly(tex->image_path);
1747 auto is_path_like = PathManager::isPathLike(tex->image_path);
1748 if (is_file_only || !is_path_like)
1749 {
1750 Logger::instance().log("localtunnel", Logger::Level::Info, "Marker Path is FILE ONLY");
1751 auto marker_folder = PathManager::getMarkersPath();
1752 auto image_path_ = marker_folder / tex->image_path;
1753 image_path = image_path_.string();
1754 }
1755 Logger::instance().log("localtunnel", Logger::Level::Info, "Marker Path Again: " + image_path);
1756 std::vector<unsigned char> img = tex ? readFileBytes(image_path) : std::vector<unsigned char>{};
1757 Logger::instance().log("localtunnel", Logger::Level::Info, "Marker Texture Byte Size: " + std::to_string(img.size()));
1758 // 1) meta
1759 auto meta = buildCreateMarkerFrame(boardId, marker, static_cast<uint64_t>(img.size()));
1760 broadcastGameFrame(meta, toPeerIds);
1761
1762 // 2) image chunks (ownerKind=1 for marker)
1763 uint64_t mid = marker.get<Identifier>()->id;
1764 sendImageChunks(msg::ImageOwnerKind::Marker, mid, img, toPeerIds);
1765 //uint64_t off = 0;
1766 //while (off < img.size())
1767 //{
1768 // size_t chunk = std::min<size_t>(kChunk, img.size() - off);
1769 // auto frame = buildImageChunkFrame(/*ownerKind=*/1, mid, off, img.data() + off, chunk);
1770 // Logger::instance().log("localtunnel", Logger::Level::Info, "Marker Texture Byte Size: " + img.size());
1771 // broadcastGameFrame(frame, toPeerIds);
1772 // off += chunk;
1773 //}
1774
1775 // 3) commit
1776 auto commit = buildCommitMarkerFrame(boardId, mid);
1777 broadcastGameFrame(commit, toPeerIds);
1778}
1779
1780void NetworkManager::sendFog(uint64_t boardId, const flecs::entity& fog, const std::vector<std::string>& toPeerIds)
1781{
1782 auto frame = buildFogCreateFrame(boardId, fog);
1783 broadcastGameFrame(frame, toPeerIds);
1784}
1785
1786// --- tuning ---
1787
1788// Sends an entire image as ImageChunk frames (reliable/ordered DC).
1789// kind: 0=Board, 1=Marker (msg::ImageOwnerKind)
1790// id: boardId when Board; markerId when Marker
1791// toPeerIds: who to send to
1792bool NetworkManager::sendImageChunks(msg::ImageOwnerKind kind, uint64_t id, const std::vector<unsigned char>& img, const std::vector<std::string>& toPeerIds)
1793{
1794 Logger::instance().log("localtunnel", Logger::Level::Info,
1795 std::string("sendImageChunks: kind=") + (kind == msg::ImageOwnerKind::Board ? "Board" : "Marker") +
1796 " id=" + std::to_string(id) + " bytes=" + std::to_string(img.size()));
1797
1798 size_t sent = 0;
1799 int paced = 0;
1800 while (sent < img.size())
1801 {
1802 const size_t chunk = std::min(kChunk, img.size() - sent);
1803 const auto frame = buildImageChunkFrame(static_cast<uint8_t>(kind), id, sent, img.data() + sent, chunk);
1804
1805 // If you have per-peer send that returns bool, check and log
1806 bool allOk = true;
1807 for (auto& pid : toPeerIds)
1808 {
1809 sendGameTo(pid, frame); // if your API is void, keep it; otherwise log ok
1810 }
1811
1812 // Fallback pacing (keeps buffers happy)
1813 if ((++paced % kPaceEveryN) == 0)
1814 std::this_thread::sleep_for(std::chrono::milliseconds(kPaceMillis));
1815
1816 sent += chunk;
1817 }
1818
1819 Logger::instance().log("localtunnel", Logger::Level::Info,
1820 "sendImageChunks: done kind=" + std::string(kind == msg::ImageOwnerKind::Board ? "Board" : "Marker") +
1821 " id=" + std::to_string(id) + " total=" + std::to_string(img.size()));
1822 return true;
1823}
1824
1825//ON PEER RECEIVING MESSAGE-----------------------------------------------------------
1826//void NetworkManager::onDcGameBinary(const std::string& fromPeer, const std::vector<uint8_t>& b)
1827//{
1828// size_t off = 0;
1829// while (off < b.size())
1830// {
1831// // Need at least one byte for the type
1832// if (!ensureRemaining(b, off, 1))
1833// {
1834// break;
1835// }
1836//
1837// auto type = static_cast<msg::DCType>(b[off]);
1838// off += 1;
1839//
1840// switch (type)
1841// {
1842// // -------- SNAPSHOT / BOARD ----------
1843// case msg::DCType::Snapshot_Board:
1844// handleBoardMeta(b, off);
1845// break;
1846//
1847// // -------- CREATE MARKER (META) ----------
1848// case msg::DCType::MarkerCreate:
1849// handleMarkerMeta(b, off);
1850// break;
1851//
1852// // -------- FOG CREATE (NO IMAGE) ----------
1853// case msg::DCType::FogCreate:
1854// handleFogCreate(b, off);
1855// break;
1856//
1857// // -------- IMAGE CHUNK (BOARD or MARKER) ----------
1858// case msg::DCType::ImageChunk:
1859// handleImageChunk(b, off);
1860// break;
1861//
1862// // -------- COMMIT BOARD ----------
1863// case msg::DCType::CommitBoard:
1864// handleCommitBoard(b, off);
1865// break;
1866//
1867// // -------- COMMIT MARKER ----------
1868// case msg::DCType::CommitMarker:
1869// handleCommitMarker(b, off);
1870// break;
1871//
1872// // -------- BOARD OPERATONS - AFTER ENTITIY CREATION ----------
1873// case msg::DCType::MarkerUpdate:
1874// //handlemarkermove(b, off);
1875// break;
1876//
1877// case msg::DCType::GridUpdate:
1878// //handlegridupdate(b, off);
1879// break;
1880//
1881// case msg::DCType::FogUpdate:
1882// //handleFogUpdate(b, off);
1883// break;
1884// default:
1885// return;
1886// }
1887// }
1888//}
1889
1890//---Message Received Handlers--------------------------------------------------------------------------------
1891//
1892
1893void NetworkManager::broadcastGridUpdate(uint64_t boardId, const flecs::entity& board)
1894{
1895 auto ids = getConnectedPeerIds();
1896 if (!ids.empty())
1897 sendGridUpdate(boardId, board, ids);
1898}
1899
1900void NetworkManager::sendGridUpdate(uint64_t boardId, const flecs::entity& board,
1901 const std::vector<std::string>& toPeerIds)
1902{
1903 if (!board.is_valid() || !board.has<Grid>())
1904 return;
1905
1906 const auto* g = board.get<Grid>();
1907 auto frame = buildGridUpdateFrame(boardId, *g);
1908 broadcastGameFrame(frame, toPeerIds);
1909}
1910
1911std::vector<unsigned char> NetworkManager::buildGridUpdateFrame(uint64_t boardId, const Grid& grid)
1912{
1913 std::vector<unsigned char> b;
1914 Serializer::serializeUInt8(b, static_cast<uint8_t>(msg::DCType::GridUpdate));
1915 Serializer::serializeUInt64(b, boardId);
1916 Serializer::serializeGrid(b, &grid);
1917 return b;
1918}
1919
1920void NetworkManager::handleGridUpdate(const std::vector<uint8_t>& b, size_t& off)
1921{
1922 // type byte already consumed by the caller switch
1923 if (!ensureRemaining(b, off, 8))
1924 return;
1925
1928 m.boardId = Serializer::deserializeUInt64(b, off);
1930 m.grid = g;
1931 inboundGame_.push(std::move(m));
1932}
1933
1934// DCType::Snapshot_GameTable (100)
1935void NetworkManager::handleGameTableSnapshot(const std::vector<uint8_t>& b, size_t& off)
1936{
1939 m.tableId = Serializer::deserializeUInt64(b, off);
1940 m.name = Serializer::deserializeString(b, off);
1941 inboundGame_.push(std::move(m));
1942}
1943
1944// DCType::Snapshot_Board (101) -- NewBoard - (TODO)Check for Existing Board in entities
1945void NetworkManager::handleBoardMeta(const std::vector<uint8_t>& b, size_t& off)
1946{
1947 msg::BoardMeta bm;
1949 bm.boardName = Serializer::deserializeString(b, off);
1950 bm.pan = Serializer::deserializePanning(b, off);
1951 bm.grid = Serializer::deserializeGrid(b, off);
1952 bm.size = Serializer::deserializeSize(b, off);
1953 uint64_t total = Serializer::deserializeUInt64(b, off); // 0 if no image
1954
1955 auto& p = imagesRx_[bm.boardId];
1957 p.id = bm.boardId;
1958 p.boardId = bm.boardId;
1959 p.boardMeta = bm;
1960 p.total = total;
1961 p.received = 0;
1962 p.buf.clear();
1963 if (total)
1964 p.buf.resize(static_cast<size_t>(total));
1965}
1966
1967// DCType::CreateEntity (4)
1968void NetworkManager::handleMarkerMeta(const std::vector<uint8_t>& b, size_t& off)
1969{
1970 msg::MarkerMeta mm;
1972 mm.markerId = Serializer::deserializeUInt64(b, off);
1973 mm.name = Serializer::deserializeString(b, off);
1974 mm.pos = Serializer::deserializePosition(b, off);
1975 mm.size = Serializer::deserializeSize(b, off);
1976 mm.vis = Serializer::deserializeVisibility(b, off);
1977 mm.mov = Serializer::deserializeMoving(b, off);
1978 uint64_t total = Serializer::deserializeUInt64(b, off); // 0 if no image
1979
1980 auto& p = imagesRx_[mm.markerId];
1982 p.id = mm.markerId;
1983 p.boardId = mm.boardId;
1984 p.markerMeta = mm;
1985 p.total = total;
1986 p.received = 0;
1987 p.buf.clear();
1988 if (total)
1989 p.buf.resize(static_cast<size_t>(total));
1990}
1991
1992// DCType::FogCreate (7)
1993void NetworkManager::handleFogCreate(const std::vector<uint8_t>& b, size_t& off)
1994{
1997 m.boardId = Serializer::deserializeUInt64(b, off);
1998 m.fogId = Serializer::deserializeUInt64(b, off);
1999 m.pos = Serializer::deserializePosition(b, off);
2000 m.size = Serializer::deserializeSize(b, off);
2001 m.vis = Serializer::deserializeVisibility(b, off);
2002 inboundGame_.push(std::move(m));
2003}
2004
2005void NetworkManager::handleCommitBoard(const std::vector<uint8_t>& b, size_t& off)
2006{
2007 uint64_t boardId = Serializer::deserializeUInt64(b, off);
2008
2009 auto it = imagesRx_.find(boardId);
2010 if (it == imagesRx_.end())
2011 {
2012 // Late/meta-less commit? Create a placeholder so tryFinalize can run if chunks arrive later.
2013 PendingImage p;
2015 p.id = boardId;
2016 imagesRx_.emplace(boardId, std::move(p));
2017 it = imagesRx_.find(boardId);
2018 }
2019
2020 it->second.commitRequested = true;
2022}
2023
2024void NetworkManager::handleImageChunk(const std::vector<uint8_t>& b, size_t& off)
2025{
2026 if (off + 1 > b.size())
2027 {
2028 Logger::instance().log("localtunnel", Logger::Level::Error, "ImageChunk: underrun(kind)");
2029 return;
2030 }
2031 auto kind = static_cast<msg::ImageOwnerKind>(b[off]);
2032 off += 1;
2033
2034 if (off + 8 + 8 + 4 > b.size())
2035 {
2036 Logger::instance().log("localtunnel", Logger::Level::Error, "ImageChunk: underrun(headers)");
2037 return;
2038 }
2039 uint64_t id = Serializer::deserializeUInt64(b, off);
2040 uint64_t off64 = Serializer::deserializeUInt64(b, off);
2041 int len = Serializer::deserializeInt(b, off);
2042
2043 if (len < 0 || off + static_cast<size_t>(len) > b.size())
2044 {
2045 Logger::instance().log("localtunnel", Logger::Level::Error,
2046 "ImageChunk: invalid len id=" + std::to_string(id) + " len=" + std::to_string(len));
2047 return;
2048 }
2049
2050 auto it = imagesRx_.find(id);
2051 if (it == imagesRx_.end())
2052 {
2053 Logger::instance().log("localtunnel", Logger::Level::Warn,
2054 "ImageChunk: unknown id=" + std::to_string(id));
2055 off += static_cast<size_t>(len);
2056 return;
2057 }
2058
2059 auto& p = it->second;
2060 if (p.kind != kind || p.total == 0)
2061 {
2062 Logger::instance().log("localtunnel", Logger::Level::Warn,
2063 "ImageChunk: kind/total mismatch id=" + std::to_string(id));
2064 off += static_cast<size_t>(len);
2065 return;
2066 }
2067
2068 if (off64 + static_cast<uint64_t>(len) > p.total)
2069 {
2070 Logger::instance().log("localtunnel", Logger::Level::Error,
2071 "ImageChunk: out-of-bounds id=" + std::to_string(id) +
2072 " off=" + std::to_string(off64) + " len=" + std::to_string(len) +
2073 " total=" + std::to_string(p.total));
2074 off += static_cast<size_t>(len);
2075 return;
2076 }
2077
2078 std::memcpy(p.buf.data() + off64, b.data() + off, static_cast<size_t>(len));
2079 off += static_cast<size_t>(len);
2080 p.received += static_cast<uint64_t>(len);
2081
2082 // Occasional progress log (e.g., every ~1MB)
2083 if ((p.received % (1u << 20)) < static_cast<uint64_t>(len))
2084 {
2085 Logger::instance().log("localtunnel", Logger::Level::Info,
2086 "ImageChunk: id=" + std::to_string(id) + " " +
2087 std::to_string(p.received) + "/" + std::to_string(p.total));
2088 }
2089}
2090
2091void NetworkManager::handleCommitMarker(const std::vector<uint8_t>& b, size_t& off)
2092{
2093 uint64_t boardId = Serializer::deserializeUInt64(b, off);
2094 uint64_t markerId = Serializer::deserializeUInt64(b, off);
2095
2096 auto it = imagesRx_.find(markerId);
2097 if (it == imagesRx_.end())
2098 {
2099 PendingImage p;
2101 p.id = markerId;
2102 p.boardId = boardId;
2103 imagesRx_.emplace(markerId, std::move(p));
2104 it = imagesRx_.find(markerId);
2105 }
2106 else
2107 {
2108 it->second.boardId = boardId; // ensure it's set
2109 }
2110
2111 it->second.commitRequested = true;
2113}
2114
2115void NetworkManager::handleUserNameUpdate(const std::vector<uint8_t>& b, size_t& off)
2116{
2119 r.fromPeerId = decodingFromPeer_;
2120 r.tableId = Serializer::deserializeUInt64(b, off);
2121
2122 // This is UNIQUE ID now:
2123 const std::string uniqueId = Serializer::deserializeString(b, off);
2124 const std::string oldU = Serializer::deserializeString(b, off);
2125 const std::string newU = Serializer::deserializeString(b, off);
2126 const uint8_t rebound = Serializer::deserializeUInt8(b, off);
2127
2128 // Keep old ReadyMessage fields for upstream handlers (uses userPeerId/name/text in your codebase):
2129 r.userUniqueId = uniqueId; // NOTE: field name is legacy; carries uniqueId now.
2130 r.text = oldU;
2131 r.name = newU;
2132 r.rebound = rebound;
2133
2135 "UserNameUpdate: tbl=" + std::to_string(r.tableId.value_or(0)) +
2136 " uid=" + uniqueId + " old=" + oldU + " new=" + newU);
2137
2138 inboundGame_.push(std::move(r));
2139}
2140
2141void NetworkManager::handleMarkerDelete(const std::vector<uint8_t>& b, size_t& off)
2142{
2145 m.boardId = Serializer::deserializeUInt64(b, off);
2146 m.markerId = Serializer::deserializeUInt64(b, off);
2147 inboundGame_.push(std::move(m));
2148}
2149
2150// FogUpdate
2151void NetworkManager::handleFogUpdate(const std::vector<uint8_t>& b, size_t& off)
2152{
2155 m.boardId = Serializer::deserializeUInt64(b, off);
2156 m.fogId = Serializer::deserializeUInt64(b, off);
2157 m.pos = Serializer::deserializePosition(b, off);
2158 m.size = Serializer::deserializeSize(b, off);
2159 m.vis = Serializer::deserializeVisibility(b, off);
2160 m.mov = Serializer::deserializeMoving(b, off); // remove if you don’t use Moving on fog
2161 inboundGame_.push(std::move(m));
2162}
2163
2164void NetworkManager::handleFogDelete(const std::vector<uint8_t>& b, size_t& off)
2165{
2168 m.boardId = Serializer::deserializeUInt64(b, off);
2169 m.fogId = Serializer::deserializeUInt64(b, off);
2170 inboundGame_.push(std::move(m));
2171}
2172
2174{
2175 msg::NetEvent ev;
2176 while (events_.try_pop(ev))
2177 {
2178 if (ev.type == msg::NetEvent::Type::DcOpen)
2179 {
2180 auto it = peers.find(ev.peerId);
2181 if (it != peers.end() && it->second)
2182 {
2183 it->second->setOpen(ev.label, true);
2184 }
2185 }
2186 else if (ev.type == msg::NetEvent::Type::DcClosed)
2187 {
2188 auto it = peers.find(ev.peerId);
2189 if (it != peers.end() && it->second)
2190 {
2191 it->second->setOpen(ev.label, false);
2192 it->second->markBootstrapReset();
2193 //reconnectPeer(ev.peerId);
2194 }
2195 }
2196 else if (ev.type == msg::NetEvent::Type::PcClosed)
2197 {
2198 auto it = peers.find(ev.peerId);
2199 if (it != peers.end() && it->second)
2200 {
2201 //reconnectPeer(ev.peerId);
2202 }
2203 }
2204 else if (ev.type == msg::NetEvent::Type::PcOpen)
2205 {
2206 pushStatusToast(std::string("[Peer] ") + ev.peerId + " Connected", ImGuiToaster::Level::Good);
2207 }
2208 }
2209
2210 // If GM: check if any peer is now fully open → bootstrap once
2212 {
2213 for (auto& [pid, link] : peers)
2214 {
2215 if (link && link->allRequiredOpen() && !link->bootstrapSent())
2216 {
2217 try
2218 {
2220 }
2221 catch (const std::exception& e)
2222 {
2223 const std::string msg = std::string("Error bootstrapping peer: ") + e.what();
2226 }
2227 catch (...)
2228 {
2229 const char* msg = "Error bootstrapping peer: unknown exception";
2232 }
2233 }
2234 }
2235 }
2236}
2237
2239{
2240 using clock = std::chrono::steady_clock;
2241 int processed = 0;
2242
2244 while (processed < maxPerTick && inboundRaw_.try_pop(r))
2245 {
2246 try
2247 {
2248 if (r.label == msg::dc::name::Game)
2249 {
2250 decodeRawGameBuffer(r.fromPeer, r.bytes);
2251 }
2252 else if (r.label == msg::dc::name::Chat)
2253 {
2254 decodeRawChatBuffer(r.fromPeer, r.bytes);
2255 }
2256 else if (r.label == msg::dc::name::Notes)
2257 {
2258 decodeRawNotesBuffer(r.fromPeer, r.bytes);
2259 }
2260 else if (r.label == msg::dc::name::MarkerMove)
2261 {
2262 decodeRawMarkerMoveBuffer(r.fromPeer, r.bytes); // coalesce into moveLatest_
2263 }
2264 }
2265 catch (...)
2266 {
2267 // swallow/log
2268 }
2269 ++processed;
2270 }
2271}
2272
2273//void NetworkManager::drainInboundRaw(int maxPerTick)
2274//{
2275// int processed = 0;
2276// msg::InboundRaw r;
2277// while (processed < maxPerTick && inboundRaw_.try_pop(r))
2278// {
2279// try
2280// {
2281// if (r.label == msg::dc::name::Game)
2282// {
2283// decodeRawGameBuffer(r.fromPeer, r.bytes);
2284// }
2285// else if (r.label == msg::dc::name::Chat)
2286// {
2287// decodeRawChatBuffer(r.fromPeer, r.bytes);
2288// }
2289// else if (r.label == msg::dc::name::Notes)
2290// {
2291// decodeRawNotesBuffer(r.fromPeer, r.bytes);
2292// }
2293// else if (r.label == msg::dc::name::MarkerMove)
2294// {
2295// decodeRawMarkerMoveBuffer(r.fromPeer, r.bytes);
2296// }
2297// }
2298// catch (...)
2299// {
2300// // swallow & optionally push a toaster
2301// }
2302// ++processed;
2303// }
2304//}
2305
2306// NetworkManager.cpp
2307//void NetworkManager::decodeRawChatBuffer(const std::string& fromPeer,
2308// const std::vector<uint8_t>& b)
2309//{
2310// size_t off = 0;
2311// while (off < b.size())
2312// {
2313// if (!ensureRemaining(b, off, 1))
2314// break;
2315// auto type = static_cast<msg::DCType>(b[off]);
2316// off += 1;
2317// Logger::instance().log("localtunnel", Logger::Level::Info, msg::DCtypeString(type) + " Received!! onChat");
2318// switch (type)
2319// {
2320// case msg::DCType::ChatGroupCreate:
2321// {
2322// msg::ReadyMessage r;
2323// r.kind = type;
2324// r.fromPeer = fromPeer;
2325// r.tableId = Serializer::deserializeUInt64(b, off);
2326// r.threadId = Serializer::deserializeUInt64(b, off); // groupId
2327// r.name = Serializer::deserializeString(b, off); // group name (unique)
2328// {
2329// const int pc = Serializer::deserializeInt(b, off);
2330// std::set<std::string> parts;
2331// for (int i = 0; i < pc; ++i)
2332// parts.insert(Serializer::deserializeString(b, off));
2333// r.participants = std::move(parts);
2334// }
2335// inboundGame_.push(std::move(r));
2336// break;
2337// }
2338//
2339// case msg::DCType::ChatGroupUpdate:
2340// {
2341// msg::ReadyMessage r;
2342// r.kind = type;
2343// r.fromPeer = fromPeer;
2344// r.tableId = Serializer::deserializeUInt64(b, off);
2345// r.threadId = Serializer::deserializeUInt64(b, off);
2346// r.name = Serializer::deserializeString(b, off);
2347// {
2348// const int pc = Serializer::deserializeInt(b, off);
2349// std::set<std::string> parts;
2350// for (int i = 0; i < pc; ++i)
2351// parts.insert(Serializer::deserializeString(b, off));
2352// r.participants = std::move(parts);
2353// }
2354// inboundGame_.push(std::move(r));
2355// break;
2356// }
2357//
2358// case msg::DCType::ChatGroupDelete:
2359// {
2360// msg::ReadyMessage r;
2361// r.kind = type;
2362// r.fromPeer = fromPeer;
2363// r.tableId = Serializer::deserializeUInt64(b, off);
2364// r.threadId = Serializer::deserializeUInt64(b, off);
2365// inboundGame_.push(std::move(r));
2366// break;
2367// }
2368//
2369// case msg::DCType::ChatMessage:
2370// {
2371// msg::ReadyMessage r;
2372// r.kind = type;
2373// r.fromPeer = fromPeer;
2374// r.tableId = Serializer::deserializeUInt64(b, off);
2375// r.threadId = Serializer::deserializeUInt64(b, off); // groupId
2376// r.ts = Serializer::deserializeUInt64(b, off);
2377// r.name = Serializer::deserializeString(b, off); // username
2378// r.text = Serializer::deserializeString(b, off); // body
2379// inboundGame_.push(std::move(r));
2380// break;
2381// }
2382//
2383// default:
2384// Logger::instance().log("localtunnel", Logger::Level::Warn, "Unkown Message Type not Handled!! onChat");
2385// return; // stop this buffer if unknown/out-of-sync
2386// }
2387// }
2388//}
2389
2390void NetworkManager::decodeRawChatBuffer(const std::string& fromPeer,
2391 const std::vector<uint8_t>& b)
2392{
2393 // ---- JSON branch ----
2394 auto first_non_ws = std::find_if(b.begin(), b.end(), [](uint8_t c)
2395 { return !std::isspace((unsigned char)c); });
2396 if (first_non_ws != b.end() && *first_non_ws == '{')
2397 {
2398 try
2399 {
2400 std::string s(b.begin(), b.end()); // UTF-8
2401 msg::Json j = msg::Json::parse(s);
2402
2404 r.fromPeerId = fromPeer;
2405
2406 // type
2407 msg::DCType t;
2408 if (!msg::DCTypeFromJson(msg::getString(j, "type"), t))
2409 {
2410 Logger::instance().log("chat", Logger::Level::Warn, "JSON chat: unknown type");
2411 return;
2412 }
2413 r.kind = t;
2414
2415 // common fields
2416 if (j.contains("tableId"))
2417 r.tableId = (uint64_t)j["tableId"].get<uint64_t>();
2418 if (j.contains("groupId"))
2419 r.threadId = (uint64_t)j["groupId"].get<uint64_t>();
2420
2421 switch (t)
2422 {
2425 {
2426 if (j.contains("name"))
2427 r.name = j["name"].get<std::string>();
2428 if (j.contains("participants") && j["participants"].is_array())
2429 {
2430 std::set<std::string> parts;
2431 for (auto& e : j["participants"])
2432 parts.insert(e.get<std::string>());
2433 r.participants = std::move(parts);
2434 }
2435 break;
2436 }
2438 {
2439 // nothing extra
2440 break;
2441 }
2443 {
2444 if (j.contains("ts"))
2445 r.ts = (uint64_t)j["ts"].get<uint64_t>();
2446 if (j.contains("username"))
2447 r.name = j["username"].get<std::string>();
2448 if (j.contains("text"))
2449 r.text = j["text"].get<std::string>();
2450 break;
2451 }
2452 default:
2453 return; // ignore non-chat types here
2454 }
2455
2456 inboundGame_.push(std::move(r));
2457 return; // handled JSON
2458 }
2459 catch (const std::exception& e)
2460 {
2461 Logger::instance().log("chat", Logger::Level::Warn, std::string("JSON parse error: ") + e.what());
2462 // fall through to legacy-binary branch if you still support it
2463 }
2464 }
2465}
2466
2467void NetworkManager::decodeRawNotesBuffer(const std::string& fromPeer, const std::vector<uint8_t>& b)
2468{
2469 size_t off = 0;
2470 while (off < b.size())
2471 {
2472 if (!ensureRemaining(b, off, 1))
2473 break;
2474
2475 auto type = static_cast<msg::DCType>(b[off]);
2476 off += 1;
2477
2478 Logger::instance().log("localtunnel", Logger::Level::Info, msg::DCtypeString(type) + " Received!!");
2479 switch (type)
2480 {
2482 //handleGameTableSnapshot(b, off); // pushes ReadyMessage internally
2483 Logger::instance().log("localtunnel", Logger::Level::Info, "NoteCreate Handled!!");
2484 break;
2485
2487 //handleBoardMeta(b, off); // fills imagesRx_
2488 Logger::instance().log("localtunnel", Logger::Level::Info, "NoteUpdate Handled!!");
2489 break;
2490
2492 //handleMarkerMeta(b, off); // fills imagesRx_
2493 Logger::instance().log("localtunnel", Logger::Level::Info, "NoteDelete Handled!!");
2494 break;
2495 default:
2496 // unknown / out-of-sync: stop this buffer
2497 Logger::instance().log("localtunnel", Logger::Level::Warn, "Unkown Message Type not Handled!! onNotes");
2498 return;
2499 }
2500 }
2501}
2503{
2504 bool expected = false;
2505 if (!rawWorkerRunning_.compare_exchange_strong(expected, true))
2506 return; // already running
2507
2508 rawWorkerStop_.store(false);
2509 rawWorker_ = std::thread([this]()
2510 {
2511 using clock = std::chrono::steady_clock;
2512 auto lastFlush = clock::now();
2513 for (;;) {
2514 if (rawWorkerStop_.load())
2515 break;
2516
2517 bool didWork = false;
2518
2520 if (inboundRaw_.try_pop(r)) {
2521 try {
2522 if (r.label == msg::dc::name::Game) {
2523 decodeRawGameBuffer(r.fromPeer, r.bytes);
2524 } else if (r.label == msg::dc::name::Chat) {
2525 decodeRawChatBuffer(r.fromPeer, r.bytes);
2526 } else if (r.label == msg::dc::name::Notes) {
2527 decodeRawNotesBuffer(r.fromPeer, r.bytes);
2528 } else if (r.label == msg::dc::name::MarkerMove) {
2529 decodeRawMarkerMoveBuffer(r.fromPeer, r.bytes);
2530 }
2531 } catch (...) {
2532 // swallow
2533 }
2534 didWork = true;
2535 }
2536
2537 } });
2538}
2539
2541{
2542 if (!rawWorkerRunning_.load())
2543 return;
2544 rawWorkerStop_.store(true);
2545 if (rawWorker_.joinable())
2546 rawWorker_.join();
2547 rawWorkerRunning_.store(false);
2548}
2549
2550void NetworkManager::bootstrapPeerIfReady(const std::string& peerId)
2551{
2552 Logger::instance().log("localtunnel", Logger::Level::Info, "BeginBoostrap");
2553 auto gm = gametable_manager.lock();
2554 auto bm = board_manager.lock();
2555 if (!gm)
2556 {
2557 throw std::exception("[NetworkManager] GametableManager Expired!!");
2558 }
2559 if (!bm)
2560 {
2561 throw std::exception("[NetworkManager] BoardManager Expired!!");
2562 }
2563
2564 auto it = peers.find(peerId);
2565 if (it == peers.end() || !it->second)
2566 return;
2567 auto& link = it->second;
2568 if (link->bootstrapSent())
2569 return; // one-shot per connection
2570 if (gm->active_game_table.is_valid() && gm->active_game_table.has<GameTable>())
2571 {
2572 sendGameTable(gm->active_game_table, {peerId});
2573 Logger::instance().log("localtunnel", Logger::Level::Info, "SentGameTable");
2574 }
2575
2576 if (bm->isBoardActive())
2577 {
2578 auto boardEnt = bm->getActiveBoard();
2579 if (boardEnt.is_valid() && boardEnt.has<Board>())
2580 {
2581 sendBoard(boardEnt, {peerId}); // this sends meta + image chunks + commit
2582 Logger::instance().log("localtunnel", Logger::Level::Info, "SentBoard");
2583 }
2584 }
2585
2586 link->markBootstrapSent();
2587}
2588
2590{
2591 auto it = imagesRx_.find(id);
2592 if (it == imagesRx_.end())
2593 return;
2594 auto& p = it->second;
2595 if (!p.commitRequested || !p.isComplete())
2596 return;
2597
2599 if (kind == msg::ImageOwnerKind::Board)
2600 {
2601 if (!p.boardMeta)
2602 return; // need meta
2604 m.boardId = p.boardMeta->boardId;
2605 m.boardMeta = p.boardMeta;
2606 }
2607 else
2608 {
2609 if (!p.markerMeta)
2610 return; // need meta
2612 m.boardId = p.boardId; // for markers we also stored the boardId
2613 m.markerMeta = p.markerMeta;
2614 }
2615 m.bytes = std::move(p.buf);
2616 inboundGame_.push(std::move(m));
2617
2618 Logger::instance().log("localtunnel", Logger::Level::Info,
2619 std::string("tryFinalizeImage: finalized ") +
2620 (kind == msg::ImageOwnerKind::Board ? "Board" : "Marker") +
2621 " id=" + std::to_string(id));
2622
2623 imagesRx_.erase(it);
2624}
2625
2626// ---------- GAME FRAME BUILDERS ----------
2627
2628std::vector<unsigned char> NetworkManager::buildMarkerDeleteFrame(uint64_t boardId, uint64_t markerId)
2629{
2630 std::vector<unsigned char> b;
2631 Serializer::serializeUInt8(b, static_cast<uint8_t>(msg::DCType::MarkerDelete));
2632 Serializer::serializeUInt64(b, boardId);
2633 Serializer::serializeUInt64(b, markerId);
2634 return b;
2635}
2636
2637// ---- FOG UPDATE/DELETE ----
2638std::vector<unsigned char> NetworkManager::buildFogUpdateFrame(uint64_t boardId, const flecs::entity& fog)
2639{
2640 std::vector<unsigned char> b;
2641 Serializer::serializeUInt8(b, static_cast<uint8_t>(msg::DCType::FogUpdate));
2642 Serializer::serializeUInt64(b, boardId);
2643
2644 const auto id = fog.get<Identifier>()->id;
2645 const auto pos = *fog.get<Position>();
2646 const auto siz = *fog.get<Size>();
2647 const auto vis = *fog.get<Visibility>();
2648
2653 return b;
2654}
2655
2656std::vector<unsigned char> NetworkManager::buildFogDeleteFrame(uint64_t boardId, uint64_t fogId)
2657{
2658 std::vector<unsigned char> b;
2659 Serializer::serializeUInt8(b, static_cast<uint8_t>(msg::DCType::FogDelete));
2660 Serializer::serializeUInt64(b, boardId);
2662 return b;
2663}
2664
2665std::vector<unsigned char> NetworkManager::buildSnapshotGameTableFrame(uint64_t gameTableId, const std::string& name)
2666{
2667 std::vector<unsigned char> b;
2669 Serializer::serializeUInt64(b, gameTableId);
2671 return b;
2672}
2673
2674std::vector<unsigned char> NetworkManager::buildSnapshotBoardFrame(const flecs::entity& board, uint64_t imageBytesTotal)
2675{
2676 std::vector<unsigned char> b;
2678
2679 // Required components
2680 auto id = board.get<Identifier>()->id;
2681 auto bd = *board.get<Board>();
2682 auto pan = *board.get<Panning>();
2683 auto grid = *board.get<Grid>();
2684 auto size = *board.get<Size>();
2685
2687 Serializer::serializeString(b, bd.board_name);
2688
2689 // Panning
2690 Serializer::serializeBool(b, pan.isPanning);
2691
2692 // Grid
2693 Serializer::serializeVec2(b, grid.offset);
2694 Serializer::serializeFloat(b, grid.cell_size);
2695 Serializer::serializeBool(b, grid.is_hex);
2696 Serializer::serializeBool(b, grid.snap_to_grid);
2697 Serializer::serializeBool(b, grid.visible);
2698 Serializer::serializeFloat(b, grid.opacity);
2699
2700 // Size
2701 Serializer::serializeFloat(b, size.width);
2702 Serializer::serializeFloat(b, size.height);
2703
2704 // Image total size in bytes (u64)
2705 Serializer::serializeUInt64(b, imageBytesTotal);
2706 return b;
2707}
2708
2709std::vector<unsigned char> NetworkManager::buildCreateMarkerFrame(uint64_t boardId, const flecs::entity& marker, uint64_t imageBytesTotal)
2710{
2711 std::vector<unsigned char> b;
2712 Serializer::serializeUInt8(b, static_cast<uint8_t>(msg::DCType::MarkerCreate));
2713
2714 auto mid = marker.get<Identifier>()->id;
2715 auto pos = *marker.get<Position>();
2716 auto sz = *marker.get<Size>();
2717 auto vis = *marker.get<Visibility>();
2718 auto mov = *marker.get<Moving>();
2719
2720 std::string name = "marker_" + std::to_string(mid);
2721
2722 Serializer::serializeUInt64(b, boardId);
2725
2730
2731 Serializer::serializeUInt64(b, imageBytesTotal);
2732 return b;
2733}
2734
2735std::vector<unsigned char> NetworkManager::buildFogCreateFrame(uint64_t boardId, const flecs::entity& fog)
2736{
2737 std::vector<unsigned char> b;
2738 Serializer::serializeUInt8(b, static_cast<uint8_t>(msg::DCType::FogCreate));
2739
2740 auto fid = fog.get<Identifier>()->id;
2741 auto pos = *fog.get<Position>();
2742 auto sz = *fog.get<Size>();
2743 auto vis = *fog.get<Visibility>();
2744
2745 Serializer::serializeUInt64(b, boardId);
2750 return b;
2751}
2752
2753std::vector<unsigned char> NetworkManager::buildImageChunkFrame(uint8_t ownerKind, uint64_t id, uint64_t offset, const unsigned char* data, size_t len)
2754{
2755 std::vector<unsigned char> b;
2756 Serializer::serializeUInt8(b, static_cast<uint8_t>(msg::DCType::ImageChunk));
2757 Serializer::serializeUInt8(b, ownerKind);
2759 Serializer::serializeUInt64(b, offset);
2760 // len as int32
2761 Serializer::serializeInt(b, static_cast<int>(len));
2762 b.insert(b.end(), data, data + len);
2763 return b;
2764}
2765
2766std::vector<unsigned char> NetworkManager::buildCommitBoardFrame(uint64_t boardId)
2767{
2768 std::vector<unsigned char> b;
2769 Serializer::serializeUInt8(b, static_cast<uint8_t>(msg::DCType::CommitBoard));
2770 Serializer::serializeUInt64(b, boardId);
2771 return b;
2772}
2773
2774std::vector<unsigned char> NetworkManager::buildCommitMarkerFrame(uint64_t boardId, uint64_t markerId)
2775{
2776 std::vector<unsigned char> b;
2777 Serializer::serializeUInt8(b, static_cast<uint8_t>(msg::DCType::CommitMarker));
2778 Serializer::serializeUInt64(b, boardId);
2779 Serializer::serializeUInt64(b, markerId);
2780 return b;
2781}
2782
2783void NetworkManager::sendGameTo(const std::string& peerId, const std::vector<unsigned char>& bytes)
2784{
2785 auto it = peers.find(peerId);
2786 if (it == peers.end() || !it->second)
2787 return;
2788 it->second->sendGame(bytes);
2789}
2790
2791void NetworkManager::broadcastGameFrame(const std::vector<unsigned char>& frame, const std::vector<std::string>& toPeerIds)
2792{
2793 for (auto& pid : toPeerIds)
2794 {
2795 sendGameTo(pid, frame);
2796 }
2797}
ConnectionType
Definition Message.h:20
Role
Definition Message.h:13
@ PLAYER
@ GAMEMASTER
static void setLocalTunnelHandlers(std::function< std::string()> startHandler, std::function< void()> stopHandler)
static void setIdentityLogger(std::function< std::string()> identityLogger)
static Logger & instance()
Definition Logger.h:39
bool try_pop(T &value)
void push(T value)
void markDraggingLocal(uint64_t markerId, bool dragging)
bool shouldApplyMarkerMoveStateFinal(const msg::ReadyMessage &m)
void handleFogUpdate(const std::vector< uint8_t > &b, size_t &off)
unsigned short getPort() const
void onPeerLocalCandidate(const std::string &peerId, const rtc::Candidate &cand)
std::vector< uint8_t > buildFogCreateFrame(uint64_t boardId, const flecs::entity &fog)
std::string local_ip_address
void setup(std::weak_ptr< BoardManager > board_manager, std::weak_ptr< GameTableManager > gametable_manager)
bool isMarkerBeingDragged(uint64_t markerId) const
void handleFogDelete(const std::vector< uint8_t > &b, size_t &off)
const std::string & getGMId() const
std::shared_ptr< ImGuiToaster > toaster_
bool removePeer(std::string peerId)
void broadcastMarker(uint64_t boardId, const flecs::entity &marker)
std::string getLocalTunnelURL()
void startServer(ConnectionType mode, unsigned short port, bool tryUpnp)
const std::string & getMyPeerId() const
static uint64_t nowMs()
void clearDragState(uint64_t markerId)
std::vector< std::string > getConnectedPeerIds() const
std::vector< unsigned char > buildFogUpdateFrame(uint64_t boardId, const flecs::entity &fog)
std::vector< uint8_t > buildCommitBoardFrame(uint64_t boardId)
std::string getNetworkInfo(ConnectionType type)
bool shouldApplyMarkerMoveStateStart(const msg::ReadyMessage &m)
void broadcastGameTable(const flecs::entity &gameTable)
void buildUserNameUpdate(std::vector< uint8_t > &out, uint64_t tableId, const std::string &userUniqueId, const std::string &oldUsername, const std::string &newUsername, bool reboundFlag) const
void broadcastMarkerMove(uint64_t boardId, const flecs::entity &marker)
std::unordered_map< uint64_t, PendingImage > imagesRx_
std::string external_ip_address
void broadcastMarkerUpdate(uint64_t boardId, const flecs::entity &marker)
void sendMarkerMove(uint64_t boardId, const flecs::entity &marker, const std::vector< std::string > &toPeerIds)
static bool tieBreakWins(const std::string &challengerPeerId, const std::string &currentOwnerPeerId)
std::shared_ptr< IdentityManager > identity_manager
std::shared_ptr< SignalingClient > signalingClient
bool broadcastChatJson(const msg::Json &j)
void sendFogUpdate(uint64_t boardId, const flecs::entity &fog, const std::vector< std::string > &toPeerIds)
std::string decodingFromPeer_
void forceCloseDrag(uint64_t markerId)
void sendMarkerUpdate(uint64_t boardId, const flecs::entity &marker, const std::vector< std::string > &toPeerIds)
void broadcastMarkerDelete(uint64_t boardId, const flecs::entity &marker)
void decodeRawNotesBuffer(const std::string &fromPeer, const std::vector< uint8_t > &b)
std::shared_ptr< PeerLink > ensurePeerLink(const std::string &peerId)
unsigned int port
void broadcastPeerDisconnect(const std::string &targetId)
std::weak_ptr< GameTableManager > gametable_manager
void pushStatusToast(const std::string &msg, ImGuiToaster::Level lvl, float durationSec=5.0f)
std::unordered_map< uint64_t, DragState > drag_
void upsertPeerIdentityWithUnique(const std::string &peerId, const std::string &uniqueId, const std::string &username)
void ShowPortForwardingHelpPopup(bool *p_open)
void handleCommitMarker(const std::vector< uint8_t > &b, size_t &off)
void handleCommitBoard(const std::vector< uint8_t > &b, size_t &off)
void handleMarkerMoveState(const std::vector< uint8_t > &b, size_t &off)
void handleUserNameUpdate(const std::vector< uint8_t > &b, size_t &off)
void sendMarkerMoveState(uint64_t boardId, const flecs::entity &marker, const std::vector< std::string > &toPeerIds)
std::string getMyUsername() const
void broadcastMarkerMoveState(uint64_t boardId, const flecs::entity &marker)
void decodeRawGameBuffer(const std::string &fromPeer, const std::vector< uint8_t > &b)
void handleGridUpdate(const std::vector< uint8_t > &b, size_t &off)
std::vector< uint8_t > buildCommitMarkerFrame(uint64_t boardId, uint64_t markerId)
void setPort(unsigned int port)
std::unordered_map< std::string, std::shared_ptr< PeerLink > > peers
std::vector< unsigned char > buildGridUpdateFrame(uint64_t boardId, const Grid &grid)
void handleImageChunk(const std::vector< uint8_t > &b, size_t &off)
void onPeerLocalDescription(const std::string &peerId, const rtc::Description &desc)
bool shouldApplyMarkerMove(const msg::ReadyMessage &m)
NetworkManager(flecs::world ecs, std::shared_ptr< IdentityManager > identity_manager)
void broadcastFogDelete(uint64_t boardId, const flecs::entity &fog)
bool hasAnyPeer() const
void broadcastBoard(const flecs::entity &board)
std::string displayNameForPeer(const std::string &peerId) const
static constexpr int kPaceMillis
void handleBoardMeta(const std::vector< uint8_t > &b, size_t &off)
void handleFogCreate(const std::vector< uint8_t > &b, size_t &off)
void sendMarker(uint64_t boardId, const flecs::entity &marker, const std::vector< std::string > &toPeerIds)
bool amIDragging(uint64_t markerId) const
void sendGameTo(const std::string &peerId, const std::vector< unsigned char > &bytes)
void parseConnectionString(std::string connection_string, std::string &server, unsigned short &port, std::string &password)
void decodeRawChatBuffer(const std::string &fromPeer, const std::vector< uint8_t > &b)
void broadcastFogUpdate(uint64_t boardId, const flecs::entity &fog)
void handleGameTableSnapshot(const std::vector< uint8_t > &b, size_t &off)
std::string debugIdentitySnapshot() const
std::weak_ptr< BoardManager > board_manager
std::atomic< bool > rawWorkerRunning_
static constexpr int kPaceEveryN
void sendBoard(const flecs::entity &board, const std::vector< std::string > &toPeerIds)
void sendGridUpdate(uint64_t boardId, const flecs::entity &board, const std::vector< std::string > &toPeerIds)
bool isHosting() const
void sendFogDelete(uint64_t boardId, const flecs::entity &fog, const std::vector< std::string > &toPeerIds)
char network_password[124]
void broadcastUserNameUpdate(const std::vector< uint8_t > &payload)
bool sendImageChunks(msg::ImageOwnerKind kind, uint64_t id, const std::vector< unsigned char > &img, const std::vector< std::string > &toPeerIds)
std::string getNetworkPassword() const
std::vector< unsigned char > buildMarkerMoveFrame(uint64_t boardId, const flecs::entity &marker, uint32_t seq)
MessageQueue< msg::ReadyMessage > inboundGame_
std::vector< uint8_t > buildCreateMarkerFrame(uint64_t boardId, const flecs::entity &marker, uint64_t imageBytesTotal)
std::vector< std::string > getConnectedUsernames() const
void tryFinalizeImage(msg::ImageOwnerKind kind, uint64_t id)
int connectedPeerCount() const
std::shared_ptr< SignalingServer > signalingServer
std::string getExternalIPAddress()
std::vector< unsigned char > readFileBytes(const std::string &path)
MessageQueue< msg::NetEvent > events_
void bootstrapPeerIfReady(const std::string &peerId)
bool connectToPeer(const std::string &connectionString)
void handleMarkerUpdate(const std::vector< uint8_t > &b, size_t &off)
std::vector< unsigned char > buildMarkerDeleteFrame(uint64_t boardId, uint64_t markerId)
std::string getLocalIPAddress()
static bool hasUrlScheme(const std::string &s)
void broadcastFog(uint64_t boardId, const flecs::entity &fog)
void sendFog(uint64_t boardId, const flecs::entity &fog, const std::vector< std::string > &toPeerIds)
std::thread rawWorker_
std::vector< unsigned char > buildMarkerUpdateFrame(uint64_t boardId, const flecs::entity &marker)
std::string getMyUniqueId() const
const std::string & getCustomHost() const
std::size_t removeDisconnectedPeers()
void setNetworkPassword(const char *password)
void drainInboundRaw(int maxPerTick)
std::vector< uint8_t > buildSnapshotGameTableFrame(uint64_t gameTableId, const std::string &name)
std::atomic< bool > rawWorkerStop_
void handleMarkerMeta(const std::vector< uint8_t > &b, size_t &off)
std::vector< uint8_t > buildImageChunkFrame(uint8_t ownerKind, uint64_t id, uint64_t offset, const uint8_t *data, size_t len)
void sendUserNameUpdateTo(const std::string &peerId, const std::vector< uint8_t > &payload)
void decodeRawMarkerMoveBuffer(const std::string &fromPeer, const std::vector< uint8_t > &b)
std::vector< unsigned char > buildFogDeleteFrame(uint64_t boardId, uint64_t fogId)
std::vector< unsigned char > buildMarkerMoveStateFrame(uint64_t boardId, const flecs::entity &marker)
void sendGameTable(const flecs::entity &gameTable, const std::vector< std::string > &toPeerIds)
void broadcastGridUpdate(uint64_t boardId, const flecs::entity &board)
void broadcastGameFrame(const std::vector< unsigned char > &frame, const std::vector< std::string > &toPeerIds)
bool sendChatJsonTo(const std::string &peerId, const msg::Json &j)
static bool ensureRemaining(const std::vector< uint8_t > &b, size_t off, size_t need)
static constexpr size_t kChunk
std::vector< uint8_t > buildSnapshotBoardFrame(const flecs::entity &board, uint64_t imageBytesTotal)
void sendMarkerDelete(uint64_t boardId, const flecs::entity &marker, const std::vector< std::string > &toPeerIds)
void handleMarkerDelete(const std::vector< uint8_t > &b, size_t &off)
void handleMarkerMove(const std::vector< uint8_t > &b, size_t &off)
MessageQueue< msg::InboundRaw > inboundRaw_
static void stopLocalTunnel()
static void setupTLS()
static std::string httpGet(const std::wstring &host, const std::wstring &path)
static std::string getLocalTunnelUrl()
static std::string getLocalIPv4Address()
static std::string startLocalTunnel(const std::string &subdomainBase, int port)
static void safeCloseWebSocket(std::shared_ptr< rtc::WebSocket > &ws)
static fs::path getMarkersPath()
Definition PathManager.h:66
static bool isFilenameOnly(const std::string &s)
static bool isPathLike(const std::string &s)
static fs::path getMapsPath()
Definition PathManager.h:61
static Size deserializeSize(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:422
static uint32_t deserializeUInt32(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:353
static void serializeBool(std::vector< unsigned char > &buffer, bool value)
Definition Serializer.h:312
static Visibility deserializeVisibility(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:435
static void serializeUInt8(std::vector< unsigned char > &buffer, uint8_t value)
Definition Serializer.h:341
static int deserializeInt(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:367
static std::string deserializeString(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:388
static Grid deserializeGrid(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:492
static void serializeVec2(std::vector< unsigned char > &buffer, const glm::vec2 &vec)
Definition Serializer.h:325
static Position deserializePosition(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:409
static void serializeUInt64(std::vector< unsigned char > &buffer, uint64_t value)
Definition Serializer.h:331
static Panning deserializePanning(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:475
static void serializeSize(std::vector< unsigned char > &buffer, const Size *size)
Definition Serializer.h:416
static void serializeString(std::vector< unsigned char > &buffer, const std::string &str)
Definition Serializer.h:318
static void serializeFloat(std::vector< unsigned char > &buffer, float value)
Definition Serializer.h:307
static void serializePosition(std::vector< unsigned char > &buffer, const Position *position)
Definition Serializer.h:403
static uint8_t deserializeUInt8(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:346
static MarkerComponent deserializeMarkerComponent(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:292
static void serializeMarkerComponent(std::vector< unsigned char > &buffer, const MarkerComponent *marker_component)
Definition Serializer.h:284
static uint64_t deserializeUInt64(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:360
static void serializeMoving(std::vector< unsigned char > &buffer, const Moving *moving)
Definition Serializer.h:442
static void serializeUInt32(std::vector< unsigned char > &buffer, uint32_t value)
Definition Serializer.h:336
static Moving deserializeMoving(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:447
static void serializeInt(std::vector< unsigned char > &buffer, int value)
Definition Serializer.h:302
static void serializeGrid(std::vector< unsigned char > &buffer, const Grid *grid)
Definition Serializer.h:482
static void serializeVisibility(std::vector< unsigned char > &buffer, const Visibility *visibility)
Definition Serializer.h:430
static bool addPortMapping(const std::string &internalIp, unsigned short internalPort, unsigned short externalPort, const std::string &protocol, const std::string &description="", unsigned int duration=0)
Definition UPnPManager.h:60
constexpr std::string Chat
Definition Message.h:298
constexpr std::string Game
Definition Message.h:297
constexpr std::string Notes
Definition Message.h:299
constexpr std::string MarkerMove
Definition Message.h:300
Definition Message.h:28
bool DCTypeFromJson(std::string_view s, DCType &out)
Definition Message.h:322
Json makeOffer(const std::string &from, const std::string &to, const std::string &sdp, const std::string &username, const std::string &uniqueId, const std::string &broadcast=msg::value::False)
Definition Message.h:417
ImageOwnerKind
Definition Message.h:140
std::string getString(const Json &j, std::string k, std::string def={})
Definition Message.h:579
DCType
Definition Message.h:31
nlohmann::json Json
Definition Message.h:363
nlohmann::json makePeerDisconnect(const std::string &targetPeerId, bool broadcast=true)
Definition Message.h:466
Json makeCandidate(const std::string &from, const std::string &to, const std::string &cand, const std::string &broadcast=msg::value::False)
Definition Message.h:448
Json makeAnswer(const std::string &from, const std::string &to, const std::string &sdp, const std::string &username, const std::string &uniqueId, const std::string &broadcast=msg::value::False)
Definition Message.h:433
std::string DCtypeString(DCType type)
Definition Message.h:61
msg::ImageOwnerKind kind
std::string image_path
Definition Components.h:47
uint64_t boardId
Definition Message.h:158
uint64_t boardId
Definition Message.h:148
std::string fromPeerId
Definition Message.h:169