feedback7 G-SMALL: settings can copy local diagnostics for a bug report (PR #321 by @Cloudnyco)

Applied 设置 → 问题反馈. The report stays in the browser: the in-memory error log, device info, and the battle on screen only while 「附上本场战斗」 is on. Nothing is uploaded.

Redaction no longer treats a long name as a bare substring. Prose replaces each secret only as a whole token, so a name does not cut a longer word or a chess id. The attached JSON replaces a string only when it equals the secret, so chess_char_1013_chen2 and a note like "charge the char" stay, while an exact "char" value becomes the placeholder. The token boundary is a consumed prefix plus a lookahead, because a lookbehind is a SyntaxError in Safari before 16.4. PLAYING section 12 stays; the other section-12 proposal is not in this batch.

Tests: node --test test/diag.test.js — 10 pass. node --test test/client-static.test.js — 315 pass. node --test test/match/runner-pending.test.js test/match/watch-bonds.test.js test/ui/playtest6-unite.test.js test/ui/spectate-effects.test.js test/ui/feedback5-hotkeys.test.js test/i18n-server.test.js test/ui/bosshit-ticker.test.js test/ui/playtest3.test.js test/docs-consistency.test.js test/docs-paths.test.js test/version.test.js test/package.test.js — 138 pass. npm run -s lint — 0 errors. npm run -s typecheck and npm run -s check:imports passed. node tools/i18n.mjs check --all --strict — 1041/1041 in en, ja, ko and zh-TW, 0 errors.
This commit is contained in:
Cloudnyco authored and sganggs committed 2026-10-08 20:08:32 +08:00
1 parent 073507eca3
commit 3f8844129b
12 files changed
+599 -7

No files matched your search

+23 -4
View File
@@ -72,6 +72,7 @@ import { net as appNet } from '../net.js';
import { store as appStore } from '../store.js';
import { unitStatsEntry, fxForm } from '../../../shared/protocol.js';
import { spectateEffects } from './observe.js';
import { recordError, setBattleSource } from '../diag.js';
const TICK = 1 / 30;
/** Fast-forward budget per frame (ticks) when far behind. */
@@ -156,9 +157,12 @@ export async function loadBrowserSim({ base = '/sim/', dataBase = '/data/', fetc
return { spec, ds: new simdata.DataSource(raw, null) };
}
/** Battle logger: content errors are isolated by the sim; report them as warnings (the server logs its own). */
/**
* Battle logger: content errors are isolated by the sim; report them as warnings (the server logs its own) and keep
* them for the diagnostics a player copies (diag.js).
*/
const SIM_LOGGER = Object.freeze({
error: (...a) => console.warn('[sim]', ...a),
error: (...a) => { console.warn('[sim]', ...a); recordError('sim', a.length === 1 ? a[0] : a.map(String).join(' ')); },
warn: (...a) => console.warn('[sim]', ...a),
info() {},
debug() {},
@@ -351,6 +355,7 @@ export function createBattleRunner(deps) {
} catch (err) {
stats.errors++;
console.warn('[runner] battle step failed', err);
recordError('runner', err, 'battle step failed');
try { b.forceEnd('timeout'); } catch { /* ignore */ }
}
const dt = now() - t;
@@ -393,7 +398,7 @@ export function createBattleRunner(deps) {
const list = catchingUp ? s.ev.filter(keepsState) : s.ev;
if (list.length) emit('ev', { t: 'b.ev', fieldId: e.fieldId, gt: s.gt, ev: list });
}
try { emit('snap', frameOf(e)); } catch (err) { console.warn('[runner] snapshot failed', err); }
try { emit('snap', frameOf(e)); } catch (err) { console.warn('[runner] snapshot failed', err); recordError('runner', err, 'snapshot failed'); }
}
/**
@@ -457,7 +462,7 @@ export function createBattleRunner(deps) {
result = e.sim.spec.compactResult(e.battle.result());
// an oversized frame would close the socket at the very end of the battle (64 KB inbound limit)
if (typeof e.sim.spec.fitResult === 'function') result = e.sim.spec.fitResult(result, { bossLike: bossLike(e), battleId: e.battleId });
} catch (err) { console.warn('[runner] result failed', err); }
} catch (err) { console.warn('[runner] result failed', err); recordError('runner', err, 'result failed'); }
if (result) {
e.result = result;
// the view answers with the settlement voice of this battle (screens/game.js → audio.voice result*): the
@@ -491,6 +496,7 @@ export function createBattleRunner(deps) {
} else {
e.delivery = 'delivered';
console.warn('[runner] b.result refused', code);
recordError('runner', code, 'b.result refused');
}
return null;
});
@@ -670,6 +676,7 @@ export function createBattleRunner(deps) {
let sim;
try { sim = await ensureSim(); } catch (err) {
console.warn('[runner] simulation unavailable', err);
recordError('runner', err, 'simulation unavailable');
return;
}
if (!wanted()) return;
@@ -678,6 +685,7 @@ export function createBattleRunner(deps) {
battle = sim.spec.createBattleFromSpec(e.spec, sim.ds, { logger });
} catch (err) {
console.warn('[runner] battle construction failed', err);
recordError('runner', err, 'battle construction failed');
return;
}
stats.battles++;
@@ -806,6 +814,16 @@ export function createBattleRunner(deps) {
return () => listeners.get(type)?.delete(fn);
},
state,
/**
* The battle on screen as a report attaches it (diag.js): its b.start fields and its game time; null without one.
* @returns {{ battleId: string, fieldId: string, kind: string, spec: object, time: number|null }|null}
*/
currentBattle() {
const e = cur;
if (!e || !e.spec) return null;
const time = e.battle && Number.isFinite(e.battle.time) ? e.battle.time : null;
return { battleId: e.battleId, fieldId: e.fieldId, kind: e.kind, spec: e.spec, time };
},
stats() {
return { ...stats, avgTickMs: stats.ticks ? stats.stepMs / stats.ticks : 0, entries: entries.size, loadingSim: !!simP };
},
@@ -869,3 +887,4 @@ export const battleRunner = typeof window !== 'undefined' && typeof document !==
? createBattleRunner({ net: appNet, store: appStore })
: null;
if (battleRunner) globalThis.__SP_RUNNER__ = battleRunner; // dev / E2E introspection
if (battleRunner) setBattleSource(() => battleRunner.currentBattle());