diff --git a/TableturfBattleClient/index.html b/TableturfBattleClient/index.html
index 21d12ab..abcaa57 100644
Binary files a/TableturfBattleClient/index.html and b/TableturfBattleClient/index.html differ
diff --git a/TableturfBattleClient/src/Pages/GamePage.ts b/TableturfBattleClient/src/Pages/GamePage.ts
index 1cff7e4..db77e4e 100644
--- a/TableturfBattleClient/src/Pages/GamePage.ts
+++ b/TableturfBattleClient/src/Pages/GamePage.ts
@@ -402,7 +402,7 @@ document.addEventListener('keydown', e => {
document.getElementById('resultLeaveButton')!.addEventListener('click', e => {
e.preventDefault();
- clearPreGameForm();
+ clearPreGameForm(true);
showSection('preGame');
newGameButton.focus();
});
diff --git a/TableturfBattleClient/src/Pages/PreGamePage.ts b/TableturfBattleClient/src/Pages/PreGamePage.ts
index faab04a..6b1be07 100644
--- a/TableturfBattleClient/src/Pages/PreGamePage.ts
+++ b/TableturfBattleClient/src/Pages/PreGamePage.ts
@@ -28,6 +28,9 @@ maxPlayersBox.addEventListener('change', () => {
let response = JSON.parse(request.responseText);
if (!clientToken)
setClientToken(response.clientToken);
+
+ setGameUrl(response.gameID);
+
getGameInfo(response.gameID, 0);
}
});
@@ -44,18 +47,34 @@ maxPlayersBox.addEventListener('change', () => {
}
});
+function setGameUrl(gameID: string | null) {
+ if (canPushState) {
+ try {
+ history.pushState(null, '', `game/${gameID}`);
+ } catch {
+ canPushState = false;
+ location.hash = `#game/${gameID}`;
+ }
+ } else
+ location.hash = `#game/${gameID}`;
+}
+
function tryJoinGame(name: string, idOrUrl: string, fromInitialLoad: boolean) {
- const gameID = idOrUrl.substring(idOrUrl.lastIndexOf('#') + 1);
- if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(gameID)) {
+ const m = /(?:^|[#/])([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.exec(idOrUrl);
+ if (!m) {
alert("Invalid game ID or link");
if (fromInitialLoad)
- clearPreGameForm();
+ clearPreGameForm(true);
else {
gameIDBox.focus();
gameIDBox.setSelectionRange(0, gameIDBox.value.length);
}
return;
}
+ const gameID = m[1];
+
+ if (!fromInitialLoad)
+ setGameUrl(gameID);
let request = new XMLHttpRequest();
request.open('POST', `${config.apiBaseUrl}/games/${gameID}/join`);
@@ -73,7 +92,7 @@ function tryJoinGame(name: string, idOrUrl: string, fromInitialLoad: boolean) {
else
alert('Unable to join the room.');
if (fromInitialLoad)
- clearPreGameForm();
+ clearPreGameForm(true);
else {
gameIDBox.focus();
gameIDBox.setSelectionRange(0, gameIDBox.value.length);
@@ -88,34 +107,63 @@ function tryJoinGame(name: string, idOrUrl: string, fromInitialLoad: boolean) {
function getGameInfo(gameID: string, myPlayerIndex: number | null) {
board.playerIndex = myPlayerIndex;
- window.location.hash = `#${gameID}`;
initLobbyPage(window.location.toString());
myPlayerIndex = setupWebSocket(gameID, myPlayerIndex);
}
-function backPreGameForm() {
+function backPreGameForm(updateUrl: boolean) {
document.getElementById('preGameDefaultSection')!.hidden = false;
document.getElementById('preGameJoinSection')!.hidden = true;
- window.location.hash = '';
+
+ if (updateUrl) {
+ if (canPushState) {
+ try {
+ history.pushState(null, '', '../..');
+ } catch {
+ canPushState = false;
+ }
+ }
+ if (location.hash)
+ location.hash = '';
+ }
}
-function clearPreGameForm() {
- backPreGameForm();
+function clearPreGameForm(updateUrl: boolean) {
+ backPreGameForm(updateUrl);
currentGame = null;
gameIDBox.value = '';
}
document.getElementById('preGameBackButton')!.addEventListener('click', e => {
e.preventDefault();
- backPreGameForm();
+ backPreGameForm(true);
})
-const playerName = window.localStorage.getItem('name');
-(document.getElementById('nameBox') as HTMLInputElement).value = playerName || '';
-if (window.location.hash != '') {
+function presetGameID(url: string) {
document.getElementById('preGameDefaultSection')!.hidden = true;
document.getElementById('preGameJoinSection')!.hidden = false;
- (document.getElementById('gameIDBox') as HTMLInputElement).value = window.location.hash;
+ (document.getElementById('gameIDBox') as HTMLInputElement).value = url;
if (playerName)
- tryJoinGame(playerName, window.location.hash, true);
+ tryJoinGame(playerName, url, true);
}
+
+const playerName = localStorage.getItem('name');
+(document.getElementById('nameBox') as HTMLInputElement).value = playerName || '';
+
+function processUrl() {
+ const m = /^(.*)\/game\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/.exec(location.toString());
+ if (m)
+ presetGameID(m[2]);
+ else if (location.hash) {
+ canPushState = false;
+ presetGameID(location.hash);
+ } else {
+ clearPreGameForm(false);
+ showSection('preGame');
+ }
+}
+
+window.addEventListener('popstate', () => {
+ processUrl();
+});
+processUrl();
diff --git a/TableturfBattleClient/src/app.ts b/TableturfBattleClient/src/app.ts
index dcd8cf7..2ee82d6 100644
--- a/TableturfBattleClient/src/app.ts
+++ b/TableturfBattleClient/src/app.ts
@@ -1,3 +1,5 @@
+let canPushState = isSecureContext && location.protocol != 'file:';
+
function delay(ms: number) { return new Promise(resolve => setTimeout(() => resolve(null), ms)); }
// Sections
@@ -90,7 +92,7 @@ function setupWebSocket(gameID: string, myPlayerIndex: number | null) {
const webSocket = new WebSocket(`${config.apiBaseUrl.replace(/(http)(s)?\:\/\//, 'ws$2://')}/websocket?gameID=${gameID}&clientToken=${clientToken}`);
webSocket.addEventListener('open', e => {
const request2 = new XMLHttpRequest();
- request2.open('GET', `${config.apiBaseUrl}/games/${gameID}/playerData/${clientToken}`);
+ request2.open('GET', `${config.apiBaseUrl}/games/${gameID}/playerData?clientToken=${clientToken}`);
request2.addEventListener('load', e => {
if (request2.status == 200) {
const response = JSON.parse(request2.responseText);
@@ -230,8 +232,6 @@ function setupWebSocket(gameID: string, myPlayerIndex: number | null) {
return myPlayerIndex;
}
-showSection('preGame');
-
function isInternetExplorer() {
return !!(window.document as any).documentMode; // This is a non-standard property implemented only by Internet Explorer.
}
diff --git a/TableturfBattleServer/Program.cs b/TableturfBattleServer/Program.cs
index 80e18cc..da80b29 100644
--- a/TableturfBattleServer/Program.cs
+++ b/TableturfBattleServer/Program.cs
@@ -16,7 +16,7 @@ using Timer = System.Timers.Timer;
namespace TableturfBattleServer;
internal class Program {
- internal static HttpServer httpServer = new(IPAddress.Loopback, 3333);
+ internal static HttpServer httpServer = new(IPAddress.Loopback, 3333) { DocumentRootPath = Path.Combine("..", "..", "..", "..", "TableturfBattleClient") };
internal static Dictionary games = new();
internal static readonly Timer timer = new(500);
private static readonly List gameIdsToRemove = new();
@@ -45,7 +45,22 @@ internal class Program {
private static void HttpServer_OnRequest(object? sender, HttpRequestEventArgs e) {
e.Response.AppendHeader("Access-Control-Allow-Origin", "*");
- if (e.Request.RawUrl == "/api/games/new") {
+ if (!e.Request.RawUrl.StartsWith("/api/")) {
+ var path = e.Request.RawUrl == "/" || e.Request.RawUrl.StartsWith("/game/") ? "index.html" : e.Request.RawUrl[1..];
+ if (e.TryReadFile(path, out var bytes))
+ SetResponse(e.Response, 200,
+ Path.GetExtension(path) switch {
+ ".html" or ".htm" => "text/html",
+ ".css" => "text/css",
+ ".js" => "text/javascript",
+ ".png" => "image/png",
+ ".woff" or ".woff2" => "font/woff",
+ _ => "application/octet-stream"
+ }, bytes);
+ else
+ SetErrorResponse(e.Response, 404, new("FileNotFound", "File not found."));
+ return;
+ } else if (e.Request.RawUrl == "/api/games/new") {
if (e.Request.HttpMethod != "POST") {
e.Response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
e.Response.AddHeader("Allow", "POST");
@@ -87,7 +102,7 @@ internal class Program {
} else if (e.Request.RawUrl == "/api/stages") {
SetStaticResponse(e.Request, e.Response, StageDatabase.JSON, StageDatabase.Version.ToString(), StageDatabase.LastModified);
} else {
- var m = Regex.Match(e.Request.RawUrl, @"^/api/games/([\w-]+)(?:/(\w+)(?:/([\w-]+))?)?$", RegexOptions.Compiled);
+ var m = Regex.Match(e.Request.RawUrl, @"^/api/games/([\w-]+)(?:/(\w+)(?:\?clientToken=([\w-]+))?)?$", RegexOptions.Compiled);
if (m.Success) {
if (!Guid.TryParse(m.Groups[1].Value, out var gameID)) {
SetResponse(e.Response, 400, "text/plain", "Invalid game ID");