diff --git a/.gitignore b/.gitignore index 42f590f1..bf07040e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ *.pyc +web/game.zip +web/game_version.txt screenshots/*.png saves/*/*.json saves/*/rooms/*.json diff --git a/src/mapimage/mapImageGenerator.py b/src/mapimage/mapImageGenerator.py index 807c60bf..5edfc8f0 100644 --- a/src/mapimage/mapImageGenerator.py +++ b/src/mapimage/mapImageGenerator.py @@ -60,7 +60,11 @@ def mapImageExists(self): def getExistingMapImage(self): mapImagePath = self.getMapImagePath() _logger.debug("loading existing map image", path=mapImagePath) - return Image.open(mapImagePath) + try: + return Image.open(mapImagePath) + except Exception: + _logger.warning("map image unreadable, recreating", path=mapImagePath) + return self.createNewMapImage() def createNewMapImage(self): _logger.debug("creating new map image") diff --git a/web/build_zip.py b/web/build_zip.py index 5dd13990..2ab3c55a 100644 --- a/web/build_zip.py +++ b/web/build_zip.py @@ -3,7 +3,12 @@ Bundles the Python source tree, schemas, and config into a single zip that the browser's Pyodide Worker downloads and unpacks into its virtual filesystem. + +Also writes web/game_version.txt containing the SHA-256 of the zip so the +Worker can request game.zip?v= — a content-addressed URL that forces +the browser to fetch a new zip whenever the source changes. """ +import hashlib import os import zipfile @@ -26,4 +31,9 @@ if os.path.exists(_web_file): z.write(_web_file, _web_file) -print("Built web/game.zip") +with open("web/game.zip", "rb") as _f: + _digest = hashlib.sha256(_f.read()).hexdigest() +with open("web/game_version.txt", "w") as _f: + _f.write(_digest) + +print(f"Built web/game.zip ({_digest[:12]})") diff --git a/web/game-worker.js b/web/game-worker.js index 7504600f..829db995 100644 --- a/web/game-worker.js +++ b/web/game-worker.js @@ -178,7 +178,16 @@ self.onmessage = async (e) => { self.postMessage({ type: 'status', msg: 'Downloading game…' }); - const resp = await fetch('/web/game.zip'); + // Fetch the content-hash written by build_zip.py so the zip URL is + // unique per build. The browser may cache game.zip?v= freely; + // a new deploy changes the hash, forcing a fresh download. + let zipUrl = '/web/game.zip'; + try { + const verResp = await fetch('/web/game_version.txt', { cache: 'no-store' }); + if (verResp.ok) zipUrl += '?v=' + (await verResp.text()).trim(); + } catch (_) { /* no version file — fall back to unversioned URL */ } + + const resp = await fetch(zipUrl); if (!resp.ok) throw new Error(`game.zip fetch failed: ${resp.status}`); const buf = await resp.arrayBuffer(); pyodide.FS.mkdir('/game'); diff --git a/web/serve.py b/web/serve.py index b9d737fa..bc252110 100644 --- a/web/serve.py +++ b/web/serve.py @@ -43,6 +43,14 @@ def end_headers(self): self.send_header("Cross-Origin-Opener-Policy", "same-origin") self.send_header("Cross-Origin-Embedder-Policy", "require-corp") self.send_header("Cross-Origin-Resource-Policy", "same-origin") + path = getattr(self, "path", "") or "" + bare = path.split("?")[0] + if bare.endswith("game_version.txt"): + # Never cache — the Worker fetches this to get the current zip hash. + self.send_header("Cache-Control", "no-store") + elif any(bare.endswith(ext) for ext in (".zip", ".js")): + # Revalidate on every request so a new deploy is always picked up. + self.send_header("Cache-Control", "no-cache") super().end_headers() def log_message(self, *args):