0.2.5-dev: a restore-defaults loadout sync is never overwritten by an older in-flight sync (PR #540 by @Xinglan233, ported); importing a loadout with ops is refused before writing when the DIY data is missing (PR #538 by @Xinglan233, ported); the player package's package.json keeps only scripts whose files ship (#531) and its docs link unshipped files to the release tag on GitHub (#530); golden: unchanged

This commit is contained in:
sganggs committed 2026-10-12 14:45:01 +08:00
1 parent 7fd0478dc0
commit 52b70434d0
13 files changed
+610 -5

No files matched your search

+18
View File
@@ -451,6 +451,20 @@ export function importRefusal(kind, ready, status = data.status) {
return null;
}
/**
* A loadout payload that carries `ops` (including empty `ops`) replaces 潜能 / 练度. 自选 operators are in that list
* and only backups.json names them, so the refusal is the 自选编队 one: chess.json and backups.json both loaded.
* Null when the payload has no `ops` (an older file leaves the current 练度 alone) or that data is ready (GitHub #538).
* @param {Record<string, any> | null | undefined} ops
* @param {boolean} ready
* @param {(name: string) => string} [status]
* @returns {{ text: string, tone: 'warn'|'error' } | null}
*/
export function loadoutOpsImportRefusal(ops, ready, status = data.status) {
if (ops == null) return null;
return importRefusal('diy', ready, status);
}
/** In place of a tab whose game data did not load (missingGameData): which files, and what to try (GitHub #173). */
export function DataMissing({ files }) {
const urls = files.map((name) => `/data/${DATA_FILES[name] || `${name}.json`}`);
@@ -586,6 +600,10 @@ function LoadoutScreen({ st }) {
}
const res = parseImport(ioText);
if (!res.ok) { toast(t('导入失败:{error}', { error: res.error }), 'error'); return; }
// ops replaces 潜能 / 练度, including 自选 operators that only backups.json names. Missing data used to drop
// them and still save, clearing a 练度 the file did not mean to erase (GitHub #538).
const opsRefused = loadoutOpsImportRefusal(res.ops, ready);
if (opsRefused) { toast(opsRefused.text, opsRefused.tone); return; }
// 0.2.2: a payload with `ops` replaces the 潜能 / 练度 too (an older export leaves them alone)
const opIds = res.ops ? cultivationCharIds(data.get('chess'), data.get('backups')) : null;
const r = applyLoadoutEntries(res.entries, getChess, res.ops ? { ops: res.ops, isOperator: (id) => opIds.has(id) } : {});
+8 -3
View File
@@ -169,6 +169,7 @@ function installPrefSync({ net, timers, target, notify, key, stateKey, msgType,
const tell = notify || ((text) => toast(t(text), 'warn'));
let timer = null;
let seq = 0; // requests sent (the reply of an older one never overrides a newer one's state)
let revision = 0; // edits and welcome invalidate a preparation still awaiting game data (GitHub #540)
let pendingJson = null; // JSON of the newest request still awaiting its reply
let lastSent = null; // JSON of the last payload the server accepted (on this session)
let edited = false; // an edit is waiting to be sent (a lock refusal is then worth telling the player)
@@ -189,9 +190,12 @@ function installPrefSync({ net, timers, target, notify, key, stateKey, msgType,
async function flush() {
if (disposed) return;
if (net.status !== 'online') { setState('idle'); return; } // the next welcome resends
const preparing = revision;
try {
const payload = await prepare();
if (disposed) return;
// a newer edit or welcome happened while prepare awaited chess/backups: do not send, and do not touch the
// sync state that newer flush owns (an older skill must not overwrite a restore-defaults)
if (disposed || preparing !== revision) return;
// never sanitise against missing data: every entry would be dropped and the server's copy cleared
if (payload == null) { setState('error'); return; }
const json = JSON.stringify(payload);
@@ -221,15 +225,16 @@ function installPrefSync({ net, timers, target, notify, key, stateKey, msgType,
else { console.warn(`[${tag}] ${msgType} refused`, code, err && err.detail); setState('error'); }
}
} catch (e) {
if (disposed || preparing !== revision) return;
console.warn(`[${tag}] sync failed`, e);
setState('error');
}
}
const offWelcome = net.on('welcome', () => { lastSent = null; pendingJson = null; seq++; schedule(50); });
const offWelcome = net.on('welcome', () => { revision++; lastSent = null; pendingJson = null; seq++; schedule(50); });
const keys = Array.isArray(key) ? key : [key];
const offStore = target.subscribe((s, prev) => {
if (keys.some((k) => s[k] !== prev[k])) { edited = true; schedule(); }
if (keys.some((k) => s[k] !== prev[k])) { revision++; edited = true; schedule(); }
// closing the overlay sends a pending edit at once (review fix): the player's next click — 准备就绪 in the solo
// briefing, 开始模拟 in the room — must not overtake the debounced message (the match locks its loadout when
// INFO_CHECK ends, so a late edit would silently only apply to the next match). Same socket ⇒ ordered.