Fetch character icons from SI files via HTTP Range requests
Replace static webp character icons with runtime extraction from INFOMAIN.SI, extending the bitmap manifest to support multiple SI files. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
|
Before Width: | Height: | Size: 918 B |
|
Before Width: | Height: | Size: 1018 B |
|
Before Width: | Height: | Size: 964 B |
|
Before Width: | Height: | Size: 1016 B |
|
Before Width: | Height: | Size: 876 B |
|
Before Width: | Height: | Size: 986 B |
|
Before Width: | Height: | Size: 948 B |
|
Before Width: | Height: | Size: 980 B |
|
Before Width: | Height: | Size: 844 B |
|
Before Width: | Height: | Size: 946 B |
@ -145,14 +145,24 @@ const TEXTURES = [
|
|||||||
['rctail4', 'Scripts/Build/RACECAR.SI', 134, 4236, '614cb9aa532ee85c119cc1432e6d65e9'],
|
['rctail4', 'Scripts/Build/RACECAR.SI', 134, 4236, '614cb9aa532ee85c119cc1432e6d65e9'],
|
||||||
];
|
];
|
||||||
|
|
||||||
// [name, objectId, size, md5] — all in ISLE.SI
|
// [name, siFile, objectId, size, md5]
|
||||||
const BITMAPS = [
|
const BITMAPS = [
|
||||||
['globe1', 1130, 5824, '12554d2a7d38bdc0e6bc1709f8404293'],
|
['globe1', 'Scripts/Isle/ISLE.SI', 1130, 5824, '12554d2a7d38bdc0e6bc1709f8404293'],
|
||||||
['globe2', 1131, 5824, 'b0b39a4b959b4bf1605a6c695d9e3dd0'],
|
['globe2', 'Scripts/Isle/ISLE.SI', 1131, 5824, 'b0b39a4b959b4bf1605a6c695d9e3dd0'],
|
||||||
['globe3', 1132, 5824, '71672bff19044f7df059c87a0759d950'],
|
['globe3', 'Scripts/Isle/ISLE.SI', 1132, 5824, '71672bff19044f7df059c87a0759d950'],
|
||||||
['globe4', 1133, 5824, 'f5421e06ae9997d9cbfc774942f097d4'],
|
['globe4', 'Scripts/Isle/ISLE.SI', 1133, 5824, 'f5421e06ae9997d9cbfc774942f097d4'],
|
||||||
['globe5', 1134, 5824, '47974b0577cab1c3775175eab074e5b5'],
|
['globe5', 'Scripts/Isle/ISLE.SI', 1134, 5824, '47974b0577cab1c3775175eab074e5b5'],
|
||||||
['globe6', 1135, 5824, '65237421063fa993167ed4af9be9180c'],
|
['globe6', 'Scripts/Isle/ISLE.SI', 1135, 5824, '65237421063fa993167ed4af9be9180c'],
|
||||||
|
['pepper', 'Scripts/Infocntr/INFOMAIN.SI', 80, 2904, '4143f58632135089b7bc695bff406077'],
|
||||||
|
['pepper-selected', 'Scripts/Infocntr/INFOMAIN.SI', 81, 2904, '01f57a3a32f7aea6bc48a78a8c95ee1b'],
|
||||||
|
['mama', 'Scripts/Infocntr/INFOMAIN.SI', 76, 2904, 'ad68ae8fe78c368cac026ae09f1ad8c4'],
|
||||||
|
['mama-selected', 'Scripts/Infocntr/INFOMAIN.SI', 77, 2904, '72f041d1080b20f713d39f7ae00d966d'],
|
||||||
|
['papa', 'Scripts/Infocntr/INFOMAIN.SI', 78, 2904, '0cd5ef1c6d68198862c102b36b2f04fe'],
|
||||||
|
['papa-selected', 'Scripts/Infocntr/INFOMAIN.SI', 79, 2904, 'ec7d3d87d796dd824dfd5110911d4aa4'],
|
||||||
|
['nick', 'Scripts/Infocntr/INFOMAIN.SI', 82, 2904, 'c29ecdce0ffae81a71b973b8af26c26c'],
|
||||||
|
['nick-selected', 'Scripts/Infocntr/INFOMAIN.SI', 83, 2904, '8db4e63632c6f90e543832e6c92c89fa'],
|
||||||
|
['laura', 'Scripts/Infocntr/INFOMAIN.SI', 84, 2904, '99abd3415870285d487da20882f3bbf3'],
|
||||||
|
['laura-selected', 'Scripts/Infocntr/INFOMAIN.SI', 85, 2904, 'f56c2efb4f744d306d5a3d4ac8d332ca'],
|
||||||
];
|
];
|
||||||
|
|
||||||
const MXCH_SIGNATURE = Buffer.from('MxCh');
|
const MXCH_SIGNATURE = Buffer.from('MxCh');
|
||||||
@ -300,20 +310,30 @@ async function main() {
|
|||||||
}
|
}
|
||||||
console.log(` ${texFound}/${TEXTURES.length} textures found\n`);
|
console.log(` ${texFound}/${TEXTURES.length} textures found\n`);
|
||||||
|
|
||||||
// --- Bitmaps (in ISLE.SI) ---
|
// --- Bitmaps (across SI files) ---
|
||||||
const bmpObjectIds = new Set(BITMAPS.map(([, objectId]) => objectId));
|
const bmpBySI = new Map();
|
||||||
const bmpRanges = findMxChByObjectId(isleSI, bmpObjectIds);
|
for (const entry of BITMAPS) {
|
||||||
|
const siFile = entry[1];
|
||||||
|
if (!bmpBySI.has(siFile)) bmpBySI.set(siFile, []);
|
||||||
|
bmpBySI.get(siFile).push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
let bmpFound = 0;
|
let bmpFound = 0;
|
||||||
for (const [name, objectId, size, expectedMd5] of BITMAPS) {
|
for (const [siFile, entries] of bmpBySI) {
|
||||||
const result = verifyRanges(isleSI, bmpRanges.get(objectId), size, expectedMd5);
|
const siBuf = await loadSI(siFile);
|
||||||
if (result) {
|
const objectIds = new Set(entries.map(([, , objectId]) => objectId));
|
||||||
manifest.bitmaps[name] = formatResult('Scripts/Isle/ISLE.SI', result);
|
const bmpRanges = findMxChByObjectId(siBuf, objectIds);
|
||||||
bmpFound++;
|
|
||||||
found++;
|
for (const [name, , objectId, size, expectedMd5] of entries) {
|
||||||
} else {
|
const result = verifyRanges(siBuf, bmpRanges.get(objectId), size, expectedMd5);
|
||||||
console.error(` FAILED: ${name} (objectId ${objectId})`);
|
if (result) {
|
||||||
failed++;
|
manifest.bitmaps[name] = formatResult(siFile, result);
|
||||||
|
bmpFound++;
|
||||||
|
found++;
|
||||||
|
} else {
|
||||||
|
console.error(` FAILED: ${name} (objectId ${objectId})`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(` ${bmpFound}/${BITMAPS.length} bitmaps found\n`);
|
console.log(` ${bmpFound}/${BITMAPS.length} bitmaps found\n`);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import BackButton from './BackButton.svelte';
|
import BackButton from './BackButton.svelte';
|
||||||
import Carousel from './Carousel.svelte';
|
import Carousel from './Carousel.svelte';
|
||||||
import MissionScoresEditor from './save-editor/MissionScoresEditor.svelte';
|
import MissionScoresEditor from './save-editor/MissionScoresEditor.svelte';
|
||||||
@ -7,6 +7,7 @@
|
|||||||
import LightPositionEditor from './save-editor/LightPositionEditor.svelte';
|
import LightPositionEditor from './save-editor/LightPositionEditor.svelte';
|
||||||
import VehicleEditor from './save-editor/VehicleEditor.svelte';
|
import VehicleEditor from './save-editor/VehicleEditor.svelte';
|
||||||
import ActorEditor from './save-editor/ActorEditor.svelte';
|
import ActorEditor from './save-editor/ActorEditor.svelte';
|
||||||
|
import { fetchBitmapAsURL } from '../core/assetLoader.js';
|
||||||
import { saveEditorState, currentPage } from '../stores.js';
|
import { saveEditorState, currentPage } from '../stores.js';
|
||||||
import { listSaveSlots, updateSaveSlot, updatePlayerName } from '../core/savegame/index.js';
|
import { listSaveSlots, updateSaveSlot, updatePlayerName } from '../core/savegame/index.js';
|
||||||
import { Actor, ActorNames } from '../core/savegame/constants.js';
|
import { Actor, ActorNames } from '../core/savegame/constants.js';
|
||||||
@ -43,20 +44,36 @@
|
|||||||
let currentAct = 0;
|
let currentAct = 0;
|
||||||
let actorId = 1;
|
let actorId = 1;
|
||||||
|
|
||||||
// Character icons mapping
|
// Character icons — loaded from SI file bitmaps
|
||||||
const characterIcons = {
|
const iconNames = ['pepper', 'mama', 'papa', 'nick', 'laura'];
|
||||||
[Actor.PEPPER]: { normal: 'images/pepper.webp', selected: 'images/pepper-selected.webp' },
|
let characterIcons = {};
|
||||||
[Actor.MAMA]: { normal: 'images/mama.webp', selected: 'images/mama-selected.webp' },
|
let iconUrls = [];
|
||||||
[Actor.PAPA]: { normal: 'images/papa.webp', selected: 'images/papa-selected.webp' },
|
|
||||||
[Actor.NICK]: { normal: 'images/nick.webp', selected: 'images/nick-selected.webp' },
|
|
||||||
[Actor.LAURA]: { normal: 'images/laura.webp', selected: 'images/laura-selected.webp' }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Carousel state (bound from Carousel component)
|
// Carousel state (bound from Carousel component)
|
||||||
let carouselHasDragged = false;
|
let carouselHasDragged = false;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadSlots();
|
await loadSlots();
|
||||||
|
|
||||||
|
// Load character icons from SI file in background
|
||||||
|
const urls = await Promise.all(iconNames.flatMap(name => [
|
||||||
|
fetchBitmapAsURL(name),
|
||||||
|
fetchBitmapAsURL(`${name}-selected`)
|
||||||
|
]));
|
||||||
|
iconUrls = urls;
|
||||||
|
characterIcons = {
|
||||||
|
[Actor.PEPPER]: { normal: urls[0], selected: urls[1] },
|
||||||
|
[Actor.MAMA]: { normal: urls[2], selected: urls[3] },
|
||||||
|
[Actor.PAPA]: { normal: urls[4], selected: urls[5] },
|
||||||
|
[Actor.NICK]: { normal: urls[6], selected: urls[7] },
|
||||||
|
[Actor.LAURA]: { normal: urls[8], selected: urls[9] }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
for (const url of iconUrls) {
|
||||||
|
if (url) URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadSlots() {
|
async function loadSlots() {
|
||||||
@ -272,12 +289,14 @@
|
|||||||
class:selected={selectedSlot === slot.slotNumber}
|
class:selected={selectedSlot === slot.slotNumber}
|
||||||
onclick={() => handleSlotSelect(slot.slotNumber)}
|
onclick={() => handleSlotSelect(slot.slotNumber)}
|
||||||
>
|
>
|
||||||
<img
|
{#if characterIcons[slot.header?.actorId]?.selected}
|
||||||
src={characterIcons[slot.header?.actorId]?.selected || 'images/pepper-selected.webp'}
|
<img
|
||||||
alt={ActorNames[slot.header?.actorId] || 'Character'}
|
src={characterIcons[slot.header?.actorId].selected}
|
||||||
class="slot-character-icon"
|
alt={ActorNames[slot.header?.actorId] || 'Character'}
|
||||||
draggable="false"
|
class="slot-character-icon"
|
||||||
/>
|
draggable="false"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<span class="slot-name">{slot.playerName}</span>
|
<span class="slot-name">{slot.playerName}</span>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
@ -358,12 +377,14 @@
|
|||||||
onclick={() => handleActorSelect(actor.id)}
|
onclick={() => handleActorSelect(actor.id)}
|
||||||
title={actor.name}
|
title={actor.name}
|
||||||
>
|
>
|
||||||
<img
|
{#if characterIcons[actor.id]}
|
||||||
src={actorId === actor.id
|
<img
|
||||||
? characterIcons[actor.id].selected
|
src={actorId === actor.id
|
||||||
: characterIcons[actor.id].normal}
|
? characterIcons[actor.id].selected
|
||||||
alt={actor.name}
|
: characterIcons[actor.id].normal}
|
||||||
/>
|
alt={actor.name}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||