RunicVTT Open Source Virtual Tabletop for TTRPG using P2P
Loading...
Searching...
No Matches
GameTableManager.cpp
Go to the documentation of this file.
1#include "GameTableManager.h"
2#include "imgui_internal.h"
3#include "Serializer.h"
4#include "SignalingServer.h"
5#include "UPnPManager.h"
6#include "Logger.h"
7#include "random"
8
9GameTableManager::GameTableManager(flecs::world ecs, std::shared_ptr<DirectoryWindow> map_directory, std::shared_ptr<DirectoryWindow> marker_directory) :
10 ecs(ecs), identity_manager(std::make_shared<IdentityManager>()), network_manager(std::make_shared<NetworkManager>(ecs, identity_manager)), map_directory(map_directory), board_manager(std::make_shared<BoardManager>(ecs, network_manager, identity_manager, map_directory, marker_directory))
11{
12 identity_manager->loadMyIdentityFromFile();
13 identity_manager->loadAddressBookFromFile();
14 if (identity_manager->myUniqueId().empty())
15 {
16 // any stable generator you like — here is a tiny one-liner fallback:
17 const std::string newUid = std::to_string(std::random_device{}()) + "-" + std::to_string(std::random_device{}());
18 identity_manager->setMyIdentity(newUid, "Player");
19 }
20 chat_manager = std::make_shared<ChatManager>(network_manager, identity_manager);
21
22 std::filesystem::path map_directory_path = PathManager::getMapsPath();
23 map_directory->directoryName = "MapDiretory";
24 map_directory->directoryPath = map_directory_path.string();
25 map_directory->startMonitoring();
26 map_directory->generateTextureIDs();
27}
28
30{
31 network_manager->setup(board_manager, weak_from_this());
32}
33
37
43
44void GameTableManager::loadGameTable(std::filesystem::path game_table_file_path)
45{
46 std::ifstream inFile(game_table_file_path, std::ios::binary);
47 if (inFile)
48 {
49 std::vector<unsigned char> buffer((std::istreambuf_iterator<char>(inFile)), std::istreambuf_iterator<char>());
50 inFile.close();
51
52 size_t offset = 0;
54 auto tableId = active_game_table.get<Identifier>()->id;
55 game_table_name = active_game_table.get<GameTable>()->gameTableName;
56 chat_manager->setActiveGameTable(tableId, game_table_name);
57
58 ecs.defer_begin();
59 try
60 {
61 active_game_table.children([&](flecs::entity child)
62 {
63 if (child.has<Board>()) {
64 auto texture = child.get_mut<TextureComponent>();
65 auto board_image = map_directory->getImageByPath(texture->image_path);
66 texture->textureID = board_image.textureID;
67 texture->size = board_image.size;
68
69 child.children([&](flecs::entity grand_child) {
70 if (grand_child.has<MarkerComponent>()) {
71 auto grand_child_texture = grand_child.get_mut<TextureComponent>();
72 auto marker_image = board_manager->marker_directory->getImageByPath(grand_child_texture->image_path);
73 grand_child_texture->textureID = marker_image.textureID;
74 grand_child_texture->size = marker_image.size;
75 }
76 });
77 board_manager->setActiveBoard(child);
78 } });
79 }
80 catch (const std::exception&)
81 {
82 std::cout << "ERROR LOADING IMAGES" << std::endl;
83 }
84 ecs.defer_end();
85 toaster_->Push(ImGuiToaster::Level::Good, "GameTable '" + game_table_name + "' Saved Successfuly!!");
86 }
87 else
88 {
89 toaster_->Push(ImGuiToaster::Level::Error, "Failed Saving GameTable!!!");
90 std::cerr << "Failed to load GameTable from " << game_table_file_path.string() << std::endl;
91 }
92}
93
95{
96 return board_manager->isBoardActive();
97}
98
100{
101 return active_game_table.is_valid();
102}
103
105{
106 return network_manager->isConnected();
107}
108
110{
111 constexpr int kMaxPerFrame = 32; // avoid long stalls
112
113 /* static uint64_t last = 0;
114 uint64_t t = nowMs();
115 if (t - last >= 30000)
116 {
117 network_manager->housekeepPeers();
118 last = t;
119 }*/
120
121 network_manager->drainInboundRaw(kMaxPerFrame);
122 network_manager->drainEvents();
123 int processed = 0;
125 while (processed < kMaxPerFrame && network_manager->tryPopReadyMessage(m))
126 {
127 Logger::instance().log("localtunnel", Logger::Level::Info, msg::DCtypeString(m.kind) + "RECEIVED ON PROCESS!!!");
128 switch (m.kind)
129 {
131 {
132 if (!m.tableId || !m.name)
133 break;
134 active_game_table = ecs.entity("GameTable")
135 .set(GameTable{*m.name})
136 .set(Identifier{*m.tableId});
137 game_table_name = *m.name;
138 chat_manager->setActiveGameTable(*m.tableId, *m.name);
139 Logger::instance().log("localtunnel", Logger::Level::Info, "GameTable Created!!");
140
141 break;
142 }
143
145 {
146 if (!m.boardId || !m.boardMeta)
147 break;
148 GLuint tex = 0;
149 glm::vec2 texSize{0, 0};
150 if (m.bytes && !m.bytes->empty())
151 {
152 auto image = board_manager->LoadTextureFromMemory(m.bytes->data(),
153 m.bytes->size());
154 tex = image.textureID;
155 texSize = image.size;
156 Logger::instance().log("localtunnel", Logger::Level::Info, "Board Texture Created: " + std::to_string(tex));
157 }
158
159 const auto& bm = *m.boardMeta;
160 auto board = ecs.entity()
161 .set(Identifier{bm.boardId})
162 .set(Board{bm.boardName})
163 .set(Panning{false})
164 .set(Grid{bm.grid})
165 .set(TextureComponent{tex, "", texSize})
166 .set(Size{texSize.x, texSize.y});
167
168 board_manager->setActiveBoard(board);
169 Logger::instance().log("localtunnel", Logger::Level::Info, "Board Created!!");
170 break;
171 }
172
174 {
175 if (!m.boardId || !m.markerMeta)
176 break;
177 auto boardEnt = board_manager->findBoardById(*m.boardId);
178 if (!boardEnt.is_valid())
179 break;
180
181 GLuint tex = 0;
182 glm::vec2 texSize{m.markerMeta->size.width, m.markerMeta->size.height};
183 Logger::instance().log("localtunnel", Logger::Level::Info, "Marker Texture Byte Size: " + std::to_string(m.bytes->size()));
184 if (m.bytes && !m.bytes->empty())
185 {
186 auto image = board_manager->LoadTextureFromMemory(m.bytes->data(),
187 m.bytes->size());
188 tex = image.textureID;
189 texSize = image.size;
190 Logger::instance().log("localtunnel", Logger::Level::Info, "Marker Texture Created: " + std::to_string(tex));
191 }
192 const auto& mm = *m.markerMeta;
193 flecs::entity marker = ecs.entity()
194 .set(Identifier{mm.markerId})
195 .set(Position{mm.pos.x, mm.pos.y}) //World Position
196 .set(Size{mm.size.width, mm.size.height})
197 .set(TextureComponent{tex, "", texSize})
198 .set(Visibility{mm.vis})
199 .set(MarkerComponent{"", "", false, false})
200 .set(Moving{mm.mov});
201 marker.add(flecs::ChildOf, boardEnt);
202 Logger::instance().log("localtunnel", Logger::Level::Info, "Marker Created");
203 break;
204 }
205
207 {
208 if (!m.boardId || !m.fogId || !m.pos || !m.size || !m.vis)
209 break;
210 auto boardEnt = board_manager->findBoardById(*m.boardId);
211 if (!boardEnt.is_valid())
212 break;
213
214 auto fog = ecs.entity()
215 .set(Identifier{*m.fogId})
216 .set(Position{m.pos->x, m.pos->y})
217 .set(Size{m.size->width, m.size->height})
218 .set(Visibility{*m.vis});
219
220 fog.add<FogOfWar>();
221 fog.add(flecs::ChildOf, boardEnt);
222 Logger::instance().log("localtunnel", Logger::Level::Info, "Fog Created");
223 break;
224 }
225
227 {
228 if (!m.boardId || !m.fogId)
229 break;
230 auto boardEnt = board_manager->findBoardById(*m.boardId);
231 if (!boardEnt.is_valid())
232 break;
233
234 flecs::entity fogEnt;
235 boardEnt.children([&](flecs::entity child)
236 {
237 if (child.has<FogOfWar>()) {
238 auto id = child.get<Identifier>()->id;
239 if (id == *m.fogId) fogEnt = child;
240 } });
241 if (!fogEnt.is_valid())
242 break;
243
244 if (m.pos)
245 fogEnt.set<Position>(*m.pos);
246 if (m.size)
247 fogEnt.set<Size>(*m.size);
248 if (m.vis)
249 fogEnt.set<Visibility>(*m.vis);
250 if (m.mov)
251 fogEnt.set<Moving>(*m.mov); // if used
252 break;
253 }
254
256 {
257 if (!m.boardId || !m.fogId)
258 break;
259 auto boardEnt = board_manager->findBoardById(*m.boardId);
260 if (!boardEnt.is_valid())
261 break;
262
263 flecs::entity fogEnt;
264 boardEnt.children([&](flecs::entity child)
265 {
266 if (child.has<FogOfWar>()) {
267 auto id = child.get<Identifier>()->id;
268 if (id == *m.fogId) fogEnt = child;
269 } });
270 if (fogEnt.is_valid())
271 fogEnt.destruct();
272 break;
273 }
274
276 {
277 // Epoch/seq gate (no side effects)
278 if (!network_manager->shouldApplyMarkerMove(m))
279 break;
280
281 if (!m.boardId || !m.markerId || !m.pos)
282 break;
283
284 auto boardEnt = board_manager->findBoardById(*m.boardId);
285 if (!boardEnt.is_valid())
286 break;
287
288 auto markerEnt = findMarkerInBoard(boardEnt, *m.markerId);
289 if (!markerEnt.is_valid())
290 break;
291
292 // Apply streaming position; keep Moving true during drag
293 markerEnt.set<Position>(*m.pos);
294 //markerEnt.set<Moving>(Moving{true});
295 break;
296 }
297
299 {
300 if (!m.boardId || !m.markerId)
301 break;
302
303 auto boardEnt = board_manager->findBoardById(*m.boardId);
304 if (!boardEnt.is_valid())
305 break;
306
307 auto markerEnt = findMarkerInBoard(boardEnt, *m.markerId);
308 if (!markerEnt.is_valid())
309 break;
310
311 // Start of drag
312 if (m.mov && m.mov->isDragging)
313 {
314 if (!network_manager->shouldApplyMarkerMoveStateStart(m))
315 break;
316
317 // optional visual sync (safe; local drags are already set locally)
318 markerEnt.set<Moving>(Moving{true});
319 }
320 else // End of drag (final)
321 {
322 if (!network_manager->shouldApplyMarkerMoveStateFinal(m))
323 break;
324
325 if (m.pos)
326 markerEnt.set<Position>(*m.pos);
327 markerEnt.set<Moving>(Moving{false}); // ensure drag ends
328 }
329 break;
330 }
331
333 {
334 if (!m.boardId || !m.markerId)
335 break;
336
337 auto boardEnt = board_manager->findBoardById(*m.boardId);
338 if (!boardEnt.is_valid())
339 break;
340
341 auto markerEnt = findMarkerInBoard(boardEnt, *m.markerId);
342 if (!markerEnt.is_valid())
343 break;
344
345 // Apply only non-movement attributes
346 if (m.size)
347 markerEnt.set<Size>(*m.size);
348 if (m.vis)
349 markerEnt.set<Visibility>(*m.vis);
350 if (m.markerComp)
351 {
352 std::string oldOwnerUid = markerEnt.get<MarkerComponent>()->ownerUniqueId;
353 if (oldOwnerUid != m.markerComp->ownerUniqueId)
354 network_manager->drag_.erase(*m.markerId);
355 markerEnt.set<MarkerComponent>(*m.markerComp);
356 }
357
358 break;
359 }
360
362 {
363 if (!m.boardId || !m.markerId)
364 break;
365 auto boardEnt = board_manager->findBoardById(*m.boardId);
366 if (!boardEnt.is_valid())
367 break;
368
369 flecs::entity markerEnt;
370 boardEnt.children([&](flecs::entity child)
371 {
372 if (child.has<MarkerComponent>()) {
373 auto id = child.get<Identifier>()->id;
374 if (id == *m.markerId) markerEnt = child;
375 } });
376 if (markerEnt.is_valid())
377 markerEnt.destruct();
378 break;
379 }
380
381 // GameTableManager.cpp — inside processReceivedMessages() switch:
383 {
384 if (!m.tableId || !m.userUniqueId || !m.name || !m.text)
385 break;
386 if (*m.tableId != chat_manager->currentTableId_)
387 break;
388
389 const std::string uniqueId = *m.userUniqueId; // now uniqueId
390 const std::string fromPeerId = m.fromPeerId;
391 const std::string newU = *m.name;
392
393 // 1) record in address book
394 network_manager->upsertPeerIdentityWithUnique(/*peerId=*/m.fromPeerId, /*uniqueId=*/uniqueId, /*username=*/newU);
395 identity_manager->setUsernameForUnique(uniqueId, newU);
396 board_manager->onUsernameChanged(uniqueId, newU);
397 chat_manager->replaceUsernameForUnique(uniqueId, newU);
398
399 break;
400 }
401
403 {
404 chat_manager->applyReady(m);
405 break;
406 }
408 {
409 chat_manager->applyReady(m);
410 break;
411 }
413 {
414 chat_manager->applyReady(m);
415 break;
416 }
418 {
419 chat_manager->applyReady(m);
420 break;
421 }
422
424 {
425 if (!m.boardId || !m.grid)
426 break;
427
428 auto boardEnt = board_manager->findBoardById(*m.boardId);
429 if (!boardEnt.is_valid())
430 break;
431
432 boardEnt.set<Grid>(*m.grid);
433 break;
434 }
435
437 {
438 break;
439 }
440
442 {
443 break;
444 }
445
447 {
448 break;
449 }
450
451 default:
452 break;
453 }
454
455 ++processed;
456 }
457}
458
459void GameTableManager::setCameraFboDimensions(glm::vec2 fbo_dimensions)
460{
461 board_manager->camera.setFboDimensions(fbo_dimensions);
462};
463
464void GameTableManager::handleInputs(glm::vec2 current_mouse_fbo_pos)
465{
466 current_mouse_world_pos = board_manager->camera.screenToWorldPosition(current_mouse_fbo_pos);
467 this->current_mouse_fbo_pos = current_mouse_fbo_pos;
468
469 // Call individual handlers
471 handleCursorInputs(); // This will primarily deal with dragging/panning
473}
474
476{
477 // Check if board is active first
478 if (!isBoardActive())
479 return;
480
481 // Left Mouse Button Press
483 {
484 if (board_manager->getCurrentTool() == Tool::MOVE)
485 {
486 if (board_manager->isMouseOverMarker(current_mouse_world_pos) /*and !board_manager->isDraggingMarker()*/)
487 {
488 board_manager->startMouseDrag(current_mouse_world_pos, false); // Drag Marker
489 }
490 else /*if(!board_manager->isPanning())*/
491 {
492 board_manager->startMouseDrag(current_mouse_world_pos, true); // Pan Board
493 }
494 }
495 if (board_manager->getCurrentTool() == Tool::FOG and !board_manager->isCreatingFog())
496 {
497 board_manager->startMouseDrag(current_mouse_world_pos, false); // Start fog drawing/erasing
498 }
499 if (board_manager->getCurrentTool() == Tool::SELECT)
500 {
501 auto entity = board_manager->getEntityAtMousePosition(current_mouse_world_pos);
502 if (entity.is_valid())
503 {
504 board_manager->setShowEditWindow(true, entity);
505 }
506 }
507 }
508
509 // Left Mouse Button Release
510 if (mouse_left_released || ImGui::IsMouseReleased(ImGuiMouseButton_Left))
511 {
512 if (board_manager->isPanning() || board_manager->isDraggingMarker())
513 {
514 board_manager->endMouseDrag();
515 }
516 if (board_manager->isCreatingFog())
517 {
518 board_manager->handleFogCreation(current_mouse_world_pos); // Use world_pos
519 board_manager->endMouseDrag();
520 }
521 }
522
523 board_manager->killIfMouseUp(ImGui::IsMouseDown(ImGuiMouseButton_Left));
524}
525
527{
528 if (!isBoardActive())
529 return;
530
531 if (board_manager->isDraggingMarker())
532 {
533 board_manager->handleMarkerDragging(current_mouse_world_pos);
534 }
535
536 if (board_manager->isPanning())
537 {
539 }
540}
541
543{
544 if (!isBoardActive())
545 return;
546 if (mouse_wheel_delta != 0.0f)
547 {
548 float zoom_factor;
549 if (mouse_wheel_delta > 0)
550 {
551 zoom_factor = 1.1f; // Zoom in by 10%
552 }
553 else
554 {
555 zoom_factor = 0.9f; // Zoom out by 10%
556 }
557 board_manager->camera.zoom(zoom_factor, current_mouse_world_pos);
558 }
559}
560
561void GameTableManager::createGameTableFile(flecs::entity game_table)
562{
563 namespace fs = std::filesystem;
564 auto game_tables_directory = PathManager::getGameTablesPath();
565 auto active_game_table_folder = game_tables_directory / game_table_name;
566 if (!fs::exists(active_game_table_folder) && !fs::is_directory(active_game_table_folder))
567 {
568 std::filesystem::create_directory(active_game_table_folder);
569 }
570
571 std::vector<unsigned char> buffer;
573
574 auto game_table_file = active_game_table_folder / (game_table_name + ".runic");
575 std::ofstream file(game_table_file.string(), std::ios::binary);
576 if (file.is_open())
577 {
578 file.write(reinterpret_cast<const char*>(buffer.data()), buffer.size());
579 file.close();
580 }
581 else
582 {
583 std::cerr << "Failed to open the file." << std::endl;
584 }
585}
586
587//
588std::vector<std::string> GameTableManager::listBoardFiles()
589{
590 namespace fs = std::filesystem;
591 auto game_tables_directory = PathManager::getGameTablesPath();
592 auto active_game_table_folder = game_tables_directory / game_table_name;
593 if (!fs::exists(active_game_table_folder) && !fs::is_directory(active_game_table_folder))
594 {
595 std::filesystem::create_directory(active_game_table_folder);
596 }
597 auto game_table_boards_folder = active_game_table_folder / "Boards";
598
599 if (!fs::exists(game_table_boards_folder) && !fs::is_directory(game_table_boards_folder))
600 {
601 std::filesystem::create_directory(game_table_boards_folder);
602 }
603 std::vector<std::string> boards;
604 for (const auto& entry : std::filesystem::directory_iterator(game_table_boards_folder))
605 {
606 if (entry.is_regular_file())
607 {
608 boards.emplace_back(entry.path().filename().string());
609 }
610 }
611
612 return boards;
613}
614
615std::vector<std::string> GameTableManager::listGameTableFiles()
616{
617 namespace fs = std::filesystem;
618 auto game_tables_directory = PathManager::getGameTablesPath();
619
620 // Verifica se o diretório "GameTables" existe
621 if (!fs::exists(game_tables_directory) || !fs::is_directory(game_tables_directory))
622 {
623 std::cerr << "GameTables directory does not exist!" << std::endl;
624 return {};
625 }
626 std::vector<std::string> game_tables;
627 // Itera sobre todos os diretórios dentro de "GameTables"
628 for (const auto& folder : fs::directory_iterator(game_tables_directory))
629 {
630 if (folder.is_directory())
631 {
632 std::string folder_name = folder.path().filename().string();
633 auto runic_file_path = folder.path() / (folder_name + ".runic");
634
635 // Verifica se o arquivo "folder_name.runic" existe dentro da pasta
636 if (fs::exists(runic_file_path) && fs::is_regular_file(runic_file_path))
637 {
638 game_tables.emplace_back(runic_file_path.filename().string()); // Adiciona o nome do arquivo na lista
639 }
640 }
641 }
642
643 return game_tables;
644}
645
646// ----------------------------- GUI --------------------------------------------------------------------------------
647
649{
650 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
651 ImGui::SetNextWindowSizeConstraints(ImVec2(800, 600), ImVec2(FLT_MAX, FLT_MAX));
652 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
653 if (ImGui::BeginPopupModal("CreateBoard"))
654 {
655 ImGui::SetItemDefaultFocus();
656 ImGuiID dockspace_id = ImGui::GetID("CreateBoardDockspace");
657
658 if (ImGui::DockBuilderGetNode(dockspace_id) == 0)
659 {
660 ImGui::DockBuilderRemoveNode(dockspace_id);
661 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace | ImGuiDockNodeFlags_PassthruCentralNode | ImGuiDockNodeFlags_NoUndocking);
662
663 ImGui::DockBuilderDockWindow("MapDiretory", dockspace_id);
664 ImGui::DockBuilderFinish(dockspace_id);
665 }
666
667 ImGui::Columns(2, nullptr, false);
668
669 ImGui::Text("Create a new board");
670
671 ImGui::InputText("Board Name", buffer, sizeof(buffer));
673 std::string board_name(buffer);
674
675 ImGui::NewLine();
676
677 DirectoryWindow::ImageData selectedImage = map_directory->getSelectedImage();
678
679 if (!selectedImage.filename.empty())
680 {
681 ImGui::Text("Selected Map: %s", selectedImage.filename.c_str());
682 ImGui::Image((void*)(intptr_t)selectedImage.textureID, ImVec2(256, 256));
683 }
684 else
685 {
686 ImGui::Text("No map selected. Please select a map.");
687 }
688
689 ImGui::NewLine();
690
691 bool saved = false;
692 if (ImGui::Button("Save") && !selectedImage.filename.empty() && buffer[0] != '\0')
693 {
694 auto board = board_manager->createBoard(board_name, selectedImage.filename, selectedImage.textureID, selectedImage.size);
695 board.add(flecs::ChildOf, active_game_table);
696 map_directory->clearSelectedImage();
697 memset(buffer, 0, sizeof(buffer));
698
699 network_manager->broadcastBoard(board_manager->getActiveBoard());
700 saved = true;
701 ImGui::CloseCurrentPopup();
702 }
703
704 ImGui::SameLine();
705
706 if (ImGui::Button("Close"))
707 {
708 map_directory->clearSelectedImage();
709 ImGui::CloseCurrentPopup();
710 }
711
712 ImGui::NextColumn();
713 ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_PassthruCentralNode);
714 map_directory->renderDirectory();
715
716 ImGui::Columns(1);
717 // Optionally show transient "Saved!"
718 UI_TransientLine("board-saved", saved, ImVec4(0.4f, 1.f, 0.4f, 1.f), "Saved!", 1.5f);
719
720 ImGui::EndPopup();
721 }
722}
723
725{
726 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
727 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
728 if (ImGui::BeginPopupModal("CloseBoard", 0, ImGuiWindowFlags_AlwaysAutoResize))
729 {
730 ImGui::Text("Close Current GameTable?? Any unsaved changes will be lost!!");
731 if (ImGui::Button("Close"))
732 {
733 board_manager->closeBoard();
734 ImGui::CloseCurrentPopup();
735 }
736
737 ImGui::SameLine();
738
739 if (ImGui::Button("Cancel"))
740 {
741 ImGui::CloseCurrentPopup();
742 }
743 ImGui::EndPopup();
744 }
745}
746
748{
749 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
750 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
751 if (ImGui::BeginPopupModal("SaveBoard", 0, ImGuiWindowFlags_AlwaysAutoResize))
752 {
753 ImGui::Text("Save current board?");
754 ImGui::NewLine();
755
756 bool saved = false;
757 if (ImGui::Button("Save"))
758 {
759 // your save logic here (you already said logic functions exist)
760 saved = true;
761 ImGui::CloseCurrentPopup();
762 }
763
764 ImGui::SameLine();
765
766 if (ImGui::Button("Close"))
767 {
768 ImGui::CloseCurrentPopup();
769 }
770
771 UI_TransientLine("save-board-ok", saved, ImVec4(0.4f, 1.f, 0.4f, 1.f), "Saved!", 1.5f);
772
773 ImGui::EndPopup();
774 }
775}
776
778{
779 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
780 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
781 if (ImGui::BeginPopupModal("LoadBoard", 0, ImGuiWindowFlags_AlwaysAutoResize))
782 {
783 ImGui::Text("Load Board:");
784
785 ImGui::NewLine();
786 ImGui::Separator();
787
788 auto boards = listBoardFiles();
789 bool loaded = false;
790 for (auto& board : boards)
791 {
792 if (ImGui::Button(board.c_str()))
793 {
794 std::filesystem::path board_file_path = PathManager::getRootDirectory() / "GameTables" / game_table_name / "Boards" / board;
795 board_manager->loadActiveBoard(board_file_path.string());
796 network_manager->broadcastBoard(board_manager->getActiveBoard());
797 loaded = true;
798 ImGui::CloseCurrentPopup();
799 }
800 }
801
802 UI_TransientLine("board-loaded", loaded, ImVec4(0.4f, 1.f, 0.4f, 1.f), "Board Loaded!", 1.5f);
803
804 ImGui::NewLine();
805 ImGui::Separator();
806
807 if (ImGui::Button("Close"))
808 {
809 ImGui::CloseCurrentPopup();
810 }
811 ImGui::EndPopup();
812 }
813}
814
816{
817 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
818 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
819 if (ImGui::BeginPopupModal("CloseGameTable", 0, ImGuiWindowFlags_AlwaysAutoResize))
820 {
821 ImGui::Text("Close Current GameTable?? Any unsaved changes will be lost!!");
822 if (ImGui::Button("Close"))
823 {
824 board_manager->closeBoard();
825 active_game_table = flecs::entity();
826 network_manager->closeServer();
827 chat_manager->saveCurrent();
828 ImGui::CloseCurrentPopup();
829 }
830
831 ImGui::SameLine();
832
833 if (ImGui::Button("Cancel"))
834 {
835 ImGui::CloseCurrentPopup();
836 }
837 ImGui::EndPopup();
838 }
839}
840
842{
843 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
844 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
845
846 if (ImGui::BeginPopupModal("ConnectToGameTable", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
847 {
848 ImGui::SetItemDefaultFocus();
849
850 // Username
851 ImGui::TextUnformatted("Enter your username and the connection string you received from the host.");
852 ImGui::InputText("Username", username_buffer, sizeof(username_buffer));
854
855 ImGui::Separator();
856
857 // Connection string
858 // Expected formats:
859 // runic:https://sub.loca.lt?PASSWORD
860 // runic:wss://host[:port/path]?PASSWORD
861 // runic:<host>:<port>?PASSWORD
862 ImGui::InputText("Connection String", buffer, sizeof(buffer), ImGuiInputTextFlags_AutoSelectAll);
864 // Small helper row
865 ImGui::BeginDisabled(true);
866 ImGui::InputText("Example", (char*)"runic:https://xyz.loca.lt?mypwd", 64);
868 ImGui::EndDisabled();
869
870 ImGui::Separator();
871
872 // UX actions
873 bool connectFailed = false;
874
875 if (ImGui::Button("Connect") && buffer[0] != '\0')
876 {
877 // set username (id will be assigned after auth)
878 auto my_unique = identity_manager->myUniqueId();
879 identity_manager->setMyIdentity(my_unique, username_buffer);
880
881 // this already accepts LocalTunnel URLs or host:port (parseConnectionString handles both)
882 if (network_manager->connectToPeer(buffer))
883 {
884 // cleanup & close
885 memset(buffer, 0, sizeof(buffer));
886 memset(username_buffer, 0, sizeof(username_buffer));
887 ImGui::CloseCurrentPopup();
888 }
889 else
890 {
891 connectFailed = true;
892 }
893 }
894
895 ImGui::SameLine();
896 if (ImGui::Button("Close"))
897 {
898 ImGui::CloseCurrentPopup();
899 memset(buffer, 0, sizeof(buffer));
900 }
901
902 // transient error (uses your helper)
903 UI_TransientLine("conn-fail",
904 connectFailed,
905 ImVec4(1.f, 0.f, 0.f, 1.f),
906 "Failed to Connect! Check your connection string and reachability.",
907 2.5f);
908
909 // Helpful hint (host-side details)
910 ImGui::Separator();
911 ImGui::TextDisabled("Tip: The host chooses the network mode while hosting.\n"
912 "LocalTunnel URLs may take a few seconds to become available.");
913
914 ImGui::EndPopup();
915 }
916}
917
918// ================== 1) Router popup (keeps your STATUS header) ==================
920{
921 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
922 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
923
924 if (ImGui::BeginPopupModal("Network Center", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
925 {
926
927 // ---------- STATUS (keep this block) ----------
928 Role role = network_manager->getPeerRole();
929 const char* roleStr =
930 role == Role::GAMEMASTER ? "Hosting (GM)" : role == Role::PLAYER ? "Connected (Player)"
931 : "Idle";
932 ImVec4 roleClr =
933 role == Role::GAMEMASTER ? ImVec4(0.2f, 0.9f, 0.2f, 1) : role == Role::PLAYER ? ImVec4(0.2f, 0.6f, 1.0f, 1)
934 : ImVec4(1.0f, 0.6f, 0.2f, 1);
935 ImGui::Text("Status: ");
936 ImGui::SameLine();
937 ImGui::TextColored(roleClr, "%s", roleStr);
938
939 ImGui::Separator();
940
941 // ---------- BODY ----------
942 if (role == Role::GAMEMASTER)
943 {
944 renderNetworkCenterGM(); // full GM center
945 }
946 else if (role == Role::PLAYER)
947 {
948 renderNetworkCenterPlayer(); // player center (read-only)
949 }
950 else
951 {
952 ImGui::TextDisabled("No network connection established.");
953 }
954
955 ImGui::Separator();
956 if (ImGui::Button("Close"))
957 ImGui::CloseCurrentPopup();
958
959 ImGui::EndPopup();
960 }
961}
962
964{
965 // ---------- INFO ----------
966 const auto local_ip = network_manager->getLocalIPAddress();
967 const auto external_ip = network_manager->getExternalIPAddress();
968 const auto port = network_manager->getPort();
969 const auto cs_local = network_manager->getNetworkInfo(ConnectionType::LOCAL);
970 const auto cs_external = network_manager->getNetworkInfo(ConnectionType::EXTERNAL);
971 const auto cs_lt = network_manager->getNetworkInfo(ConnectionType::LOCALTUNNEL);
972
973 ImGui::TextUnformatted("Local IP:");
974 ImGui::SameLine();
975 ImGui::TextUnformatted(local_ip.c_str());
976 ImGui::TextUnformatted("External IP:");
977 ImGui::SameLine();
978 ImGui::TextUnformatted(external_ip.c_str());
979 ImGui::TextUnformatted("Port:");
980 ImGui::SameLine();
981 ImGui::Text("%u", port);
982
983 ImGui::Separator();
984
985 auto copyRow = [this](const char* label, const std::string& value,
986 const char* btnId, const char* toastId)
987 {
988 ImGui::TextUnformatted(label);
989 ImGui::SameLine();
990 ImGui::TextUnformatted(value.c_str());
991 ImGui::SameLine();
992 UI_CopyButtonWithToast(btnId, value, toastId, 1.5f);
993 };
994
995 copyRow("LocalTunnel URL:", cs_lt, "Copy##lt", "toast-lt");
996 copyRow("Local Connection String:", cs_local, "Copy##loc", "toast-loc");
997 copyRow("External Connection String:", cs_external, "Copy##ext", "toast-ext");
998 const auto cs_custom = network_manager->getNetworkInfo(ConnectionType::CUSTOM);
999 if (!cs_custom.empty())
1000 {
1001 copyRow("Custom Connection String:", cs_custom, "Copy##custom", "toast-custom");
1002 }
1003 // ---------- PLAYERS (P2P) ----------
1004 ImGui::Separator();
1005 ImGui::Text("Players (P2P)");
1006 if (ImGui::BeginTable("PeersTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
1007 {
1008 ImGui::TableSetupColumn("Username", ImGuiTableColumnFlags_WidthStretch, 1.5f);
1009 ImGui::TableSetupColumn("Peer ID", ImGuiTableColumnFlags_WidthStretch, 2.0f);
1010 ImGui::TableSetupColumn("PC State", ImGuiTableColumnFlags_WidthFixed, 100.f);
1011 ImGui::TableSetupColumn("DataChannel", ImGuiTableColumnFlags_WidthFixed, 100.f);
1012 ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 120.f);
1013 ImGui::TableHeadersRow();
1014
1015 for (auto& [peerId, link] : network_manager->getPeers())
1016 {
1017 if (!link)
1018 continue;
1019
1020 ImGui::TableNextRow();
1021
1022 // Username
1023 ImGui::TableSetColumnIndex(0);
1024 ImGui::TextUnformatted(network_manager->displayNameForPeer(peerId).c_str());
1025
1026 // Peer ID
1027 ImGui::TableSetColumnIndex(1);
1028 ImGui::TextUnformatted(peerId.c_str());
1029
1030 // PC State (with color)
1031 ImGui::TableSetColumnIndex(2);
1032 const char* pcStr = link->pcStateString();
1033 ImVec4 pcCol = ImVec4(0.8f, 0.8f, 0.8f, 1);
1034 if (strcmp(pcStr, "Connected") == 0)
1035 pcCol = ImVec4(0.3f, 1.0f, 0.3f, 1);
1036 else if (strcmp(pcStr, "Connecting") == 0)
1037 pcCol = ImVec4(1.0f, 0.8f, 0.2f, 1);
1038 else if (strcmp(pcStr, "Disconnected") == 0 || strcmp(pcStr, "Failed") == 0)
1039 pcCol = ImVec4(1.0f, 0.3f, 0.3f, 1);
1040 ImGui::TextColored(pcCol, "%s", pcStr);
1041
1042 // DC state
1043 ImGui::TableSetColumnIndex(3);
1044 const bool dcOpen = link->isDataChannelOpen();
1045 ImGui::TextUnformatted(dcOpen ? "Open" : "Closed");
1046
1047 ImGui::TableSetColumnIndex(4);
1048
1049 ImGui::PushID(peerId.c_str());
1050 if (ImGui::SmallButton("Disconnect"))
1051 {
1052 ImGui::OpenPopup("ConfirmKickPeer");
1053 }
1054 if (UI_ConfirmModal("ConfirmKickPeer", "Disconnect this peer?",
1055 "This will disconnect the selected peer and notify others to drop it."))
1056 {
1057 // 1) Broadcast PeerDisconnect (so other peers drop links to this id)
1058 network_manager->broadcastPeerDisconnect(peerId);
1059 // 2) Optionally kick WS client too (server-side)
1060 if (auto srv = network_manager->getSignalingServer())
1061 {
1062 srv->disconnectClient(peerId); // add this helper to server if not present
1063 }
1064 // 3) Locally close our peer link
1065 network_manager->removePeer(peerId);
1066 }
1067 ImGui::PopID();
1068 }
1069 ImGui::EndTable();
1070 }
1071
1072 // ---------- CLIENTS (WS) ----------
1073 ImGui::Separator();
1074 ImGui::Text("Clients (WebSocket)");
1075 if (ImGui::BeginTable("ClientsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
1076 {
1077 ImGui::TableSetupColumn("ClientId");
1078 ImGui::TableSetupColumn("Username");
1079 ImGui::TableSetupColumn("Actions");
1080 ImGui::TableHeadersRow();
1081
1082 if (auto srv = network_manager->getSignalingServer())
1083 {
1084 for (auto& [cid, ws] : srv->authClients())
1085 {
1086 ImGui::TableNextRow();
1087
1088 ImGui::TableSetColumnIndex(0);
1089 ImGui::TextUnformatted(cid.c_str());
1090
1091 ImGui::TableSetColumnIndex(1);
1092 ImGui::TextUnformatted(network_manager->displayNameForPeer(cid).c_str());
1093
1094 ImGui::TableSetColumnIndex(2);
1095 if (ImGui::SmallButton((std::string("Disconnect##") + cid).c_str()))
1096 {
1097 srv->disconnectClient(cid);
1098 }
1099 }
1100 }
1101 else
1102 {
1103 ImGui::TableNextRow();
1104 ImGui::TableSetColumnIndex(0);
1105 ImGui::TextDisabled("No signaling server");
1106 }
1107 ImGui::EndTable();
1108 }
1109
1110 // ---------- Controls ----------
1111 ImGui::Separator();
1112 if (ImGui::Button("Disconnect All"))
1113 {
1114 ImGui::OpenPopup("ConfirmGMDisconnectAll");
1115 }
1116 ImGui::SameLine();
1117 if (UI_ConfirmModal("ConfirmGMDisconnectAll", "Disconnect ALL clients?",
1118 "This will broadcast a shutdown and disconnect all clients, "
1119 "close all peer links, and stop the signaling server."))
1120 {
1121 network_manager->disconnectAllPeers(); // your GM teardown
1122 ImGui::CloseCurrentPopup();
1123 }
1124
1125 if (ImGui::Button("Close Network"))
1126 {
1127 ImGui::OpenPopup("ConfirmCloseNetwork");
1128 }
1129
1130 if (UI_ConfirmModal("ConfirmCloseNetwork", "Close Network?",
1131 "This close will close the server"
1132 "and stop the signaling server."))
1133 {
1134 network_manager->closeServer();
1135 ImGui::CloseCurrentPopup();
1136 }
1137}
1138
1140{
1141 // Username
1142 ImGui::Text("Username: %s", network_manager->getMyUsername().c_str());
1143 ImGui::NewLine();
1144
1145 // Peers (read-only)
1146 if (ImGui::BeginTable("PeersPlayer", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
1147 {
1148 ImGui::TableSetupColumn("Username", ImGuiTableColumnFlags_WidthStretch, 1.5f);
1149 ImGui::TableSetupColumn("Peer ID", ImGuiTableColumnFlags_WidthStretch, 2.0f);
1150 ImGui::TableSetupColumn("PC State", ImGuiTableColumnFlags_WidthFixed, 100.f);
1151 ImGui::TableSetupColumn("DataChannel", ImGuiTableColumnFlags_WidthFixed, 100.f);
1152 ImGui::TableHeadersRow();
1153
1154 for (auto& [peerId, link] : network_manager->getPeers())
1155 {
1156 if (!link)
1157 continue;
1158
1159 ImGui::TableNextRow();
1160
1161 // Username
1162 ImGui::TableSetColumnIndex(0);
1163 ImGui::TextUnformatted(network_manager->displayNameForPeer(peerId).c_str());
1164
1165 // Peer ID
1166 ImGui::TableSetColumnIndex(1);
1167 ImGui::TextUnformatted(peerId.c_str());
1168
1169 // PC state
1170 ImGui::TableSetColumnIndex(2);
1171 const char* pcStr = link->pcStateString();
1172 ImVec4 pcCol = ImVec4(0.8f, 0.8f, 0.8f, 1);
1173 if (strcmp(pcStr, "Connected") == 0)
1174 pcCol = ImVec4(0.3f, 1.0f, 0.3f, 1);
1175 else if (strcmp(pcStr, "Connecting") == 0)
1176 pcCol = ImVec4(1.0f, 0.8f, 0.2f, 1);
1177 else if (strcmp(pcStr, "Disconnected") == 0 || strcmp(pcStr, "Failed") == 0)
1178 pcCol = ImVec4(1.0f, 0.3f, 0.3f, 1);
1179 ImGui::TextColored(pcCol, "%s", pcStr);
1180
1181 // DC state
1182 ImGui::TableSetColumnIndex(3);
1183 ImGui::TextUnformatted(link->isDataChannelOpen() ? "Open" : "Closed");
1184 }
1185
1186 ImGui::EndTable();
1187 }
1188 // At end of renderNetworkCenterPlayer()
1189 if (ImGui::Button("Disconnect"))
1190 {
1191 ImGui::OpenPopup("ConfirmPlayerDisconnect");
1192 }
1193 if (UI_ConfirmModal("ConfirmPlayerDisconnect", "Disconnect?",
1194 "This will close your WebSocket and all peer connections."))
1195 {
1196 network_manager->disconectFromPeers(); // your player teardown
1197 ImGui::CloseCurrentPopup(); // close Network Center
1198 }
1199}
1200
1201// ======================= Host GameTable (Create / Load + Network) =======================
1203{
1205 static bool tryUpnp = false; // only used for EXTERNAL
1206 static char custom_host_buf[64] = "";
1207
1208 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
1209 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
1210
1211 if (ImGui::BeginPopupModal("Host GameTable", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1212 {
1213
1214 if (ImGui::BeginTabBar("HostTabs"))
1215 {
1216
1217 // ----------------------- CREATE TAB -----------------------
1218 if (ImGui::BeginTabItem("Create"))
1219 {
1220
1221 ImGui::InputText("GameTable Name", buffer, sizeof(buffer));
1223 ImGui::InputText("Username", username_buffer, sizeof(username_buffer));
1225 ImGui::InputText("Password", pass_buffer, sizeof(pass_buffer), ImGuiInputTextFlags_Password);
1227 ImGui::InputText("Port", port_buffer, sizeof(port_buffer),
1228 ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsNoBlank);
1230 // Mode selection
1231 ImGui::Separator();
1232 /*ImGui::TextUnformatted("Connection Mode:");
1233 int m = (hostMode == ConnectionType::LOCALTUNNEL ? 0 : hostMode == ConnectionType::LOCAL ? 1
1234 : 2);
1235 if (ImGui::RadioButton("LocalTunnel", m == 0))
1236 m = 0;
1237 ImGui::SameLine();
1238 if (ImGui::RadioButton("Local (LAN)", m == 1))
1239 m = 1;
1240 ImGui::SameLine();
1241 if (ImGui::RadioButton("External (Internet)", m == 2))
1242 m = 2;
1243 hostMode = (m == 0 ? ConnectionType::LOCALTUNNEL : m == 1 ? ConnectionType::LOCAL
1244 : ConnectionType::EXTERNAL);
1245 */
1246 ImGui::TextUnformatted("Connection Mode:");
1247 int m = (hostMode == ConnectionType::LOCALTUNNEL ? 0 : hostMode == ConnectionType::LOCAL ? 1
1248 : hostMode == ConnectionType::EXTERNAL ? 2
1249 : 3);
1250 if (ImGui::RadioButton("LocalTunnel", m == 0))
1251 m = 0;
1252 ImGui::SameLine();
1253 if (ImGui::RadioButton("Local (LAN)", m == 1))
1254 m = 1;
1255 ImGui::SameLine();
1256 if (ImGui::RadioButton("External (Internet)", m == 2))
1257 m = 2;
1258 ImGui::SameLine();
1259 if (ImGui::RadioButton("Custom IP (Overlay/Other)", m == 3))
1260 m = 3;
1261
1263
1264 // Explanations
1265 if (hostMode == ConnectionType::LOCALTUNNEL)
1266 {
1267 ImGui::TextDisabled("LocalTunnel: exposes your local server via a public URL.\n"
1268 "The URL becomes available a few seconds after the server starts.\n"
1269 "You can copy the connection string from Network Center.");
1270 tryUpnp = false;
1271 }
1272 else if (hostMode == ConnectionType::LOCAL)
1273 {
1274 ImGui::TextDisabled("Local (LAN): works only on the same local network.\n"
1275 "Share your LAN IP + port with players.");
1276 tryUpnp = false;
1277 }
1278 else if (hostMode == ConnectionType::EXTERNAL)
1279 {
1280 ImGui::TextDisabled("External: reachable from the Internet.\n"
1281 "Requires a public IP and port forwarding on your router (UPnP or manual).\n"
1282 "It might not work depending on your network.");
1283 ImGui::Checkbox("Try UPnP (auto port forward)", &tryUpnp);
1284 }
1285 else
1286 { // CUSTOM
1287 ImGui::TextDisabled("Custom IP (Overlay/Other): share a specific address (e.g. overlay adapter IP).\n"
1288 "Server still binds 0.0.0.0; this only builds a copyable connection string.");
1289 ImGui::InputText("Custom Host/IP", custom_host_buf, sizeof(custom_host_buf));
1290 tryUpnp = false;
1291 }
1292
1293 ImGui::Separator();
1294
1295 const bool valid = buffer[0] != '\0' && port_buffer[0] != '\0';
1296 if (!valid)
1297 {
1298 ImGui::TextColored(ImVec4(1, 0.6f, 0.2f, 1), "Name and Port are required.");
1299 }
1300
1301 if (ImGui::Button("Create & Host") && valid)
1302 {
1303 // Close whatever is running
1304 board_manager->closeBoard();
1305 active_game_table = flecs::entity();
1306 network_manager->closeServer();
1307
1308 // Create GT entity + file
1310 auto identifier = Identifier{board_manager->generateUniqueId()};
1311 auto game_table = ecs.entity("GameTable").set(identifier).set(GameTable{game_table_name});
1312 active_game_table = game_table;
1313 chat_manager->setActiveGameTable(identifier.id, game_table_name);
1314 createGameTableFile(game_table);
1315
1316 // Identity + network
1317 auto my_unique = identity_manager->myUniqueId();
1318 identity_manager->setMyIdentity(my_unique, username_buffer);
1319 network_manager->setCustomHost(custom_host_buf);
1320 network_manager->setNetworkPassword(pass_buffer);
1321 const unsigned p = static_cast<unsigned>(atoi(port_buffer));
1322 network_manager->startServer(hostMode, static_cast<unsigned short>(p), tryUpnp);
1323
1324 // cleanup + close
1325 memset(buffer, 0, sizeof(buffer));
1326 memset(username_buffer, 0, sizeof(username_buffer));
1327 memset(pass_buffer, 0, sizeof(pass_buffer));
1328 //memset(port_buffer, 0, sizeof(port_buffer));
1329 tryUpnp = false;
1330
1331 ImGui::CloseCurrentPopup();
1332 }
1333
1334 ImGui::SameLine();
1335 if (ImGui::Button("Cancel"))
1336 {
1337 ImGui::CloseCurrentPopup();
1338 }
1339
1340 ImGui::EndTabItem();
1341 }
1342
1343 // ------------------------ LOAD TAB ------------------------
1344 if (ImGui::BeginTabItem("Load"))
1345 {
1346 ImGui::BeginChild("GTList", ImVec2(260, 360), true);
1347 auto game_tables = listGameTableFiles();
1348 for (auto& file : game_tables)
1349 {
1350 if (ImGui::Selectable(file.c_str()))
1351 {
1352 strncpy(buffer, file.c_str(), sizeof(buffer) - 1);
1353 buffer[sizeof(buffer) - 1] = '\0';
1354 }
1355 }
1356 ImGui::EndChild();
1357
1358 ImGui::SameLine();
1359
1360 ImGui::BeginChild("GTDetails", ImVec2(620, 360), true, ImGuiWindowFlags_AlwaysAutoResize);
1361 ImGui::Text("Selected:");
1362 ImGui::SameLine();
1363 ImGui::TextUnformatted(buffer[0] ? buffer : "(none)");
1364
1365 ImGui::Separator();
1366 ImGui::InputText("Username", username_buffer, sizeof(username_buffer));
1368 ImGui::InputText("Password", pass_buffer, sizeof(pass_buffer), ImGuiInputTextFlags_Password);
1370 ImGui::InputText("Port", port_buffer, sizeof(port_buffer), ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsNoBlank);
1372
1373 // Mode
1374 ImGui::Separator();
1375 ImGui::TextUnformatted("Connection Mode:");
1376 int m = (hostMode == ConnectionType::LOCALTUNNEL ? 0 : hostMode == ConnectionType::LOCAL ? 1
1377 : hostMode == ConnectionType::EXTERNAL ? 2
1378 : 3);
1379 if (ImGui::RadioButton("LocalTunnel", m == 0))
1380 m = 0;
1381 ImGui::SameLine();
1382 if (ImGui::RadioButton("Local (LAN)", m == 1))
1383 m = 1;
1384 ImGui::SameLine();
1385 if (ImGui::RadioButton("External (Internet)", m == 2))
1386 m = 2;
1387 ImGui::SameLine();
1388 if (ImGui::RadioButton("Custom IP (Overlay/Other)", m == 3))
1389 m = 3;
1390
1392
1393 if (hostMode == ConnectionType::LOCALTUNNEL)
1394 {
1395 ImGui::TextDisabled("LocalTunnel: URL appears after server starts.\n"
1396 "Copy connection string from Network Center.");
1397 tryUpnp = false;
1398 }
1399 else if (hostMode == ConnectionType::LOCAL)
1400 {
1401 ImGui::TextDisabled("Local (LAN): for players on the same network.");
1402 tryUpnp = false;
1403 }
1404 else if (hostMode == ConnectionType::EXTERNAL)
1405 {
1406 ImGui::TextDisabled("External: reachable from the Internet.\n"
1407 "Requires a public IP and port forwarding on your router (UPnP or manual).\n"
1408 "It might not work depending on your network.");
1409 ImGui::Checkbox("Try UPnP (auto port forward)", &tryUpnp);
1410 }
1411 else
1412 { // CUSTOM
1413 ImGui::TextDisabled("Custom IP (Overlay/Other): share a specific address (e.g. overlay adapter IP).\n"
1414 "Server still binds 0.0.0.0; this only builds a copyable connection string.");
1415 ImGui::InputText("Custom Host/IP", custom_host_buf, sizeof(custom_host_buf));
1416 tryUpnp = false;
1417 }
1418
1419 ImGui::Separator();
1420
1421 const bool valid = buffer[0] != '\0' && port_buffer[0] != '\0';
1422 if (ImGui::Button("Load & Host") && valid)
1423 {
1424 // strip ".runic" → game_table_name
1425 std::string file = buffer;
1426 std::string name = file;
1427 const std::string suffix = ".runic";
1428 if (name.size() >= suffix.size() &&
1429 name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0)
1430 {
1431 name.resize(name.size() - suffix.size());
1432 }
1433 game_table_name = name;
1434
1435 // close current + load
1436 board_manager->closeBoard();
1437 active_game_table = flecs::entity(); //Clean before load from file
1438 network_manager->closeServer();
1439
1440 std::filesystem::path game_table_file_path =
1441 PathManager::getRootDirectory() / "GameTables" / game_table_name / file;
1442 loadGameTable(game_table_file_path);
1443
1444 // start network
1445 auto my_unique = identity_manager->myUniqueId();
1446 identity_manager->setMyIdentity(my_unique, username_buffer);
1447 network_manager->setNetworkPassword(pass_buffer);
1448 network_manager->setCustomHost(custom_host_buf);
1449 const unsigned p = static_cast<unsigned>(atoi(port_buffer));
1450 network_manager->startServer(hostMode, static_cast<unsigned short>(p), tryUpnp);
1451
1452 // cleanup
1453 memset(buffer, 0, sizeof(buffer));
1454 memset(username_buffer, 0, sizeof(username_buffer));
1455 memset(pass_buffer, 0, sizeof(pass_buffer));
1456 //memset(port_buffer, 0, sizeof(port_buffer));
1457 tryUpnp = false;
1458
1459 ImGui::CloseCurrentPopup();
1460 }
1461
1462 ImGui::SameLine();
1463 if (ImGui::Button("Cancel"))
1464 {
1465 ImGui::CloseCurrentPopup();
1466 }
1467
1468 ImGui::EndChild();
1469 ImGui::EndTabItem();
1470 }
1471
1472 ImGui::EndTabBar();
1473 }
1474
1475 // Note: Share/copyable connection strings live in Network Center (as you prefer)
1476 ImGui::EndPopup();
1477 }
1478}
1480{
1481 namespace fs = std::filesystem;
1482 if (!ImGui::BeginPopupModal("Manage GameTables", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1483 return;
1484
1485 // --- local UI state ---
1486 static bool refreshFS = true;
1487 static std::vector<std::string> tables; // folder names under GameTables/
1488 static std::vector<std::string> boards; // *.runic in GameTables/<table>/Boards
1489 static std::string selectedTable; // folder name
1490 static std::string pendingBoardToRename; // filename (with .runic)
1491 static std::string pendingBoardToDelete; // filename (with .runic)
1492 static char tableRenameBuf[128] = {0};
1493 static char boardRenameBuf[128] = {0};
1494
1495 const fs::path root = PathManager::getGameTablesPath();
1496
1497 // inline "reload" blocks so we don't create helpers
1498 if (refreshFS)
1499 {
1500 tables.clear();
1501 if (fs::exists(root))
1502 {
1503 for (auto& e : fs::directory_iterator(root))
1504 if (e.is_directory())
1505 tables.emplace_back(e.path().filename().string());
1506 std::sort(tables.begin(), tables.end());
1507 }
1508 // ensure selection is valid
1509 if (!selectedTable.empty() && !fs::exists(root / selectedTable))
1510 selectedTable.clear();
1511
1512 boards.clear();
1513 if (!selectedTable.empty())
1514 {
1515 const fs::path boardsDir = root / selectedTable / "Boards";
1516 if (fs::exists(boardsDir))
1517 {
1518 for (auto& e : fs::directory_iterator(boardsDir))
1519 if (e.is_regular_file() && e.path().extension() == ".runic")
1520 boards.emplace_back(e.path().filename().string());
1521 std::sort(boards.begin(), boards.end());
1522 }
1523 }
1524 refreshFS = false;
1525 }
1526
1527 const float leftW = 260.f;
1528
1529 // LEFT panel: tables
1530 ImGui::BeginChild("tables-left", ImVec2(leftW, 420), true);
1531 ImGui::TextUnformatted("GameTables");
1532 ImGui::Separator();
1533
1534 for (const auto& t : tables)
1535 {
1536 const bool sel = (t == selectedTable);
1537 if (ImGui::Selectable(t.c_str(), sel))
1538 {
1539 selectedTable = t;
1540 std::snprintf(tableRenameBuf, IM_ARRAYSIZE(tableRenameBuf), "%s", t.c_str());
1541 // refresh boards for this selection
1542 refreshFS = true;
1543 }
1544 }
1545
1546 ImGui::Separator();
1547 ImGui::TextUnformatted("Selected Table:");
1548 ImGui::SameLine();
1549 ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.f, 1.f), "%s", selectedTable.empty() ? "(none)" : selectedTable.c_str());
1550
1551 ImGui::BeginDisabled(selectedTable.empty());
1552 if (ImGui::Button("Delete Table"))
1553 ImGui::OpenPopup("DeleteTable");
1554 ImGui::EndDisabled();
1555
1556 // Delete table popup
1557 if (ImGui::BeginPopupModal("DeleteTable", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1558 {
1559 ImGui::Text("Delete table '%s'?\nThis removes the folder permanently.", selectedTable.c_str());
1560 ImGui::Separator();
1561 if (ImGui::Button("Delete", ImVec2(120, 0)))
1562 {
1563 try
1564 {
1565 const fs::path dir = root / selectedTable;
1566 if (fs::exists(dir))
1567 fs::remove_all(dir);
1568 selectedTable.clear();
1569 refreshFS = true;
1570 }
1571 catch (...)
1572 { /* ignore */
1573 }
1574 ImGui::CloseCurrentPopup();
1575 }
1576 ImGui::SameLine();
1577 if (ImGui::Button("Cancel", ImVec2(120, 0)))
1578 ImGui::CloseCurrentPopup();
1579 ImGui::EndPopup();
1580 }
1581
1582 ImGui::EndChild();
1583
1584 ImGui::SameLine();
1585
1586 // RIGHT panel: boards in selected table
1587 ImGui::BeginChild("boards-right", ImVec2(540, 420), true);
1588 ImGui::TextUnformatted("Boards");
1589 ImGui::Separator();
1590
1591 if (selectedTable.empty())
1592 {
1593 ImGui::TextDisabled("Select a table to see its boards.");
1594 }
1595 else
1596 {
1597 for (const auto& f : boards)
1598 {
1599 ImGui::PushID(f.c_str());
1600 ImGui::TextUnformatted(f.c_str());
1601 ImGui::SameLine();
1602
1603 ImGui::SameLine();
1604 if (ImGui::Button("Delete"))
1605 {
1606 pendingBoardToDelete = f;
1607 ImGui::OpenPopup("DeleteBoard");
1608 }
1609
1610 // Delete board popup
1611 if (ImGui::BeginPopupModal("DeleteBoard", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1612 {
1613 ImGui::Text("Delete board '%s'?", pendingBoardToDelete.c_str());
1614 ImGui::Separator();
1615 if (ImGui::Button("Delete", ImVec2(120, 0)))
1616 {
1617 try
1618 {
1619 const fs::path boardsDir = root / selectedTable / "Boards";
1620 const fs::path p = boardsDir / pendingBoardToDelete;
1621 if (fs::exists(p))
1622 fs::remove(p);
1623 refreshFS = true;
1624 }
1625 catch (...)
1626 { /* ignore */
1627 }
1628 ImGui::CloseCurrentPopup();
1629 }
1630 ImGui::SameLine();
1631 if (ImGui::Button("Cancel", ImVec2(120, 0)))
1632 ImGui::CloseCurrentPopup();
1633 ImGui::EndPopup();
1634 }
1635
1636 ImGui::PopID();
1637 }
1638 if (boards.empty())
1639 ImGui::TextDisabled("(no boards in this table)");
1640 }
1641
1642 ImGui::EndChild();
1643
1644 ImGui::Separator();
1645 if (ImGui::Button("Close", ImVec2(120, 0)))
1646 ImGui::CloseCurrentPopup();
1647
1648 ImGui::EndPopup();
1649}
1650
1652{
1653 // You already call OpenPopup("Change Username") elsewhere
1654 if (ImGui::BeginPopupModal("Change Username", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1655 {
1656 static char usernameBuf[64] = {0};
1657
1658 // Seed the input when the popup appears
1659 if (ImGui::IsWindowAppearing())
1660 {
1661 const std::string cur = network_manager->getMyUsername();
1662 std::snprintf(usernameBuf, sizeof(usernameBuf), "%s", cur.c_str());
1663 }
1664
1665 ImGui::TextUnformatted("Username for this table:");
1666 ImGui::InputText("##uname", usernameBuf, (int)sizeof(usernameBuf));
1668 ImGui::Separator();
1669
1670 const bool hasTable = active_game_table.is_alive() && active_game_table.has<Identifier>();
1671 ImGui::BeginDisabled(!hasTable);
1672 if (ImGui::Button("Apply", ImVec2(120, 0)))
1673 {
1674 if (hasTable)
1675 {
1676 const uint64_t tableId = active_game_table.get<Identifier>()->id;
1677
1678 const std::string oldU = identity_manager->myUsername();
1679 const std::string newU = usernameBuf;
1680 if (!newU.empty() && newU != oldU)
1681 {
1682 identity_manager->setMyIdentity(identity_manager->myUniqueId(), newU);
1683
1684 // 2) Broadcast UsernameUpdate over Game DC (carry uniqueId!)
1685 // If you currently have a binary builder, change it to write uniqueId instead of peerId.
1686 std::vector<uint8_t> payload;
1687 network_manager->buildUserNameUpdate(payload,
1688 tableId,
1689 /*userUniqueId*/ identity_manager->myUniqueId(),
1690 /*oldUsername*/ oldU,
1691 /*newUsername*/ newU,
1692 /*reboundFlag*/ false);
1693 network_manager->broadcastUserNameUpdate(payload);
1694
1695 // 3) Apply locally (by uniqueId) for instant UI
1696 board_manager->onUsernameChanged(identity_manager->myUniqueId(), newU);
1697 chat_manager->replaceUsernameForUnique(identity_manager->myUniqueId(), newU);
1698 }
1699 }
1700 ImGui::CloseCurrentPopup();
1701 }
1702 ImGui::EndDisabled();
1703
1704 ImGui::SameLine();
1705 if (ImGui::Button("Cancel", ImVec2(120, 0)))
1706 {
1707 ImGui::CloseCurrentPopup();
1708 }
1709
1710 ImGui::EndPopup();
1711 }
1712}
1713
1714// INFORMATIONAL POPUPS
1715// ======================= Guide (Mini Wiki) =======================
1717{
1718 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
1719 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
1720
1721 if (ImGui::BeginPopupModal("Guide", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1722 {
1723 // --- state ---
1724 static int section = 0;
1725 static const char* kSections[] = {
1726 "Getting Started",
1727 "Connecting to a Game",
1728 "Game Tables",
1729 "Boards",
1730 "Toolbar & Interface",
1731 "Markers",
1732 "Fog of War",
1733 "Networking & Security",
1734 "Known Issues",
1735 "Appendix"};
1736
1737 // quick helper
1738 auto Para = [](const char* s)
1739 {
1740 ImGui::TextWrapped("%s", s);
1741 ImGui::Dummy(ImVec2(0, 4));
1742 };
1743
1744 ImGui::TextUnformatted("RunicVTT Guide");
1745 ImGui::Separator();
1746
1747 // main area (Left nav + Right content)
1748 ImVec2 full = ImVec2(860, 520);
1749 ImGui::BeginChild("GuideContent", full, true);
1750
1751 // Left: nav
1752 ImGui::BeginChild("GuideNav", ImVec2(240, 0), true);
1753 for (int i = 0; i < IM_ARRAYSIZE(kSections); ++i)
1754 {
1755 if (ImGui::Selectable(kSections[i], section == i))
1756 section = i;
1757 }
1758 ImGui::EndChild();
1759
1760 ImGui::SameLine();
1761
1762 // Right: body (scroll)
1763 ImGui::BeginChild("GuideBody", ImVec2(0, 0), true, ImGuiWindowFlags_AlwaysVerticalScrollbar);
1764
1765 switch (section)
1766 {
1767 case 0: // Getting Started
1768 {
1769 ImGui::SeparatorText("Overview");
1770 Para("RunicVTT is a virtual tabletop for sharing boards, markers, and fog of war in real-time across peers.");
1771
1772 ImGui::SeparatorText("Basic Flow");
1773 Para("Create or load a Game Table -> Host or Join -> Add a Board -> Place Markers / Fog -> Play.");
1774
1775 ImGui::SeparatorText("Requirements");
1776 Para("- Windows 10/11 recommended.\n"
1777 "- Internet connection for WAN.\n"
1778 "- Allow the app in your firewall/antivirus.\n"
1779 "- Read/write access for assets (Boards/Markers folders).");
1780
1781 ImGui::SeparatorText("Terminology");
1782 Para("- Game Table: a saved session containing chat and world state.\n"
1783 "- Board: a map image displayed to all players.\n"
1784 "- Marker: a token/object placed on a board.\n"
1785 "- Fog: an overlay hiding/revealing areas.\n"
1786 "- Peer: a connected client (GM or Player).");
1787 break;
1788 }
1789
1790 case 1: // Connecting to a Game
1791 {
1792 ImGui::SeparatorText("Connection String");
1793 Para("A connection string identifies the host session. Example formats:\n"
1794 "- https://runic-<yourLocalIp>.loca.lt?PASSWORD\n"
1795 "- runic:<host>:<port>?PASSWORD");
1796
1797 ImGui::SeparatorText("Hosting");
1798 Para("Choose a connection mode: LocalTunnel (public URL), Local (LAN), or External (WAN with port forwarding). "
1799 "Start hosting and copy the connection string from the Network Center.");
1800
1801 ImGui::SeparatorText("Joining");
1802 Para("Ask the host for a connection string and password, then use 'Connect to GameTable' and paste it.");
1803
1804 ImGui::SeparatorText("Troubleshooting");
1805 Para("- If connection closes over WAN: ensure firewall allows the app, port was fowarded manually or via UPnP and the host is reachable. \n"
1806 "- If connection closes over LAN: ensure firewall allows the app, and the moldem dont block local connections. \n"
1807 "- If connection closes over LocalTunnel: ensure firewall allows the app, and the generated URL is in the correct format https://runic-<YOURLOCALIP>.loca.lt. if not in the formar generate new by hosting again \n"
1808 "- Make sure you are connected to a Wifi or Ethernet moldem connection, 4G/5G mobile network arent supported due to their complex NAT protection. \n"
1809 "- Corporate networks or strict NAT may require TURN/relay.");
1810 break;
1811 }
1812
1813 case 2: // Game Tables
1814 {
1815 ImGui::SeparatorText("Create a Game Table");
1816 Para("Open 'Host GameTable' -> Create tab -> set name/username/password/port -> choose mode -> Host.");
1817
1818 ImGui::SeparatorText("Load a Game Table");
1819 Para("Open 'Host GameTable' -> Load tab -> select a saved table -> set credentials/port -> Host.");
1820
1821 ImGui::SeparatorText("Lifecycle");
1822 Para("Networking is tied to the Game Table. Closing it stops all connections. "
1823 "Chat and boards are saved per table.");
1824 break;
1825 }
1826
1827 case 3: // Boards
1828 {
1829 ImGui::SeparatorText("Create a Board");
1830 Para("Use 'Add Board' or board toolbar -> choose an image (PNG/JPG). The image is shared to peers.");
1831
1832 ImGui::SeparatorText("Edit Board");
1833 Para("Adjust size/scale, toggle grid, panning/zoom. Visibility affects whether players see it fully.");
1834
1835 ImGui::SeparatorText("Networking Notes");
1836 Para("Large images are chunked and sent reliably. Very large files transfer but take longer.");
1837 break;
1838 }
1839
1840 case 4: // Toolbar & Interface
1841 {
1842 ImGui::SeparatorText("Toolbar Overview");
1843 Para("- Move Tool: pan map or drag owned markers.\n"
1844 "- Fog Tool: create fog areas.\n"
1845 "- Edit/Delete: open edit window or remove entities.\n"
1846 "- Zoom/Pan: mouse wheel and drag (when panning).\n"
1847 "- Grid: open grid window to configure it.\n"
1848 "- Camera: open camera window to configure it.\n");
1849
1850 ImGui::SeparatorText("Windows & Panels");
1851 Para("- Chat Window: General chat + dice roller (/roll).\n"
1852 "- Edit Window: per-entity size/visibility/ownership.\n"
1853 "- Grid Window: per-board grid cell size/offset/visibility/snap to grid.\n"
1854 "- Camera Window: per-board camera zoom via button and sliders and reset.\n"
1855 "- Host Window: create or load gametable with credentials and port, start network and sets active gametable.\n"
1856 "- Connect Window: connect to hosted gametable, connection string and credential.\n"
1857 "- Network Center: peers, connection strings, status.");
1858
1859 break;
1860 }
1861
1862 case 5: // Fog of War
1863 {
1864 ImGui::SeparatorText("Create Fog");
1865 Para("Use the Fog tool to add opaque overlays to hide areas from players.");
1866
1867 ImGui::SeparatorText("Edit/Remove");
1868 Para("Move/resize fog areas or delete them. Fog updates are synchronized to all peers.");
1869
1870 ImGui::SeparatorText("Authority");
1871 Para("Fog is GM-controlled; players do not send fog updates.");
1872 break;
1873 }
1874
1875 case 6: // Markers
1876 {
1877 ImGui::SeparatorText("Create Markers");
1878 Para("Use the Marker directory to place tokens. Drag markers to the board from the Markers directory window.");
1879
1880 ImGui::SeparatorText("Edit & Ownership");
1881 Para("Edit window lets the GM set: owner peer ID, allow-all-players move, and locked state. \n"
1882 "Players can only move owned/unlocked markers; the GM can always move.");
1883
1884 ImGui::SeparatorText("Movement");
1885 Para("Drag markers to move. Updates are broadcast at a limited rate to reduce spam.");
1886 break;
1887 }
1888
1889 case 7: // Networking & Security
1890 {
1891 ImGui::SeparatorText("How It Works");
1892 Para("Peer-to-peer data channels synchronize boards, markers, fog, and chat.");
1893
1894 ImGui::SeparatorText("Firewall / Antivirus");
1895 Para("Allow the executable on first run. Some AVs may slow initial connection.");
1896
1897 ImGui::SeparatorText("Quality & Reliability");
1898 Para("Images are sent on reliable channels.");
1899 break;
1900 }
1901
1902 case 8: // Known Issues
1903 {
1904 ImGui::SeparatorText("Limitations");
1905 Para("- Very large images transfer slowly.\n"
1906 "- Network Over the 4G/5G doesnt work due to NAT restrictions. \n"
1907 "- Network do not work on Mobile Internet of any form(USB Anchoring, Wifi Hotspot, Ethernet and Bluetooth). ");
1908
1909 ImGui::SeparatorText("Troubleshooting");
1910 Para("If desync occurs, rejoin the session or have the host re-broadcast state via Game Table snapshot.");
1911
1912 ImGui::SeparatorText("Reporting Bugs");
1913 Para("Collect logs from the log window/folder and describe steps to reproduce.");
1914 break;
1915 }
1916
1917 case 9: // Appendix
1918 {
1919 ImGui::SeparatorText("File Paths");
1920 Para("- GameTables folder: <root path>/GameTables/ \n"
1921 "- Boards folder: <GameTableFolder>/<GameTableName>/Boards/ \n"
1922 "- Maps folder: <root path>/Maps/ \n"
1923 "- Marker folder: <root path>/Marker/.");
1924
1925 ImGui::SeparatorText("Glossary");
1926 Para("- GM: Game Master (host/authority).\n"
1927 "- Player: peer participant.\n"
1928 "- Peer: a connected client.");
1929
1930 break;
1931 }
1932 }
1933
1934 ImGui::EndChild(); // GuideBody
1935 ImGui::EndChild(); // GuideContent
1936
1937 ImGui::Separator();
1938 if (ImGui::Button("Close"))
1939 ImGui::CloseCurrentPopup();
1940
1941 ImGui::EndPopup();
1942 }
1943}
1944
1945// ======================= About =======================
1947{
1948 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
1949 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
1950
1951 if (ImGui::BeginPopupModal("About", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1952 {
1953 const char* appName = "RunicVTT";
1954 const char* version = "0.0.1";
1955 const char* author = "Pedro Vicente dos Santos";
1956 const char* yearRange = "2025";
1957 const char* repoUrl = "https://github.com/PedroVicente98/RunicVTT";
1958
1959 ImGui::Text("%s v%s", appName, version);
1960 ImGui::Separator();
1961
1962 ImGui::TextWrapped(
1963 "RunicVTT is a virtual tabletop focused on fast peer-to-peer play using WebRTC data channels. "
1964 "It’s built with C++ and integrates ImGui, GLFW, GLEW, and Flecs.");
1965
1966 ImGui::NewLine();
1967 ImGui::Text("Author: %s", author);
1968 ImGui::Text("Year: %s", yearRange);
1969
1970 ImGui::NewLine();
1971 ImGui::TextUnformatted("GitHub:");
1972 ImGui::SameLine();
1973 ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 1.0f), "%s", repoUrl);
1974 ImGui::SameLine();
1975 if (ImGui::SmallButton("Copy URL"))
1976 {
1977 ImGui::SetClipboardText(repoUrl);
1978 }
1979
1980 // If you later want a button to open the browser on Windows:
1981 // if (ImGui::SmallButton("Open in Browser")) {
1982 // #ifdef _WIN32
1983 // ShellExecuteA(nullptr, "open", repoUrl, nullptr, nullptr, SW_SHOWNORMAL);
1984 // #endif
1985 // }
1986
1987 ImGui::Separator();
1988 if (ImGui::Button("Close"))
1989 ImGui::CloseCurrentPopup();
1990
1991 ImGui::EndPopup();
1992 }
1993}
1994
1995void GameTableManager::render(VertexArray& va, IndexBuffer& ib, Shader& shader, Shader& grid_shader, Renderer& renderer)
1996{
1997 if (isGameTableActive())
1998 {
1999 chat_manager->render();
2000 }
2001 if (board_manager->isBoardActive())
2002 {
2003 if (board_manager->isEditWindowOpen())
2004 {
2005 board_manager->renderEditWindow();
2006 }
2007 else
2008 {
2009 board_manager->setShowEditWindow(false);
2010 }
2011
2012 if (network_manager->getPeerRole() == Role::GAMEMASTER)
2013 {
2014 board_manager->marker_directory->renderDirectory();
2015 }
2016 board_manager->renderBoard(va, ib, shader, grid_shader, renderer);
2017 }
2018}
@ SELECT
ConnectionType
Definition Message.h:20
Role
Definition Message.h:13
@ PLAYER
@ GAMEMASTER
std::vector< std::string > listBoardFiles()
bool UI_CopyButtonWithToast(const char *btnId, const std::string &toCopy, const char *toastId, float seconds=1.5f)
std::string game_table_name
void createGameTableFile(flecs::entity game_table)
std::shared_ptr< ImGuiToaster > toaster_
static flecs::entity findMarkerInBoard(flecs::entity boardEnt, uint64_t markerId)
glm::vec2 current_mouse_world_pos
void handleInputs(glm::vec2 current_mouse_fbo_pos)
void setCameraFboDimensions(glm::vec2 fbo_dimensions)
std::shared_ptr< IdentityManager > identity_manager
void render(VertexArray &va, IndexBuffer &ib, Shader &shader, Shader &grid_shader, Renderer &renderer)
GameTableManager(flecs::world ecs, std::shared_ptr< DirectoryWindow > map_directory, std::shared_ptr< DirectoryWindow > marker_directory)
glm::vec2 current_mouse_fbo_pos
std::shared_ptr< NetworkManager > network_manager
bool UI_ConfirmModal(const char *popupId, const char *title, const char *text)
std::vector< std::string > listGameTableFiles()
std::shared_ptr< DirectoryWindow > map_directory
std::shared_ptr< BoardManager > board_manager
void UI_TransientLine(const char *key, bool trigger, const ImVec4 &color, const char *text, float seconds=2.0f)
flecs::entity active_game_table
void loadGameTable(std::filesystem::path game_table_file_path)
std::shared_ptr< ChatManager > chat_manager
static Logger & instance()
Definition Logger.h:39
static fs::path getRootDirectory()
Definition PathManager.h:55
static fs::path getGameTablesPath()
Definition PathManager.h:76
static fs::path getMapsPath()
Definition PathManager.h:61
static flecs::entity deserializeGameTableEntity(const std::vector< unsigned char > &buffer, size_t &offset, flecs::world &ecs)
Definition Serializer.h:174
static void serializeGameTableEntity(std::vector< unsigned char > &buffer, const flecs::entity entity, flecs::world &ecs)
Definition Serializer.h:150
void TrackThisInput()
std::string DCtypeString(DCType type)
Definition Message.h:61
float x
Definition Components.h:20
float width
Definition Components.h:27