-
- Drag to pan. Scroll or pinch to zoom.
-
-
-
- Choose a detected map to build and view it.
- diff --git a/map_renderer/.dockerignore b/map_renderer/.dockerignore deleted file mode 100644 index 409d710..0000000 --- a/map_renderer/.dockerignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules/ -.cache/ -coverage/ -dist/ -.env -.env.* -STATIC/ -STATIC_REGRET/ diff --git a/map_renderer/.gitignore b/map_renderer/.gitignore deleted file mode 100644 index 72376ee..0000000 --- a/map_renderer/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -node_modules/ -.cache/ -coverage/ -dist/ -.env -.env.* -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -STATIC/ -STATIC_REGRET/ diff --git a/map_renderer/Dockerfile b/map_renderer/Dockerfile deleted file mode 100644 index b06d8b2..0000000 --- a/map_renderer/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM node:20-alpine - -WORKDIR /app - -COPY package.json package-lock.json* ./ -RUN npm ci --omit=dev --no-audit --no-fund - -COPY src ./src - -ENV PORT=3000 -EXPOSE 3000 - -CMD ["npm", "start"] diff --git a/map_renderer/README.md b/map_renderer/README.md deleted file mode 100644 index d87773c..0000000 --- a/map_renderer/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Crusader Map Renderer - -Node web app that renders Crusader maps on the server and streams only finished PNG tiles to the browser. - -## Goals - -- Keep Crusader source assets server-side. -- Detect maps from `STATIC` and `STATIC_REGRET` automatically. -- Build map render state on demand after the user selects a map. -- Serve large maps as draggable and zoomable image tiles. -- Run locally with Node or inside Docker. - -## Local Run - -```powershell -cd map_renderer -npm install -npm start -``` - -Open `http://localhost:3000`. - -Viewer behavior: - -- drag with the mouse or one finger to pan -- use the scroll wheel to zoom directly at the pointer -- pinch to zoom on touch devices -- toggle roofs and editor-only elements independently before building - -The app expects asset folders under the app root: - -- `map_renderer/STATIC` -- `map_renderer/STATIC_REGRET` - -## Docker Run - -The Docker image excludes the Crusader assets on purpose. Mount them at runtime so they stay outside the image and are never served directly to clients. - -```powershell -cd map_renderer -docker build -t crusader-map-renderer . -docker run --rm -p 3000:3000 ` - -v ${PWD}/STATIC:/app/STATIC:ro ` - -v ${PWD}/STATIC_REGRET:/app/STATIC_REGRET:ro ` - crusader-map-renderer -``` - -If only one game is available, mount only that folder. - -## Docker Compose - -The compose file mounts `STATIC` and `STATIC_REGRET` from the host filesystem into the container as read-only volumes. They are excluded from the image build by `.dockerignore`, so the assets are never copied into the image. - -```powershell -cd map_renderer -docker compose up --build -``` - -## HTTP Surface - -- `GET /api/maps` returns the detected catalog. -- `POST /api/builds` starts or reuses a build. -- `GET /api/builds/:id` returns build status. -- `GET /api/maps/:game/:mapId/metadata?buildId=...` returns map bounds and tile settings. -- `GET /api/maps/:game/:mapId/tiles/:tileX/:tileY.png?buildId=...` returns rendered PNG tiles. - -No raw Crusader asset files are exposed over HTTP. diff --git a/map_renderer/compose.yaml b/map_renderer/compose.yaml deleted file mode 100644 index 614534d..0000000 --- a/map_renderer/compose.yaml +++ /dev/null @@ -1,13 +0,0 @@ -services: - map-renderer: - build: - context: . - ports: - - "3000:3000" - environment: - PORT: "3000" - REMORSE_STATIC_DIR: /app/STATIC - REGRET_STATIC_DIR: /app/STATIC_REGRET - volumes: - - ./STATIC:/app/STATIC:ro - - ./STATIC_REGRET:/app/STATIC_REGRET:ro \ No newline at end of file diff --git a/map_renderer/package-lock.json b/map_renderer/package-lock.json deleted file mode 100644 index ef8c63d..0000000 --- a/map_renderer/package-lock.json +++ /dev/null @@ -1,1387 +0,0 @@ -{ - "name": "crusader-map-renderer", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "crusader-map-renderer", - "version": "0.1.0", - "dependencies": { - "express": "^5.1.0", - "pngjs": "^7.0.0", - "sharp": "^0.34.2" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", - "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pngjs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", - "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", - "license": "MIT", - "engines": { - "node": ">=14.19.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - } - } -} diff --git a/map_renderer/package.json b/map_renderer/package.json deleted file mode 100644 index 51fb4c2..0000000 --- a/map_renderer/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "crusader-map-renderer", - "version": "0.1.0", - "private": true, - "type": "module", - "description": "Server-side tiled Crusader map renderer for browser viewing.", - "scripts": { - "start": "node src/server.js", - "dev": "node --watch src/server.js" - }, - "engines": { - "node": ">=20" - }, - "dependencies": { - "express": "^5.1.0", - "pngjs": "^7.0.0", - "sharp": "^0.34.2" - } -} diff --git a/map_renderer/phase-plan.md b/map_renderer/phase-plan.md deleted file mode 100644 index c90024f..0000000 --- a/map_renderer/phase-plan.md +++ /dev/null @@ -1,36 +0,0 @@ -# Map Renderer Phase Plan - -## Phase 1 - -Goal: tighten the current viewer flow without changing the map semantics. - -- remove the explicit manual build button because map selection and filter changes already trigger builds automatically -- hide the viewport's "choose a detected map" message as soon as a map is selected and a build starts -- add build feedback in the side panel with a spinner and a simple phase-based loading bar -- remove visible tile seam lines and the synthetic background grid so the map reads as one surface -- use a more efficient streamed visualization format for interactive tiles while keeping PNG as the final export format - -Phase 1 implementation choice: - -- interactive tiles switch from PNG to lossless WebP because it is broadly supported in current browsers and is more bandwidth-efficient for repeated server-rendered tile delivery -- full-map download remains PNG so export quality and compatibility stay unchanged - -## Phase 2 - -Goal: promote editor-only content from "baked into the raster" to interactive overlay objects. - -- keep the base map rendered as a flat server-generated tile surface -- extract editor-only objects into a standalone overlay data stream -- render those overlay items in the client as positioned interactive markers or sprites above the base map -- on hover, slightly scale the hovered item and show a tooltip with its decoded metadata payload -- improve roof detection before or during the overlay split because the current roof filtering still lets some roofs render when they should not -- identify occluding helper geometry such as invisible walls and render those semitransparently so they remain legible without hiding too much of the map beneath them -- fix pipe rendering because pipes currently are not showing up correctly -- investigate force-field rendering because they appear yellow instead of the expected blue semitransparent look; this may be a debug-shape choice issue or a palette/color-rendering issue -- likely revisit ScummVM Crusader handling in `D:\source\scummvm` to confirm what editor/debug records carry and how best to decode them for display - -Open questions for phase 2: - -- which editor-only families should become interactive overlays versus remain baked into the base render -- what exact metadata fields are reliable enough to expose in the tooltip -- whether some editor-only entries should be clustered, filtered, or toggled by family to keep dense maps usable diff --git a/map_renderer/src/config.js b/map_renderer/src/config.js deleted file mode 100644 index 31803cf..0000000 --- a/map_renderer/src/config.js +++ /dev/null @@ -1,24 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -export const APP_ROOT = path.resolve(__dirname, ".."); -export const PUBLIC_ROOT = path.join(APP_ROOT, "src", "public"); -export const TILE_SIZE = Number.parseInt(process.env.TILE_SIZE ?? "1024", 10); -export const PORT = Number.parseInt(process.env.PORT ?? "3000", 10); -export const CACHE_ROOT = path.join(APP_ROOT, ".cache"); -export const TILE_CACHE_ROOT = path.join(CACHE_ROOT, "tiles"); -export const GAMES = [ - { - id: "remorse", - label: "No Remorse", - staticDir: process.env.REMORSE_STATIC_DIR || path.join(APP_ROOT, "STATIC") - }, - { - id: "regret", - label: "No Regret", - staticDir: process.env.REGRET_STATIC_DIR || path.join(APP_ROOT, "STATIC_REGRET") - } -]; diff --git a/map_renderer/src/lib/binary.js b/map_renderer/src/lib/binary.js deleted file mode 100644 index 04ea581..0000000 --- a/map_renderer/src/lib/binary.js +++ /dev/null @@ -1,15 +0,0 @@ -export function readU16LE(buffer, offset) { - return buffer.readUInt16LE(offset); -} - -export function readU24LE(buffer, offset) { - return buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16); -} - -export function readU32LE(buffer, offset) { - return buffer.readUInt32LE(offset); -} - -export function readI32LE(buffer, offset) { - return buffer.readInt32LE(offset); -} diff --git a/map_renderer/src/lib/build-manager.js b/map_renderer/src/lib/build-manager.js deleted file mode 100644 index de7794b..0000000 --- a/map_renderer/src/lib/build-manager.js +++ /dev/null @@ -1,467 +0,0 @@ -import crypto from "node:crypto"; -import fs from "node:fs"; -import path from "node:path"; -import sharp from "sharp"; - -import { TILE_CACHE_ROOT, TILE_SIZE } from "../config.js"; -import { - FLAG_FLIPPED, - ShapeArchive, - collectRenderItems, - loadGlobs, - loadMapItems, - loadPalette, - loadTypeflags, - resolveStaticFile, - summarizeRenderClasses -} from "./formats.js"; -import { blitFrame, encodePng, rgbaBuffer } from "./png.js"; -import { prepareSortedItems } from "./sorting.js"; - -function nowIso() { - return new Date().toISOString(); -} - -function ensureDir(dirPath) { - fs.mkdirSync(dirPath, { recursive: true }); -} - -const DOWNLOAD_CACHE_ROOT = path.join(TILE_CACHE_ROOT, "downloads"); -sharp.cache(false); - -function normalizeBuildOptions(options = {}) { - return { - includeEditor: options.includeEditor !== false, - includeRoofs: options.includeRoofs === true - }; -} - -function buildOptionSuffix(options) { - return `editor-${options.includeEditor ? "on" : "off"}_roofs-${options.includeRoofs ? "on" : "off"}`; -} - -function makeUsageInfo(gameId, mapId, baseItems, renderItems) { - const itemMapNums = [...new Set(baseItems.map((item) => item.mapNum))].sort((left, right) => left - right); - return { - status: "unknown", - confidence: "unknown", - knownHints: [], - itemMapNums, - nonzeroItemMapNums: itemMapNums.filter((value) => value !== 0), - npcLinkedItemCount: baseItems.filter((item) => item.npcNum !== 0).length, - note: "No authoritative mission-to-map ownership table has been extracted yet. Unknown does not imply orphaned.", - hasRenderableContent: renderItems.length > 0, - game: gameId, - map: mapId - }; -} - -function buildEmptyMetadata(gameConfig, mapId, baseItems, reason) { - return { - game: gameConfig.id, - gameLabel: gameConfig.label, - map: mapId, - rawItemCount: baseItems.length, - itemCount: 0, - paintedItemCount: 0, - occludedItemCount: 0, - invalidItemCount: 0, - invalidItems: [], - usage: makeUsageInfo(gameConfig.id, mapId, baseItems, []), - baseItemSummary: { - roofItems: 0, - editorItems: 0, - eggFamilyItems: 0, - invisibleFlaggedItems: 0, - npcLinkedItems: 0 - }, - sorter: "scummvm_dependency_graph", - isEmpty: true, - emptyReason: reason, - filters: { - includeEditor: true, - includeRoofs: false - }, - bounds: { - screenLeft: 0, - screenTop: 0, - screenRight: TILE_SIZE, - screenBottom: TILE_SIZE, - width: TILE_SIZE, - height: TILE_SIZE - }, - tileSize: TILE_SIZE, - tileCountX: 1, - tileCountY: 1, - zoom: { - min: 0.01, - max: 8, - step: 0.1, - initial: 1 - } - }; -} - -export class BuildManager { - constructor(catalog) { - this.catalog = catalog; - this.assetCache = new Map(); - this.jobs = new Map(); - this.jobsByKey = new Map(); - this.tileCache = new Map(); - ensureDir(TILE_CACHE_ROOT); - } - - listCatalog() { - return this.catalog; - } - - getJob(jobId) { - return this.jobs.get(jobId) ?? null; - } - - async createOrReuseBuild(gameConfig, mapId, rawOptions = {}) { - const options = normalizeBuildOptions(rawOptions); - const key = `${gameConfig.id}:${mapId}:${buildOptionSuffix(options)}`; - const existing = this.jobsByKey.get(key); - if (existing) { - if (existing.status === "ready" || existing.status === "building") { - return existing; - } - this.jobsByKey.delete(key); - } - - const job = { - id: crypto.randomUUID(), - key, - game: gameConfig.id, - mapId, - options, - status: "queued", - phase: "queued", - createdAt: nowIso(), - updatedAt: nowIso(), - progress: [], - error: null, - metadata: null, - build: null - }; - this.jobs.set(job.id, job); - this.jobsByKey.set(key, job); - void this.runBuild(job, gameConfig); - return job; - } - - async runBuild(job, gameConfig) { - try { - job.status = "building"; - job.phase = "loading-assets"; - this.touchJob(job, `Loading ${gameConfig.label} assets`); - const assets = this.getAssets(gameConfig); - - job.phase = "loading-map"; - this.touchJob(job, `Loading map ${job.mapId}`); - const fixedDatPath = resolveStaticFile(gameConfig.staticDir, "FIXED.DAT"); - const baseItems = loadMapItems(fixedDatPath, job.mapId); - this.touchJob(job, `Loaded ${baseItems.length} fixed records`); - - job.phase = "collecting-items"; - const renderItems = collectRenderItems(baseItems, assets.shapeInfos, assets.globs, { - includeEditor: job.options.includeEditor, - expandGlobs: true, - worldRect: null, - includeRoofs: job.options.includeRoofs, - includeHiddenMarkers: true, - checkpointEvery: 2000, - progress: (message) => this.touchJob(job, message) - }); - if (!renderItems.length) { - job.build = { - assets, - prepared: [], - minLeft: 0, - minTop: 0, - width: TILE_SIZE, - height: TILE_SIZE - }; - job.metadata = buildEmptyMetadata(gameConfig, job.mapId, baseItems, "This map has no renderable items in FIXED.DAT."); - job.metadata.filters = { - includeEditor: job.options.includeEditor, - includeRoofs: job.options.includeRoofs - }; - job.status = "ready"; - job.phase = "ready"; - this.touchJob(job, "Build ready: map is empty, serving a blank placeholder tile"); - return; - } - - job.phase = "sorting"; - const sorted = prepareSortedItems(renderItems, assets.shapeArchive, assets.shapeInfos, { - checkpointEvery: 2000, - maxInvalidDetails: 20, - progress: (message) => this.touchJob(job, message) - }); - if (!sorted.prepared.length) { - job.build = { - assets, - prepared: [], - minLeft: 0, - minTop: 0, - width: TILE_SIZE, - height: TILE_SIZE - }; - job.metadata = buildEmptyMetadata( - gameConfig, - job.mapId, - baseItems, - "This map resolved to no valid shape or frame pairs after decoding." - ); - job.metadata.filters = { - includeEditor: job.options.includeEditor, - includeRoofs: job.options.includeRoofs - }; - job.status = "ready"; - job.phase = "ready"; - this.touchJob(job, "Build ready: no valid frames were renderable, serving a blank placeholder tile"); - return; - } - - const width = sorted.maxRight - sorted.minLeft; - const height = sorted.maxBottom - sorted.minTop; - if (width <= 0 || height <= 0) { - throw new Error("Computed image bounds are invalid"); - } - - const metadata = { - game: gameConfig.id, - gameLabel: gameConfig.label, - map: job.mapId, - rawItemCount: baseItems.length, - itemCount: renderItems.length, - paintedItemCount: sorted.prepared.length, - occludedItemCount: sorted.occludedCount, - invalidItemCount: sorted.invalidItemCount, - invalidItems: sorted.invalidItems, - usage: makeUsageInfo(gameConfig.id, job.mapId, baseItems, renderItems), - baseItemSummary: summarizeRenderClasses(baseItems, assets.shapeInfos), - sorter: "scummvm_dependency_graph", - isEmpty: false, - emptyReason: null, - filters: { - includeEditor: job.options.includeEditor, - includeRoofs: job.options.includeRoofs - }, - bounds: { - screenLeft: sorted.minLeft, - screenTop: sorted.minTop, - screenRight: sorted.maxRight, - screenBottom: sorted.maxBottom, - width, - height - }, - tileSize: TILE_SIZE, - tileCountX: Math.ceil(width / TILE_SIZE), - tileCountY: Math.ceil(height / TILE_SIZE), - zoom: { - min: 0.01, - max: 8, - step: 0.1, - initial: 1 - } - }; - - job.build = { - assets, - prepared: sorted.prepared, - minLeft: sorted.minLeft, - minTop: sorted.minTop, - width, - height - }; - job.metadata = metadata; - job.status = "ready"; - job.phase = "ready"; - this.touchJob(job, `Build ready with ${metadata.tileCountX}x${metadata.tileCountY} tiles`); - } catch (error) { - job.status = "failed"; - job.phase = "failed"; - job.error = error instanceof Error ? error.message : String(error); - this.touchJob(job, `Build failed: ${job.error}`); - } - } - - getAssets(gameConfig) { - if (this.assetCache.has(gameConfig.id)) { - return this.assetCache.get(gameConfig.id); - } - const assets = { - palette: loadPalette(resolveStaticFile(gameConfig.staticDir, "GAMEPAL.PAL")), - shapeInfos: loadTypeflags(resolveStaticFile(gameConfig.staticDir, "TYPEFLAG.DAT")), - globs: loadGlobs(resolveStaticFile(gameConfig.staticDir, "GLOB.FLX")), - shapeArchive: new ShapeArchive(resolveStaticFile(gameConfig.staticDir, "SHAPES.FLX")) - }; - this.assetCache.set(gameConfig.id, assets); - return assets; - } - - touchJob(job, message) { - job.updatedAt = nowIso(); - job.progress.push({ - at: job.updatedAt, - phase: job.phase, - message - }); - if (job.progress.length > 100) { - job.progress.splice(0, job.progress.length - 100); - } - } - - getPublicJob(job) { - return { - id: job.id, - game: job.game, - mapId: job.mapId, - options: job.options, - status: job.status, - phase: job.phase, - createdAt: job.createdAt, - updatedAt: job.updatedAt, - error: job.error, - metadata: job.status === "ready" ? job.metadata : null, - progress: job.progress - }; - } - - getMetadata(jobId, gameId, mapId) { - const job = this.requireReadyJob(jobId, gameId, mapId); - return job.metadata; - } - - async renderFullMap(jobId, gameId, mapId) { - const job = this.requireReadyJob(jobId, gameId, mapId); - const outputPath = path.join( - DOWNLOAD_CACHE_ROOT, - gameId, - `map-${mapId}`, - `${gameId}-map-${mapId}-${buildOptionSuffix(job.options)}.png` - ); - if (fs.existsSync(outputPath)) { - return outputPath; - } - - ensureDir(path.dirname(outputPath)); - const composites = []; - for (let tileY = 0; tileY < job.metadata.tileCountY; tileY += 1) { - for (let tileX = 0; tileX < job.metadata.tileCountX; tileX += 1) { - composites.push({ - input: await this.renderTile(jobId, gameId, mapId, tileX, tileY, "png"), - left: tileX * job.metadata.tileSize, - top: tileY * job.metadata.tileSize - }); - } - } - - await sharp({ - create: { - width: job.metadata.bounds.width, - height: job.metadata.bounds.height, - channels: 4, - background: { r: 10, g: 12, b: 18, alpha: 1 } - }, - limitInputPixels: false - }) - .composite(composites) - .png() - .toFile(outputPath); - - return outputPath; - } - - async renderTile(jobId, gameId, mapId, tileX, tileY, format = "webp") { - const job = this.requireReadyJob(jobId, gameId, mapId); - const tileKey = `${job.id}:${format}:${tileX}:${tileY}`; - if (this.tileCache.has(tileKey)) { - return this.tileCache.get(tileKey); - } - - const extension = format === "png" ? "png" : "webp"; - - const tilePath = path.join( - TILE_CACHE_ROOT, - gameId, - `map-${mapId}`, - buildOptionSuffix(job.options), - `${tileX}-${tileY}.${extension}` - ); - if (fs.existsSync(tilePath)) { - const cached = fs.readFileSync(tilePath); - this.tileCache.set(tileKey, cached); - return cached; - } - - const tileLeft = tileX * TILE_SIZE; - const tileTop = tileY * TILE_SIZE; - const tileWidth = Math.max(0, Math.min(TILE_SIZE, job.build.width - tileLeft)); - const tileHeight = Math.max(0, Math.min(TILE_SIZE, job.build.height - tileTop)); - if (tileWidth <= 0 || tileHeight <= 0) { - throw new Error("Requested tile is outside the rendered map bounds"); - } - - const buffer = rgbaBuffer(tileWidth, tileHeight); - const screenLeft = job.build.minLeft + tileLeft; - const screenTop = job.build.minTop + tileTop; - const screenRight = screenLeft + tileWidth; - const screenBottom = screenTop + tileHeight; - - for (const node of job.build.prepared) { - if (node.right <= screenLeft || node.left >= screenRight || node.bottom <= screenTop || node.top >= screenBottom) { - continue; - } - blitFrame( - buffer, - tileWidth, - tileHeight, - node.left - screenLeft, - node.top - screenTop, - node.frame, - node.pixels, - job.build.assets.palette, - Boolean(node.item.flags & FLAG_FLIPPED) - ); - } - - let output; - if (format === "png") { - output = encodePng(tileWidth, tileHeight, buffer); - } else { - output = await sharp(buffer, { - raw: { - width: tileWidth, - height: tileHeight, - channels: 4 - }, - limitInputPixels: false - }) - .webp({ lossless: true, effort: 4 }) - .toBuffer(); - } - ensureDir(path.dirname(tilePath)); - fs.writeFileSync(tilePath, output); - this.tileCache.set(tileKey, output); - return output; - } - - requireReadyJob(jobId, gameId, mapId) { - const job = this.getJob(jobId); - if (!job) { - throw new Error("Unknown build id"); - } - if (job.game !== gameId || job.mapId !== mapId) { - throw new Error("Build id does not match the requested map"); - } - if (job.status !== "ready") { - throw new Error("Build is not ready yet"); - } - return job; - } -} diff --git a/map_renderer/src/lib/catalog.js b/map_renderer/src/lib/catalog.js deleted file mode 100644 index a58eb64..0000000 --- a/map_renderer/src/lib/catalog.js +++ /dev/null @@ -1,34 +0,0 @@ -import fs from "node:fs"; - -import { GAMES } from "../config.js"; -import { getMapSummaries, resolveStaticFile } from "./formats.js"; - -export function detectCatalog() { - const games = []; - for (const game of GAMES) { - const fixedDat = resolveStaticFile(game.staticDir, "FIXED.DAT"); - if (!fs.existsSync(fixedDat)) { - continue; - } - const maps = getMapSummaries(fixedDat) - .filter((map) => map.isValid && map.rawItemCount > 0) - .map((map) => ({ - id: map.id, - label: `${game.label} Map ${map.id}`, - rawItemCount: map.rawItemCount - })); - if (maps.length > 0) { - games.push({ - id: game.id, - label: game.label, - mapCount: maps.length, - maps - }); - } - } - return { games }; -} - -export function getGameConfig(gameId) { - return GAMES.find((game) => game.id === gameId) ?? null; -} diff --git a/map_renderer/src/lib/formats.js b/map_renderer/src/lib/formats.js deleted file mode 100644 index 830a28e..0000000 --- a/map_renderer/src/lib/formats.js +++ /dev/null @@ -1,475 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; - -import { readI32LE, readU16LE, readU24LE, readU32LE } from "./binary.js"; - -export const FLEX_TABLE_OFFSET = 0x80; -export const FLEX_COUNT_OFFSET = 0x54; -export const FIXED_MAP_COUNT_OFFSET = 0x54; -export const FIXED_MAP_TABLE_OFFSET = 0x80; -export const CRUSADER_COORD_SCALE = 2; -export const GLOB_COORD_MASK = ~0x3ff; -export const GLOB_COORD_SHIFT = 2; -export const GLOB_COORD_OFFSET = 2; -export const FLAG_INVISIBLE = 0x0010; -export const FLAG_FLIPPED = 0x0020; -export const EGG_FAMILIES = new Set([3, 4, 7, 8]); - -export const SI_FIXED = 0x0001; -export const SI_SOLID = 0x0002; -export const SI_LAND = 0x0008; -export const SI_OCCL = 0x0010; -export const SI_NOISY = 0x0080; -export const SI_DRAW = 0x0100; -export const SI_ROOF = 0x0400; -export const SI_TRANSL = 0x0800; - -export function getMapCount(fixedDatPath) { - const data = fs.readFileSync(fixedDatPath); - return readU16LE(data, FIXED_MAP_COUNT_OFFSET); -} - -export function getMapSummaries(fixedDatPath) { - const data = fs.readFileSync(fixedDatPath); - const mapCount = readU16LE(data, FIXED_MAP_COUNT_OFFSET); - const maps = []; - for (let mapId = 0; mapId < mapCount; mapId += 1) { - const tableOffset = FIXED_MAP_TABLE_OFFSET + mapId * 8; - const mapOffset = readU32LE(data, tableOffset); - const mapSize = readU32LE(data, tableOffset + 4); - maps.push({ - id: mapId, - offset: mapOffset, - byteSize: mapSize, - rawItemCount: Math.floor(mapSize / 16), - isValid: mapSize >= 16 - }); - } - return maps; -} - -export class FlexArchive { - constructor(filePath) { - this.path = filePath; - this.data = fs.readFileSync(filePath); - this.entries = FlexArchive.readEntries(this.data); - } - - static readEntries(data) { - const count = readU32LE(data, FLEX_COUNT_OFFSET); - const entries = []; - for (let index = 0; index < count; index += 1) { - const base = FLEX_TABLE_OFFSET + index * 8; - entries.push({ - offset: readU32LE(data, base), - size: readU32LE(data, base + 4) - }); - } - return entries; - } - - get(index) { - const entry = this.entries[index]; - if (!entry || entry.size === 0) { - return Buffer.alloc(0); - } - return this.data.subarray(entry.offset, entry.offset + entry.size); - } - - get length() { - return this.entries.length; - } -} - -export class ShapeArchive { - constructor(filePath) { - this.archive = new FlexArchive(filePath); - this.shapeCache = new Map(); - this.decodedFrameCache = new Map(); - } - - getFrame(shapeIndex, frameIndex) { - const frames = this.getShape(shapeIndex); - if (frameIndex < 0 || frameIndex >= frames.length) { - throw new RangeError(`shape ${shapeIndex} frame ${frameIndex} out of range`); - } - return frames[frameIndex]; - } - - decodeFrame(shapeIndex, frameIndex) { - const cacheKey = `${shapeIndex}:${frameIndex}`; - let decoded = this.decodedFrameCache.get(cacheKey); - const frame = this.getFrame(shapeIndex, frameIndex); - if (!decoded) { - decoded = decodePixels(frame); - this.decodedFrameCache.set(cacheKey, decoded); - } - return { frame, pixels: decoded }; - } - - getShape(shapeIndex) { - if (this.shapeCache.has(shapeIndex)) { - return this.shapeCache.get(shapeIndex); - } - const raw = this.archive.get(shapeIndex); - if (!raw.length) { - throw new Error(`shape ${shapeIndex} has no data`); - } - const frames = parseShape(raw); - this.shapeCache.set(shapeIndex, frames); - return frames; - } -} - -function parseShape(data) { - const frameCount = readU16LE(data, 4); - const frames = []; - for (let index = 0; index < frameCount; index += 1) { - const headerOffset = 6 + index * 8; - const frameOffset = readU24LE(data, headerOffset); - const frameSize = readU32LE(data, headerOffset + 4); - const frameData = data.subarray(frameOffset, frameOffset + frameSize); - if (frameData.length < 28) { - throw new Error(`frame ${index} too small: ${frameData.length}`); - } - const compressed = Boolean(readU32LE(frameData, 8)); - const width = readU32LE(frameData, 12); - const height = readU32LE(frameData, 16); - const xoff = readI32LE(frameData, 20); - const yoff = readI32LE(frameData, 24); - const lineOffsets = []; - for (let row = 0; row < height; row += 1) { - lineOffsets.push(readU32LE(frameData, 28 + row * 4) - ((height - row) * 4)); - } - const rleOffset = 28 + height * 4; - frames.push({ - compressed, - width, - height, - xoff, - yoff, - lineOffsets, - rleData: frameData.subarray(rleOffset) - }); - } - return frames; -} - -function decodePixels(frame) { - const pixels = new Int16Array(frame.width * frame.height); - pixels.fill(-1); - const rle = frame.rleData; - for (let row = 0; row < frame.height; row += 1) { - let pos = frame.lineOffsets[row]; - let xpos = 0; - while (xpos < frame.width) { - if (pos >= rle.length) { - throw new Error(`row ${row} overran RLE data`); - } - xpos += rle[pos]; - pos += 1; - if (xpos === frame.width) { - break; - } - if (pos >= rle.length) { - throw new Error(`row ${row} missing run header`); - } - let dlen = rle[pos]; - pos += 1; - let runType = 0; - if (frame.compressed) { - runType = dlen & 1; - dlen >>= 1; - } - if (dlen <= 0 || xpos + dlen > frame.width) { - throw new Error(`invalid run length ${dlen} at row ${row}`); - } - const rowBase = row * frame.width + xpos; - if (runType === 0) { - const end = pos + dlen; - if (end > rle.length) { - throw new Error(`row ${row} literal run overruns RLE data`); - } - for (let index = 0; index < dlen; index += 1) { - pixels[rowBase + index] = rle[pos + index]; - } - pos = end; - } else { - if (pos >= rle.length) { - throw new Error(`row ${row} repeated-color run missing color byte`); - } - const color = rle[pos]; - pos += 1; - for (let index = 0; index < dlen; index += 1) { - pixels[rowBase + index] = color; - } - } - xpos += dlen; - } - } - return pixels; -} - -export function loadPalette(filePath) { - const data = fs.readFileSync(filePath); - if (data.length < 768) { - throw new Error(`palette too small: ${filePath}`); - } - const palette = []; - for (let index = 0; index < 256; index += 1) { - const r = Math.floor((data[index * 3] * 255) / 63); - const g = Math.floor((data[index * 3 + 1] * 255) / 63); - const b = Math.floor((data[index * 3 + 2] * 255) / 63); - palette.push([r, g, b]); - } - return palette; -} - -export function loadTypeflags(filePath) { - const data = fs.readFileSync(filePath); - const infos = []; - for (let base = 0; base + 9 <= data.length; base += 9) { - const block = data.subarray(base, base + 9); - let flags = 0; - if (block[0] & 0x01) flags |= 0x0001; - if (block[0] & 0x02) flags |= 0x0002; - if (block[0] & 0x04) flags |= 0x0004; - if (block[0] & 0x08) flags |= 0x0008; - if (block[0] & 0x10) flags |= 0x0010; - if (block[0] & 0x20) flags |= 0x0020; - if (block[0] & 0x40) flags |= 0x0040; - if (block[0] & 0x80) flags |= 0x0080; - if (block[1] & 0x01) flags |= 0x0100; - if (block[1] & 0x02) flags |= 0x0200; - if (block[1] & 0x04) flags |= 0x0400; - if (block[1] & 0x08) flags |= 0x0800; - if (block[6] & 0x01) flags |= 0x1000; - if (block[6] & 0x02) flags |= 0x2000; - if (block[6] & 0x04) flags |= 0x4000; - if (block[6] & 0x08) flags |= 0x8000; - if (block[6] & 0x10) flags |= 0x10000; - if (block[6] & 0x20) flags |= 0x20000; - if (block[6] & 0x40) flags |= 0x40000; - if (block[6] & 0x80) flags |= 0x80000; - const family = (block[1] >> 4) + ((block[2] & 1) << 4); - const x = ((block[3] << 3) | (block[2] >> 5)) & 0x1f; - const y = (block[3] >> 2) & 0x1f; - const z = ((block[4] << 1) | (block[3] >> 7)) & 0x1f; - const animType = block[4] >> 4; - infos.push({ - family, - flags, - x, - y, - z, - animType, - isEditor: Boolean(flags & 0x1000), - isFixed: Boolean(flags & SI_FIXED), - isSolid: Boolean(flags & SI_SOLID), - isLand: Boolean(flags & SI_LAND), - isOccl: Boolean(flags & SI_OCCL), - isNoisy: Boolean(flags & SI_NOISY), - isDraw: Boolean(flags & SI_DRAW), - isRoof: Boolean(flags & SI_ROOF), - isTranslucent: Boolean(flags & SI_TRANSL), - isInvitem: family === 13 - }); - } - return infos; -} - -export function loadGlobs(filePath) { - const archive = new FlexArchive(filePath); - const globs = []; - for (let index = 0; index < archive.length; index += 1) { - const raw = archive.get(index); - if (!raw.length) { - globs.push([]); - continue; - } - const count = readU16LE(raw, 0); - const items = []; - for (let itemIndex = 0; itemIndex < count; itemIndex += 1) { - const base = 2 + itemIndex * 6; - items.push({ - x: raw[base], - y: raw[base + 1], - z: raw[base + 2], - shape: readU16LE(raw, base + 3), - frame: raw[base + 5] - }); - } - globs.push(items); - } - return globs; -} - -export function loadMapItems(filePath, mapIndex) { - if (!fs.existsSync(filePath)) { - throw new Error(`Missing file: ${filePath}`); - } - const data = fs.readFileSync(filePath); - const mapCount = readU16LE(data, FIXED_MAP_COUNT_OFFSET); - if (mapIndex < 0 || mapIndex >= mapCount) { - throw new Error(`map index ${mapIndex} out of range 0..${mapCount - 1}`); - } - const tableOffset = FIXED_MAP_TABLE_OFFSET + mapIndex * 8; - const mapOffset = readU32LE(data, tableOffset); - const mapSize = readU32LE(data, tableOffset + 4); - const payload = data.subarray(mapOffset, mapOffset + mapSize); - if (payload.length !== mapSize) { - throw new Error(`map ${mapIndex} payload truncated`); - } - const items = []; - for (let base = 0; base + 16 <= payload.length; base += 16) { - const record = payload.subarray(base, base + 16); - items.push({ - x: readU16LE(record, 0) * CRUSADER_COORD_SCALE, - y: readU16LE(record, 2) * CRUSADER_COORD_SCALE, - z: record[4], - shape: readU16LE(record, 5), - frame: record[7], - flags: readU16LE(record, 8), - quality: readU16LE(record, 10), - npcNum: record[12], - mapNum: record[13], - nextItem: readU16LE(record, 14), - source: "fixed" - }); - } - return items; -} - -export function expandGlobItem(item, globs) { - if (item.quality < 0 || item.quality >= globs.length) { - return []; - } - return globs[item.quality].map((globItem) => ({ - x: (item.x & GLOB_COORD_MASK) + (globItem.x << GLOB_COORD_SHIFT) + GLOB_COORD_OFFSET, - y: (item.y & GLOB_COORD_MASK) + (globItem.y << GLOB_COORD_SHIFT) + GLOB_COORD_OFFSET, - z: item.z + globItem.z, - shape: globItem.shape, - frame: globItem.frame, - flags: 0, - quality: 0, - npcNum: 0, - mapNum: item.mapNum, - nextItem: 0, - source: "glob" - })); -} - -export function collectRenderItems(baseItems, shapeInfos, globs, options) { - const { - includeEditor, - expandGlobs, - worldRect, - includeRoofs, - includeHiddenMarkers, - progress, - checkpointEvery = 0 - } = options; - - const renderItems = []; - const pending = [...baseItems]; - let index = 0; - let skippedInvisible = 0; - let skippedWorldRect = 0; - let skippedInvalidShape = 0; - let skippedEditor = 0; - let skippedEgg = 0; - let skippedRoof = 0; - let skippedHidden = 0; - let expandedGlobs = 0; - - while (index < pending.length) { - const item = pending[index]; - index += 1; - if (item.flags & FLAG_INVISIBLE) { - if (!includeHiddenMarkers) { - skippedHidden += 1; - continue; - } - skippedInvisible += 1; - } - if (worldRect) { - const [minX, minY, maxX, maxY] = worldRect; - if (item.x < minX || item.y < minY || item.x > maxX || item.y > maxY) { - skippedWorldRect += 1; - continue; - } - } - if (item.shape >= shapeInfos.length) { - skippedInvalidShape += 1; - continue; - } - const info = shapeInfos[item.shape]; - if (info.isEditor && !includeEditor) { - skippedEditor += 1; - continue; - } - if (info.isRoof && !includeRoofs) { - skippedRoof += 1; - continue; - } - if (expandGlobs && info.family === 3 && item.source === "fixed") { - pending.push(...expandGlobItem(item, globs)); - expandedGlobs += 1; - if (!includeHiddenMarkers) { - continue; - } - } - if (EGG_FAMILIES.has(info.family) && !includeHiddenMarkers) { - skippedEgg += 1; - continue; - } - renderItems.push(item); - if (progress && checkpointEvery > 0 && index % checkpointEvery === 0) { - progress( - `collect processed=${index} pending=${pending.length} rendered=${renderItems.length} expanded_globs=${expandedGlobs} ` + - `skipped=(invisible:${skippedInvisible}, world:${skippedWorldRect}, shape:${skippedInvalidShape}, editor:${skippedEditor}, ` + - `roof:${skippedRoof}, hidden:${skippedHidden}, egg:${skippedEgg})` - ); - } - } - - if (progress) { - progress( - `collect complete processed=${index} pending=${pending.length} rendered=${renderItems.length} expanded_globs=${expandedGlobs} ` + - `skipped=(invisible:${skippedInvisible}, world:${skippedWorldRect}, shape:${skippedInvalidShape}, editor:${skippedEditor}, ` + - `roof:${skippedRoof}, hidden:${skippedHidden}, egg:${skippedEgg})` - ); - } - - return renderItems; -} - -export function summarizeRenderClasses(baseItems, shapeInfos) { - const summary = { - roofItems: 0, - editorItems: 0, - eggFamilyItems: 0, - invisibleFlaggedItems: 0, - npcLinkedItems: 0 - }; - for (const item of baseItems) { - if (item.flags & FLAG_INVISIBLE) { - summary.invisibleFlaggedItems += 1; - } - if (item.npcNum !== 0) { - summary.npcLinkedItems += 1; - } - if (item.shape >= shapeInfos.length) { - continue; - } - const info = shapeInfos[item.shape]; - if (info.isRoof) summary.roofItems += 1; - if (info.isEditor) summary.editorItems += 1; - if (EGG_FAMILIES.has(info.family)) summary.eggFamilyItems += 1; - } - return summary; -} - -export function resolveStaticFile(staticDir, name) { - return path.join(staticDir, name); -} diff --git a/map_renderer/src/lib/png.js b/map_renderer/src/lib/png.js deleted file mode 100644 index bd38b87..0000000 --- a/map_renderer/src/lib/png.js +++ /dev/null @@ -1,48 +0,0 @@ -import { PNG } from "pngjs"; - -export const DEFAULT_BACKGROUND = [10, 12, 18, 255]; - -export function rgbaBuffer(width, height, color = DEFAULT_BACKGROUND) { - const [r, g, b, a] = color; - const pixels = Buffer.alloc(width * height * 4); - for (let offset = 0; offset < pixels.length; offset += 4) { - pixels[offset] = r; - pixels[offset + 1] = g; - pixels[offset + 2] = b; - pixels[offset + 3] = a; - } - return pixels; -} - -export function blitFrame(buffer, canvasWidth, canvasHeight, left, top, frame, pixels, palette, flipped) { - for (let srcY = 0; srcY < frame.height; srcY += 1) { - const dstY = top + srcY; - if (dstY < 0 || dstY >= canvasHeight) { - continue; - } - const rowBase = srcY * frame.width; - for (let srcX = 0; srcX < frame.width; srcX += 1) { - const pixelIndex = rowBase + (flipped ? frame.width - 1 - srcX : srcX); - const colorIndex = pixels[pixelIndex]; - if (colorIndex < 0) { - continue; - } - const dstX = left + srcX; - if (dstX < 0 || dstX >= canvasWidth) { - continue; - } - const pixelBase = (dstY * canvasWidth + dstX) * 4; - const [r, g, b] = palette[colorIndex]; - buffer[pixelBase] = r; - buffer[pixelBase + 1] = g; - buffer[pixelBase + 2] = b; - buffer[pixelBase + 3] = 255; - } - } -} - -export function encodePng(width, height, data) { - const png = new PNG({ width, height }); - data.copy(png.data); - return PNG.sync.write(png, { colorType: 6, inputColorType: 6 }); -} diff --git a/map_renderer/src/lib/sorting.js b/map_renderer/src/lib/sorting.js deleted file mode 100644 index f8d5f17..0000000 --- a/map_renderer/src/lib/sorting.js +++ /dev/null @@ -1,394 +0,0 @@ -import { FLAG_FLIPPED } from "./formats.js"; - -function rectIntersects(left, right) { - return left.left < right.right && left.right > right.left && left.top < right.bottom && left.bottom > right.top; -} - -function rectContains(outer, inner) { - return outer.left <= inner.left && outer.top <= inner.top && outer.right >= inner.right && outer.bottom >= inner.bottom; -} - -function listLessThan(left, right) { - if (left.sprite !== right.sprite) { - return left.sprite < right.sprite; - } - if (left.z !== right.z) { - return left.z < right.z; - } - return left.flat > right.flat; -} - -function overlap(left, right) { - if (!rectIntersects(left, right)) { - return false; - } - const pointTopDiff = [left.sx_top - right.sx_bot, left.sy_top - right.sy_bot]; - const pointBotDiff = [left.sx_bot - right.sx_top, left.sy_bot - right.sy_top]; - const dotTopLeft = pointTopDiff[0] + pointTopDiff[1] * 2; - const dotTopRight = -pointTopDiff[0] + pointTopDiff[1] * 2; - const dotBotLeft = pointBotDiff[0] - pointBotDiff[1] * 2; - const dotBotRight = -pointBotDiff[0] - pointBotDiff[1] * 2; - const rightClear = left.sx_right <= right.sx_left; - const leftClear = left.sx_left >= right.sx_right; - const topLeftClear = dotTopLeft >= 0; - const topRightClear = dotTopRight >= 0; - const botLeftClear = dotBotLeft >= 0; - const botRightClear = dotBotRight >= 0; - const clear = rightClear || leftClear || botRightClear || botLeftClear || topRightClear || topLeftClear; - return !clear; -} - -function occludes(left, right) { - if (!rectContains(left, right)) { - return false; - } - const pointTopDiff = [left.sx_top - right.sx_top, left.sy_top - right.sy_top]; - const pointBotDiff = [left.sx_bot - right.sx_bot, left.sy_bot - right.sy_bot]; - const dotTopLeft = pointTopDiff[0] + pointTopDiff[1] * 2; - const dotTopRight = -pointTopDiff[0] + pointTopDiff[1] * 2; - const dotBotLeft = pointBotDiff[0] - pointBotDiff[1] * 2; - const dotBotRight = -pointBotDiff[0] - pointBotDiff[1] * 2; - const rightRes = left.sx_right >= right.sx_right; - const leftRes = left.sx_left <= right.sx_left; - const topLeftRes = dotTopLeft <= 0; - const topRightRes = dotTopRight <= 0; - const botLeftRes = dotBotLeft <= 0; - const botRightRes = dotBotRight <= 0; - return rightRes && leftRes && botRightRes && botLeftRes && topRightRes && topLeftRes; -} - -function below(left, right) { - if (left.sprite !== right.sprite) { - return left.sprite < right.sprite; - } - - if (left.flat && right.flat) { - if (left.z !== right.z) { - return left.z < right.z; - } - } else if (left.invitem === right.invitem) { - if (left.z_top <= right.z) { - return true; - } - if (left.z >= right.z_top) { - return false; - } - } - - const yFlatLeft = left.y_far === left.y; - const yFlatRight = right.y_far === right.y; - if (yFlatLeft && yFlatRight) { - if (Math.floor(left.y / 32) !== Math.floor(right.y / 32)) { - return left.y < right.y; - } - } else { - if (left.y <= right.y_far) { - return true; - } - if (left.y_far >= right.y) { - return false; - } - } - - const xFlatLeft = left.x_left === left.x; - const xFlatRight = right.x_left === right.x; - if (xFlatLeft && xFlatRight) { - if (Math.floor(left.x / 32) !== Math.floor(right.x / 32)) { - return left.x < right.x; - } - } else { - if (left.x <= right.x_left) { - return true; - } - if (left.x_left >= right.x) { - return false; - } - } - - if (left.z_top - 8 <= right.z && left.z < right.z_top - 8) { - return true; - } - if (left.z >= right.z_top - 8 && left.z_top - 8 > right.z) { - return false; - } - - if (yFlatLeft !== yFlatRight) { - if (Math.floor(left.y / 32) <= Math.floor(right.y_far / 32)) { - return true; - } - if (Math.floor(left.y_far / 32) >= Math.floor(right.y / 32)) { - return false; - } - const yCenterLeft = Math.floor((Math.floor(left.y_far / 32) + Math.floor(left.y / 32)) / 2); - const yCenterRight = Math.floor((Math.floor(right.y_far / 32) + Math.floor(right.y / 32)) / 2); - if (yCenterLeft !== yCenterRight) { - return yCenterLeft < yCenterRight; - } - } - - if (xFlatLeft !== xFlatRight) { - if (Math.floor(left.x / 32) <= Math.floor(right.x_left / 32)) { - return true; - } - if (Math.floor(left.x_left / 32) >= Math.floor(right.x / 32)) { - return false; - } - const xCenterLeft = Math.floor((Math.floor(left.x_left / 32) + Math.floor(left.x / 32)) / 2); - const xCenterRight = Math.floor((Math.floor(right.x_left / 32) + Math.floor(right.x / 32)) / 2); - if (xCenterLeft !== xCenterRight) { - return xCenterLeft < xCenterRight; - } - } - - if (left.flat || right.flat) { - if (left.z !== right.z) return left.z < right.z; - if (left.invitem !== right.invitem) return left.invitem < right.invitem; - if (left.flat !== right.flat) return left.flat > right.flat; - if (left.trans !== right.trans) return left.trans < right.trans; - if (left.anim !== right.anim) return left.anim < right.anim; - if (left.draw !== right.draw) return left.draw > right.draw; - if (left.solid !== right.solid) return left.solid > right.solid; - if (left.occl !== right.occl) return left.occl > right.occl; - if (left.fbigsq !== right.fbigsq) return left.fbigsq > right.fbigsq; - } - - if (left.x === right.x && left.y === right.y && left.trans !== right.trans) { - return left.trans < right.trans; - } - - if (left.land && right.land && left.roof !== right.roof) { - return left.roof < right.roof; - } - if (left.roof !== right.roof) { - return left.roof > right.roof; - } - if (left.z !== right.z) { - return left.z < right.z; - } - - if (xFlatLeft || xFlatRight || yFlatLeft || yFlatRight) { - if (left.sx_left !== right.sx_left) { - return left.sx_left > right.sx_left; - } - if (left.sy_bot !== right.sy_bot) { - return left.sy_bot < right.sy_bot; - } - } - - if (left.x + left.y !== right.x + right.y) return left.x + left.y < right.x + right.y; - if (left.x_left + left.y_far !== right.x_left + right.y_far) return left.x_left + left.y_far < right.x_left + right.y_far; - if (left.y !== right.y) return left.y < right.y; - if (left.x !== right.x) return left.x < right.x; - if (left.item.shape !== right.item.shape) return left.item.shape < right.item.shape; - return left.item.frame < right.item.frame; -} - -function buildSortNode(item, info, frame, pixels) { - const flipped = Boolean(item.flags & FLAG_FLIPPED); - const xdim = (flipped ? info.y : info.x) * 32; - const ydim = (flipped ? info.x : info.y) * 32; - const zdim = info.z * 8; - const x = item.x; - const y = item.y; - const z = item.z; - const xLeft = x - xdim; - const yFar = y - ydim; - const zTop = z + zdim; - const sxLeft = Math.trunc(xLeft / 4 - y / 4); - const sxRight = Math.trunc(x / 4 - yFar / 4); - const sxTop = Math.trunc(xLeft / 4 - yFar / 4); - const syTop = Math.trunc(xLeft / 8 + yFar / 8 - zTop); - const sxBot = Math.trunc(x / 4 - y / 4); - const syBot = Math.trunc(x / 8 + y / 8 - z); - const left = flipped ? sxBot + frame.xoff - frame.width : sxBot - frame.xoff; - const top = syBot - frame.yoff; - const right = left + frame.width; - const bottom = top + frame.height; - - return { - item, - info, - frame, - pixels, - left, - top, - right, - bottom, - x, - x_left: xLeft, - y, - y_far: yFar, - z, - z_top: zTop, - sx_left: sxLeft, - sx_right: sxRight, - sx_top: sxTop, - sy_top: syTop, - sx_bot: sxBot, - sy_bot: syBot, - fbigsq: xdim === ydim && xdim >= 128, - flat: zdim === 0, - occl: info.isOccl && !info.isTranslucent, - solid: info.isSolid, - draw: info.isDraw, - roof: info.isRoof, - noisy: info.isNoisy, - anim: info.animType !== 0, - trans: info.isTranslucent, - fixed: info.isFixed, - land: info.isLand, - sprite: false, - invitem: info.isInvitem, - occluded: false, - order: -1, - depends: [] - }; -} - -function insertDependencySorted(depends, node) { - for (let index = 0; index < depends.length; index += 1) { - const current = depends[index]; - if (current === node) { - return false; - } - if (listLessThan(node, current)) { - depends.splice(index, 0, node); - return true; - } - } - depends.push(node); - return true; -} - -function resolvePaintOrder(ordered, progress, checkpointEvery = 0) { - const painted = []; - - function visit(node) { - if (node.occluded || node.order >= 0) { - return; - } - node.order = -2; - for (const dependency of node.depends) { - if (dependency.order === -2) { - break; - } - if (dependency.order === -1) { - visit(dependency); - } - } - node.order = painted.length ? painted[painted.length - 1].order + 1 : 0; - painted.push(node); - if (progress && checkpointEvery > 0 && painted.length % checkpointEvery === 0) { - progress(`paint resolved=${painted.length} of ${ordered.length}`); - } - } - - for (const node of ordered) { - if (node.order === -1) { - visit(node); - } - } - if (progress) { - progress(`paint complete resolved=${painted.length} of ${ordered.length}`); - } - return painted; -} - -export function prepareSortedItems(items, archive, shapeInfos, options = {}) { - const { progress, checkpointEvery = 0, maxInvalidDetails = 20 } = options; - const ordered = []; - let minLeft = Number.MAX_SAFE_INTEGER; - let minTop = Number.MAX_SAFE_INTEGER; - let maxRight = -Number.MAX_SAFE_INTEGER; - let maxBottom = -Number.MAX_SAFE_INTEGER; - let occludedCount = 0; - let invalidItemCount = 0; - const invalidItems = []; - let dependencyCount = 0; - - for (let itemIndex = 0; itemIndex < items.length; itemIndex += 1) { - const item = items[itemIndex]; - let frame; - let pixels; - try { - const decoded = archive.decodeFrame(item.shape, item.frame); - frame = decoded.frame; - pixels = decoded.pixels; - } catch (error) { - invalidItemCount += 1; - if (invalidItems.length < maxInvalidDetails) { - invalidItems.push({ - shape: item.shape, - frame: item.frame, - x: item.x, - y: item.y, - z: item.z, - source: item.source, - reason: error instanceof Error ? error.message : String(error) - }); - } - continue; - } - - const node = buildSortNode(item, shapeInfos[item.shape], frame, pixels); - minLeft = Math.min(minLeft, node.left); - minTop = Math.min(minTop, node.top); - maxRight = Math.max(maxRight, node.right); - maxBottom = Math.max(maxBottom, node.bottom); - - let insertAt = ordered.length; - for (let index = 0; index < ordered.length; index += 1) { - const other = ordered[index]; - if (insertAt === ordered.length && listLessThan(node, other)) { - insertAt = index; - } - if (other.occluded) { - continue; - } - if (!overlap(node, other)) { - continue; - } - if (below(node, other)) { - if (other.occl && occludes(other, node)) { - node.occluded = true; - occludedCount += 1; - break; - } - if (insertDependencySorted(other.depends, node)) { - dependencyCount += 1; - } - } else if (node.occl && occludes(node, other)) { - if (!other.occluded) { - other.occluded = true; - occludedCount += 1; - } - } else if (insertDependencySorted(node.depends, other)) { - dependencyCount += 1; - } - } - ordered.splice(insertAt, 0, node); - - if (progress && checkpointEvery > 0 && (itemIndex + 1) % checkpointEvery === 0) { - progress( - `sort processed=${itemIndex + 1} valid=${ordered.length} occluded=${occludedCount} invalid=${invalidItemCount} dependencies=${dependencyCount}` - ); - } - } - - if (progress) { - progress( - `sort complete processed=${items.length} valid=${ordered.length} occluded=${occludedCount} invalid=${invalidItemCount} dependencies=${dependencyCount}` - ); - } - - return { - minLeft, - minTop, - maxRight, - maxBottom, - prepared: resolvePaintOrder(ordered, progress, checkpointEvery), - occludedCount, - invalidItemCount, - invalidItems - }; -} diff --git a/map_renderer/src/public/app.css b/map_renderer/src/public/app.css deleted file mode 100644 index 46514d3..0000000 --- a/map_renderer/src/public/app.css +++ /dev/null @@ -1,325 +0,0 @@ -:root { - color-scheme: light dark; - --bg: #f1ead6; - --panel: rgba(255, 248, 232, 0.92); - --card: rgba(255, 255, 255, 0.58); - --panel-border: rgba(94, 73, 37, 0.25); - --ink: #2d2212; - --muted: #6e5a37; - --accent: #0d6c7d; - --accent-strong: #114f59; - --viewport: #0e1218; - --tile-border: rgba(255, 255, 255, 0.04); - --shadow: 0 18px 45px rgba(59, 40, 8, 0.16); - --font-ui: "Segoe UI Variable Text", "Aptos", "Trebuchet MS", sans-serif; -} - -@media (prefers-color-scheme: dark) { - :root { - --bg: #12161d; - --panel: rgba(22, 28, 38, 0.92); - --card: rgba(28, 35, 46, 0.96); - --panel-border: rgba(166, 187, 211, 0.16); - --ink: #edf2fa; - --muted: #aab8cc; - --accent: #46a7bc; - --accent-strong: #2a7b8d; - --viewport: #06080d; - --tile-border: rgba(255, 255, 255, 0.03); - --shadow: 0 18px 45px rgba(0, 0, 0, 0.35); - } -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - min-height: 100vh; - font-family: var(--font-ui); - color: var(--ink); - background: - radial-gradient(circle at top left, rgba(255, 255, 255, 0.14), transparent 32%), - linear-gradient(135deg, var(--bg) 0%, color-mix(in srgb, var(--bg) 75%, #6e8aa3 25%) 100%); -} - -.shell { - display: grid; - grid-template-columns: 340px minmax(0, 1fr); - min-height: 100vh; -} - -.panel { - padding: 24px; - background: var(--panel); - backdrop-filter: blur(16px); - border-right: 1px solid var(--panel-border); - box-shadow: var(--shadow); -} - -.panel h1 { - margin: 0; - font-size: 1.6rem; - line-height: 1.1; -} - -.lede, -.muted { - color: var(--muted); -} - -.stack { - display: grid; - gap: 10px; - margin-top: 20px; -} - -label { - font-weight: 700; - font-size: 0.95rem; -} - -select, -.action-link { - width: 100%; - border-radius: 12px; - border: 1px solid rgba(65, 48, 21, 0.18); - padding: 12px 14px; - font: inherit; -} - -.action-link { - cursor: pointer; - color: white; - background: linear-gradient(180deg, var(--accent) 0%, var(--accent-strong) 100%); - text-decoration: none; - text-align: center; -} - -select:disabled, -.action-link.is-disabled { - cursor: not-allowed; - opacity: 0.55; -} - -.button-row { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px; -} - -.toggle-grid, -.status, -.meta-panel { - padding: 12px 14px; - border-radius: 14px; - background: var(--card); - border: 1px solid rgba(65, 48, 21, 0.08); - overflow-wrap: anywhere; -} - -.toggle-grid { - display: grid; - gap: 10px; -} - -.toggle { - display: flex; - align-items: center; - gap: 10px; - font-weight: 600; - font-size: 0.92rem; -} - -.toggle input { - width: 18px; - height: 18px; - margin: 0; -} - -.action-link { - display: inline-flex; - align-items: center; - justify-content: center; - font-weight: 700; -} - -.status { - min-height: 92px; -} - -.status-row { - display: flex; - align-items: center; - gap: 10px; -} - -.status-text { - min-height: 24px; -} - -.spinner { - width: 18px; - height: 18px; - border-radius: 999px; - border: 2px solid rgba(255, 255, 255, 0.2); - border-top-color: var(--accent); - animation: spin 0.8s linear infinite; - flex: 0 0 auto; -} - -.progress-wrap { - margin-top: 12px; -} - -.progress-track { - width: 100%; - height: 8px; - border-radius: 999px; - overflow: hidden; - background: rgba(0, 0, 0, 0.08); -} - -.progress-fill { - height: 100%; - width: 0%; - border-radius: inherit; - background: linear-gradient(90deg, var(--accent) 0%, var(--accent-strong) 100%); - transition: width 180ms ease; -} - -.meta-panel { - min-height: 240px; -} - -.meta-empty { - margin: 0; - color: var(--muted); -} - -.meta-section + .meta-section { - margin-top: 14px; - padding-top: 14px; - border-top: 1px solid color-mix(in srgb, var(--panel-border) 70%, transparent 30%); -} - -.meta-title { - margin: 0 0 8px; - font-size: 0.84rem; - font-weight: 800; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--muted); -} - -.meta-grid { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 6px 12px; - font-size: 0.92rem; -} - -.meta-grid dt { - color: var(--muted); -} - -.meta-grid dd { - margin: 0; - text-align: right; - font-weight: 600; -} - -.workspace { - min-width: 0; - padding: 18px; -} - -.viewport { - position: relative; - width: 100%; - height: calc(100vh - 36px); - overflow: hidden; - border-radius: 24px; - background: radial-gradient(circle at top left, rgba(255,255,255,0.04), transparent 26%), var(--viewport); - box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05), var(--shadow); - touch-action: none; - cursor: grab; - user-select: none; -} - -.scene { - position: absolute; - left: 0; - top: 0; - transform-origin: top left; - will-change: transform; -} - -.layer { - position: absolute; - inset: 0 auto auto 0; -} - -.tile { - position: absolute; - image-rendering: pixelated; - image-rendering: crisp-edges; - pointer-events: none; -} - -@keyframes spin { - from { - transform: rotate(0deg); - } - - to { - transform: rotate(360deg); - } -} - -.viewport.is-dragging { - cursor: grabbing; -} - -.viewport-hint { - position: absolute; - top: 16px; - left: 16px; - z-index: 2; - padding: 8px 12px; - border-radius: 999px; - background: rgba(6, 8, 12, 0.66); - color: rgba(255, 255, 255, 0.82); - font-size: 0.86rem; - backdrop-filter: blur(10px); -} - -.empty-state { - position: absolute; - inset: 0; - display: grid; - place-items: center; - padding: 24px; - color: rgba(255,255,255,0.72); - text-align: center; -} - -.empty-state.is-hidden { - display: none; -} - -@media (max-width: 900px) { - .shell { - grid-template-columns: 1fr; - } - - .panel { - border-right: 0; - border-bottom: 1px solid var(--panel-border); - } - - .viewport { - height: 70vh; - } -} diff --git a/map_renderer/src/public/app.js b/map_renderer/src/public/app.js deleted file mode 100644 index 4af8465..0000000 --- a/map_renderer/src/public/app.js +++ /dev/null @@ -1,630 +0,0 @@ -const mapForm = document.querySelector("#map-form"); -const mapSelect = document.querySelector("#map-select"); -const includeEditorCheckbox = document.querySelector("#include-editor"); -const includeRoofsCheckbox = document.querySelector("#include-roofs"); -const downloadButton = document.querySelector("#download-button"); -const spinner = document.querySelector("#spinner"); -const progressWrap = document.querySelector("#progress-wrap"); -const progressFill = document.querySelector("#progress-fill"); -const statusBox = document.querySelector("#status"); -const metaBox = document.querySelector("#meta"); -const viewport = document.querySelector("#viewport"); -const scene = document.querySelector("#scene"); -const emptyState = document.querySelector("#empty-state"); -const zoomLabel = document.querySelector("#zoom-label"); -const zoomInButton = document.querySelector("#zoom-in"); -const zoomOutButton = document.querySelector("#zoom-out"); -const zoomResetButton = document.querySelector("#zoom-reset"); -const zoomFitButton = document.querySelector("#zoom-fit"); - -let activeLayer = document.querySelector("#active-layer"); -let autoBuildTimer = null; - -const state = { - catalog: null, - current: null, - zoom: 1, - offsetX: 0, - offsetY: 0, - buildPollTimer: null, - buildToken: 0, - drag: null, - pointers: new Map(), - pinch: null -}; - -const ZOOM_FACTOR = 1.2; -const FIT_PADDING = 24; - -function setEmptyStateVisible(visible) { - emptyState.hidden = !visible; - emptyState.classList.toggle("is-hidden", !visible); -} - -async function fetchJson(url, init) { - const response = await fetch(url, init); - if (!response.ok) { - let message = `HTTP ${response.status}`; - try { - const body = await response.json(); - if (body.error) { - message = body.error; - } - } catch { - // Ignore parse failures. - } - throw new Error(message); - } - return response.json(); -} - -function setStatus(message) { - statusBox.textContent = message; -} - -function phaseProgress(build) { - const phaseToValue = { - queued: 5, - "loading-assets": 18, - "loading-map": 32, - "collecting-items": 58, - sorting: 84, - ready: 100, - failed: 100 - }; - return phaseToValue[build?.phase] ?? 8; -} - -function setLoadingState(active, build = null) { - spinner.hidden = !active; - progressWrap.hidden = !active; - if (active) { - progressFill.style.width = `${phaseProgress(build)}%`; - } else { - progressFill.style.width = "0%"; - } -} - -function setMeta(metadata) { - if (!metadata) { - metaBox.innerHTML = '
'; - return; - } - - metaBox.innerHTML = ` - - - - `; -} - -function enableZoomControls(enabled) { - zoomInButton.disabled = !enabled; - zoomOutButton.disabled = !enabled; - zoomResetButton.disabled = !enabled; - zoomFitButton.disabled = !enabled; -} - -function setDownloadState(enabled, href = "#") { - downloadButton.href = href; - downloadButton.classList.toggle("is-disabled", !enabled); - downloadButton.setAttribute("aria-disabled", String(!enabled)); -} - -function updateZoomLabel() { - zoomLabel.textContent = `Zoom: ${Math.round(state.zoom * 100)}%`; -} - -function updateSceneLayout() { - if (!state.current) { - scene.style.width = "0px"; - scene.style.height = "0px"; - scene.style.transform = "translate(0px, 0px) scale(1)"; - return; - } - const { metadata } = state.current; - scene.style.width = `${metadata.bounds.width}px`; - scene.style.height = `${metadata.bounds.height}px`; - scene.style.transform = `translate(${state.offsetX}px, ${state.offsetY}px) scale(${state.zoom})`; -} - -function clampZoom(nextZoom) { - if (!state.current) { - return 1; - } - const { min, max } = state.current.metadata.zoom; - return Math.min(max, Math.max(min, nextZoom)); -} - -function clampOffsets() { - if (!state.current) { - return; - } - const { width, height } = state.current.metadata.bounds; - const scaledWidth = width * state.zoom; - const scaledHeight = height * state.zoom; - - if (scaledWidth <= viewport.clientWidth) { - state.offsetX = (viewport.clientWidth - scaledWidth) / 2; - } else { - state.offsetX = Math.min(0, Math.max(viewport.clientWidth - scaledWidth, state.offsetX)); - } - - if (scaledHeight <= viewport.clientHeight) { - state.offsetY = (viewport.clientHeight - scaledHeight) / 2; - } else { - state.offsetY = Math.min(0, Math.max(viewport.clientHeight - scaledHeight, state.offsetY)); - } -} - -function setZoom(nextZoom, anchor = null) { - if (!state.current) { - return; - } - const clamped = clampZoom(nextZoom); - if (clamped === state.zoom) { - updateZoomLabel(); - return; - } - - const focus = anchor ?? { x: viewport.clientWidth / 2, y: viewport.clientHeight / 2 }; - const worldX = (focus.x - state.offsetX) / state.zoom; - const worldY = (focus.y - state.offsetY) / state.zoom; - - state.zoom = clamped; - state.offsetX = focus.x - worldX * state.zoom; - state.offsetY = focus.y - worldY * state.zoom; - clampOffsets(); - updateSceneLayout(); - updateZoomLabel(); -} - -function fitMap() { - if (!state.current) { - return; - } - const { width, height } = state.current.metadata.bounds; - const scaleX = Math.max(0.01, (viewport.clientWidth - FIT_PADDING * 2) / width); - const scaleY = Math.max(0.01, (viewport.clientHeight - FIT_PADDING * 2) / height); - state.zoom = clampZoom(Math.min(scaleX, scaleY)); - clampOffsets(); - updateSceneLayout(); - updateZoomLabel(); -} - -function tileUrl(buildContext, tileX, tileY) { - const { selected, jobId } = buildContext; - return `/api/maps/${selected.game}/${selected.mapId}/tiles/${tileX}/${tileY}.webp?buildId=${encodeURIComponent(jobId)}`; -} - -function createTileElement(tileX, tileY, buildContext, metadata) { - const tileSize = metadata.tileSize; - const tile = document.createElement("img"); - tile.className = "tile"; - tile.alt = `Tile ${tileX},${tileY}`; - tile.loading = "eager"; - tile.decoding = "async"; - tile.draggable = false; - tile.src = tileUrl(buildContext, tileX, tileY); - tile.style.left = `${tileX * tileSize}px`; - tile.style.top = `${tileY * tileSize}px`; - tile.style.width = `${Math.min(tileSize, metadata.bounds.width - tileX * tileSize)}px`; - tile.style.height = `${Math.min(tileSize, metadata.bounds.height - tileY * tileSize)}px`; - return tile; -} - -function waitForImage(tile) { - if (tile.complete && tile.naturalWidth > 0) { - return tile.decode?.().catch(() => undefined) ?? Promise.resolve(); - } - return new Promise((resolve, reject) => { - tile.addEventListener( - "load", - () => { - const decodePromise = tile.decode?.().catch(() => undefined); - Promise.resolve(decodePromise).then(resolve); - }, - { once: true } - ); - tile.addEventListener("error", () => reject(new Error(`Failed to load ${tile.alt}`)), { once: true }); - }); -} - -async function buildLayer(buildContext) { - const layer = document.createElement("div"); - layer.className = "layer"; - const { metadata } = buildContext; - layer.style.width = `${metadata.bounds.width}px`; - layer.style.height = `${metadata.bounds.height}px`; - - const tilePromises = []; - for (let tileY = 0; tileY < metadata.tileCountY; tileY += 1) { - for (let tileX = 0; tileX < metadata.tileCountX; tileX += 1) { - const tile = createTileElement(tileX, tileY, buildContext, metadata); - layer.append(tile); - tilePromises.push(waitForImage(tile)); - } - } - - return { - layer, - ready: Promise.all(tilePromises) - }; -} - -function getSelectedMap() { - if (!mapSelect.value) { - return null; - } - return JSON.parse(mapSelect.value); -} - -function currentSelectionMatches(selected) { - return Boolean( - state.current && - state.current.selected.game === selected.game && - state.current.selected.mapId === selected.mapId - ); -} - -function currentFiltersMatch() { - return Boolean( - state.current && - state.current.metadata.filters.includeEditor === includeEditorCheckbox.checked && - state.current.metadata.filters.includeRoofs === includeRoofsCheckbox.checked - ); -} - -function scheduleAutoBuild() { - clearTimeout(autoBuildTimer); - setEmptyStateVisible(false); - autoBuildTimer = window.setTimeout(() => { - const selected = getSelectedMap(); - if (!selected) { - setEmptyStateVisible(true); - return; - } - if (currentSelectionMatches(selected) && currentFiltersMatch()) { - return; - } - startBuild(selected).catch((error) => { - setStatus(error instanceof Error ? error.message : String(error)); - }); - }, 100); -} - -function populateCatalog(catalog) { - state.catalog = catalog; - mapSelect.innerHTML = ""; - const placeholder = document.createElement("option"); - placeholder.textContent = "Select a map"; - placeholder.value = ""; - mapSelect.append(placeholder); - - for (const game of catalog.games) { - const group = document.createElement("optgroup"); - group.label = `${game.label} (${game.mapCount} maps)`; - for (const map of game.maps) { - const option = document.createElement("option"); - option.value = JSON.stringify({ game: game.id, mapId: map.id }); - option.textContent = `${map.label} (${map.rawItemCount} items)`; - group.append(option); - } - mapSelect.append(group); - } - - mapSelect.disabled = catalog.games.length === 0; - setDownloadState(false); - if (catalog.games.length === 0) { - setStatus("No usable STATIC folders were detected under the app root."); - } else { - setStatus("Select a map to build it immediately."); - } -} - -async function startBuild(selected) { - clearTimeout(state.buildPollTimer); - const token = ++state.buildToken; - const preserveView = currentSelectionMatches(selected); - - setEmptyStateVisible(false); - - if (!state.current) { - enableZoomControls(false); - setMeta(null); - setDownloadState(false); - } - - setLoadingState(true, { phase: "queued" }); - - setStatus( - preserveView - ? `Rebuilding ${selected.game} map ${selected.mapId}. The current view stays visible until the new tiles are ready.` - : `Building ${selected.game} map ${selected.mapId}...` - ); - - const build = await fetchJson("/api/builds", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - ...selected, - includeEditor: includeEditorCheckbox.checked, - includeRoofs: includeRoofsCheckbox.checked - }) - }); - - await pollBuild(build.id, selected, token, preserveView); -} - -async function pollBuild(jobId, selected, token, preserveView) { - if (token !== state.buildToken) { - return; - } - - const build = await fetchJson(`/api/builds/${encodeURIComponent(jobId)}`); - if (token !== state.buildToken) { - return; - } - - const latest = build.progress.at(-1); - setLoadingState(build.status !== "ready" && build.status !== "failed", build); - setStatus(latest ? `${build.phase}: ${latest.message}` : `${build.phase}...`); - if (build.status === "failed") { - setLoadingState(false); - throw new Error(build.error || "Build failed"); - } - if (build.status !== "ready") { - state.buildPollTimer = window.setTimeout(() => { - pollBuild(jobId, selected, token, preserveView).catch((error) => { - setStatus(error.message); - }); - }, 1000); - return; - } - - const metadata = await fetchJson( - `/api/maps/${selected.game}/${selected.mapId}/metadata?buildId=${encodeURIComponent(jobId)}` - ); - if (token !== state.buildToken) { - return; - } - - const nextContext = { selected, jobId, metadata }; - const nextLayerBuild = await buildLayer(nextContext); - if (token !== state.buildToken) { - return; - } - - state.current = nextContext; - setMeta(metadata); - setDownloadState( - true, - `/api/maps/${selected.game}/${selected.mapId}/download.png?buildId=${encodeURIComponent(jobId)}` - ); - setEmptyStateVisible(false); - enableZoomControls(true); - - if (!preserveView) { - activeLayer.replaceWith(nextLayerBuild.layer); - activeLayer = nextLayerBuild.layer; - fitMap(); - setStatus(`Rendering complete. Streaming tiles for ${selected.game} map ${selected.mapId}...`); - nextLayerBuild.ready.then(() => { - if (token !== state.buildToken) { - return; - } - setLoadingState(false); - setStatus(`Ready. ${selected.game} map ${selected.mapId} is fully loaded.`); - }).catch((error) => { - if (token !== state.buildToken) { - return; - } - setLoadingState(false); - setStatus(error instanceof Error ? error.message : String(error)); - }); - return; - } - - setStatus(`Rendering complete. Streaming updated tiles for ${selected.game} map ${selected.mapId}...`); - try { - await nextLayerBuild.ready; - } catch (error) { - if (token !== state.buildToken) { - return; - } - setLoadingState(false); - throw error; - } - if (token !== state.buildToken) { - return; - } - - activeLayer.replaceWith(nextLayerBuild.layer); - activeLayer = nextLayerBuild.layer; - clampOffsets(); - updateSceneLayout(); - updateZoomLabel(); - setLoadingState(false); - setStatus(`Ready. ${selected.game} map ${selected.mapId} is fully loaded.`); -} - -async function loadCatalog() { - const catalog = await fetchJson("/api/maps"); - populateCatalog(catalog); -} - -mapForm.addEventListener("submit", async (event) => { - event.preventDefault(); - const selected = getSelectedMap(); - if (!selected) { - setStatus("Choose a map first."); - return; - } - - try { - await startBuild(selected); - } catch (error) { - setStatus(error instanceof Error ? error.message : String(error)); - } -}); - -mapSelect.addEventListener("change", scheduleAutoBuild); -includeEditorCheckbox.addEventListener("change", scheduleAutoBuild); -includeRoofsCheckbox.addEventListener("change", scheduleAutoBuild); - -downloadButton.addEventListener("click", (event) => { - if (downloadButton.classList.contains("is-disabled")) { - event.preventDefault(); - } -}); - -window.addEventListener("resize", () => { - if (state.current) { - clampOffsets(); - updateSceneLayout(); - } -}); - -zoomInButton.addEventListener("click", () => { - setZoom(state.zoom * ZOOM_FACTOR); -}); - -zoomOutButton.addEventListener("click", () => { - setZoom(state.zoom / ZOOM_FACTOR); -}); - -zoomResetButton.addEventListener("click", () => { - setZoom(1); -}); - -zoomFitButton.addEventListener("click", () => { - fitMap(); -}); - -viewport.addEventListener( - "wheel", - (event) => { - if (!state.current) { - return; - } - event.preventDefault(); - const rect = viewport.getBoundingClientRect(); - const nextZoom = event.deltaY < 0 ? state.zoom * ZOOM_FACTOR : state.zoom / ZOOM_FACTOR; - setZoom(nextZoom, { x: event.clientX - rect.left, y: event.clientY - rect.top }); - }, - { passive: false } -); - -viewport.addEventListener("pointerdown", (event) => { - if (!state.current) { - return; - } - event.preventDefault(); - viewport.setPointerCapture(event.pointerId); - state.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY }); - - if (state.pointers.size === 1) { - state.drag = { - pointerId: event.pointerId, - startX: event.clientX, - startY: event.clientY, - originX: state.offsetX, - originY: state.offsetY - }; - viewport.classList.add("is-dragging"); - } - - if (state.pointers.size === 2) { - const [first, second] = [...state.pointers.values()]; - state.pinch = { - distance: Math.hypot(second.x - first.x, second.y - first.y), - zoom: state.zoom - }; - state.drag = null; - } -}); - -viewport.addEventListener("pointermove", (event) => { - if (!state.current || !state.pointers.has(event.pointerId)) { - return; - } - state.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY }); - - if (state.pointers.size === 2 && state.pinch) { - const [first, second] = [...state.pointers.values()]; - const distance = Math.hypot(second.x - first.x, second.y - first.y); - if (distance > 0) { - const rect = viewport.getBoundingClientRect(); - const center = { - x: (first.x + second.x) / 2 - rect.left, - y: (first.y + second.y) / 2 - rect.top - }; - setZoom(state.pinch.zoom * (distance / state.pinch.distance), center); - } - return; - } - - if (!state.drag || state.drag.pointerId !== event.pointerId) { - return; - } - - state.offsetX = state.drag.originX + (event.clientX - state.drag.startX); - state.offsetY = state.drag.originY + (event.clientY - state.drag.startY); - clampOffsets(); - updateSceneLayout(); -}); - -function releasePointer(event) { - state.pointers.delete(event.pointerId); - if (viewport.hasPointerCapture(event.pointerId)) { - viewport.releasePointerCapture(event.pointerId); - } - if (state.pointers.size < 2) { - state.pinch = null; - } - if (state.drag?.pointerId === event.pointerId) { - state.drag = null; - } - if (state.pointers.size === 0) { - viewport.classList.remove("is-dragging"); - } -} - -viewport.addEventListener("pointerup", releasePointer); -viewport.addEventListener("pointercancel", releasePointer); -viewport.addEventListener("lostpointercapture", releasePointer); - -enableZoomControls(false); -updateZoomLabel(); -setMeta(null); -setDownloadState(false); -setLoadingState(false); -setEmptyStateVisible(true); -loadCatalog().catch((error) => { - setStatus(error instanceof Error ? error.message : String(error)); -}); diff --git a/map_renderer/src/public/index.html b/map_renderer/src/public/index.html deleted file mode 100644 index 70a2da9..0000000 --- a/map_renderer/src/public/index.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - -