Fixed palettes
This commit is contained in:
parent
9fe261610f
commit
93bc6e7a07
6 changed files with 328 additions and 100 deletions
|
|
@ -525,7 +525,15 @@ function summarizeRenderedLayers(items) {
|
|||
}
|
||||
|
||||
function derivePaletteDiagnostics(record, bundle) {
|
||||
const rawWords = Array.isArray(record.rawWords) ? record.rawWords : [];
|
||||
// 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 token06HighByte = rawWords.length >= 4 ? ((rawWords[3] >>> 8) & 0xff) : null;
|
||||
const token0cHighByte = rawWords.length >= 7 ? ((rawWords[6] >>> 8) & 0xff) : null;
|
||||
|
||||
|
|
@ -1008,29 +1016,57 @@ function resolveBundlePalettes(bundles, paletteSets, options = {}) {
|
|||
let paletteFormula = null;
|
||||
|
||||
if (bundle.mode === 2) {
|
||||
if (!Number.isInteger(resolvedPaletteIndex) || resolvedPaletteIndex < 0 || resolvedPaletteIndex >= paletteSets.palettes16.length) {
|
||||
resolvedPaletteIndex = choosePalette(paletteSets.palettes16, bundle.frames, bundle.mode);
|
||||
// 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) {
|
||||
palette = paletteSets.palettes16[resolvedPaletteIndex];
|
||||
paletteFormula = 'mode2-bundle-or-usage-index';
|
||||
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';
|
||||
}
|
||||
}
|
||||
} else if (bundle.mode === 1) {
|
||||
if (options.mode1RuntimePalette?.length === 256) {
|
||||
// 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (palette) {
|
||||
paletteFormula = options.mode1RuntimePalette?.length === 256
|
||||
? 'mode1-live-gpu-ram-row-f0-x0'
|
||||
: 'mode1-runtime-clut-band-start-index-0';
|
||||
paletteFormula = palette ? 'mode1-runtime-clut-band-start-index-0' : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1050,7 +1086,14 @@ async function buildSceneItems(region04, records, bundles, options = {}) {
|
|||
const skippedRecords = [];
|
||||
|
||||
const nonMapFacingRootTypes = new Set([0x42, 0x49]);
|
||||
const nonMapFacingBundleOffsets = new Set([0x000d84f4]);
|
||||
// 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.
|
||||
const nonMapFacingBundleOffsets = new Set([0x000d84f4, 0x00074f44]);
|
||||
for (const record of records) {
|
||||
if (record.sourceFamily === 'section0_dispatch_roots' && nonMapFacingRootTypes.has(record.typeWord)) {
|
||||
skippedRecords.push({
|
||||
|
|
@ -1102,6 +1145,9 @@ 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,
|
||||
|
|
@ -1134,12 +1180,49 @@ async function buildSceneItems(region04, records, bundles, options = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
// 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. `authoredLayer`: `constructors` is the static geometry lane (walls,
|
||||
// floors, architecture placed by `psx_dispatch_section0_constructor_placements`).
|
||||
// `roots` is the dispatch-root lane — interactive/dynamic objects such as
|
||||
// crates, terminals, doors, pickups placed by
|
||||
// `psx_dispatch_section0_dispatch_roots`. Geometry draws first so that
|
||||
// props (roots) sit ON TOP of the room they occupy.
|
||||
// 3. Isometric depth within a layer: back-to-front by world `X + Y` (ground
|
||||
// depth along the engine's isometric axis), then by `Z` ascending so a
|
||||
// lower object at the same ground cell does not occlude a taller one
|
||||
// behind it. Falls back to screenY when world coords are unavailable.
|
||||
// 4. `screenX` ascending: stable tie-breaker left-to-right.
|
||||
const layerPriority = (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;
|
||||
}
|
||||
if (left.screenY !== right.screenY) {
|
||||
return left.screenY - right.screenY;
|
||||
const leftLayer = layerPriority(left);
|
||||
const rightLayer = layerPriority(right);
|
||||
if (leftLayer !== rightLayer) {
|
||||
return leftLayer - rightLayer;
|
||||
}
|
||||
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;
|
||||
}
|
||||
return left.screenX - right.screenX;
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue