Files
ark-stronghold-protocol/tools/assets/atlas.mjs
T
sganggs 01a44cd288 卫戍协议:盟约 web remake — initial import
Online co-op browser remake of Arknights' Stronghold Protocol: Alliance
(client-simulated combat, official waves/bonds/items/strategies, 3D board,
facing wheel, operator loadout, official emotes, deployment scripts).
2026-09-29 16:21:31 +08:00

131 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Spine (3.8-style) texture-atlas parsing and normalization.
//
// Semantics follow pixi-spine 4's TextureAtlas reader (the runtime we ship):
// a page starts at the first non-blank line after a blank line (or file start);
// the page name is followed by `key: value` page fields (size/format/filter/
// repeat/pma); every other non-blank line starts a region, whose `key: value`
// lines follow it. Normalization (research 07 §5.3):
// - insert `size: W,H` right after the page name when missing (fexli atlases
// omit it; spine-ts divides by 0 and pixi-spine warns), or correct it when it
// disagrees with the real PNG size;
// - insert `pma: true` for premultiplied-alpha textures (Ark-Models enemies).
// Normalization is idempotent: running it twice yields the same text.
/**
* @typedef {{ name: string, line: number, fields: Record<string,string>, fieldLines: Record<string,number>, lastFieldLine: number, regions: string[] }} AtlasPage
*/
function splitEntry(line) {
const t = line.trim();
if (!t) return null;
const colon = t.indexOf(':');
if (colon === -1) return null;
return [t.slice(0, colon).trim(), t.slice(colon + 1).trim()];
}
/**
* Parse an atlas into pages (with page fields and region names).
* @param {string} text
* @returns {{ lines: string[], pages: AtlasPage[] }}
*/
export function parseAtlas(text) {
const lines = String(text).replace(/^/, '').replace(/\r\n?/g, '\n').split('\n');
/** @type {AtlasPage[]} */
const pages = [];
let page = null;
let i = 0;
while (i < lines.length) {
const line = lines[i];
if (!line.trim()) { page = null; i++; continue; }
if (page === null) {
page = { name: line.trim(), line: i, fields: {}, fieldLines: {}, lastFieldLine: i, regions: [] };
pages.push(page);
i++;
while (i < lines.length) {
const e = splitEntry(lines[i]);
if (!e) break;
page.fields[e[0]] = e[1];
page.fieldLines[e[0]] = i;
page.lastFieldLine = i;
i++;
}
continue;
}
// Region: name line followed by entries.
page.regions.push(line);
i++;
while (i < lines.length && splitEntry(lines[i])) i++;
}
return { lines, pages };
}
/**
* Normalize an atlas text.
* @param {string} text original atlas text
* @param {object} opts
* @param {(pageName:string)=>({width:number,height:number}|null)} opts.pageSize real texture size lookup
* @param {boolean} [opts.pma] add `pma: true` to every page
* @param {(pageName:string)=>string} [opts.renamePage] local file name for a page (rewrites the page line)
* @returns {{ text: string, changed: boolean, pages: string[], missingSize: string[], fixedSize: string[] }}
*/
export function normalizeAtlas(text, { pageSize, pma = false, renamePage = null }) {
const { lines, pages } = parseAtlas(text);
/** insertions after a given line index: Map<lineIndex, string[]> */
const inserts = new Map();
const replace = new Map();
const missingSize = [];
const fixedSize = [];
const addAfter = (idx, s) => { if (!inserts.has(idx)) inserts.set(idx, []); inserts.get(idx).push(s); };
for (const p of pages) {
if (renamePage) {
const local = renamePage(p.name);
if (typeof local === 'string' && local && local !== p.name) replace.set(p.line, local);
}
const real = pageSize ? pageSize(p.name) : null;
if (p.fields.size === undefined) {
if (real) addAfter(p.line, `size: ${real.width},${real.height}`);
else missingSize.push(p.name);
} else if (real) {
const m = /^(\d+)\s*,\s*(\d+)$/.exec(p.fields.size);
if (!m || Number(m[1]) !== real.width || Number(m[2]) !== real.height) {
replace.set(p.fieldLines.size, `size: ${real.width},${real.height}`);
fixedSize.push(p.name);
}
}
if (pma && p.fields.pma === undefined) {
// Append after the last page field (or after the inserted size line).
addAfter(p.lastFieldLine, 'pma: true');
}
}
const out = [];
for (let i = 0; i < lines.length; i++) {
out.push(replace.has(i) ? replace.get(i) : lines[i]);
const extra = inserts.get(i);
if (extra) {
// size must come right after the page name; pma after the other fields.
extra.sort((a, b) => (a.startsWith('size:') ? -1 : 0) - (b.startsWith('size:') ? -1 : 0));
out.push(...extra);
}
}
const result = out.join('\n');
const original = String(text).replace(/^/, '').replace(/\r\n?/g, '\n');
return { text: result, changed: result !== original, pages: pages.map((p) => (replace.get(p.line) ?? p.name)), missingSize, fixedSize };
}
/**
* List page image names and region names of an atlas.
* @param {string} text
* @returns {{ pages: string[], regions: Set<string>, hasSize: boolean, hasPma: boolean }}
*/
export function atlasInfo(text) {
const { pages } = parseAtlas(text);
const regions = new Set();
for (const p of pages) for (const r of p.regions) regions.add(r);
return {
pages: pages.map((p) => p.name),
regions,
hasSize: pages.length > 0 && pages.every((p) => /^\d+\s*,\s*\d+$/.test(p.fields.size || '')),
hasPma: pages.length > 0 && pages.every((p) => p.fields.pma === 'true'),
};
}