174 lines
8.2 KiB
JavaScript
174 lines
8.2 KiB
JavaScript
// Fixes applied to the copied desktop renderer scripts by tools/extract-legacy.mjs (the copies in
|
||
// public/legacy are regenerated, so a fix lives here). Each patch must match exactly once, or the
|
||
// extraction stops — a changed desktop source then gets looked at instead of silently skipped.
|
||
export const PATCHES = [
|
||
// ---------- Neutral wording in a copied comment ----------
|
||
{
|
||
file: "lsformats.js",
|
||
why: "comment names other servers",
|
||
find: / \/\/ Index width: 16-bit in every \S+ client file;[^\r\n]*/,
|
||
replace: " // Index width: 16-bit in the client files we checked; some newer engines read 32-bit.",
|
||
},
|
||
// ---------- Heroes: a helmet changes the default hair like the game ----------
|
||
// LSClient ioHelmetItem / ioEquipSlot: hair_change_all0_hm (man) / _hw (woman) of the helmet:
|
||
// 0 = half hair ("half" + hair mesh, the default), 1 = no hair, 2 = "bind" + hair, 3 = full hair.
|
||
// The desktop preview always kept the full default hair, so it stuck out of helmets.
|
||
{
|
||
file: "model.js",
|
||
why: "hero preview: helmet hair rule",
|
||
find: "async function previewHero(src, meshes, animation, { embedded = false, host = null } = {}) {",
|
||
replace: "async function previewHero(src, meshes, animation, { embedded = false, host = null, hair = null } = {}) {",
|
||
},
|
||
{
|
||
file: "model.js",
|
||
why: "hero preview: helmet hair rule",
|
||
find: " const missing = [];\n for (const name of [...new Set(meshes)].filter(Boolean)) {",
|
||
replace: [
|
||
" // web fix: the helmet's hair rule (0 half, 1 none, 2 bind, 3 full)",
|
||
" if (hair !== null && hair !== undefined && Number(hair) !== 3) {",
|
||
" const baseHair = MDL.scene.meshes.find((m) => /^hair/i.test(m.name));",
|
||
" if (baseHair) {",
|
||
" removeMesh(baseHair);",
|
||
" const alt = Number(hair) === 0 ? `half${baseHair.name}` : Number(hair) === 2 ? `bind${baseHair.name}` : \"\";",
|
||
" const h = alt ? await readNamed(`${alt}.msh`, \"mesh\") : null;",
|
||
" if (h) await addMesh(h, alt);",
|
||
" }",
|
||
" }",
|
||
" const missing = [];",
|
||
" for (const name of [...new Set(meshes)].filter(Boolean)) {",
|
||
].join("\n"),
|
||
},
|
||
{
|
||
file: "hero.js",
|
||
why: "hero preview: pass the helmet hair rule",
|
||
find: " await MDL.previewHero(HERO.src, meshes, animation, { embedded: true });",
|
||
replace: " await MDL.previewHero(HERO.src, meshes, animation, { embedded: true, hair: helmetHair() });",
|
||
},
|
||
{
|
||
file: "hero.js",
|
||
why: "hero preview: pass the helmet hair rule (animation previews)",
|
||
find: " await MDL.previewHero(HERO.src, gearMeshes(), name);",
|
||
replace: " await MDL.previewHero(HERO.src, gearMeshes(), name, { hair: helmetHair() });",
|
||
},
|
||
{
|
||
file: "hero.js",
|
||
why: "hero preview: helmet hair rule + STR(n) names from the text table",
|
||
find: " const gearMeshes = () =>",
|
||
replace: [
|
||
" // web fix: the helmet section (item_type HELMET) and its hair rule for the man body (default 0 = half hair)",
|
||
" function helmetHair() {",
|
||
" const s = itemSections().find((x) => /^helmet$/i.test(value(\"item\", x.name, \"item_type\")) && value(\"item\", x.name, \"equip_mesh0\"));",
|
||
" if (!s) return null;",
|
||
" const v = value(\"item\", s.name, \"hair_change_all0_hm\");",
|
||
" return v === \"\" ? 0 : Number(v);",
|
||
" }",
|
||
" const gearMeshes = () =>",
|
||
].join("\n"),
|
||
},
|
||
// ---------- Heroes: STR(n) names read from the game's text table ----------
|
||
{
|
||
file: "hero.js",
|
||
why: "STR(n) names",
|
||
find: " const value = (kind, section, key) => { const d = doc(kind); return d ? HeroIni.get(d, section, key) : \"\"; };",
|
||
replace: [
|
||
" const value = (kind, section, key) => { const d = doc(kind); return d ? HeroIni.get(d, section, key) : \"\"; };",
|
||
" // web fix: STR(n) → the text the game shows (text table key ini_<file stem>::<section>_<n>)",
|
||
" const shown = (kind, section, v) => {",
|
||
" const m = /^\\s*STR\\(\\s*(\\d+)/i.exec(v || \"\");",
|
||
" return m ? HERO.strs?.[`ini_${HERO.id}_${kind}::${section}_${m[1]}`.toLowerCase()] || v : v;",
|
||
" };",
|
||
" const strText = (kind, section, key) => shown(kind, section, value(kind, section, key));",
|
||
].join("\n"),
|
||
},
|
||
{
|
||
file: "hero.js",
|
||
why: "STR(n) names: load the hero's strings with its configs",
|
||
find: " if (!docs.item) throw new Error(`No ${id}_item.ini in this source`);",
|
||
replace: [
|
||
" if (!docs.item) throw new Error(`No ${id}_item.ini in this source`);",
|
||
" try { HERO.strs = await call(\"text:forStems\", HERO.src, Object.keys(docs).map((k) => `${id}_${k}`)); } catch { HERO.strs = {}; }",
|
||
].join("\n"),
|
||
},
|
||
{
|
||
file: "hero.js",
|
||
why: "STR(n) names: moveset cards",
|
||
find: 'cards.push(card(`${value("item", s.name, "item_type") || s.name} · ${value("item", s.name, "name")}`',
|
||
replace: 'cards.push(card(`${value("item", s.name, "item_type") || s.name} · ${strText("item", s.name, "name")}`',
|
||
},
|
||
{
|
||
file: "hero.js",
|
||
why: "STR(n) names: skill cards",
|
||
find: 'return card(`${s.name} · ${value("skill", s.name, "desc_name") || value("skill", s.name, "name")}`',
|
||
replace: 'return card(`${s.name} · ${strText("skill", s.name, "desc_name") || strText("skill", s.name, "name")}`',
|
||
},
|
||
{
|
||
file: "hero.js",
|
||
why: "STR(n) names: skill buttons",
|
||
find: ' const title = value("skill", section, "desc_name") || value("skill", section, "name");',
|
||
replace: ' const title = strText("skill", section, "desc_name") || strText("skill", section, "name");',
|
||
},
|
||
{
|
||
file: "hero.js",
|
||
why: "STR(n) names: gear cards",
|
||
find: ' const name = value("item", s.name, "name");',
|
||
replace: ' const name = strText("item", s.name, "name");',
|
||
},
|
||
];
|
||
|
||
PATCHES.push(
|
||
// ---------- Textures: the list renders in chunks (a game folder has 10 000+ textures) ----------
|
||
// Search still filters the whole list (an array in memory); only the rows drawn are limited:
|
||
// the first 300, then 300 more each time the list is scrolled near its end.
|
||
{
|
||
file: "texture.js",
|
||
why: "textures list in chunks",
|
||
find: [
|
||
" $(\"tex-count\").textContent = rows.length ? String(rows.length) : \"\";",
|
||
" $(\"tex-list\").replaceChildren(",
|
||
" ...rows.map((f) =>",
|
||
].join("\n"),
|
||
replace: [
|
||
" $(\"tex-count\").textContent = rows.length ? String(rows.length) : \"\";",
|
||
" // web fix: draw 300 rows at a time, more while scrolling (10 000+ textures froze the page)",
|
||
" const box = $(\"tex-list\");",
|
||
" const CHUNK = 300;",
|
||
" let shown = Math.min(rows.length, Math.max(CHUNK, TEX._shown || 0));",
|
||
" const rowEl = (f) =>",
|
||
" el(\"div\", { class: `tex-row${TEX.file?.name === f.name && TEX.file?.dir === TEX.dir ? \" on\" : \"\"}`, title: `${f.name} — ${size(f.size)}`, onclick: () => openFile(join(TEX.dir, f.name)) }, [",
|
||
" el(\"span\", { class: `tex-ext ${extOf(f.name)}`, text: extOf(f.name) }),",
|
||
" el(\"span\", { class: \"grow\", text: f.name }),",
|
||
" el(\"span\", { class: \"dim\", text: size(f.size) }),",
|
||
" ]);",
|
||
" box.onscroll = () => {",
|
||
" if (shown >= rows.length || box.scrollTop + box.clientHeight < box.scrollHeight - 400) return;",
|
||
" box.append(...rows.slice(shown, shown + CHUNK).map(rowEl));",
|
||
" shown = Math.min(rows.length, shown + CHUNK);",
|
||
" TEX._shown = shown;",
|
||
" };",
|
||
" if (TEX._q !== q) (TEX._q = q), (shown = Math.min(rows.length, CHUNK)), (box.scrollTop = 0);",
|
||
" TEX._shown = shown;",
|
||
" $(\"tex-list\").replaceChildren(",
|
||
" ...rows.slice(0, shown).map((f) =>",
|
||
].join("\n"),
|
||
},
|
||
);
|
||
|
||
/** text → patched text; throws when a patch doesn't match exactly once. */
|
||
export function applyPatches(file, text) {
|
||
const crlf = text.includes("\r\n");
|
||
const eol = (s) => (crlf ? s.replace(/\r?\n/g, "\r\n") : s);
|
||
for (const p of PATCHES.filter((x) => x.file === file)) {
|
||
if (p.find instanceof RegExp) {
|
||
const n = (text.match(new RegExp(p.find.source, p.find.flags.replace("g", "") + "g")) || []).length;
|
||
if (n !== 1) throw new Error(`legacy patch "${p.why}" matches ${n}× in ${file}`);
|
||
text = text.replace(p.find, () => eol(p.replace));
|
||
continue;
|
||
}
|
||
const find = eol(p.find);
|
||
const n = text.split(find).length - 1;
|
||
if (n !== 1) throw new Error(`legacy patch "${p.why}" matches ${n}× in ${file}`);
|
||
text = text.replace(find, () => eol(p.replace));
|
||
}
|
||
return text;
|
||
}
|