diff --git a/.gitignore b/.gitignore index ca901c9..f1d1d90 100644 --- a/.gitignore +++ b/.gitignore @@ -44,11 +44,4 @@ bin/** USECODE/REGRET/REGRET_USECODE_extracted/chunks/** exports/** out/** -binary/** -psx-map-exporter/.output-render/** - -# JavaScript / Node -**/node_modules/** -**/dist/** -**/.vite/** -*.log +binary/** \ No newline at end of file diff --git a/Crusader.rep/idata/01/~0000001b.db/change.data.gbf b/Crusader.rep/idata/01/~0000001b.db/change.data.gbf index 04590df..dc09744 100644 Binary files a/Crusader.rep/idata/01/~0000001b.db/change.data.gbf and b/Crusader.rep/idata/01/~0000001b.db/change.data.gbf differ diff --git a/Crusader.rep/idata/01/~0000001b.db/change.map.gbf b/Crusader.rep/idata/01/~0000001b.db/change.map.gbf index bfa28d6..8acd9ec 100644 Binary files a/Crusader.rep/idata/01/~0000001b.db/change.map.gbf and b/Crusader.rep/idata/01/~0000001b.db/change.map.gbf differ diff --git a/Crusader.rep/idata/01/~0000001b.db/db.68.gbf b/Crusader.rep/idata/01/~0000001b.db/db.66.gbf similarity index 99% rename from Crusader.rep/idata/01/~0000001b.db/db.68.gbf rename to Crusader.rep/idata/01/~0000001b.db/db.66.gbf index cf10513..f632e1e 100644 Binary files a/Crusader.rep/idata/01/~0000001b.db/db.68.gbf and b/Crusader.rep/idata/01/~0000001b.db/db.66.gbf differ diff --git a/Crusader.rep/user/00/~00000008.db/db.62.gbf b/Crusader.rep/user/00/~00000008.db/db.60.gbf similarity index 99% rename from Crusader.rep/user/00/~00000008.db/db.62.gbf rename to Crusader.rep/user/00/~00000008.db/db.60.gbf index 392b73a..e10876c 100644 Binary files a/Crusader.rep/user/00/~00000008.db/db.62.gbf and b/Crusader.rep/user/00/~00000008.db/db.60.gbf differ diff --git a/Crusader.rep/user/00/~0000000d.db/db.12.gbf b/Crusader.rep/user/00/~0000000d.db/db.10.gbf similarity index 99% rename from Crusader.rep/user/00/~0000000d.db/db.12.gbf rename to Crusader.rep/user/00/~0000000d.db/db.10.gbf index e4d7f31..e78bbc3 100644 Binary files a/Crusader.rep/user/00/~0000000d.db/db.12.gbf and b/Crusader.rep/user/00/~0000000d.db/db.10.gbf differ diff --git a/check_type.cjs b/check_type.cjs deleted file mode 100644 index 65d1a48..0000000 --- a/check_type.cjs +++ /dev/null @@ -1,4 +0,0 @@ -const fs = require('fs'); -const data = JSON.parse(fs.readFileSync('psx-map-exporter/.output-render/L0/auto/L0.json', 'utf8')); -console.log('Type of data:', Array.isArray(data) ? 'Array' : typeof data); -if (!Array.isArray(data)) console.log('Keys:', Object.keys(data)); diff --git a/inspect_l0.cjs b/inspect_l0.cjs deleted file mode 100644 index d1b7495..0000000 --- a/inspect_l0.cjs +++ /dev/null @@ -1,15 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const jsonPath = 'psx-map-exporter/.output-render/L0/auto/L0.json'; -const cacheDir = 'psx-map-exporter/.cache/L0/sprites'; -const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); -const items = (data.items || []).filter(item => item.bundleAbsoluteOffset !== undefined && item.frameIndex !== undefined && item.width > 0).slice(0, 5); -items.forEach(item => { - const bundleHex = item.bundleAbsoluteOffset.toString(16).padStart(8, '0'); - const frameIdx = item.frameIndex.toString().padStart(3, '0'); - const fileName = `bundle_${bundleHex}/frame_${frameIdx}.png`; - const fullPath = path.join(cacheDir, fileName); - const exists = fs.existsSync(fullPath); - console.log(`recordIndex: ${item.recordIndex}, bundleAbsoluteOffset: ${item.bundleAbsoluteOffset}, frameIndex: ${item.frameIndex}, width: ${item.width}, height: ${item.height}`); - console.log(`File: ${fileName}, Exists: ${exists}`); -}); diff --git a/psx-map-exporter/.gitignore b/psx-map-exporter/.gitignore index da86674..7d70366 100644 --- a/psx-map-exporter/.gitignore +++ b/psx-map-exporter/.gitignore @@ -1,7 +1,3 @@ .cache/** .output/** -.output-render/** -.tmp/** -node_modules/** -viewer/dist/** -viewer/.vite/** \ No newline at end of file +node_modules/** \ No newline at end of file diff --git a/psx-map-exporter/docs/spec.md b/psx-map-exporter/docs/spec.md index f796d09..dbccc85 100644 --- a/psx-map-exporter/docs/spec.md +++ b/psx-map-exporter/docs/spec.md @@ -201,81 +201,27 @@ For each record: - `screenX - originX` - `screenY - originY` -### Projection sign convention +Current draw order is conservative: -`psx_project_object_main_visible @ 0x80040d44` writes `proj_x = Y - X` and -`proj_y = 2*Z - (X + Y) / 2` to `obj+0x78/0x7c`. The engine's draw step at -`0x80040e3c/0x80040e5c` then computes `screen = (cam - proj) - origin`, which -flips the sign of the projection relative to canvas Y-down space. For a -camera-less full-map export the exporter bakes that flip into -`projectCtorPlacement`, so higher world-Z and smaller (X+Y) sit visibly higher -on the output PNG. The same projection is applied to both constructor -placements and dispatch-root records; dispatch-root X/Y fields are in world -coordinates, not pre-projected screen coordinates, despite the runtime -`camera +/- 0x140` cull comparing against them directly. +- main-visible before special-visible +- then ascending `screenY` +- then ascending `screenX` -### Authored layer semantics +This is a probe approximation. The later graph-based stage-1 ordering still belongs to a future pass. -The two authored lanes carry different responsibilities: - -- `constructors`: static level geometry — walls, floors, architecture placed - by `psx_dispatch_section0_constructor_placements`. -- `roots`: interactive / dynamic objects — crates, terminals, doors, pickups - placed by `psx_dispatch_section0_dispatch_roots`. - -Despite the dispatcher name, the `roots` lane is not the map background; it is -the live-object seed list. For the exporter, "constructors" is the geometry -layer and "roots" is the object layer. - -### Painter's order - -The exporter sorts items before blitting using the following keys, in order: - -1. `stage` ascending. `stage = 1` when `typeWord === 4` or `laneWord & 0x0400` - is set; those overlays draw last. -2. Authored-layer priority: `constructors` (0) before `roots` (1). Static - geometry draws first so interactive props in the same room do not get - hidden behind the floor or wall pieces that occupy the same cell. -3. Isometric depth ascending: back-to-front by world `X + Y` (isometric - ground-depth axis). Falls back to projected `screenY` when world - coordinates are unavailable. -4. World `Z` ascending within the same ground cell so lower elevations draw - before taller objects sharing the same footprint. -5. `screenX` ascending as a stable tie-breaker. - -This is still an approximation of the engine's stage-1 graph order but is -closer to what an isometric painter's algorithm would produce than the earlier -screenY-only sort. - -The rendered PNG uses a neutral opaque background by default so probe -silhouettes are legible without relying on transparency. +The rendered PNG uses a neutral opaque background by default so probe silhouettes are legible without relying on transparency. ## Color Rule -The exporter resolves palettes entirely from the WDL contents. It does not -require any RAM or VRAM dump; those paths are now optional research overrides. +`v0` emits grayscale art from raw pixel indices. -The map-local palette blob lives at `headerWords[2] .. headerWords[2] + 0x1000` -(4096 bytes = 2048 colors = 128 × 16-entry CLUTs). The blob is what the engine -uploads to VRAM rows `0xf0 .. 0xf7` on map load; each VRAM row is 16 CLUTs -wide so the 128 CLUTs tile exactly 8 rows of 16 CLUTs. +Reason: -Resolution rules by bundle mode: +- bundle frame decode is already well constrained +- full CLUT parity is not +- grayscale preserves shape/variant evidence without pretending the palette problem is solved -- **Mode 2 (4bpp)**: the bundle header's `paletteIndex` at `+0x14` is the - 16-entry CLUT index into the WDL `palettes16` bank. When that index points - at a sparse/empty CLUT the exporter falls back to a per-bundle palette sweep - that picks a CLUT covering the pixel-index set used by the bundle frames. -- **Mode 1 (8bpp)**: the 256-color CLUT is the concatenation of 16 consecutive - 16-entry CLUTs from the WDL bank. The bundle's `paletteIndex` is treated as - the starting CLUT index. For the current L0 dataset every mode-1 bundle - stores `paletteIndex = 0`, which is the top-left 256-color bank. Mode-1 - color fidelity is therefore approximate until the level-specific 256-CLUT - source (suspected to live in the `stateBank` block) is decoded — tracked as - a follow-up. - -Transparent pixel index `0` stays transparent during blit regardless of the -color value stored at CLUT index 0. +Transparent index `0` stays transparent. ## CLI @@ -308,13 +254,8 @@ Supported options: ## Planned Follow-Ups -- decode the `stateBank` and `stateBank2` blocks to recover the level-specific - 256-color CLUT used by mode-1 sprites. Current mode-1 palettes default to - CLUT-bank start 0, which produces plausible colors for some sprites but - renders many indoor floor tiles as solid green plates. -- extend `sceneInterpretation` so it reflects the landed loader-faithful - binding instead of the older repeated-wrong-art warning. -- recover the engine's stage-1 graph ordering instead of approximating with - isometric `(X + Y, Z, screenX)` sort keys. -- compare the probe scene against fixed live samples such as `map 104` without - reintroducing viewer-side donor assumptions. +- extend `sceneInterpretation` so it reflects the landed loader-faithful binding instead of the older repeated-wrong-art warning +- identify and parse the separate static-world or subordinate level substrate that complements the constructor-fed live-object lane, instead of treating section-0 constructor placements as the whole map +- add palette/CLUT reconstruction +- add stage-1 graph ordering recovery +- compare the probe scene against fixed live samples such as `map 104` without reintroducing viewer-side donor assumptions diff --git a/psx-map-exporter/scripts/export-all.mjs b/psx-map-exporter/scripts/export-all.mjs deleted file mode 100644 index 3735207..0000000 --- a/psx-map-exporter/scripts/export-all.mjs +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env node -// Batch-export every LSET*/L*.WDL found under the PSX disc root into a -// permanent .output-render/// directory tree. Each export -// runs as a separate child process so an OOM or crash on one map cannot kill -// the batch. - -import { spawn } from 'node:child_process'; -import fs from 'node:fs/promises'; -import path from 'node:path'; -import process from 'node:process'; -import { fileURLToPath } from 'node:url'; - -const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); -const PROJECT_ROOT = path.resolve(SCRIPT_DIR, '..'); -const CLI_PATH = path.join(PROJECT_ROOT, 'src', 'cli.js'); - -const VARIANTS = [ - { label: 'auto', mapSource: 'auto' }, - { label: 'region01', mapSource: 'region01' }, -]; - -function parseArgs(argv) { - const options = { - discRoot: 'E:/emu/psx/Crusader - No Remorse', - outputRoot: path.join(PROJECT_ROOT, '.output-render'), - debugLabels: true, - only: null, - timeoutMs: 300000, - }; - for (let index = 2; index < argv.length; index += 1) { - const arg = argv[index]; - const next = argv[index + 1]; - if (arg === '--disc-root') { options.discRoot = path.resolve(next); index += 1; } - else if (arg === '--output-root') { options.outputRoot = path.resolve(next); index += 1; } - else if (arg === '--no-debug-labels') { options.debugLabels = false; } - else if (arg === '--only') { - options.only = String(next).split(',').map((v) => v.trim()).filter(Boolean); - index += 1; - } else if (arg === '--timeout-ms') { - options.timeoutMs = Number(next); index += 1; - } - } - return options; -} - -async function discoverMaps(discRoot) { - const entries = await fs.readdir(discRoot, { withFileTypes: true }); - const maps = []; - for (const entry of entries) { - if (!entry.isDirectory()) continue; - if (!/^LSET\d+$/i.test(entry.name)) continue; - const dir = path.join(discRoot, entry.name); - const files = await fs.readdir(dir); - for (const file of files) { - if (!/^L\d+\.WDL$/i.test(file)) continue; - maps.push({ - set: entry.name, - name: path.parse(file).name, - wdlPath: path.join(dir, file), - sourceRel: `${entry.name}/${file}`, - }); - } - } - maps.sort((a, b) => { - const an = Number.parseInt(a.name.replace(/^L/i, ''), 10); - const bn = Number.parseInt(b.name.replace(/^L/i, ''), 10); - return an - bn; - }); - return maps; -} - -function runExport(mapEntry, variant, options) { - return new Promise((resolve) => { - const outDir = path.join(options.outputRoot, mapEntry.name, variant.label); - const args = [ - '--max-old-space-size=4096', - CLI_PATH, - '--disc-root', options.discRoot, - '--source', mapEntry.sourceRel, - '--map-source', variant.mapSource, - '--output-root', outDir, - ]; - if (options.debugLabels) args.push('--debug-labels'); - - const started = Date.now(); - const child = spawn(process.execPath, args, { stdio: ['ignore', 'pipe', 'pipe'] }); - let stdout = ''; - let stderr = ''; - let killed = false; - const timer = setTimeout(() => { - killed = true; - child.kill('SIGKILL'); - }, options.timeoutMs); - - child.stdout.on('data', (chunk) => { stdout += chunk; }); - child.stderr.on('data', (chunk) => { stderr += chunk; }); - child.on('close', (code, signal) => { - clearTimeout(timer); - const ms = Date.now() - started; - resolve({ outDir, code, signal, killed, ms, stdout, stderr }); - }); - }); -} - -async function main() { - const options = parseArgs(process.argv); - const maps = await discoverMaps(options.discRoot); - const filtered = options.only ? maps.filter((m) => options.only.includes(m.name)) : maps; - - console.log(`Found ${filtered.length} maps under ${options.discRoot}`); - await fs.mkdir(options.outputRoot, { recursive: true }); - - const summary = []; - let okCount = 0; - let failCount = 0; - for (const mapEntry of filtered) { - for (const variant of VARIANTS) { - const tag = `[${mapEntry.set}/${mapEntry.name}] variant=${variant.label}`; - process.stdout.write(`${tag} ... `); - const result = await runExport(mapEntry, variant, options); - if (result.code === 0) { - okCount += 1; - process.stdout.write(`OK (${result.ms}ms)\n`); - summary.push({ - set: mapEntry.set, - map: mapEntry.name, - variant: variant.label, - ms: result.ms, - ok: true, - outDir: path.relative(options.outputRoot, result.outDir), - }); - } else { - failCount += 1; - const reason = result.killed - ? `TIMEOUT after ${result.ms}ms` - : `exit ${result.code}${result.signal ? ' signal ' + result.signal : ''}`; - process.stdout.write(`FAIL (${reason})\n`); - if (result.stderr) { - process.stdout.write(` stderr: ${result.stderr.trim().split(/\r?\n/).slice(-5).join('\n stderr: ')}\n`); - } - summary.push({ - set: mapEntry.set, - map: mapEntry.name, - variant: variant.label, - ms: result.ms, - ok: false, - reason, - stderrTail: result.stderr.trim().split(/\r?\n/).slice(-10).join('\n'), - outDir: path.relative(options.outputRoot, result.outDir), - }); - } - } - } - - const indexPath = path.join(options.outputRoot, 'index.json'); - await fs.writeFile(indexPath, JSON.stringify({ - discRoot: options.discRoot, - generatedAt: new Date().toISOString(), - variants: VARIANTS.map((v) => v.label), - okCount, - failCount, - maps: summary, - }, null, 2)); - console.log(`\nWrote index ${indexPath} (ok=${okCount} fail=${failCount})`); -} - -main().catch((error) => { - console.error(error); - process.exitCode = 1; -}); diff --git a/psx-map-exporter/src/bundles.js b/psx-map-exporter/src/bundles.js index 147552e..7c8bb16 100644 --- a/psx-map-exporter/src/bundles.js +++ b/psx-map-exporter/src/bundles.js @@ -8,10 +8,6 @@ function readU16LE(buffer, offset) { return buffer.readUInt16LE(offset); } -function readS16LE(buffer, offset) { - return buffer.readInt16LE(offset); -} - function rowByteWidth(width, mode) { return mode === 2 ? Math.ceil(width / 2) : width; } @@ -30,45 +26,17 @@ function psx555ToRgba(color) { } export function extractPaletteSets(buffer, headerWords) { - if (!Array.isArray(headerWords) || headerWords.length < 9) { + if (!Array.isArray(headerWords) || headerWords.length < 4) { return { palettes16: [], palettes256: [] }; } - // The runtime CLUT table is the LAST chunk of the level "section pack" that - // wdl_resource_bundle_load_by_index @ 0x80039918 reads from the per-map .WDL - // file. The 0x38-byte header is an array of 14 u32 sizes: - // [c8, c4, c0, bc, b8, b4, b0, ac, a8, a4, a0, 9c, 98, 94] - // The section pack covers the first sum(headerWords[0..9]) bytes after the - // header. Within it, the engine derives section pointers as: - // dispatch_roots @ +headerWords[0] - // ctor_placement_records @ +headerWords[1] - // ctor_placement_section @ +headerWords[2] - // (local_bc scratch gap) @ headerWords[3] <-- SKIPPED in pointer math - // section_pack_base @ +headerWords[4] - // type_policy_table @ +headerWords[5] - // 8006754c table @ +headerWords[6] - // control_opcode_stream @ +headerWords[7] - // psx_level_clut_table @ +headerWords[8] <-- CLUT starts here - // Note that the engine's chain skips headerWords[3] (`local_bc`); that field - // is a separate scratch reservation released by psx_level_heap_rewind after - // level_palette_header_apply returns. The CLUT absolute file offset is - // therefore 0x38 plus the sum of headerWords[0..2, 4..8]. - // The CLUT spans 0x1000 bytes (128 x 32-byte 16-color CLUTs) which matches - // the engine's psx_clut_table_by_resource_bank layout (8 rows x 16 cols). - // The exporter previously used headerWords[2] / headerWords[3] which are - // actually section sizes, not a palette offset; that mismatch produced the - // wrong colors (e.g. neon-green floors instead of grey concrete). - let clutOffset = 0x38; - for (let i = 0; i < 9 && i < headerWords.length; i += 1) { - if (i === 3) continue; // skip local_bc scratch reservation - clutOffset += headerWords[i] >>> 0; - } - const paletteSize = 0x1000; - if (clutOffset < 0 || clutOffset + paletteSize > buffer.length) { + const paletteOffset = headerWords[2]; + const paletteSize = headerWords[3]; + if (paletteSize !== 0x1000 || paletteOffset < 0 || paletteOffset + paletteSize > buffer.length) { return { palettes16: [], palettes256: [] }; } - const blob = buffer.subarray(clutOffset, clutOffset + paletteSize); + const blob = buffer.subarray(paletteOffset, paletteOffset + paletteSize); const palettes16 = []; const palettes256 = []; @@ -88,7 +56,7 @@ export function extractPaletteSets(buffer, headerWords) { palettes256.push(palette); } - return { palettes16, palettes256, clutOffset }; + return { palettes16, palettes256 }; } export function buildMode1RuntimePaletteForIndex(palettes16, startIndex = 0) { @@ -318,8 +286,8 @@ export function extractBundleFromHeader(payloadBuffer, absoluteHeaderOffset) { const relativeDataOffset = readU32LE(payloadBuffer, entryOffset + 0x08); const frameWidth = readU16LE(payloadBuffer, entryOffset + 0x0c); const frameHeight = readU16LE(payloadBuffer, entryOffset + 0x0e); - const originX = readS16LE(payloadBuffer, entryOffset + 0x10); - const originY = readS16LE(payloadBuffer, entryOffset + 0x12); + const originX = readU16LE(payloadBuffer, entryOffset + 0x10); + const originY = readU16LE(payloadBuffer, entryOffset + 0x12); const dataStart = dataOffset + (((flags & 1) === 1) ? relativeDataOffset * 4 : relativeDataOffset); const rawSize = rowByteWidth(frameWidth, mode) * frameHeight; diff --git a/psx-map-exporter/src/cli.js b/psx-map-exporter/src/cli.js index 661a9f9..101a870 100644 --- a/psx-map-exporter/src/cli.js +++ b/psx-map-exporter/src/cli.js @@ -1,38 +1,28 @@ -import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; import { exportMap } from './export-map.js'; -// Resolve the on-disc PSX asset root. We prefer the ORIGINAL extracted disc -// files so the exporter never depends on a pre-processed cache; the cached -// STATIC_PSX copy in the sibling viewer workspace is kept only as a fallback -// for environments without the disc mounted. -function resolveDefaultDiscRoot(moduleDir) { - const candidates = [ - 'E:/emu/psx/Crusader - No Remorse', - 'E:/emu/psx/Crusader 2 Pre-Pre Alpha', - path.resolve(moduleDir, '..', '..', '..', 'crusader_map_viewer', 'map_renderer', 'STATIC_PSX'), - ]; - for (const candidate of candidates) { - try { - const stat = fs.statSync(candidate); - if (stat.isDirectory()) { - return candidate; - } - } catch { - // try next - } - } - return candidates[candidates.length - 1]; -} - function parseArgs(argv) { const moduleDir = path.dirname(fileURLToPath(import.meta.url)); const options = { - discRoot: resolveDefaultDiscRoot(moduleDir), - gpuRamDump: null, + discRoot: path.resolve( + moduleDir, + '..', + '..', + '..', + 'Crusader-Map-Viewer', + 'map_renderer', + 'STATIC_PSX' + ), + gpuRamDump: path.resolve( + moduleDir, + '..', + '..', + 'binary', + 'Crusader - No Remorse (USA) GPU RAM 2.bin' + ), mapSource: 'auto', bindingMode: 'raw', sceneScope: 'probe', @@ -91,15 +81,6 @@ function parseArgs(argv) { index += 1; continue; } - if (arg === '--output-root') { - options.outputRoot = path.resolve(next); - index += 1; - continue; - } - if (arg === '--no-reset-output') { - options.resetOutputRoot = false; - continue; - } if (arg === '--debug-labels') { options.debugLabels = true; continue; @@ -165,8 +146,6 @@ async function main() { gpuRamDumpPath: options.gpuRamDump, validationBundles: options.validationBundles, outName: options.outName, - outputRoot: options.outputRoot, - resetOutputRoot: options.resetOutputRoot, debugLabels: Boolean(options.debugLabels), }); diff --git a/psx-map-exporter/src/export-map.js b/psx-map-exporter/src/export-map.js index 3cfaa72..49a7455 100644 --- a/psx-map-exporter/src/export-map.js +++ b/psx-map-exporter/src/export-map.js @@ -431,21 +431,6 @@ function chooseBundleBinding(record, bundles, options = {}) { return null; } - // Mark high-typeWord dispatch_root entries (>= 0xAC) as non-renderable - // placeholders rather than guessing art for them. The PSX engine switches - // its palette-token sourcing at typeWord 0xAC: types below use the 16-byte - // ctor record (renderable world objects) and types at or above use the - // 24-byte dispatch-root record (spawners, triggers, NPCs, UI panels) and - // typically have no sprite art at all. Without this guard the - // `bundles[typeWord]` fallback below picks an arbitrary bundle by raw scan - // order and produces wildly wrong sprites (e.g. invisible spawners - // rendered as brick walls / turrets / teleporters). The viewer still gets - // a placeholder so these entities remain inspectable. - // Cross-ref `/memories/repo/psx-typeword-renderable-boundary-2026-04-19.md`. - if (record.sourceFamily === 'section0_dispatch_roots' && record.typeWord >= 0xAC) { - return { bundle: null, mappingSource: 'placeholder-dispatch-root-high-typeword', runtimeBinding: null, placeholder: true }; - } - const rawTypeBundle = chooseBundleForType(bundles, record.typeWord); if (!rawTypeBundle) { return null; @@ -540,15 +525,7 @@ function summarizeRenderedLayers(items) { } function derivePaletteDiagnostics(record, bundle) { - // For dispatch-root records the meaningful per-record palette token lives - // in the full 24-byte raw row (12 u16 words), not the projected 6-word - // record we keep for shared scene math. Constructor placements only have - // the 12-byte form, so falling back to record.rawWords is correct there. - const rawWords = Array.isArray(record.dispatchRootRawWords) - ? record.dispatchRootRawWords - : Array.isArray(record.rawWords) - ? record.rawWords - : []; + const rawWords = Array.isArray(record.rawWords) ? record.rawWords : []; const token06HighByte = rawWords.length >= 4 ? ((rawWords[3] >>> 8) & 0xff) : null; const token0cHighByte = rawWords.length >= 7 ? ((rawWords[6] >>> 8) & 0xff) : null; @@ -1031,57 +1008,29 @@ function resolveBundlePalettes(bundles, paletteSets, options = {}) { let paletteFormula = null; if (bundle.mode === 2) { - // Mode 2 (4bpp) descriptor binder at psx_resource_bind_single_image_vram_slot - // (0x800444e4) takes the bundle's `paletteIndex` from descriptor+0x14 - // and ADDS 0x10 (= 16) before storing it as the resource's CLUT bank - // index at resource+0x08. The submitters then read - // psx_clut_table_by_resource_bank[resource+8] which is identical to - // palettes16[paletteIndex + 16]. The exporter therefore offsets the - // bundle index by +16 for mode-2 art. - const baseIndex = Number.isInteger(bundle.paletteIndex) ? bundle.paletteIndex : null; - const adjustedIndex = baseIndex !== null ? baseIndex + 16 : null; - if (adjustedIndex !== null && adjustedIndex >= 0 && adjustedIndex < paletteSets.palettes16.length) { - resolvedPaletteIndex = adjustedIndex; - palette = paletteSets.palettes16[adjustedIndex]; - paletteFormula = 'mode2-bundle-index-plus-16'; + if (!Number.isInteger(resolvedPaletteIndex) || resolvedPaletteIndex < 0 || resolvedPaletteIndex >= paletteSets.palettes16.length) { + resolvedPaletteIndex = choosePalette(paletteSets.palettes16, bundle.frames, bundle.mode); } - if (!palette) { - if (!Number.isInteger(resolvedPaletteIndex) || resolvedPaletteIndex < 0 || resolvedPaletteIndex >= paletteSets.palettes16.length) { - resolvedPaletteIndex = choosePalette(paletteSets.palettes16, bundle.frames, bundle.mode); - } - if (Number.isInteger(resolvedPaletteIndex) && resolvedPaletteIndex >= 0 && resolvedPaletteIndex < paletteSets.palettes16.length) { - palette = paletteSets.palettes16[resolvedPaletteIndex]; - paletteFormula = 'mode2-bundle-or-usage-index-fallback'; - } + if (Number.isInteger(resolvedPaletteIndex) && resolvedPaletteIndex >= 0 && resolvedPaletteIndex < paletteSets.palettes16.length) { + palette = paletteSets.palettes16[resolvedPaletteIndex]; + paletteFormula = 'mode2-bundle-or-usage-index'; } } else if (bundle.mode === 1) { - // Mode 1 is an 8bpp image with a 256-entry CLUT. In this engine the - // 256-color CLUT is NOT a dedicated palette-256 block — it is the - // concatenation of 16 consecutive 16-entry CLUTs taken from the - // `palettes16` bank. The bundle header's `paletteIndex` is the starting - // CLUT index into `palettes16`. Falling back to `0` matches the legacy - // behavior but is almost always wrong for mode-1 art; the per-bundle - // index is the real engine-equivalent rule. - const bankStart = Number.isInteger(bundle.paletteIndex) && bundle.paletteIndex >= 0 - ? bundle.paletteIndex - : 0; - const fromBundleIndex = mode1PaletteBank[bankStart]; - if (fromBundleIndex?.length === 256) { - resolvedPaletteIndex = bankStart; - palette = fromBundleIndex; - paletteFormula = 'mode1-palette16-bank-start-index-bundle'; - } else if (options.mode1RuntimePalette?.length === 256) { + if (options.mode1RuntimePalette?.length === 256) { resolvedPaletteIndex = 0; palette = options.mode1RuntimePalette; - paletteFormula = 'mode1-live-gpu-ram-row-f0-x0'; } else if (mode1PaletteBank[0]?.length) { resolvedPaletteIndex = 0; palette = mode1PaletteBank[0]; - paletteFormula = 'mode1-palette16-bank-start-index-0-fallback'; } else { resolvedPaletteIndex = null; palette = mode1RuntimePalette; - paletteFormula = palette ? 'mode1-runtime-clut-band-start-index-0' : null; + } + + if (palette) { + paletteFormula = options.mode1RuntimePalette?.length === 256 + ? 'mode1-live-gpu-ram-row-f0-x0' + : 'mode1-runtime-clut-band-start-index-0'; } } @@ -1101,18 +1050,7 @@ async function buildSceneItems(region04, records, bundles, options = {}) { const skippedRecords = []; const nonMapFacingRootTypes = new Set([0x42, 0x49]); - // Bundles that bind via dispatch_roots but are HUD/UI artwork, not authored - // map placements. They surface in the scene because they share the dispatch - // table format with world objects, but rendering them produces a stray UI - // panel floating in the middle of an empty area. - // 0x000d84f4 - portrait/talk panel (already known) - // 0x00074f44 - type 0xAC (172) full-screen UI panel, 216x126, kind-4 - // mode-2; user-confirmed as the lone "TAC:b4f44" leak. - // 0x00086810 - "ceiling" tile (T0x43, kind-5 mode-2, 128x65). Engine uses - // it at runtime to temporarily obscure rooms the player has - // not entered yet; for static map rendering it just hides - // the geometry the user wants to see, so it is suppressed. - const nonMapFacingBundleOffsets = new Set([0x000d84f4, 0x00074f44, 0x00086810]); + const nonMapFacingBundleOffsets = new Set([0x000d84f4]); for (const record of records) { if (record.sourceFamily === 'section0_dispatch_roots' && nonMapFacingRootTypes.has(record.typeWord)) { skippedRecords.push({ @@ -1126,62 +1064,7 @@ async function buildSceneItems(region04, records, bundles, options = {}) { const binding = chooseBundleBinding(record, bundles, options); const bundle = binding?.bundle ?? null; - if (!binding) { - continue; - } - if (binding.placeholder) { - // Non-renderable entity (spawner, trigger, NPC, UI marker). Emit a small - // square footprint so the viewer can still display and inspect it. - const PLACEHOLDER_SIZE = 12; - items.push({ - id: record.index, - instanceId: record.index, - recordIndex: record.index, - recordSource: record.source, - sourceFamily: record.sourceFamily, - authoredLayer: record.authoredLayer ?? record.sourceRole ?? null, - recordSide: record.recordSide, - rowIndex: record.rowIndex, - typeWord: record.typeWord, - laneWord: record.laneWord, - worldX: record.xWord ?? null, - worldY: record.yWord ?? null, - worldZ: record.zWord ?? null, - screenX: record.screenX, - screenY: record.screenY, - bundleSlot: null, - bundleAbsoluteOffset: null, - bundleSource: null, - requestedFrameIndex: record.selectorWord, - frameIndex: null, - defaultPaletteIndex: null, - resolvedPaletteIndex: null, - paletteFormula: null, - mappingSource: binding.mappingSource, - templateTypeId: null, - donorTypeId: null, - runtimeBindingMaskedAbsoluteOffset: null, - runtimeBindingOffsetDelta: null, - runtimeBindingVisibleCount: null, - runtimeBindingRawRecordCount: null, - rawWords: record.rawWords ?? record.words, - flipped: false, - width: PLACEHOLDER_SIZE, - height: PLACEHOLDER_SIZE, - originX: PLACEHOLDER_SIZE / 2, - originY: PLACEHOLDER_SIZE / 2, - drawX: record.screenX - PLACEHOLDER_SIZE / 2, - drawY: record.screenY - PLACEHOLDER_SIZE / 2, - stage: 1, // overlays so placeholders sit on top of geometry - isFlat: false, - resourceKey: 'placeholder', - paletteDiagnostics: null, - sprite: null, - placeholder: true, - }); - continue; - } - if (!bundle) { + if (!bundle || !binding) { continue; } if (nonMapFacingBundleOffsets.has(bundle.absoluteOffset)) { @@ -1219,9 +1102,6 @@ async function buildSceneItems(region04, records, bundles, options = {}) { rowIndex: record.rowIndex, typeWord: record.typeWord, laneWord: record.laneWord, - worldX: record.xWord ?? null, - worldY: record.yWord ?? null, - worldZ: record.zWord ?? null, screenX: record.screenX, screenY: record.screenY, bundleSlot: bundle.slot, @@ -1245,89 +1125,21 @@ async function buildSceneItems(region04, records, bundles, options = {}) { height: sprite.height, originX: sprite.originX, originY: sprite.originY, - // Engine-accurate anchor selection per psx_project_object_main_visible - // (0x80040d44 / 0x80040ddc): when laneWord bit 0x0002 is set the engine - // mirrors the horizontal anchor from origin_x to (frame_w - origin_x). - // The blit step then flips the sprite. Applying both keeps the visible - // anchor on the same world point, fixing flipped walls that previously - // landed (frame_w - 2*origin_x) px too far left. - drawX: ((record.laneWord & 0x0002) !== 0) - ? record.screenX - (sprite.width - sprite.originX) - : record.screenX - sprite.originX, + drawX: record.screenX - sprite.originX, drawY: record.screenY - sprite.originY, stage: record.typeWord === 4 || (record.laneWord & 0x0400) !== 0 ? 1 : 0, - // Heuristic: flat floor/ceiling decals are tile-sized sprites whose - // anchor sits at the bottom edge AND whose silhouette is wider than - // tall (i.e. matches the engine's 2:1 isometric ground footprint). - // Upright sprites (walls, crates, terminals, props) extend well above - // the anchor and have height >= width. - isFlat: - sprite.width >= 48 && - sprite.height > 0 && - sprite.height / sprite.width <= 0.6 && - sprite.originY >= sprite.height - 4, resourceKey, paletteDiagnostics: derivePaletteDiagnostics(record, bundle), sprite, }); } - // Painter's order for an isometric top-down full-map render. - // 1. `stage`: runtime sub-stage flag (0 = default, 1 = overlays flagged via - // typeWord===4 or laneWord bit 0x0400). Overlays always on top. - // 2. Isometric depth: back-to-front by world `X + Y` (ground depth along the - // engine's isometric axis). Constructors (static geometry: walls, floors, - // architecture) and roots (dynamic props: crates, terminals, doors) are - // interleaved by depth so a crate placed in front of a wall does not get - // painted over by a wall placed further back. Falls back to screenY when - // world coords are unavailable. - // 3. `isFlat`: at the SAME depth, flat ground decals (floor/ceiling tiles) - // must draw BEFORE upright sprites (walls, crates, props) so the props - // standing on the tile sit visually on top of it. Without this bias, - // floors painted on the same world tile as a prop will overpaint the - // prop whenever screenX happens to sort the floor last. - // 4. `worldZ` ascending: lower objects at the same ground cell draw before - // taller ones at that cell. - // 5. `authoredLayer`: when world coords + flatness tie, draw constructors - // (geometry) before roots (props) so props sit on top of their floor. - // 6. `screenX` ascending: stable tie-breaker left-to-right. - const layerTieBreak = (item) => { - if (item.authoredLayer === 'constructors') return 0; - if (item.authoredLayer === 'roots') return 1; - return 2; - }; - const depthKey = (item) => { - if (Number.isFinite(item.worldX) && Number.isFinite(item.worldY)) { - return item.worldX + item.worldY; - } - return item.screenY; - }; items.sort((left, right) => { if (left.stage !== right.stage) { return left.stage - right.stage; } - // Two-pass painter: ALL flat ground decals first (back-to-front by depth), - // then ALL upright sprites (back-to-front by depth). This avoids the - // floor-anchor problem where a flat tile's authored anchor sits at its - // FRONT tip (largest world X+Y in the footprint), which a single-pass - // depth sort would draw AFTER walls/props that visually stand on it. - if (left.isFlat !== right.isFlat) { - return left.isFlat ? -1 : 1; - } - const leftDepth = depthKey(left); - const rightDepth = depthKey(right); - if (leftDepth !== rightDepth) { - return leftDepth - rightDepth; - } - const leftZ = Number.isFinite(left.worldZ) ? left.worldZ : 0; - const rightZ = Number.isFinite(right.worldZ) ? right.worldZ : 0; - if (leftZ !== rightZ) { - return leftZ - rightZ; - } - const leftLayer = layerTieBreak(left); - const rightLayer = layerTieBreak(right); - if (leftLayer !== rightLayer) { - return leftLayer - rightLayer; + if (left.screenY !== right.screenY) { + return left.screenY - right.screenY; } return left.screenX - right.screenX; }); @@ -1388,19 +1200,13 @@ export async function exportMap(options) { const cacheBaseRoot = path.join(options.projectRoot, '.cache'); const cacheRoot = path.join(options.projectRoot, '.cache', mapStem); const spriteRoot = path.join(cacheRoot, 'sprites'); - const outputRoot = options.outputRoot - ? path.resolve(options.outputRoot) - : path.join(options.projectRoot, '.output'); + const outputRoot = path.join(options.projectRoot, '.output'); await ensureDirectory(cacheBaseRoot); await resetDirectory(cacheRoot); await ensureDirectory(cacheRoot); await ensureDirectory(spriteRoot); - if (options.resetOutputRoot === false) { - await ensureDirectory(outputRoot); - } else { - await resetDirectory(outputRoot); - } + await resetDirectory(outputRoot); const recordSet = chooseRecordSet(wdl, options.mapSource); const region04 = wdl.regions.find((region) => region.name === 'post_audio_region_04'); @@ -1557,9 +1363,6 @@ export async function exportMap(options) { rawWords: item.rawWords, typeWord: item.typeWord, laneWord: item.laneWord, - worldX: item.worldX ?? null, - worldY: item.worldY ?? null, - worldZ: item.worldZ ?? null, screenX: item.screenX, screenY: item.screenY, bundleSlot: item.bundleSlot, @@ -1592,7 +1395,6 @@ export async function exportMap(options) { originX: item.originX, originY: item.originY, stage: item.stage, - placeholder: item.placeholder === true, })), }; diff --git a/psx-map-exporter/src/render.js b/psx-map-exporter/src/render.js index 44608a1..0b2881e 100644 --- a/psx-map-exporter/src/render.js +++ b/psx-map-exporter/src/render.js @@ -13,18 +13,6 @@ const GLYPHS = { '7': ['111', '001', '001', '001', '001'], '8': ['111', '101', '111', '101', '111'], '9': ['111', '101', '111', '001', '111'], - 'a': ['010', '101', '111', '101', '101'], - 'b': ['110', '101', '110', '101', '110'], - 'c': ['111', '100', '100', '100', '111'], - 'd': ['110', '101', '101', '101', '110'], - 'e': ['111', '100', '110', '100', '111'], - 'f': ['111', '100', '110', '100', '100'], - 'p': ['110', '101', '110', '100', '100'], - 't': ['111', '010', '010', '010', '010'], - '#': ['101', '111', '101', '111', '101'], - ':': ['000', '010', '000', '010', '000'], - '/': ['001', '001', '010', '100', '100'], - '-': ['000', '000', '111', '000', '000'], }; function clearCanvas(width, height, background = null) { @@ -99,18 +87,6 @@ function drawLabel(canvas, canvasWidth, canvasHeight, text, x, y) { } } -function drawBoundingBox(canvas, canvasWidth, canvasHeight, x, y, width, height, alpha = 220) { - if (width <= 0 || height <= 0) { - return; - } - // Top + bottom edges - fillRect(canvas, canvasWidth, canvasHeight, x, y, width, 1, 255, 255, 255, alpha); - fillRect(canvas, canvasWidth, canvasHeight, x, y + height - 1, width, 1, 255, 255, 255, alpha); - // Left + right edges - fillRect(canvas, canvasWidth, canvasHeight, x, y, 1, height, 255, 255, 255, alpha); - fillRect(canvas, canvasWidth, canvasHeight, x + width - 1, y, 1, height, 255, 255, 255, alpha); -} - function blitRgba(canvas, canvasWidth, canvasHeight, sprite, dstX, dstY, flipped = false) { for (let y = 0; y < sprite.height; y += 1) { const canvasY = dstY + y; @@ -139,68 +115,12 @@ function blitRgba(canvas, canvasWidth, canvasHeight, sprite, dstX, dstY, flipped } } -// Magenta diamond marker for placeholder (non-renderable) entities such as -// spawners, triggers, NPCs, and UI panels. Drawn in place of a sprite blit so -// the viewer still surfaces the entity for inspection without guessing wrong -// art. The diamond is 12x12 by default (caller passes the placeholder width). -function drawPlaceholderMarker(canvas, canvasWidth, canvasHeight, dstX, dstY, w, h) { - const half = Math.max(2, Math.floor(Math.min(w, h) / 2)); - const cx = Math.round(dstX + w / 2); - const cy = Math.round(dstY + h / 2); - // Filled diamond by scanline. - for (let dy = -half; dy <= half; dy += 1) { - const span = half - Math.abs(dy); - const y = cy + dy; - if (y < 0 || y >= canvasHeight) continue; - for (let dx = -span; dx <= span; dx += 1) { - const x = cx + dx; - if (x < 0 || x >= canvasWidth) continue; - // Magenta interior with darker outline on the diamond edge. - const onEdge = Math.abs(dx) + Math.abs(dy) === half; - const target = ((y * canvasWidth) + x) * 4; - canvas[target + 0] = onEdge ? 90 : 220; - canvas[target + 1] = onEdge ? 0 : 60; - canvas[target + 2] = onEdge ? 110 : 200; - canvas[target + 3] = 255; - } - } -} - export function renderMap(items, options = {}) { if (items.length === 0) { throw new Error('No renderable scene items were produced.'); } - // Guard against a single bad item (e.g. corrupt sprite origin) inflating the - // canvas to multiple gigabytes. We compute the median centroid of all items - // and drop anything farther than MAX_EXTENT px from it before computing - // bounds. This keeps render output sane even if the parser produces a few - // outliers, while preserving every real authored item (the playfield is - // never larger than a few thousand pixels in either axis). - const MAX_EXTENT = 4096; - const centroids = items.map((it) => ({ x: it.drawX + (it.width ?? 0) / 2, y: it.drawY + (it.height ?? 0) / 2 })); - const sortedX = centroids.map((c) => c.x).sort((a, b) => a - b); - const sortedY = centroids.map((c) => c.y).sort((a, b) => a - b); - const medianX = sortedX[sortedX.length >> 1]; - const medianY = sortedY[sortedY.length >> 1]; - const droppedOutliers = []; - const visibleItems = items.filter((it, idx) => { - const dx = Math.abs(centroids[idx].x - medianX); - const dy = Math.abs(centroids[idx].y - medianY); - if (dx > MAX_EXTENT || dy > MAX_EXTENT) { - droppedOutliers.push({ recordIndex: it.recordIndex, typeWord: it.typeWord, drawX: it.drawX, drawY: it.drawY, dx, dy }); - return false; - } - return true; - }); - if (droppedOutliers.length > 0 && process.env.PSX_TRACE) { - console.error('[renderMap] dropped ' + droppedOutliers.length + ' outlier item(s) beyond ' + MAX_EXTENT + 'px from median; first: ' + JSON.stringify(droppedOutliers[0])); - } - if (visibleItems.length === 0) { - throw new Error('All scene items were classified as outliers; nothing to render.'); - } - - const bounds = visibleItems.reduce( + const bounds = items.reduce( (state, item) => ({ minX: Math.min(state.minX, item.drawX), minY: Math.min(state.minY, item.drawY), @@ -213,27 +133,9 @@ export function renderMap(items, options = {}) { const padding = 16; const width = Math.max(1, (bounds.maxX - bounds.minX) + (padding * 2)); const height = Math.max(1, (bounds.maxY - bounds.minY) + (padding * 2)); - if (process.env.PSX_TRACE) { - console.error('[renderMap] items=' + visibleItems.length + ' bounds=' + JSON.stringify(bounds) + ' size=' + width + 'x' + height); - } const canvas = clearCanvas(width, height, options.background ?? DEFAULT_BACKGROUND); - for (const item of visibleItems) { - if (item.placeholder || !item.sprite) { - // Draw a small magenta diamond marker for non-renderable entities - // (spawners, triggers, NPCs). Keeps them visible and inspectable in - // the viewer overlay without guessing wrong art. - drawPlaceholderMarker( - canvas, - width, - height, - item.drawX - bounds.minX + padding, - item.drawY - bounds.minY + padding, - item.width, - item.height - ); - continue; - } + for (const item of items) { blitRgba( canvas, width, @@ -246,37 +148,15 @@ export function renderMap(items, options = {}) { } if (options.drawLabels) { - // Cap labels at 10 instances per (typeWord, bundleAbsoluteOffset) pair so - // dense root layers (e.g. floor tiles) do not bury the labels we actually - // need to read. The bounding box is also suppressed once the label budget - // is exhausted so duplicate-instance clusters do not redraw white outlines - // on every tile and obscure surrounding art. - const labelBudgetPerBundle = 10; - const labelTally = new Map(); - for (const item of visibleItems) { - const tallyKey = `${item.typeWord ?? '?'}|${item.bundleAbsoluteOffset ?? '?'}`; - const used = labelTally.get(tallyKey) ?? 0; - if (used >= labelBudgetPerBundle) { - continue; - } - labelTally.set(tallyKey, used + 1); - const boxX = item.drawX - bounds.minX + padding; - const boxY = item.drawY - bounds.minY + padding; - const boxW = item.width ?? item.sprite?.width ?? 0; - const boxH = item.height ?? item.sprite?.height ?? 0; - drawBoundingBox(canvas, width, height, boxX, boxY, boxW, boxH); - // Compose a useful label: typeWord (hex) + bundle absolute offset (last 4 hex) - const labelParts = []; - if (Number.isInteger(item.typeWord)) { - labelParts.push('t' + item.typeWord.toString(16)); - } - if (Number.isInteger(item.bundleAbsoluteOffset)) { - labelParts.push('b' + (item.bundleAbsoluteOffset & 0xffff).toString(16).padStart(4, '0')); - } - if (labelParts.length === 0 && (item.labelId ?? item.id) !== undefined) { - labelParts.push('#' + (item.labelId ?? item.id)); - } - drawLabel(canvas, width, height, labelParts.join(':'), boxX, boxY); + for (const item of items) { + drawLabel( + canvas, + width, + height, + item.labelId ?? item.id, + item.drawX - bounds.minX + padding, + item.drawY - bounds.minY + padding + ); } } @@ -284,7 +164,6 @@ export function renderMap(items, options = {}) { width, height, bounds, - droppedOutlierCount: droppedOutliers.length, png: encodePng(canvas, width, height), }; } diff --git a/psx-map-exporter/src/wdl.js b/psx-map-exporter/src/wdl.js index 162dedf..25b6ecb 100644 --- a/psx-map-exporter/src/wdl.js +++ b/psx-map-exporter/src/wdl.js @@ -286,34 +286,14 @@ function isStructuredCandidate(words) { return true; } -function projectCtorPlacement(xWord, yWord, zWord) { - // Engine-accurate projection, matching `psx_project_object_main_visible` - // (0x80040d44) followed by the camera subtraction at 0x80040e3c/0x80040e5c. - // - // The projection step writes to obj+0x78/0x7c: - // proj_x = Y - X - // proj_y = 2*Z - (X+Y)/2 - // The draw step then writes to obj+0x20/0x22: - // screen_x = (cam_X - proj_x) - origin_X - // screen_y = (cam_Y - proj_y) - origin_Y - // i.e. the engine renders the world with the sign flipped relative to the - // raw projection. For a camera-less full-map export we fold that flip into - // the projection so higher world Z appears higher on the canvas (Y-down). - const projX = yWord - xWord; - const projY = (2 * zWord) - Math.floor((xWord + yWord) / 2); - return { - screenX: -projX * PSX_SCREEN_SCALE, - screenY: -projY * PSX_SCREEN_SCALE, - }; -} - function buildRecord(words, source, offset, rawWords = words) { if (!isPlausibleRecord(words)) { return null; } const [typeWord, xWord, yWord, zWord, selectorWord, laneWord] = words; - const { screenX, screenY } = projectCtorPlacement(xWord, yWord, zWord); + const screenX = (yWord - xWord) * PSX_SCREEN_SCALE; + const screenY = ((2 * zWord) - Math.floor((xWord + yWord) / 2)) * PSX_SCREEN_SCALE; return { index: -1, @@ -615,17 +595,11 @@ export function parseRegion01Records(region) { // Loader-faithful 12-byte constructor-placement records straight out of the // `ctorPlacements` subrange of the section pack. Layout per -// `psx_object_create_compound_record @ 0x80025040`: -// +0x00 u16 typeWord -> descriptor_table index -// +0x02 u16 X -> obj world X (<<16 seed) -// +0x04 u16 Y -> obj world Y (<<16 seed) -// +0x06 u8 Z (low byte only!)-> obj world Z (<<16 seed). The high byte at -// +0x07 is not read as part of Z. -// +0x08 u8 selector -> state-script seed -// +0x09 u8 (padding/unknown) -// +0x0a u16 laneWord -> OR'd into obj+0x1c (flags/lane) -// The previous implementation decoded Z as a u16 from +0x06, which mixed the -// selector byte into Z and produced large bogus elevations. +// `psx_dispatch_section0_constructor_placements @ 0x800258cc`: +// [u32 count][count * { u16 typeWord; u16 X; u16 Y; u16 Z; u16 selector; +// u16 flags }] +// Each record is called as `descriptor_table[typeWord].slot0(record, 0)` and +// `psx_object_create_compound_record` then reads exactly those 6 u16 fields. export function parseCtorPlacementsBlock(block, variant = 'lset') { if (!block || !block.buffer || block.size < 4) { return { source: 'ctorPlacements', records: [], count: 0 }; @@ -640,17 +614,11 @@ export function parseCtorPlacementsBlock(block, variant = 'lset') { if (recordOffset + 12 > block.buffer.length) { break; } - const typeWord = readU16LE(block.buffer, recordOffset + 0x00); - const xWord = readU16LE(block.buffer, recordOffset + 0x02); - const yWord = readU16LE(block.buffer, recordOffset + 0x04); - const zWord = block.buffer[recordOffset + 0x06]; - const z2Byte = block.buffer[recordOffset + 0x07]; - const selectorWord = block.buffer[recordOffset + 0x08]; - const selectorHighByte = block.buffer[recordOffset + 0x09]; - const laneWord = readU16LE(block.buffer, recordOffset + 0x0a); - const rawWords = [typeWord, xWord, yWord, (z2Byte << 8) | zWord, (selectorHighByte << 8) | selectorWord, laneWord]; - const words = [typeWord, xWord, yWord, zWord, selectorWord, laneWord]; - const record = buildRecord(words, 'ctorPlacements', recordOffset, rawWords); + const words = []; + for (let cursor = 0; cursor < 12; cursor += 2) { + words.push(readU16LE(block.buffer, recordOffset + cursor)); + } + const record = buildRecord(words, 'ctorPlacements', recordOffset, words); if (!record) { continue; } @@ -678,17 +646,17 @@ export function parseCtorPlacementsBlock(block, variant = 'lset') { // `psx_dispatch_section0_dispatch_roots @ 0x800256b0`: // [u32 count][count * 24 bytes] // Within each record the dispatcher reads: -// +0x04 u16 typeId (descriptor_table index) -// +0x08 u16 worldX (compared to cam+0x3e +/- 0x140 — cam+0x3e is the -// camera's world-X short, not the projected integer -// part; proof: root worldX values overlap constructor -// placement worldX values in the same map) -// +0x0a u16 worldY (compared to cam+0x42 +/- 0x140) -// +0x10 u16 flags (bit 3 = skip this record) -// We therefore project root worldX/worldY through the same projection as -// constructor placements (with Z=0 since dispatch roots do not carry a Z byte -// at a known offset) and render them on the same canvas. The trailing raw -// fields are preserved as `dispatchRootRawWords` for future probes. +// +4 u16 typeId (argument to descriptor_table[typeId]) +// +8 u16 screenX (for +/-0x140 cull) +// +10 u16 screenY (for +/-0x140 cull) +// +16 u16 flags (bit 3 = skip this record) +// Z, selector and lane are not universally used by the dispatcher. The empirical +// best mapping that matches constructor-placement convention is: +// +6 u16 zeta (often 0) +// +12 u16 selector/rotation +// +14 u16 lane/flags-lo +// We keep them in rawWords so downstream consumers can probe further, but +// buildRecord uses the cull-verified X/Y/typeId for positioning. export function parseDispatchRootsBlock(block, variant = 'lset') { if (!block || !block.buffer || block.size < 4) { return { source: 'dispatchRoots', records: [], count: 0 }; @@ -707,6 +675,17 @@ export function parseDispatchRootsBlock(block, variant = 'lset') { for (let cursor = 0; cursor < 24; cursor += 2) { rawWords.push(readU16LE(block.buffer, recordOffset + cursor)); } + // Project into buildRecord's 6-word ctor-placement shape using the fields + // the live dispatcher reads. rawWords indices: + // [0..1] (+0..+3) prefix + // [2] (+4) typeId (dispatch) + // [3] (+6) zeta / z-ish + // [4] (+8) X (cull) + // [5] (+10) Y (cull) + // [6] (+12) selector-ish + // [7] (+14) lane-ish + // [8] (+16) flags (bit 3 skip) + // [9..11] trailing const flags = rawWords[8]; if ((flags & 0x8) !== 0) { continue; @@ -714,16 +693,14 @@ export function parseDispatchRootsBlock(block, variant = 'lset') { const typeWord = rawWords[2]; const xWord = rawWords[4]; const yWord = rawWords[5]; - // Engine reads Z from the LOW byte of the u16 at +0x06 of the shared - // authored prefix (constructor view), even when the record is iterated - // through the dispatcher view. Confirmed via psx_object_create_compound_record - // @ 0x80024eec (memory note psx-coordinate-decode-2026-04-18.md). The HIGH - // byte of the same u16 is a palette-override token, NOT a Z extension. - // The constructor-view u16 at +0x06 lives at rawWords[3] in this 12-u16 - // expansion of the 24-byte dispatch-root record. const zWord = rawWords[3] & 0xff; const selectorWord = rawWords[6]; const laneWord = rawWords[7]; + // Relaxed plausibility: dispatch-root records can have lane==0 or large + // selectors (e.g. behavior opcodes) because the dispatcher only reads + // typeId, X, Y, and the +0x10 skip flag. We only require typeId in the + // scene-relevant range and X/Y not both zero. This is looser than + // `isPlausibleRecord` on purpose. if (typeWord < 0x20 || typeWord > 0x1ff) { continue; } @@ -731,7 +708,8 @@ export function parseDispatchRootsBlock(block, variant = 'lset') { continue; } const words = [typeWord, xWord, yWord, zWord, selectorWord, laneWord]; - const { screenX, screenY } = projectCtorPlacement(xWord, yWord, zWord); + const screenX = (yWord - xWord) * PSX_SCREEN_SCALE; + const screenY = ((2 * zWord) - Math.floor((xWord + yWord) / 2)) * PSX_SCREEN_SCALE; const record = { index: -1, source: 'dispatchRoots', diff --git a/psx-map-exporter/viewer/index.html b/psx-map-exporter/viewer/index.html deleted file mode 100644 index 08251b9..0000000 --- a/psx-map-exporter/viewer/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - PSX Map Debug Viewer - - -
- - - diff --git a/psx-map-exporter/viewer/package-lock.json b/psx-map-exporter/viewer/package-lock.json deleted file mode 100644 index 634f0dc..0000000 --- a/psx-map-exporter/viewer/package-lock.json +++ /dev/null @@ -1,1207 +0,0 @@ -{ - "name": "psx-map-debug-viewer", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "psx-map-debug-viewer", - "version": "0.0.0", - "dependencies": { - "vue": "^3.4.0" - }, - "devDependencies": { - "@vitejs/plugin-vue": "^5.0.0", - "vite": "^5.4.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", - "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", - "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", - "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", - "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", - "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", - "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", - "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", - "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", - "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", - "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", - "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", - "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", - "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", - "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", - "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", - "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", - "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", - "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", - "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", - "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", - "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", - "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", - "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", - "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", - "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitejs/plugin-vue": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", - "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0", - "vue": "^3.2.25" - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", - "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.2", - "@vue/shared": "3.5.32", - "entities": "^7.0.1", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", - "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.32", - "@vue/shared": "3.5.32" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", - "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.2", - "@vue/compiler-core": "3.5.32", - "@vue/compiler-dom": "3.5.32", - "@vue/compiler-ssr": "3.5.32", - "@vue/shared": "3.5.32", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.21", - "postcss": "^8.5.8", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", - "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.32", - "@vue/shared": "3.5.32" - } - }, - "node_modules/@vue/reactivity": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", - "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.32" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", - "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.32", - "@vue/shared": "3.5.32" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", - "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.32", - "@vue/runtime-core": "3.5.32", - "@vue/shared": "3.5.32", - "csstype": "^3.2.3" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", - "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", - "license": "MIT", - "dependencies": { - "@vue/compiler-ssr": "3.5.32", - "@vue/shared": "3.5.32" - }, - "peerDependencies": { - "vue": "3.5.32" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", - "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/rollup": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", - "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.1", - "@rollup/rollup-android-arm64": "4.60.1", - "@rollup/rollup-darwin-arm64": "4.60.1", - "@rollup/rollup-darwin-x64": "4.60.1", - "@rollup/rollup-freebsd-arm64": "4.60.1", - "@rollup/rollup-freebsd-x64": "4.60.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", - "@rollup/rollup-linux-arm-musleabihf": "4.60.1", - "@rollup/rollup-linux-arm64-gnu": "4.60.1", - "@rollup/rollup-linux-arm64-musl": "4.60.1", - "@rollup/rollup-linux-loong64-gnu": "4.60.1", - "@rollup/rollup-linux-loong64-musl": "4.60.1", - "@rollup/rollup-linux-ppc64-gnu": "4.60.1", - "@rollup/rollup-linux-ppc64-musl": "4.60.1", - "@rollup/rollup-linux-riscv64-gnu": "4.60.1", - "@rollup/rollup-linux-riscv64-musl": "4.60.1", - "@rollup/rollup-linux-s390x-gnu": "4.60.1", - "@rollup/rollup-linux-x64-gnu": "4.60.1", - "@rollup/rollup-linux-x64-musl": "4.60.1", - "@rollup/rollup-openbsd-x64": "4.60.1", - "@rollup/rollup-openharmony-arm64": "4.60.1", - "@rollup/rollup-win32-arm64-msvc": "4.60.1", - "@rollup/rollup-win32-ia32-msvc": "4.60.1", - "@rollup/rollup-win32-x64-gnu": "4.60.1", - "@rollup/rollup-win32-x64-msvc": "4.60.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vue": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", - "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.32", - "@vue/compiler-sfc": "3.5.32", - "@vue/runtime-dom": "3.5.32", - "@vue/server-renderer": "3.5.32", - "@vue/shared": "3.5.32" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - } - } -} diff --git a/psx-map-exporter/viewer/package.json b/psx-map-exporter/viewer/package.json deleted file mode 100644 index 61bd95d..0000000 --- a/psx-map-exporter/viewer/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "psx-map-debug-viewer", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "vue": "^3.4.0" - }, - "devDependencies": { - "@vitejs/plugin-vue": "^5.0.0", - "vite": "^5.4.0" - } -} diff --git a/psx-map-exporter/viewer/src/App.vue b/psx-map-exporter/viewer/src/App.vue deleted file mode 100644 index 1528afb..0000000 --- a/psx-map-exporter/viewer/src/App.vue +++ /dev/null @@ -1,536 +0,0 @@ - - - - - diff --git a/psx-map-exporter/viewer/src/ItemPanel.vue b/psx-map-exporter/viewer/src/ItemPanel.vue deleted file mode 100644 index 1c7beca..0000000 --- a/psx-map-exporter/viewer/src/ItemPanel.vue +++ /dev/null @@ -1,102 +0,0 @@ - - - - - diff --git a/psx-map-exporter/viewer/src/main.js b/psx-map-exporter/viewer/src/main.js deleted file mode 100644 index 8dd6bc1..0000000 --- a/psx-map-exporter/viewer/src/main.js +++ /dev/null @@ -1,5 +0,0 @@ -import { createApp } from 'vue'; -import App from './App.vue'; -import './style.css'; - -createApp(App).mount('#app'); diff --git a/psx-map-exporter/viewer/src/style.css b/psx-map-exporter/viewer/src/style.css deleted file mode 100644 index fc66b02..0000000 --- a/psx-map-exporter/viewer/src/style.css +++ /dev/null @@ -1,38 +0,0 @@ -:root { - color-scheme: dark; - font-family: ui-sans-serif, system-ui, sans-serif; - --bg: #121212; - --panel: #1c1c1f; - --panel-2: #25252a; - --border: #2f2f36; - --text: #e6e6e6; - --muted: #9aa0a6; - --accent: #4ea1ff; - --warn: #ffb24a; -} - -* { box-sizing: border-box; } - -html, body, #app { - margin: 0; - height: 100%; - background: var(--bg); - color: var(--text); -} - -button, select { - background: var(--panel-2); - color: var(--text); - border: 1px solid var(--border); - border-radius: 4px; - padding: 4px 8px; - font: inherit; - cursor: pointer; -} - -button:hover, select:hover { border-color: var(--accent); } - -input[type="checkbox"] { accent-color: var(--accent); } - -a { color: var(--accent); text-decoration: none; } -a:hover { text-decoration: underline; } diff --git a/psx-map-exporter/viewer/vite.config.js b/psx-map-exporter/viewer/vite.config.js deleted file mode 100644 index 28f4ac7..0000000 --- a/psx-map-exporter/viewer/vite.config.js +++ /dev/null @@ -1,110 +0,0 @@ -import { defineConfig } from 'vite'; -import vue from '@vitejs/plugin-vue'; -import path from 'node:path'; -import fs from 'node:fs/promises'; - -const RENDER_ROOT = path.resolve(__dirname, '..', '.output-render'); -const CACHE_ROOT = path.resolve(__dirname, '..', '.cache'); - -// Tiny dev plugin that exposes the .output-render directory at /render and -// serves a /api/index endpoint enumerating maps and variants. Keeps the app -// dependency-free of any external server. -function renderRootPlugin() { - return { - name: 'psx-render-root', - configureServer(server) { - server.middlewares.use('/render', async (req, res, next) => { - try { - const url = decodeURIComponent(req.url.split('?')[0]); - const filePath = path.join(RENDER_ROOT, url); - if (!filePath.startsWith(RENDER_ROOT)) { - res.statusCode = 403; - res.end('Forbidden'); - return; - } - const stat = await fs.stat(filePath); - if (stat.isDirectory()) { - const entries = await fs.readdir(filePath, { withFileTypes: true }); - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(entries.map((e) => ({ name: e.name, isDir: e.isDirectory() })))); - return; - } - const ext = path.extname(filePath).toLowerCase(); - const mime = ext === '.png' ? 'image/png' - : ext === '.json' ? 'application/json' - : ext === '.log' ? 'text/plain' - : 'application/octet-stream'; - res.setHeader('Content-Type', mime); - res.setHeader('Cache-Control', 'no-cache'); - const data = await fs.readFile(filePath); - res.end(data); - } catch (error) { - if (error.code === 'ENOENT') { - res.statusCode = 404; - res.end('Not found'); - return; - } - next(error); - } - }); - - server.middlewares.use('/api/index', async (req, res) => { - try { - const indexPath = path.join(RENDER_ROOT, 'index.json'); - const data = await fs.readFile(indexPath, 'utf8'); - res.setHeader('Content-Type', 'application/json'); - res.setHeader('Cache-Control', 'no-cache'); - res.end(data); - } catch (error) { - res.statusCode = 500; - res.end(JSON.stringify({ error: error.message })); - } - }); - - // Serve per-map sprite cache: /sprites//bundle_/frame_NNN.png - // The exporter writes individual decoded sprite PNGs into - // /.cache//sprites/, sharing the cache between - // both the auto and region01 variants of the same map (bundle decoding - // is invariant to the record-set choice). - server.middlewares.use('/sprites', async (req, res, next) => { - try { - const url = decodeURIComponent(req.url.split('?')[0]); - // url like "/L2/bundle_00085c40/frame_000.png" - // map to "/L2/sprites/bundle_00085c40/frame_000.png" - const segments = url.split('/').filter(Boolean); - if (segments.length < 2) { - res.statusCode = 404; - res.end('Not found'); - return; - } - const [mapStem, ...rest] = segments; - const filePath = path.join(CACHE_ROOT, mapStem, 'sprites', ...rest); - if (!filePath.startsWith(CACHE_ROOT)) { - res.statusCode = 403; - res.end('Forbidden'); - return; - } - const data = await fs.readFile(filePath); - res.setHeader('Content-Type', 'image/png'); - res.setHeader('Cache-Control', 'no-cache'); - res.end(data); - } catch (error) { - if (error.code === 'ENOENT') { - res.statusCode = 404; - res.end('Not found'); - return; - } - next(error); - } - }); - }, - }; -} - -export default defineConfig({ - plugins: [vue(), renderRootPlugin()], - server: { - port: 5180, - strictPort: false, - }, -}); diff --git a/temp_head.json b/temp_head.json deleted file mode 100644 index e69de29..0000000