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'],
|
||||
];
|
||||
|
||||
// [name, objectId, size, md5] — all in ISLE.SI
|
||||
// [name, siFile, objectId, size, md5]
|
||||
const BITMAPS = [
|
||||
['globe1', 1130, 5824, '12554d2a7d38bdc0e6bc1709f8404293'],
|
||||
['globe2', 1131, 5824, 'b0b39a4b959b4bf1605a6c695d9e3dd0'],
|
||||
['globe3', 1132, 5824, '71672bff19044f7df059c87a0759d950'],
|
||||
['globe4', 1133, 5824, 'f5421e06ae9997d9cbfc774942f097d4'],
|
||||
['globe5', 1134, 5824, '47974b0577cab1c3775175eab074e5b5'],
|
||||
['globe6', 1135, 5824, '65237421063fa993167ed4af9be9180c'],
|
||||
['globe1', 'Scripts/Isle/ISLE.SI', 1130, 5824, '12554d2a7d38bdc0e6bc1709f8404293'],
|
||||
['globe2', 'Scripts/Isle/ISLE.SI', 1131, 5824, 'b0b39a4b959b4bf1605a6c695d9e3dd0'],
|
||||
['globe3', 'Scripts/Isle/ISLE.SI', 1132, 5824, '71672bff19044f7df059c87a0759d950'],
|
||||
['globe4', 'Scripts/Isle/ISLE.SI', 1133, 5824, 'f5421e06ae9997d9cbfc774942f097d4'],
|
||||
['globe5', 'Scripts/Isle/ISLE.SI', 1134, 5824, '47974b0577cab1c3775175eab074e5b5'],
|
||||
['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');
|
||||
@ -300,15 +310,24 @@ async function main() {
|
||||
}
|
||||
console.log(` ${texFound}/${TEXTURES.length} textures found\n`);
|
||||
|
||||
// --- Bitmaps (in ISLE.SI) ---
|
||||
const bmpObjectIds = new Set(BITMAPS.map(([, objectId]) => objectId));
|
||||
const bmpRanges = findMxChByObjectId(isleSI, bmpObjectIds);
|
||||
// --- Bitmaps (across SI files) ---
|
||||
const bmpBySI = new Map();
|
||||
for (const entry of BITMAPS) {
|
||||
const siFile = entry[1];
|
||||
if (!bmpBySI.has(siFile)) bmpBySI.set(siFile, []);
|
||||
bmpBySI.get(siFile).push(entry);
|
||||
}
|
||||
|
||||
let bmpFound = 0;
|
||||
for (const [name, objectId, size, expectedMd5] of BITMAPS) {
|
||||
const result = verifyRanges(isleSI, bmpRanges.get(objectId), size, expectedMd5);
|
||||
for (const [siFile, entries] of bmpBySI) {
|
||||
const siBuf = await loadSI(siFile);
|
||||
const objectIds = new Set(entries.map(([, , objectId]) => objectId));
|
||||
const bmpRanges = findMxChByObjectId(siBuf, objectIds);
|
||||
|
||||
for (const [name, , objectId, size, expectedMd5] of entries) {
|
||||
const result = verifyRanges(siBuf, bmpRanges.get(objectId), size, expectedMd5);
|
||||
if (result) {
|
||||
manifest.bitmaps[name] = formatResult('Scripts/Isle/ISLE.SI', result);
|
||||
manifest.bitmaps[name] = formatResult(siFile, result);
|
||||
bmpFound++;
|
||||
found++;
|
||||
} else {
|
||||
@ -316,6 +335,7 @@ async function main() {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(` ${bmpFound}/${BITMAPS.length} bitmaps found\n`);
|
||||
|
||||
if (failed > 0) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import BackButton from './BackButton.svelte';
|
||||
import Carousel from './Carousel.svelte';
|
||||
import MissionScoresEditor from './save-editor/MissionScoresEditor.svelte';
|
||||
@ -7,6 +7,7 @@
|
||||
import LightPositionEditor from './save-editor/LightPositionEditor.svelte';
|
||||
import VehicleEditor from './save-editor/VehicleEditor.svelte';
|
||||
import ActorEditor from './save-editor/ActorEditor.svelte';
|
||||
import { fetchBitmapAsURL } from '../core/assetLoader.js';
|
||||
import { saveEditorState, currentPage } from '../stores.js';
|
||||
import { listSaveSlots, updateSaveSlot, updatePlayerName } from '../core/savegame/index.js';
|
||||
import { Actor, ActorNames } from '../core/savegame/constants.js';
|
||||
@ -43,20 +44,36 @@
|
||||
let currentAct = 0;
|
||||
let actorId = 1;
|
||||
|
||||
// Character icons mapping
|
||||
const characterIcons = {
|
||||
[Actor.PEPPER]: { normal: 'images/pepper.webp', selected: 'images/pepper-selected.webp' },
|
||||
[Actor.MAMA]: { normal: 'images/mama.webp', selected: 'images/mama-selected.webp' },
|
||||
[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' }
|
||||
};
|
||||
// Character icons — loaded from SI file bitmaps
|
||||
const iconNames = ['pepper', 'mama', 'papa', 'nick', 'laura'];
|
||||
let characterIcons = {};
|
||||
let iconUrls = [];
|
||||
|
||||
// Carousel state (bound from Carousel component)
|
||||
let carouselHasDragged = false;
|
||||
|
||||
onMount(async () => {
|
||||
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() {
|
||||
@ -272,12 +289,14 @@
|
||||
class:selected={selectedSlot === slot.slotNumber}
|
||||
onclick={() => handleSlotSelect(slot.slotNumber)}
|
||||
>
|
||||
{#if characterIcons[slot.header?.actorId]?.selected}
|
||||
<img
|
||||
src={characterIcons[slot.header?.actorId]?.selected || 'images/pepper-selected.webp'}
|
||||
src={characterIcons[slot.header?.actorId].selected}
|
||||
alt={ActorNames[slot.header?.actorId] || 'Character'}
|
||||
class="slot-character-icon"
|
||||
draggable="false"
|
||||
/>
|
||||
{/if}
|
||||
<span class="slot-name">{slot.playerName}</span>
|
||||
</button>
|
||||
{/each}
|
||||
@ -358,12 +377,14 @@
|
||||
onclick={() => handleActorSelect(actor.id)}
|
||||
title={actor.name}
|
||||
>
|
||||
{#if characterIcons[actor.id]}
|
||||
<img
|
||||
src={actorId === actor.id
|
||||
? characterIcons[actor.id].selected
|
||||
: characterIcons[actor.id].normal}
|
||||
alt={actor.name}
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||