From 3a24383e88a1f0bd70d946e7ebd117bc01b8aee4 Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Sun, 3 May 2026 22:30:25 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20map=20land=20tile=20lookup=20broken=20by?= =?UTF-8?q?=20asset=20URL=20migration=20=F0=9F=97=BA=EF=B8=8F=20(#3826)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: `MapLandTiles` was fetching map manifests via HTTP at `http://localhost:3000/maps//manifest.json`. This stopped working after PR #3494 moved all public assets from stable paths (e.g. `/maps/...`) to content-hashed paths under `/_assets/...`. The master server no longer serves `/maps/` -- requests fell through to the SPA handler, returned `index.html`, failed JSON parsing, and silently fell back to the default `1_000_000` land tile count. With 1M tiles, `calculateMapPlayerCounts` produces `[50, 40, 25]` instead of the real values (e.g. `[185, 140, 95]` for Alps), causing all public lobbies to be capped at 25-50 players regardless of map. Fixes this by reading map manifests directly from disk instead of via HTTP: - In production: resolves the source path through `getRuntimeAssetManifest()` to the hashed file under `static/_assets/`, then reads it with `fs.readFile`. - In dev: falls back to `resources/maps//manifest.json` directly (the Dockerfile removes `resources/maps` in production, so this branch only runs locally). Also adds an in-process cache so each map manifest is only read once per server lifetime. Verified that it works on https://fix-map-land-tiles-asset-manifest.openfront.dev/ and locally. ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin --- src/server/MapLandTiles.ts | 59 +++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/src/server/MapLandTiles.ts b/src/server/MapLandTiles.ts index 83d47bab8..b4fd9d1e4 100644 --- a/src/server/MapLandTiles.ts +++ b/src/server/MapLandTiles.ts @@ -1,26 +1,57 @@ -import { FetchGameMapLoader } from "src/core/game/FetchGameMapLoader"; +import fs from "fs/promises"; +import path from "path"; +import { normalizeAssetPath } from "src/core/AssetUrls"; import { GameMapType } from "src/core/game/Game"; -import { GameMapLoader } from "src/core/game/GameMapLoader"; +import { fileURLToPath } from "url"; import { logger } from "./Logger"; - -let mapLoader: GameMapLoader | null = null; +import { getRuntimeAssetManifest } from "./RuntimeAssetManifest"; const log = logger.child({ component: "MapLandTiles" }); -// Gets or creates the map loader, uses FetchGameMapLoader pointing to the master server. -function getMapLoader(): GameMapLoader { - mapLoader ??= new FetchGameMapLoader("http://localhost:3000/maps"); - return mapLoader; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const staticDir = path.join(__dirname, "../../static"); +const resourcesDir = path.join(__dirname, "../../resources"); + +const landTilesCache = new Map(); + +function mapDirName(map: GameMapType): string { + const key = ( + Object.keys(GameMapType) as Array + ).find((k) => GameMapType[k] === map); + if (!key) throw new Error(`Unknown map: ${map}`); + return key.toLowerCase(); } -// Gets the number of land tiles for a map -// FetchGameMapLoader already caches maps, so no need for additional caching here. +async function readManifestFile(map: GameMapType): Promise { + const relativePath = `maps/${mapDirName(map)}/manifest.json`; + + // Production: resolve via the asset manifest to the hashed file under static/_assets/. + const assetManifest = await getRuntimeAssetManifest(); + const hashedUrl = assetManifest[relativePath]; + if (hashedUrl) { + return fs.readFile( + path.join(staticDir, normalizeAssetPath(hashedUrl)), + "utf8", + ); + } + + // Dev: read directly from resources/. The Dockerfile deletes resources/maps in + // production, so this branch only runs locally. + return fs.readFile(path.join(resourcesDir, relativePath), "utf8"); +} + +// Gets the number of land tiles for a map. export async function getMapLandTiles(map: GameMapType): Promise { + const cached = landTilesCache.get(map); + if (cached !== undefined) return cached; + try { - const loader = getMapLoader(); - const mapData = loader.getMapData(map); - const manifest = await mapData.manifest(); - return manifest.map.num_land_tiles; + const raw = await readManifestFile(map); + const tiles = (JSON.parse(raw) as { map: { num_land_tiles: number } }).map + .num_land_tiles; + landTilesCache.set(map, tiles); + return tiles; } catch (error) { log.error(`Failed to load manifest for ${map}: ${error}`, { map }); return 1_000_000; // Default fallback