mirror of
https://github.com/isledecomp/isle.pizza.git
synced 2026-02-28 13:57:38 +00:00
Some checks failed
Build / build (push) Has been cancelled
* Building editor Add a Buildings tab to the Save Game Editor that lets users browse all 16 buildings, preview them in 3D, and customize their properties (sound, move, mood, variant) by clicking, matching the original game behavior per character. - Parse 16 buildings + nextVariant from save files instead of skipping - Add serializer methods to patch building fields in-place - Create BuildingRenderer (extends AnimatedRenderer) for 3D preview with click animations from SNDANIM.SI - Create BuildingEditor component with per-character click behavior (Pepper: variants, Mama: sounds, Papa: moves, Laura: moods) - Extract 18 building animations and 2 building sounds into asset bundle - Fix centerAndScaleModel to account for scale in position offset * Add Building Editor to February 2026 changelog * DRY up renderer hierarchy: extract shared logic into base classes Move duplicated animation tree utilities (findAnimatedNode, evaluateNodeChain, findNodePath, evaluateLocalTransform), click animation (queueClickAnimation, playQueuedAnimation, buildRotationTracks), and raycast hit testing (getClickedMesh) from PlantRenderer and BuildingRenderer into AnimatedRenderer. Add loadTextures() and createMeshMaterial() helpers to BaseRenderer, replacing identical texture-loading loops and material-creation code across all four renderers. PlantRenderer: 279 → 73 lines (-74%) BuildingRenderer: 245 → 57 lines (-77%)
523 lines
25 KiB
JavaScript
523 lines
25 KiB
JavaScript
#!/usr/bin/env node
|
|
// Scans LEGO Island SI files to extract embedded assets into a packed binary bundle.
|
|
// Writes save-editor.bin: [U32LE index length][JSON index][fragment data].
|
|
|
|
import * as crypto from 'node:crypto';
|
|
import * as fs from 'node:fs/promises';
|
|
import * as path from 'node:path';
|
|
|
|
// [name, objectId, size, md5]
|
|
const ANIMATIONS = [
|
|
['CNs001Bd', 223, 657, 'eec976da0035968ee65cde2444d66fdd'],
|
|
['CNs001Br', 207, 1617, 'ca07df295c5146da01efd57aa42e6f43'],
|
|
['CNs001La', 179, 1335, '2f3c2d17a404e3ee06e24c9b79a5bc93'],
|
|
['CNs001Ma', 117, 1545, '6d7eb4527cab9d589f61e931f76a7ccf'],
|
|
['CNs001Ni', 145, 1233, '8777efe8556288cedba7ac67ae5b8e75'],
|
|
['CNs001Pa', 131, 1277, '222c7c997e79d77228c3607ed6ff9754'],
|
|
['CNs001Pe', 107, 1209, 'f0057004a852fb8a6385c075bcb425f8'],
|
|
['CNs001Pg', 224, 657, 'c9293ae1cadcd769c57f16d721189cbe'],
|
|
['CNs001Rd', 225, 657, 'cb6c0ec1203b644f16451f7caf5538e9'],
|
|
['CNs001Sk', 227, 1590, 'b9e19a58b4a6d8bb9e63a12af08ce5ee'],
|
|
['CNs001Sy', 226, 657, '986645d7598ba921113aef9d72ff74fd'],
|
|
['CNs001xx', 80, 1101, '79a32fb54e881403cf785caf2f07fb99'],
|
|
['CNs002Br', 208, 1393, 'f489e8ae4ed591f35738a9f4d1259450'],
|
|
['CNs002La', 196, 1447, '0738817ecc1ed6afc8c605537afbcbc9'],
|
|
['CNs002Ma', 118, 1461, 'b8b1a40dd9ee102de0724fa4f0fa4047'],
|
|
['CNs002Ni', 146, 1201, '6e761fa60aea9e5c8d37ffa39a3aaf24'],
|
|
['CNs002Pa', 132, 1149, '7774a58da0fc3d3dd9bdaec4a4feebcb'],
|
|
['CNs002Pe', 108, 1065, '1bcb02590f35c2fc7a248fcfb00e6e58'],
|
|
['CNs002xx', 87, 1472, '9541d8c34615d28998985de01cbc7dfd'],
|
|
['CNs003Br', 209, 1497, 'a2d6094d7f4da36772d7d4a739a2831c'],
|
|
['CNs003La', 197, 1675, '7687af38a09b16be8028e73b39b99981'],
|
|
['CNs003Ma', 119, 1749, '5ca15ea1f1fd84a775b044c6ca068ba1'],
|
|
['CNs003Ni', 147, 1369, 'd2533306f8ce771d958e2839a0fc0ae9'],
|
|
['CNs003Pa', 133, 1369, '1fef78d548e65839093d2ff0b1a4460f'],
|
|
['CNs003Pe', 109, 1373, 'f9815a0266a1c63a3f3518cca85beb49'],
|
|
['CNs003xx', 88, 1377, 'f61d8fd473121bface739c7a557c05ec'],
|
|
['CNs004Br', 210, 1741, 'e777c1dabff4f0c756445c588b7c2ef2'],
|
|
['CNs004La', 198, 2139, '8dc93c005ebcedecbea0db2bdf351434'],
|
|
['CNs004Ma', 120, 2597, '332eaac68b2fb4244fd62789bc2c5508'],
|
|
['CNs004Ni', 148, 1853, '8a209f3790a1d9b7b58f8909151de2bf'],
|
|
['CNs004Pa', 134, 1961, '80bb689a754f174ead41f5b01019a00e'],
|
|
['CNs004Pe', 110, 1577, '70bf03ce873583554dcc8955ff307975'],
|
|
['CNs004xx', 89, 1581, '65d27ca748b437676c4a162e7d5a599c'],
|
|
['CNs005Br', 211, 1373, '837455903a2d23ea77833e9d96697b18'],
|
|
['CNs005La', 199, 1427, '9b4d20d1bbca5a81ec5805ac768b3f70'],
|
|
['CNs005Ma', 121, 2117, '8d5bb9ec4905efbc0a0d29b700eeee0d'],
|
|
['CNs005Ni', 149, 1245, '2696c22fa69a403e5a07406d9425400a'],
|
|
['CNs005Pa', 135, 1545, '8217a905ce20b4958695c9e5de3fc372'],
|
|
['CNs005Pe', 111, 1249, 'ed5a06a87baf507ccd35c96407af1da7'],
|
|
['CNs005xx', 90, 1253, 'bfe52167e229966987552da768ab5e44'],
|
|
['CNs006Br', 213, 1269, '1d451aee9a06118c5a944750ddf4fedc'],
|
|
['CNs006La', 201, 1587, '99f0f187b6128626ac2106e010a30373'],
|
|
['CNs006Ma', 123, 1445, '13f5e003ff05a3d87a7866bcb195fbfc'],
|
|
['CNs006Ni', 157, 1365, 'e76dbff6c14206cc20bbe02be377ee2f'],
|
|
['CNs006Pa', 137, 1665, '831badb8a5065d1f64b6123891b7907e'],
|
|
['CNs006Pe', 113, 1369, '619630dec24433c8f4863c16d09ebc14'],
|
|
['CNs006xx', 101, 1373, '18a8f17d235c89b5365e6f7a83dc9311'],
|
|
['CNs007Br', 212, 1353, 'b7ae9c9eb305e75572a472d65683f040'],
|
|
['CNs007La', 200, 1567, '0a0da9bf5043c1dcb537069f9febbeae'],
|
|
['CNs007Ma', 122, 1501, '81f94684fb3f508b697ba47af1c6079a'],
|
|
['CNs007Ni', 156, 1205, 'b228c9b6fce5ce909963b614cd2e4163'],
|
|
['CNs007Pa', 136, 1293, '2708a4b6e72f62ae96da476e6d67210e'],
|
|
['CNs007Pe', 112, 1249, '2b2aba18edb8945800203a1c4d985c34'],
|
|
['CNs007xx', 100, 2144, 'b4be30f4c60a1ecc8b01f4414af14455'],
|
|
['CNs008Br', 214, 1113, 'b11de30162588dfcc0bf1512469dd72a'],
|
|
['CNs008La', 202, 1047, '1bad27d6ff48c93bf5c9d476e5d64fbb'],
|
|
['CNs008Ma', 124, 1045, '4a12974aee2e9f81aa995120b5efd54b'],
|
|
['CNs008Ni', 158, 1005, 'ad3f7af37af89fe1b29175943cee52c2'],
|
|
['CNs008Pa', 138, 1305, '075566a5c2905b0126761750488a5f8d'],
|
|
['CNs008Pe', 114, 1009, 'ea176943a17569bea30695e10e6277f4'],
|
|
['CNs008xx', 102, 1013, 'b5610e2318eb760a9d6ecd915026edfa'],
|
|
['CNs009Br', 215, 1005, '4e8633be87a0d3e86018bb79d6df6647'],
|
|
['CNs009La', 203, 1179, '172d6534981dc3c82a6edf46b0a46a90'],
|
|
['CNs009Ma', 125, 917, '122d10aceea520188e614f4100cff493'],
|
|
['CNs009Ni', 159, 1269, 'd075ca06d9bca5509cd01367282d6322'],
|
|
['CNs009Pa', 139, 1757, '85ee3e3915ed3a81470b30da9985ee94'],
|
|
['CNs009Pe', 115, 941, '368209e2db8b682b9f5ee8c2449b3c14'],
|
|
['CNs009xx', 103, 945, '8a15d1553d6d81a0d741c1e11adb5b29'],
|
|
['CNs010Br', 216, 1469, 'b7c1a5844f0a4710d15c67eed8c19418'],
|
|
['CNs010La', 204, 1543, '77347221f51e10470ea06d514430663a'],
|
|
['CNs010Ma', 126, 1493, '846a971fe79f314ef9eb4788776d337d'],
|
|
['CNs010Ni', 168, 1541, '6e25bd94e5cb76e0acb642d4a52a922f'],
|
|
['CNs010Pa', 140, 1637, '94be270722ab8a69c6c81c9e0467a97c'],
|
|
['CNs010Pe', 116, 1465, 'ba3abad56fb12c025840df86ea5ea7b1'],
|
|
['CNs010xx', 104, 1469, 'aebf4eb621c5eb4b202751c11383d3c9'],
|
|
['CNs011Br', 217, 1161, '2e0e6495387746460c66a37f6ea4d9b3'],
|
|
['CNs011La', 205, 798, 'ce98ce48a5396559ee55fb786f122fcc'],
|
|
['CNs011Ma', 127, 1561, '1a4f6b4d89c9bd867d4433a81423d178'],
|
|
['CNs011Ni', 169, 699, '0b24eb57d4225b8737bf959aecf14430'],
|
|
['CNs011Pa', 141, 3747, '94cb14868ed2957db9a156f832221637'],
|
|
['CNs011xx', 105, 990, '5b09efc169a758325b76e6654f9777e0'],
|
|
['CNs012Br', 218, 2245, 'b83f53d4361a980de9abb186c3a2806b'],
|
|
['CNs012Ma', 128, 1593, '17e75f084d52d712012adeb92f7cda54'],
|
|
['CNs012Pa', 142, 6900, 'e61e78454d7ff3e7bae7def8b3dfa9d6'],
|
|
['CNs012xx', 106, 1830, '17ab918c42f311064f4e4c3560c93f51'],
|
|
['CNs013Br', 219, 3417, '8b7482f00475111add6829dcb6434d96'],
|
|
['CNs013Ma', 129, 3169, '8f017d5092a216d2a32a33e1de4ac118'],
|
|
['CNs013Pa', 143, 3647, '3e3cf7409b766b80653f8571801ebf71'],
|
|
['CNs014Br', 220, 3174, '1a47cb0ebd2c812273a5338b8495204f'],
|
|
['CNs0x4Ma', 130, 2657, '81e016edfb3acdd25fe4cb3a1b74d376'],
|
|
['CNs0x4Pa', 144, 2005, 'a7272eb818e1cd00d9b5ee65f5b592f4'],
|
|
['CNs900Br', 221, 3617, 'd9d4c57e6ec4061a464b45bb0560fd47'],
|
|
['CNs901BR', 222, 3917, '6378d58ab123dd09a0ca5460ef2a1112'],
|
|
['CNsx11La', 206, 881, 'b1ac6017e17a0f93e6af3962a8c3ae66'],
|
|
['CNsx11Ni', 178, 879, '0e09f9119f37308af94956c38527e758'],
|
|
];
|
|
|
|
// Click animations from SNDANIM.SI (objectId = m_move + 10)
|
|
// [name, objectId, size, md5]
|
|
const CLICK_ANIMATIONS = [
|
|
['ClickAnim0', 10, 1898, 'e8bb524cc29c6bdc9416ae3a95727dd1'],
|
|
['ClickAnim1', 11, 2038, '21444b8952df188cb338e830a8ee1e00'],
|
|
['ClickAnim2', 12, 2606, '5b49aeb7dcd7e52f22febc6502b9f8a2'],
|
|
['ClickAnim3', 13, 4218, 'e25f074d7012f89868011dc2bd5c0586'],
|
|
];
|
|
|
|
// Click sounds from SNDANIM.SI (objectId = m_sound + 50)
|
|
// [name, objectId, size, md5]
|
|
const CLICK_SOUNDS = [
|
|
['ClickSound0', 50, 10078, '928eeb70f8dadbc400f5c150727fde69'],
|
|
['ClickSound1', 51, 15988, '9c8aa04b0e4683976c3f2c2be868b37e'],
|
|
['ClickSound2', 52, 4114, 'a94a6dc7ae24fc42b1b9be962bbf3bf1'],
|
|
['ClickSound3', 53, 7741, '96bd26dc212ffd31da365ea1d088bfa3'],
|
|
['ClickSound4', 54, 23705, 'ca79cc736729c12aed6da018725fb0e3'],
|
|
['ClickSound5', 55, 24179, 'b7c97cb776f0afbba40f2e21fc0b309d'],
|
|
['ClickSound6', 56, 17675, 'b69b07bba21c6667d0af651c89828815'],
|
|
['ClickSound7', 57, 18953, '65d9cc0d09e3bfb831cee014a84085f7'],
|
|
['ClickSound8', 58, 7344, '7bbc41251b750835989cb3b35c8546a4'],
|
|
];
|
|
|
|
// Plant animations from SNDANIM.SI (objectId = g_plantAnimationId[variant] + move)
|
|
// [name, objectId, size, md5]
|
|
const PLANT_ANIMATIONS = [
|
|
['PlantAnimF0', 30, 911, 'cbc2f4d870099238a79130268e48f981'],
|
|
['PlantAnimF1', 31, 539, '1df6d082935ffa780f5867d0018870a1'],
|
|
['PlantAnimF2', 32, 451, 'f541f69207d849179704c55956bbf883'],
|
|
['PlantAnimT0', 33, 1719, 'ac41608766049001502a70f655cdf731'],
|
|
['PlantAnimT1', 34, 1022, '778c0c7fb646d85a2e056f430f21562f'],
|
|
['PlantAnimT2', 35, 794, '89f16250457fdd3a732fdd6030d92e2c'],
|
|
['PlantAnimB0', 36, 1066, 'c00ca3e2566846d94ce75ff7700f5a5b'],
|
|
['PlantAnimB1', 37, 850, '97d86074a3aa606e1fe3f3bd01690ae7'],
|
|
['PlantAnimB2', 38, 502, '7f08bf6093478c653ff82d058d86f900'],
|
|
['PlantAnimP0', 39, 978, '4f9af3721ba3a49e478da5566a4923de'],
|
|
['PlantAnimP1', 40, 682, '41d0ca14af41cc4cd7f737d7b0e74ef2'],
|
|
['PlantAnimP2', 41, 294, '5ddaff70e2b57fdb294769eaa14e42a0'],
|
|
];
|
|
|
|
// Building animations from SNDANIM.SI (objectId = g_buildingAnimationId[idx] + move)
|
|
// [name, objectId, size, md5]
|
|
const BUILDING_ANIMATIONS = [
|
|
['BuildingAnim9_0', 70, 452, '1292875d15dec79d2fe719f08e6f4b25'],
|
|
['BuildingAnim9_1', 71, 356, '0285456907450609d820b0bbd923f3b8'],
|
|
['BuildingAnim9_2', 72, 900, '91f87ce4bcb5d02854d0aa23929a4233'],
|
|
['BuildingAnim10_0', 73, 502, 'a8c2a3b47aaf7f01ec831b6af2924c0d'],
|
|
['BuildingAnim10_1', 74, 370, '8d39ae6fd092cf1586234e80ebe27815'],
|
|
['BuildingAnim10_2', 75, 894, '0c59954cf2be82b4221ffb26dbc40d5c'],
|
|
['BuildingAnim11_0', 76, 494, '926437967083a0eca288f7b3beba5c98'],
|
|
['BuildingAnim11_1', 77, 334, '6899b0fe1510db35a76d04e677f415c1'],
|
|
['BuildingAnim11_2', 78, 722, '98fc44fb00024c459e6e0808906f0f95'],
|
|
['BuildingAnim12_0', 79, 932, '71e06ffe92b26fe6ceb139f04c8f9556'],
|
|
['BuildingAnim12_1', 80, 916, '1fc7a77bff41496a7ca3eadf0681544e'],
|
|
['BuildingAnim12_2', 81, 868, '085ca884f316c3436b7fdf9fc2505e57'],
|
|
['BuildingAnim13_0', 82, 1024, '855b50251602ce8196bd4cc30f1ce1fa'],
|
|
['BuildingAnim13_1', 83, 876, '3893d16e60f7dad4c724c18fdecaf49c'],
|
|
['BuildingAnim13_2', 84, 888, '4681ff613c88b07c3a14c2e5b27edf21'],
|
|
['BuildingAnim14_0', 85, 972, 'a73d9da4e1c7c2d586d22997b9781fe3'],
|
|
['BuildingAnim14_1', 86, 948, 'ee469b2326c7d9e2f3b1fff6177842be'],
|
|
['BuildingAnim14_2', 87, 868, '693423f24d6f371d522076ecf4c589d5'],
|
|
];
|
|
|
|
// Building sounds from SNDANIM.SI (objectId = sound + 60, sounds 4-5 only)
|
|
// [name, objectId, size, md5]
|
|
const BUILDING_SOUNDS = [
|
|
['BuildingSound4', 64, 8215, '3066d58d6b26db751d0d0ded1055d886'],
|
|
['BuildingSound5', 65, 11534, '91379f36012f600a4b7432e003e16c3a'],
|
|
];
|
|
|
|
// Plant sounds from SNDANIM.SI (objectId = sound + 56, sounds 3-7)
|
|
// [name, objectId, size, md5]
|
|
const PLANT_SOUNDS = [
|
|
['PlantSound3', 59, 12184, '31a837c2420056e0a4f431d06801e746'],
|
|
['PlantSound4', 60, 10409, 'd8e8eb75668c57fcb45ba7a75e4612e5'],
|
|
['PlantSound5', 61, 12107, 'd60acd5c0962e15cc7c25de95553357f'],
|
|
['PlantSound6', 62, 15900, 'acfba6e91b047a43b673b0e2087bd3f5'],
|
|
['PlantSound7', 63, 11545, '53cfd93d7e81c85d5c39b4af624bc370'],
|
|
];
|
|
|
|
// Mood sounds from SNDANIM.SI (objectId = m_mood + 66)
|
|
// [name, objectId, size, md5]
|
|
const MOOD_SOUNDS = [
|
|
['MoodSound0', 66, 11534, '91379f36012f600a4b7432e003e16c3a'],
|
|
['MoodSound1', 67, 11534, '91379f36012f600a4b7432e003e16c3a'],
|
|
['MoodSound2', 68, 11534, '91379f36012f600a4b7432e003e16c3a'],
|
|
['MoodSound3', 69, 11534, '91379f36012f600a4b7432e003e16c3a'],
|
|
];
|
|
|
|
// [name, siFile, objectId, size, md5]
|
|
const TEXTURES = [
|
|
['CHJETL1', 'Scripts/Build/COPTER.SI', 112, 4235, 'af5010e9de08240c1ff7ad08ae90087e'],
|
|
['CHJETL2', 'Scripts/Build/COPTER.SI', 118, 4235, '130322a91a293b85551f59e1b5fb1c6f'],
|
|
['CHJETL3', 'Scripts/Build/COPTER.SI', 115, 4235, 'a922a1cf56da0ab47426cc3d0f581339'],
|
|
['CHJETL4', 'Scripts/Build/COPTER.SI', 121, 4235, '624b3aa949f2e2db5c8820caffbe8f58'],
|
|
['CHJETR1', 'Scripts/Build/COPTER.SI', 127, 4235, '924b8ae4db6c60003aba9994720ad0d6'],
|
|
['CHJETR2', 'Scripts/Build/COPTER.SI', 133, 4235, 'b4edeba59b44b2b37124a8da02693a10'],
|
|
['CHJETR3', 'Scripts/Build/COPTER.SI', 130, 4235, '18b50ad01a7aee7cf47f378d278be9eb'],
|
|
['CHJETR4', 'Scripts/Build/COPTER.SI', 136, 4235, 'f6874aec4931186782aa2e360fe1861f'],
|
|
['CHWIND1', 'Scripts/Build/COPTER.SI', 97, 4235, '860a0c8cacf27d3e2faed9030cc1be69'],
|
|
['CHWIND2', 'Scripts/Build/COPTER.SI', 103, 4235, 'b99fec10adf4660aa19b067483970f8f'],
|
|
['CHWIND3', 'Scripts/Build/COPTER.SI', 100, 4235, '05d7068d58105292632cdab56d3f67e4'],
|
|
['CHWIND4', 'Scripts/Build/COPTER.SI', 106, 4235, '24eb83a84cad5e5926fc2db010bd93d8'],
|
|
['Dbfrfn1', 'Scripts/Build/DUNECAR.SI', 96, 16524, '255fd145075b02d16fee2ac8bfeab3de'],
|
|
['Dbfrfn2', 'Scripts/Build/DUNECAR.SI', 99, 16524, 'a6b3e5a02bb1ab0b139cb76b8cc00e9b'],
|
|
['Dbfrfn3', 'Scripts/Build/DUNECAR.SI', 102, 16524, '60b6758fd34a74abf868fca98be3a3fa'],
|
|
['Dbfrfn4', 'Scripts/Build/DUNECAR.SI', 105, 16524, 'b57ff13872e67c8e52953b943d4244a8'],
|
|
['JSWNSH1', 'Scripts/Build/JETSKI.SI', 124, 16511, 'faf25d963756e335bd3e97b5383ed3a4'],
|
|
['JSWNSH2', 'Scripts/Build/JETSKI.SI', 130, 16484, 'bce7b364358238f8adf15a2f7242ed1b'],
|
|
['JSWNSH3', 'Scripts/Build/JETSKI.SI', 136, 16484, 'c502a5ca2f43f73320960c201ecef96a'],
|
|
['JSWNSH4', 'Scripts/Build/JETSKI.SI', 142, 16511, '9dbddced239fe2e6f04704116a6dc98c'],
|
|
['jsfrnt1', 'Scripts/Build/JETSKI.SI', 100, 8325, '3bc6cee56e1b282e1271d823c932b140'],
|
|
['jsfrnt2', 'Scripts/Build/JETSKI.SI', 106, 8331, 'f0b3ba901b7302d6ea72fdadaee5def0'],
|
|
['jsfrnt3', 'Scripts/Build/JETSKI.SI', 112, 8325, '13431821186bb466fce71c34ecc008e7'],
|
|
['jsfrnt4', 'Scripts/Build/JETSKI.SI', 118, 8331, 'a173f79e05be78fba888d89aa5ee5ed1'],
|
|
['rcback1', 'Scripts/Build/RACECAR.SI', 110, 16524, '97c1c6f3673bcceb340149627a5d656c'],
|
|
['rcback2', 'Scripts/Build/RACECAR.SI', 113, 16524, 'd730986dc5a0b4f3199dcd31246c32a3'],
|
|
['rcback3', 'Scripts/Build/RACECAR.SI', 116, 16524, 'a2574a6c6d9c16d41f001fb0eb908726'],
|
|
['rcback4', 'Scripts/Build/RACECAR.SI', 119, 16512, '37291480ea6c96145c99e659d4d6cbd4'],
|
|
['rcfrnt1', 'Scripts/Build/RACECAR.SI', 95, 16524, '8962ce972e6122ab7f7b87efa40591c2'],
|
|
['rcfrnt2', 'Scripts/Build/RACECAR.SI', 98, 16524, 'ca235a1cdd432f6cc5b64610e62eb94f'],
|
|
['rcfrnt3', 'Scripts/Build/RACECAR.SI', 101, 16524, '017301e33ff6dc8dfe41afc6558055f1'],
|
|
['rcfrnt4', 'Scripts/Build/RACECAR.SI', 104, 16512, '3064a4627325d2325ee07b98b49f4a58'],
|
|
['rctail1', 'Scripts/Build/RACECAR.SI', 125, 4227, '5c28aa88d5971f73575315b09359ce57'],
|
|
['rctail2', 'Scripts/Build/RACECAR.SI', 128, 4236, 'a69ae67432ecccfd88a567ce2d8973c0'],
|
|
['rctail3', 'Scripts/Build/RACECAR.SI', 131, 4230, '55d628507bf0968037422aefb3494184'],
|
|
['rctail4', 'Scripts/Build/RACECAR.SI', 134, 4236, '614cb9aa532ee85c119cc1432e6d65e9'],
|
|
];
|
|
|
|
// [name, siFile, objectId, size, md5]
|
|
const BITMAPS = [
|
|
['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');
|
|
const MXCH_HEADER_SIZE = 22; // MxCh(4) + chunkSize(4) + flags(2) + objectId(4) + time(4) + dataSize(4)
|
|
|
|
const LEGO_DIR = path.join(process.cwd(), 'LEGO');
|
|
const BIN_PATH = path.join(process.cwd(), 'save-editor.bin');
|
|
|
|
const siCache = new Map();
|
|
|
|
async function loadSI(siRelPath) {
|
|
if (siCache.has(siRelPath)) return siCache.get(siRelPath);
|
|
const buf = await fs.readFile(path.join(LEGO_DIR, siRelPath));
|
|
siCache.set(siRelPath, buf);
|
|
return buf;
|
|
}
|
|
|
|
function md5(buf) {
|
|
return crypto.createHash('md5').update(buf).digest('hex');
|
|
}
|
|
|
|
/**
|
|
* Scan a SI buffer for all MxCh chunks and group data ranges by objectId.
|
|
* Clips each chunk's data to the physical space before the next MxCh header,
|
|
* since interleaving can split a logical chunk across sector boundaries.
|
|
* Returns Map<objectId, [[dataOffset, dataSize], ...]>.
|
|
*/
|
|
function findMxChByObjectId(siBuf, targetIds) {
|
|
// First pass: collect all MxCh header positions
|
|
const allPositions = [];
|
|
let pos = 0;
|
|
while (pos <= siBuf.length - MXCH_HEADER_SIZE) {
|
|
const idx = siBuf.indexOf(MXCH_SIGNATURE, pos);
|
|
if (idx === -1) break;
|
|
allPositions.push(idx);
|
|
pos = idx + 4;
|
|
}
|
|
|
|
// Second pass: extract data ranges for target objectIds
|
|
const result = new Map();
|
|
for (const id of targetIds) result.set(id, []);
|
|
|
|
for (let i = 0; i < allPositions.length; i++) {
|
|
const idx = allPositions[i];
|
|
const dataSize = siBuf.readUInt32LE(idx + 18);
|
|
const objectId = siBuf.readUInt32LE(idx + 10);
|
|
|
|
if (dataSize > 0 && result.has(objectId)) {
|
|
const dataStart = idx + MXCH_HEADER_SIZE;
|
|
const physicalEnd = i + 1 < allPositions.length ? allPositions[i + 1] : siBuf.length;
|
|
const actualSize = Math.min(dataSize, physicalEnd - dataStart);
|
|
if (actualSize > 0) {
|
|
result.get(objectId).push([dataStart, actualSize]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Assemble MxCh data from ranges, verify against expected size and md5.
|
|
* Only assembles up to `size` bytes (objectIds can be reused across streams).
|
|
* Returns the assembled Buffer, or null on failure.
|
|
*/
|
|
function extractAndVerify(siBuf, ranges, size, expectedMd5) {
|
|
if (!ranges || ranges.length === 0) return null;
|
|
|
|
const assembled = Buffer.alloc(size);
|
|
let writePos = 0;
|
|
for (const [rOff, rLen] of ranges) {
|
|
if (writePos >= size) break;
|
|
const take = Math.min(rLen, size - writePos);
|
|
siBuf.copy(assembled, writePos, rOff, rOff + take);
|
|
writePos += take;
|
|
}
|
|
|
|
if (writePos !== size || md5(assembled) !== expectedMd5) return null;
|
|
return assembled;
|
|
}
|
|
|
|
async function main() {
|
|
console.log('Generating asset fragment bundle...\n');
|
|
|
|
const fragments = []; // [{type, name, data: Buffer}, ...]
|
|
let found = 0;
|
|
let failed = 0;
|
|
|
|
// --- Animations (all in ISLE.SI) ---
|
|
const isleSI = await loadSI('Scripts/Isle/ISLE.SI');
|
|
console.log(`Loaded ISLE.SI (${(isleSI.length / 1024 / 1024).toFixed(1)} MB)`);
|
|
|
|
const aniObjectIds = new Set(ANIMATIONS.map(([, objectId]) => objectId));
|
|
const aniRanges = findMxChByObjectId(isleSI, aniObjectIds);
|
|
|
|
for (const [name, objectId, size, expectedMd5] of ANIMATIONS) {
|
|
const data = extractAndVerify(isleSI, aniRanges.get(objectId), size, expectedMd5);
|
|
if (data) {
|
|
fragments.push({ type: 'animations', name, data });
|
|
found++;
|
|
} else {
|
|
console.error(` FAILED: ${name}.ani (objectId ${objectId})`);
|
|
failed++;
|
|
}
|
|
}
|
|
console.log(` ${found}/${ANIMATIONS.length} walking animations found\n`);
|
|
|
|
// --- Click Animations (in SNDANIM.SI) ---
|
|
const sndanimSI = await loadSI('Scripts/SNDANIM.SI');
|
|
console.log(`Loaded SNDANIM.SI (${(sndanimSI.length / 1024 / 1024).toFixed(1)} MB)`);
|
|
|
|
const clickObjectIds = new Set(CLICK_ANIMATIONS.map(([, objectId]) => objectId));
|
|
const clickRanges = findMxChByObjectId(sndanimSI, clickObjectIds);
|
|
|
|
let clickFound = 0;
|
|
for (const [name, objectId, size, expectedMd5] of CLICK_ANIMATIONS) {
|
|
const data = extractAndVerify(sndanimSI, clickRanges.get(objectId), size, expectedMd5);
|
|
if (data) {
|
|
fragments.push({ type: 'animations', name, data });
|
|
clickFound++;
|
|
found++;
|
|
} else {
|
|
console.error(` FAILED: ${name} (objectId ${objectId})`);
|
|
failed++;
|
|
}
|
|
}
|
|
console.log(` ${clickFound}/${CLICK_ANIMATIONS.length} click animations found\n`);
|
|
|
|
// --- Sounds (in SNDANIM.SI) ---
|
|
const allSounds = [...CLICK_SOUNDS, ...MOOD_SOUNDS, ...PLANT_SOUNDS, ...BUILDING_SOUNDS];
|
|
const soundObjectIds = new Set(allSounds.map(([, objectId]) => objectId));
|
|
const soundRanges = findMxChByObjectId(sndanimSI, soundObjectIds);
|
|
|
|
let soundFound = 0;
|
|
for (const [name, objectId, size, expectedMd5] of allSounds) {
|
|
const data = extractAndVerify(sndanimSI, soundRanges.get(objectId), size, expectedMd5);
|
|
if (data) {
|
|
fragments.push({ type: 'sounds', name, data });
|
|
soundFound++;
|
|
found++;
|
|
} else {
|
|
console.error(` FAILED: ${name} (objectId ${objectId})`);
|
|
failed++;
|
|
}
|
|
}
|
|
console.log(` ${soundFound}/${allSounds.length} sounds found\n`);
|
|
|
|
// --- Plant Animations (in SNDANIM.SI) ---
|
|
const plantAnimObjectIds = new Set(PLANT_ANIMATIONS.map(([, objectId]) => objectId));
|
|
const plantAnimRanges = findMxChByObjectId(sndanimSI, plantAnimObjectIds);
|
|
|
|
let plantAnimFound = 0;
|
|
for (const [name, objectId, size, expectedMd5] of PLANT_ANIMATIONS) {
|
|
const data = extractAndVerify(sndanimSI, plantAnimRanges.get(objectId), size, expectedMd5);
|
|
if (data) {
|
|
fragments.push({ type: 'animations', name, data });
|
|
plantAnimFound++;
|
|
found++;
|
|
} else {
|
|
console.error(` FAILED: ${name} (objectId ${objectId})`);
|
|
failed++;
|
|
}
|
|
}
|
|
console.log(` ${plantAnimFound}/${PLANT_ANIMATIONS.length} plant animations found\n`);
|
|
|
|
// --- Building Animations (in SNDANIM.SI) ---
|
|
const buildingAnimObjectIds = new Set(BUILDING_ANIMATIONS.map(([, objectId]) => objectId));
|
|
const buildingAnimRanges = findMxChByObjectId(sndanimSI, buildingAnimObjectIds);
|
|
|
|
let buildingAnimFound = 0;
|
|
for (const [name, objectId, size, expectedMd5] of BUILDING_ANIMATIONS) {
|
|
const data = extractAndVerify(sndanimSI, buildingAnimRanges.get(objectId), size, expectedMd5);
|
|
if (data) {
|
|
fragments.push({ type: 'animations', name, data });
|
|
buildingAnimFound++;
|
|
found++;
|
|
} else {
|
|
console.error(` FAILED: ${name} (objectId ${objectId})`);
|
|
failed++;
|
|
}
|
|
}
|
|
console.log(` ${buildingAnimFound}/${BUILDING_ANIMATIONS.length} building animations found\n`);
|
|
|
|
// --- Textures (across Build SI files) ---
|
|
const texBySI = new Map();
|
|
for (const entry of TEXTURES) {
|
|
const siFile = entry[1];
|
|
if (!texBySI.has(siFile)) texBySI.set(siFile, []);
|
|
texBySI.get(siFile).push(entry);
|
|
}
|
|
|
|
let texFound = 0;
|
|
for (const [siFile, entries] of texBySI) {
|
|
const siBuf = await loadSI(siFile);
|
|
const objectIds = new Set(entries.map(([, , objectId]) => objectId));
|
|
const texRanges = findMxChByObjectId(siBuf, objectIds);
|
|
console.log(`Loaded ${siFile} (${(siBuf.length / 1024).toFixed(0)} KB)`);
|
|
|
|
for (const [name, , objectId, size, expectedMd5] of entries) {
|
|
const data = extractAndVerify(siBuf, texRanges.get(objectId), size, expectedMd5);
|
|
if (data) {
|
|
fragments.push({ type: 'textures', name, data });
|
|
texFound++;
|
|
found++;
|
|
} else {
|
|
console.error(` FAILED: ${name}.tex in ${siFile} (objectId ${objectId})`);
|
|
failed++;
|
|
}
|
|
}
|
|
}
|
|
console.log(` ${texFound}/${TEXTURES.length} textures found\n`);
|
|
|
|
// --- 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 [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 data = extractAndVerify(siBuf, bmpRanges.get(objectId), size, expectedMd5);
|
|
if (data) {
|
|
fragments.push({ type: 'bitmaps', name, data });
|
|
bmpFound++;
|
|
found++;
|
|
} else {
|
|
console.error(` FAILED: ${name} (objectId ${objectId})`);
|
|
failed++;
|
|
}
|
|
}
|
|
}
|
|
console.log(` ${bmpFound}/${BITMAPS.length} bitmaps found\n`);
|
|
|
|
if (failed > 0) {
|
|
console.error(`Failed to find ${failed} assets. Bundle not written.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// --- Write single bundle: [U32LE indexLen][JSON index][data] ---
|
|
const index = {};
|
|
let offset = 0;
|
|
for (const { type, name, data } of fragments) {
|
|
index[`${type}/${name}`] = [offset, data.length];
|
|
offset += data.length;
|
|
}
|
|
|
|
const indexBuf = Buffer.from(JSON.stringify(index));
|
|
const header = Buffer.alloc(4);
|
|
header.writeUInt32LE(indexBuf.length);
|
|
const dataBuf = Buffer.concat(fragments.map(f => f.data));
|
|
const bundle = Buffer.concat([header, indexBuf, dataBuf]);
|
|
await fs.writeFile(BIN_PATH, bundle);
|
|
console.log(`Wrote ${BIN_PATH} (${(bundle.length / 1024).toFixed(1)} KB, ${Object.keys(index).length} entries)`);
|
|
|
|
console.log(`Total: ${found} assets (${ANIMATIONS.length} walking + ${CLICK_ANIMATIONS.length} click + ${PLANT_ANIMATIONS.length} plant + ${BUILDING_ANIMATIONS.length} building animations, ${allSounds.length} sounds, ${TEXTURES.length} textures, ${BITMAPS.length} bitmaps)`);
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error('Error:', err.message);
|
|
process.exit(1);
|
|
});
|