Files

174 lines
8.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}