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))
17 const std::string newUid = std::to_string(std::random_device{}()) +
"-" + std::to_string(std::random_device{}());
46 std::ifstream inFile(game_table_file_path, std::ios::binary);
49 std::vector<unsigned char>
buffer((std::istreambuf_iterator<char>(inFile)), std::istreambuf_iterator<char>());
63 if (child.has<
Board>()) {
65 auto board_image =
map_directory->getImageByPath(texture->image_path);
66 texture->
textureID = board_image.textureID;
67 texture->size = board_image.size;
69 child.children([&](flecs::entity grand_child) {
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;
80 catch (
const std::exception&)
82 std::cout <<
"ERROR LOADING IMAGES" << std::endl;
90 std::cerr <<
"Failed to load GameTable from " << game_table_file_path.string() << std::endl;
111 constexpr int kMaxPerFrame = 32;
125 while (processed < kMaxPerFrame && network_manager->tryPopReadyMessage(m))
132 if (!m.tableId || !m.name)
146 if (!m.boardId || !m.boardMeta)
149 glm::vec2 texSize{0, 0};
150 if (m.bytes && !m.bytes->empty())
152 auto image =
board_manager->LoadTextureFromMemory(m.bytes->data(),
154 tex = image.textureID;
155 texSize = image.size;
159 const auto& bm = *m.boardMeta;
160 auto board =
ecs.entity()
162 .set(
Board{bm.boardName})
166 .set(
Size{texSize.x, texSize.y});
175 if (!m.boardId || !m.markerMeta)
178 if (!boardEnt.is_valid())
182 glm::vec2 texSize{m.markerMeta->size.width, m.markerMeta->size.height};
184 if (m.bytes && !m.bytes->empty())
186 auto image =
board_manager->LoadTextureFromMemory(m.bytes->data(),
188 tex = image.textureID;
189 texSize = image.size;
192 const auto& mm = *m.markerMeta;
193 flecs::entity marker =
ecs.entity()
196 .set(
Size{mm.size.
width, mm.size.height})
201 marker.add(flecs::ChildOf, boardEnt);
208 if (!m.boardId || !m.fogId || !m.pos || !m.size || !m.vis)
211 if (!boardEnt.is_valid())
214 auto fog =
ecs.entity()
217 .set(
Size{m.size->
width, m.size->height})
221 fog.add(flecs::ChildOf, boardEnt);
228 if (!m.boardId || !m.fogId)
231 if (!boardEnt.is_valid())
234 flecs::entity fogEnt;
235 boardEnt.children([&](flecs::entity child)
238 auto id = child.get<Identifier>()->id;
239 if (id == *m.fogId) fogEnt = child;
241 if (!fogEnt.is_valid())
247 fogEnt.set<
Size>(*m.size);
251 fogEnt.set<
Moving>(*m.mov);
257 if (!m.boardId || !m.fogId)
260 if (!boardEnt.is_valid())
263 flecs::entity fogEnt;
264 boardEnt.children([&](flecs::entity child)
268 if (
id == *m.fogId) fogEnt = child;
270 if (fogEnt.is_valid())
281 if (!m.boardId || !m.markerId || !m.pos)
285 if (!boardEnt.is_valid())
289 if (!markerEnt.is_valid())
300 if (!m.boardId || !m.markerId)
304 if (!boardEnt.is_valid())
308 if (!markerEnt.is_valid())
312 if (m.mov && m.mov->isDragging)
334 if (!m.boardId || !m.markerId)
338 if (!boardEnt.is_valid())
342 if (!markerEnt.is_valid())
347 markerEnt.set<
Size>(*m.size);
352 std::string oldOwnerUid = markerEnt.get<
MarkerComponent>()->ownerUniqueId;
353 if (oldOwnerUid != m.markerComp->ownerUniqueId)
363 if (!m.boardId || !m.markerId)
366 if (!boardEnt.is_valid())
369 flecs::entity markerEnt;
370 boardEnt.children([&](flecs::entity child)
374 if (
id == *m.markerId) markerEnt = child;
376 if (markerEnt.is_valid())
377 markerEnt.destruct();
384 if (!m.tableId || !m.userUniqueId || !m.name || !m.text)
389 const std::string uniqueId = *m.userUniqueId;
390 const std::string fromPeerId = m.fromPeerId;
391 const std::string newU = *m.name;
394 network_manager->upsertPeerIdentityWithUnique(m.fromPeerId, uniqueId, newU);
425 if (!m.boardId || !m.grid)
429 if (!boardEnt.is_valid())
432 boardEnt.set<
Grid>(*m.grid);
590 namespace fs = std::filesystem;
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))
595 std::filesystem::create_directory(active_game_table_folder);
597 auto game_table_boards_folder = active_game_table_folder /
"Boards";
599 if (!fs::exists(game_table_boards_folder) && !fs::is_directory(game_table_boards_folder))
601 std::filesystem::create_directory(game_table_boards_folder);
603 std::vector<std::string> boards;
604 for (
const auto& entry : std::filesystem::directory_iterator(game_table_boards_folder))
606 if (entry.is_regular_file())
608 boards.emplace_back(entry.path().filename().string());
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"))
655 ImGui::SetItemDefaultFocus();
656 ImGuiID dockspace_id = ImGui::GetID(
"CreateBoardDockspace");
658 if (ImGui::DockBuilderGetNode(dockspace_id) == 0)
660 ImGui::DockBuilderRemoveNode(dockspace_id);
661 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace | ImGuiDockNodeFlags_PassthruCentralNode | ImGuiDockNodeFlags_NoUndocking);
663 ImGui::DockBuilderDockWindow(
"MapDiretory", dockspace_id);
664 ImGui::DockBuilderFinish(dockspace_id);
667 ImGui::Columns(2,
nullptr,
false);
669 ImGui::Text(
"Create a new board");
673 std::string board_name(
buffer);
679 if (!selectedImage.filename.empty())
681 ImGui::Text(
"Selected Map: %s", selectedImage.filename.c_str());
682 ImGui::Image((
void*)(intptr_t)selectedImage.textureID, ImVec2(256, 256));
686 ImGui::Text(
"No map selected. Please select a map.");
692 if (ImGui::Button(
"Save") && !selectedImage.filename.empty() &&
buffer[0] !=
'\0')
694 auto board =
board_manager->createBoard(board_name, selectedImage.filename, selectedImage.textureID, selectedImage.size);
701 ImGui::CloseCurrentPopup();
706 if (ImGui::Button(
"Close"))
709 ImGui::CloseCurrentPopup();
713 ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_PassthruCentralNode);
718 UI_TransientLine(
"board-saved", saved, ImVec4(0.4f, 1.f, 0.4f, 1.f),
"Saved!", 1.5f);
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))
753 ImGui::Text(
"Save current board?");
757 if (ImGui::Button(
"Save"))
761 ImGui::CloseCurrentPopup();
766 if (ImGui::Button(
"Close"))
768 ImGui::CloseCurrentPopup();
771 UI_TransientLine(
"save-board-ok", saved, ImVec4(0.4f, 1.f, 0.4f, 1.f),
"Saved!", 1.5f);
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))
783 ImGui::Text(
"Load Board:");
790 for (
auto& board : boards)
792 if (ImGui::Button(board.c_str()))
798 ImGui::CloseCurrentPopup();
802 UI_TransientLine(
"board-loaded", loaded, ImVec4(0.4f, 1.f, 0.4f, 1.f),
"Board Loaded!", 1.5f);
807 if (ImGui::Button(
"Close"))
809 ImGui::CloseCurrentPopup();
843 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
844 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
846 if (ImGui::BeginPopupModal(
"ConnectToGameTable",
nullptr, ImGuiWindowFlags_AlwaysAutoResize))
848 ImGui::SetItemDefaultFocus();
851 ImGui::TextUnformatted(
"Enter your username and the connection string you received from the host.");
862 ImGui::InputText(
"Connection String",
buffer,
sizeof(
buffer), ImGuiInputTextFlags_AutoSelectAll);
865 ImGui::BeginDisabled(
true);
866 ImGui::InputText(
"Example", (
char*)
"runic:https://xyz.loca.lt?mypwd", 64);
868 ImGui::EndDisabled();
873 bool connectFailed =
false;
875 if (ImGui::Button(
"Connect") &&
buffer[0] !=
'\0')
887 ImGui::CloseCurrentPopup();
891 connectFailed =
true;
896 if (ImGui::Button(
"Close"))
898 ImGui::CloseCurrentPopup();
905 ImVec4(1.f, 0.f, 0.f, 1.f),
906 "Failed to Connect! Check your connection string and reachability.",
911 ImGui::TextDisabled(
"Tip: The host chooses the network mode while hosting.\n"
912 "LocalTunnel URLs may take a few seconds to become available.");
921 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
922 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
924 if (ImGui::BeginPopupModal(
"Network Center",
nullptr, ImGuiWindowFlags_AlwaysAutoResize))
929 const char* roleStr =
934 : ImVec4(1.0f, 0.6f, 0.2f, 1);
935 ImGui::Text(
"Status: ");
937 ImGui::TextColored(roleClr,
"%s", roleStr);
952 ImGui::TextDisabled(
"No network connection established.");
956 if (ImGui::Button(
"Close"))
957 ImGui::CloseCurrentPopup();
973 ImGui::TextUnformatted(
"Local IP:");
975 ImGui::TextUnformatted(local_ip.c_str());
976 ImGui::TextUnformatted(
"External IP:");
978 ImGui::TextUnformatted(external_ip.c_str());
979 ImGui::TextUnformatted(
"Port:");
981 ImGui::Text(
"%u", port);
985 auto copyRow = [
this](
const char* label,
const std::string& value,
986 const char* btnId,
const char* toastId)
988 ImGui::TextUnformatted(label);
990 ImGui::TextUnformatted(value.c_str());
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");
999 if (!cs_custom.empty())
1001 copyRow(
"Custom Connection String:", cs_custom,
"Copy##custom",
"toast-custom");
1005 ImGui::Text(
"Players (P2P)");
1006 if (ImGui::BeginTable(
"PeersTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
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();
1020 ImGui::TableNextRow();
1023 ImGui::TableSetColumnIndex(0);
1024 ImGui::TextUnformatted(
network_manager->displayNameForPeer(peerId).c_str());
1027 ImGui::TableSetColumnIndex(1);
1028 ImGui::TextUnformatted(peerId.c_str());
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);
1043 ImGui::TableSetColumnIndex(3);
1044 const bool dcOpen = link->isDataChannelOpen();
1045 ImGui::TextUnformatted(dcOpen ?
"Open" :
"Closed");
1047 ImGui::TableSetColumnIndex(4);
1049 ImGui::PushID(peerId.c_str());
1050 if (ImGui::SmallButton(
"Disconnect"))
1052 ImGui::OpenPopup(
"ConfirmKickPeer");
1055 "This will disconnect the selected peer and notify others to drop it."))
1062 srv->disconnectClient(peerId);
1074 ImGui::Text(
"Clients (WebSocket)");
1075 if (ImGui::BeginTable(
"ClientsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
1077 ImGui::TableSetupColumn(
"ClientId");
1078 ImGui::TableSetupColumn(
"Username");
1079 ImGui::TableSetupColumn(
"Actions");
1080 ImGui::TableHeadersRow();
1084 for (
auto& [cid, ws] : srv->authClients())
1086 ImGui::TableNextRow();
1088 ImGui::TableSetColumnIndex(0);
1089 ImGui::TextUnformatted(cid.c_str());
1091 ImGui::TableSetColumnIndex(1);
1092 ImGui::TextUnformatted(
network_manager->displayNameForPeer(cid).c_str());
1094 ImGui::TableSetColumnIndex(2);
1095 if (ImGui::SmallButton((std::string(
"Disconnect##") + cid).c_str()))
1097 srv->disconnectClient(cid);
1103 ImGui::TableNextRow();
1104 ImGui::TableSetColumnIndex(0);
1105 ImGui::TextDisabled(
"No signaling server");
1112 if (ImGui::Button(
"Disconnect All"))
1114 ImGui::OpenPopup(
"ConfirmGMDisconnectAll");
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."))
1122 ImGui::CloseCurrentPopup();
1125 if (ImGui::Button(
"Close Network"))
1127 ImGui::OpenPopup(
"ConfirmCloseNetwork");
1131 "This close will close the server"
1132 "and stop the signaling server."))
1135 ImGui::CloseCurrentPopup();
1142 ImGui::Text(
"Username: %s",
network_manager->getMyUsername().c_str());
1146 if (ImGui::BeginTable(
"PeersPlayer", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
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();
1159 ImGui::TableNextRow();
1162 ImGui::TableSetColumnIndex(0);
1163 ImGui::TextUnformatted(
network_manager->displayNameForPeer(peerId).c_str());
1166 ImGui::TableSetColumnIndex(1);
1167 ImGui::TextUnformatted(peerId.c_str());
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);
1182 ImGui::TableSetColumnIndex(3);
1183 ImGui::TextUnformatted(link->isDataChannelOpen() ?
"Open" :
"Closed");
1189 if (ImGui::Button(
"Disconnect"))
1191 ImGui::OpenPopup(
"ConfirmPlayerDisconnect");
1194 "This will close your WebSocket and all peer connections."))
1197 ImGui::CloseCurrentPopup();
1205 static bool tryUpnp =
false;
1206 static char custom_host_buf[64] =
"";
1208 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
1209 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
1211 if (ImGui::BeginPopupModal(
"Host GameTable",
nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1214 if (ImGui::BeginTabBar(
"HostTabs"))
1218 if (ImGui::BeginTabItem(
"Create"))
1221 ImGui::InputText(
"GameTable Name",
buffer,
sizeof(
buffer));
1228 ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsNoBlank);
1246 ImGui::TextUnformatted(
"Connection Mode:");
1250 if (ImGui::RadioButton(
"LocalTunnel", m == 0))
1253 if (ImGui::RadioButton(
"Local (LAN)", m == 1))
1256 if (ImGui::RadioButton(
"External (Internet)", m == 2))
1259 if (ImGui::RadioButton(
"Custom IP (Overlay/Other)", m == 3))
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.");
1274 ImGui::TextDisabled(
"Local (LAN): works only on the same local network.\n"
1275 "Share your LAN IP + port with players.");
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);
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));
1298 ImGui::TextColored(ImVec4(1, 0.6f, 0.2f, 1),
"Name and Port are required.");
1301 if (ImGui::Button(
"Create & Host") && valid)
1321 const unsigned p =
static_cast<unsigned>(atoi(
port_buffer));
1322 network_manager->startServer(hostMode,
static_cast<unsigned short>(p), tryUpnp);
1331 ImGui::CloseCurrentPopup();
1335 if (ImGui::Button(
"Cancel"))
1337 ImGui::CloseCurrentPopup();
1340 ImGui::EndTabItem();
1344 if (ImGui::BeginTabItem(
"Load"))
1346 ImGui::BeginChild(
"GTList", ImVec2(260, 360),
true);
1348 for (
auto& file : game_tables)
1350 if (ImGui::Selectable(file.c_str()))
1360 ImGui::BeginChild(
"GTDetails", ImVec2(620, 360),
true, ImGuiWindowFlags_AlwaysAutoResize);
1361 ImGui::Text(
"Selected:");
1370 ImGui::InputText(
"Port",
port_buffer,
sizeof(
port_buffer), ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsNoBlank);
1375 ImGui::TextUnformatted(
"Connection Mode:");
1379 if (ImGui::RadioButton(
"LocalTunnel", m == 0))
1382 if (ImGui::RadioButton(
"Local (LAN)", m == 1))
1385 if (ImGui::RadioButton(
"External (Internet)", m == 2))
1388 if (ImGui::RadioButton(
"Custom IP (Overlay/Other)", m == 3))
1395 ImGui::TextDisabled(
"LocalTunnel: URL appears after server starts.\n"
1396 "Copy connection string from Network Center.");
1401 ImGui::TextDisabled(
"Local (LAN): for players on the same network.");
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);
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));
1422 if (ImGui::Button(
"Load & Host") && valid)
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)
1431 name.resize(name.size() - suffix.size());
1440 std::filesystem::path game_table_file_path =
1449 const unsigned p =
static_cast<unsigned>(atoi(
port_buffer));
1450 network_manager->startServer(hostMode,
static_cast<unsigned short>(p), tryUpnp);
1459 ImGui::CloseCurrentPopup();
1463 if (ImGui::Button(
"Cancel"))
1465 ImGui::CloseCurrentPopup();
1469 ImGui::EndTabItem();
1481 namespace fs = std::filesystem;
1482 if (!ImGui::BeginPopupModal(
"Manage GameTables",
nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1486 static bool refreshFS =
true;
1487 static std::vector<std::string> tables;
1488 static std::vector<std::string> boards;
1489 static std::string selectedTable;
1490 static std::string pendingBoardToRename;
1491 static std::string pendingBoardToDelete;
1492 static char tableRenameBuf[128] = {0};
1493 static char boardRenameBuf[128] = {0};
1501 if (fs::exists(root))
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());
1509 if (!selectedTable.empty() && !fs::exists(root / selectedTable))
1510 selectedTable.clear();
1513 if (!selectedTable.empty())
1515 const fs::path boardsDir = root / selectedTable /
"Boards";
1516 if (fs::exists(boardsDir))
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());
1527 const float leftW = 260.f;
1530 ImGui::BeginChild(
"tables-left", ImVec2(leftW, 420),
true);
1531 ImGui::TextUnformatted(
"GameTables");
1534 for (
const auto& t : tables)
1536 const bool sel = (t == selectedTable);
1537 if (ImGui::Selectable(t.c_str(), sel))
1540 std::snprintf(tableRenameBuf, IM_ARRAYSIZE(tableRenameBuf),
"%s", t.c_str());
1547 ImGui::TextUnformatted(
"Selected Table:");
1549 ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.f, 1.f),
"%s", selectedTable.empty() ?
"(none)" : selectedTable.c_str());
1551 ImGui::BeginDisabled(selectedTable.empty());
1552 if (ImGui::Button(
"Delete Table"))
1553 ImGui::OpenPopup(
"DeleteTable");
1554 ImGui::EndDisabled();
1557 if (ImGui::BeginPopupModal(
"DeleteTable",
nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1559 ImGui::Text(
"Delete table '%s'?\nThis removes the folder permanently.", selectedTable.c_str());
1561 if (ImGui::Button(
"Delete", ImVec2(120, 0)))
1565 const fs::path dir = root / selectedTable;
1566 if (fs::exists(dir))
1567 fs::remove_all(dir);
1568 selectedTable.clear();
1574 ImGui::CloseCurrentPopup();
1577 if (ImGui::Button(
"Cancel", ImVec2(120, 0)))
1578 ImGui::CloseCurrentPopup();
1587 ImGui::BeginChild(
"boards-right", ImVec2(540, 420),
true);
1588 ImGui::TextUnformatted(
"Boards");
1591 if (selectedTable.empty())
1593 ImGui::TextDisabled(
"Select a table to see its boards.");
1597 for (
const auto& f : boards)
1599 ImGui::PushID(f.c_str());
1600 ImGui::TextUnformatted(f.c_str());
1604 if (ImGui::Button(
"Delete"))
1606 pendingBoardToDelete = f;
1607 ImGui::OpenPopup(
"DeleteBoard");
1611 if (ImGui::BeginPopupModal(
"DeleteBoard",
nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1613 ImGui::Text(
"Delete board '%s'?", pendingBoardToDelete.c_str());
1615 if (ImGui::Button(
"Delete", ImVec2(120, 0)))
1619 const fs::path boardsDir = root / selectedTable /
"Boards";
1620 const fs::path p = boardsDir / pendingBoardToDelete;
1628 ImGui::CloseCurrentPopup();
1631 if (ImGui::Button(
"Cancel", ImVec2(120, 0)))
1632 ImGui::CloseCurrentPopup();
1639 ImGui::TextDisabled(
"(no boards in this table)");
1645 if (ImGui::Button(
"Close", ImVec2(120, 0)))
1646 ImGui::CloseCurrentPopup();
1654 if (ImGui::BeginPopupModal(
"Change Username",
nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1656 static char usernameBuf[64] = {0};
1659 if (ImGui::IsWindowAppearing())
1662 std::snprintf(usernameBuf,
sizeof(usernameBuf),
"%s", cur.c_str());
1665 ImGui::TextUnformatted(
"Username for this table:");
1666 ImGui::InputText(
"##uname", usernameBuf, (
int)
sizeof(usernameBuf));
1671 ImGui::BeginDisabled(!hasTable);
1672 if (ImGui::Button(
"Apply", ImVec2(120, 0)))
1679 const std::string newU = usernameBuf;
1680 if (!newU.empty() && newU != oldU)
1686 std::vector<uint8_t> payload;
1700 ImGui::CloseCurrentPopup();
1702 ImGui::EndDisabled();
1705 if (ImGui::Button(
"Cancel", ImVec2(120, 0)))
1707 ImGui::CloseCurrentPopup();
1718 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
1719 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
1721 if (ImGui::BeginPopupModal(
"Guide",
nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1724 static int section = 0;
1725 static const char* kSections[] = {
1727 "Connecting to a Game",
1730 "Toolbar & Interface",
1733 "Networking & Security",
1738 auto Para = [](
const char* s)
1740 ImGui::TextWrapped(
"%s", s);
1741 ImGui::Dummy(ImVec2(0, 4));
1744 ImGui::TextUnformatted(
"RunicVTT Guide");
1748 ImVec2 full = ImVec2(860, 520);
1749 ImGui::BeginChild(
"GuideContent", full,
true);
1752 ImGui::BeginChild(
"GuideNav", ImVec2(240, 0),
true);
1753 for (
int i = 0; i < IM_ARRAYSIZE(kSections); ++i)
1755 if (ImGui::Selectable(kSections[i], section == i))
1763 ImGui::BeginChild(
"GuideBody", ImVec2(0, 0),
true, ImGuiWindowFlags_AlwaysVerticalScrollbar);
1769 ImGui::SeparatorText(
"Overview");
1770 Para(
"RunicVTT is a virtual tabletop for sharing boards, markers, and fog of war in real-time across peers.");
1772 ImGui::SeparatorText(
"Basic Flow");
1773 Para(
"Create or load a Game Table -> Host or Join -> Add a Board -> Place Markers / Fog -> Play.");
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).");
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).");
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");
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.");
1801 ImGui::SeparatorText(
"Joining");
1802 Para(
"Ask the host for a connection string and password, then use 'Connect to GameTable' and paste it.");
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.");
1815 ImGui::SeparatorText(
"Create a Game Table");
1816 Para(
"Open 'Host GameTable' -> Create tab -> set name/username/password/port -> choose mode -> Host.");
1818 ImGui::SeparatorText(
"Load a Game Table");
1819 Para(
"Open 'Host GameTable' -> Load tab -> select a saved table -> set credentials/port -> Host.");
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.");
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.");
1832 ImGui::SeparatorText(
"Edit Board");
1833 Para(
"Adjust size/scale, toggle grid, panning/zoom. Visibility affects whether players see it fully.");
1835 ImGui::SeparatorText(
"Networking Notes");
1836 Para(
"Large images are chunked and sent reliably. Very large files transfer but take longer.");
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");
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.");
1864 ImGui::SeparatorText(
"Create Fog");
1865 Para(
"Use the Fog tool to add opaque overlays to hide areas from players.");
1867 ImGui::SeparatorText(
"Edit/Remove");
1868 Para(
"Move/resize fog areas or delete them. Fog updates are synchronized to all peers.");
1870 ImGui::SeparatorText(
"Authority");
1871 Para(
"Fog is GM-controlled; players do not send fog updates.");
1877 ImGui::SeparatorText(
"Create Markers");
1878 Para(
"Use the Marker directory to place tokens. Drag markers to the board from the Markers directory window.");
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.");
1884 ImGui::SeparatorText(
"Movement");
1885 Para(
"Drag markers to move. Updates are broadcast at a limited rate to reduce spam.");
1891 ImGui::SeparatorText(
"How It Works");
1892 Para(
"Peer-to-peer data channels synchronize boards, markers, fog, and chat.");
1894 ImGui::SeparatorText(
"Firewall / Antivirus");
1895 Para(
"Allow the executable on first run. Some AVs may slow initial connection.");
1897 ImGui::SeparatorText(
"Quality & Reliability");
1898 Para(
"Images are sent on reliable channels.");
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). ");
1909 ImGui::SeparatorText(
"Troubleshooting");
1910 Para(
"If desync occurs, rejoin the session or have the host re-broadcast state via Game Table snapshot.");
1912 ImGui::SeparatorText(
"Reporting Bugs");
1913 Para(
"Collect logs from the log window/folder and describe steps to reproduce.");
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/.");
1925 ImGui::SeparatorText(
"Glossary");
1926 Para(
"- GM: Game Master (host/authority).\n"
1927 "- Player: peer participant.\n"
1928 "- Peer: a connected client.");
1938 if (ImGui::Button(
"Close"))
1939 ImGui::CloseCurrentPopup();
1948 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
1949 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
1951 if (ImGui::BeginPopupModal(
"About",
nullptr, ImGuiWindowFlags_AlwaysAutoResize))
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";
1959 ImGui::Text(
"%s v%s", appName, version);
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.");
1967 ImGui::Text(
"Author: %s", author);
1968 ImGui::Text(
"Year: %s", yearRange);
1971 ImGui::TextUnformatted(
"GitHub:");
1973 ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 1.0f),
"%s", repoUrl);
1975 if (ImGui::SmallButton(
"Copy URL"))
1977 ImGui::SetClipboardText(repoUrl);
1988 if (ImGui::Button(
"Close"))
1989 ImGui::CloseCurrentPopup();