feedback5 R19B: 干员调配 says plainly when its game data did not load — useData counts a /data file that never arrived (404, blocked, bad JSON: 'missing') as settled, so the tab drew an empty roster, the loadout import blamed the file (「这份数据在当前版本没有可用的调配」) and the 干员持有 / 自选编队 imports stored an empty list over the saved one; now missingGameData(tab) (chess.json, plus backups.json for 自选编队) shows DataMissing in place of the roster (which files, saved settings unaffected, reload, then the ad blocker and the network) and importRefusal(kind, ready) refuses every import before anything is parsed or applied, with the same words; three UI strings with English; GitHub #173; golden: unchanged (client only)
This commit is contained in:
1 parent
7d97b80bfd
commit
b4d5a72a43
6 files changed
+181
-4
No files matched your search
@@ -82,6 +82,7 @@
|
||||
- 持续的范围特效(如炼金单元)叠在一起时不再越叠越亮(社区反馈)。
|
||||
- 结算卡不再显示调和、协防干员、独行、绝技的层数:这几个盟约照常累计层数,官方也不显示(社区反馈)。
|
||||
- 银灰的普通攻击使用自己的命中音效,不再借用三技能的挥砍和命中音效;雷蛇、初雪、圣约送葬人的普通攻击也不再用技能的命中音效(社区反馈)。
|
||||
- 游戏数据没能下载时,「干员调配」不再显示空的干员列表,导入也不再提示「这份数据在当前版本没有可用的调配」:现在会直接写明游戏数据没有载入、缺的是哪个文件,并提示刷新页面、检查广告拦截插件和网络。这时三个标签页都不执行导入,已保存的干员持有和自选编队也不会再被清空(GitHub #173,感谢 @sarilendorf)。
|
||||
|
||||
### 联机与大厅
|
||||
|
||||
|
||||
@@ -1873,3 +1873,32 @@ are.
|
||||
idea only: its fourth `alive` argument sits on top of the #189 / #191 stack and Match.js changes, so it was not merged.
|
||||
- Golden: unchanged (client text only). Tests: `test/ui/gameLogic.test.js` +1 (a player still in, an eliminated
|
||||
player, a spectator seat, the screen's call). It fails on the parent: 「资金已到账」 where 「观战中」 is expected.
|
||||
|
||||
#### 25.22.3 GitHub #173 「bug:网页版游戏文件未加载」 (@sarilendorf): a 干员调配 tab without its game data says so, and every import is refused — `screens/loadout.js missingGameData / importRefusal / DataMissing`
|
||||
|
||||
- **Seen**: on the reporter's own server (Node 24) the operators, bonds, maps and enemies did not load. Servers other
|
||||
people host worked, and a loadout exported there failed to import locally. The screenshots, as described in the
|
||||
thread: 干员调配 empty, the import toast 「导入失败:这份数据在当前版本没有可用的调配,未做任何改动」, the `npm start` log
|
||||
with every check passed, an ad blocker showing one block. Reproduced with every /data/*.json request answering 404. All five files of the screen end
|
||||
`missing`, the screen counts them as loaded, the roster is empty (「没有符合条件的干员」) and the loadout import gives the
|
||||
reporter's toast. Two imports did worse: 干员持有 said 「已导入:全部持有」 and stored [] over the saved list, and
|
||||
自选编队 said 「已导入:全部名额为空」 and stored {} over the saved picks. With the files served, the same three imports
|
||||
work. 0.1.3 and 0.1.4 behave the same.
|
||||
- **Cause**: `useData` (public/js/data.js:489–501) counts a file that never arrived (status `missing`) as settled, so
|
||||
the UI can carry on without optional files. In screens/loadout.js `ready` (:399) was then true. The tab drew its empty
|
||||
roster, and the import guard `if (!ready)` (:479) let the import run. It exists to stop an import before chess.json
|
||||
loads, because such an import would sanitise every entry away. The loadout import kept the loadout but blamed the data
|
||||
(:501). `applyOwnershipImport` / `applyDiyImport` (ui/loadoutSync.js:93, :113) stored the empty result. The server sync
|
||||
was already safe: the loadout sync sends nothing without chess.json, and the other two send the stored lists.
|
||||
- **Official**: no rule involved; the page reads its game data from the server it was opened from (/data/*.json).
|
||||
- **Now**: `missingGameData(tab)` names the files a tab cannot work without: chess.json, plus backups.json for 自选编队.
|
||||
When one is `missing`, the tab shows `DataMissing` in place of the roster: 「游戏数据没有载入」, the file paths (e.g.
|
||||
/data/chess.json), that the saved settings are unaffected, and what to try (reload; then the ad blocker and the
|
||||
network). `importRefusal(kind, ready)` is the one guard of every import. It refuses while the files load (as before) and
|
||||
when the data is missing (「导入失败:游戏数据没有载入,未做任何改动。请刷新页面;仍不行时,请检查广告拦截插件和网络」),
|
||||
before anything is parsed or applied. Three new UI strings, with English.
|
||||
- [ASSUMED]: the cause on the reporter's machine is still unknown: the ad blocker, a proxy or security software, or a
|
||||
different address. The new message and the console line `[data] /data/<file> unavailable (<reason>)` are what to ask for.
|
||||
- Golden: unchanged (client only). Tests: `test/ui/feedback5-loadout-data-missing.test.js` (4, all fail on the parent:
|
||||
the tab's files, the refusals, the screen's guard order, the message in Chinese and English); loadout, loadout-stats,
|
||||
module-trait, moduleIcons, diy-ui and standin-ui tests pass unchanged.
|
||||
@@ -73,6 +73,10 @@
|
||||
.lo-note.is-locked .icon { color: var(--amber); }
|
||||
|
||||
.lo-loading { flex: 1; display: flex; align-items: center; justify-content: center; gap: .14rem; color: var(--text-lo); font-size: max(.18rem, 12px); }
|
||||
/* the tab's game data did not load (GitHub #173): said in place of the roster */
|
||||
.lo-missing { flex-direction: column; gap: .1rem; padding: .3rem; text-align: center; color: var(--amber); }
|
||||
.lo-missing .icon { width: max(.32rem, 22px); height: max(.32rem, 22px); }
|
||||
.lo-missing p { margin: 0; max-width: 9rem; color: var(--text-lo); font-size: max(.16rem, 11px); line-height: 1.6; text-wrap: balance; }
|
||||
|
||||
/* ---- body layout -------------------------------------------------------------------------------------------------- */
|
||||
.lo-body {
|
||||
|
||||
@@ -295,6 +295,9 @@
|
||||
"读取文件失败": "Failed to read the file",
|
||||
"干员数据仍在载入,请稍候再导入": "Operator data is still loading, please wait before importing",
|
||||
"导入失败:这份数据在当前版本没有可用的调配,未做任何改动": "Import failed: this data has no loadout usable in the current version. Nothing was changed",
|
||||
"导入失败:游戏数据没有载入,未做任何改动。请刷新页面;仍不行时,请检查广告拦截插件和网络": "Import failed: the game data did not load. Nothing was changed. Reload the page; if that does not help, check your ad blocker and your network",
|
||||
"游戏数据没有载入": "The game data did not load",
|
||||
"{files} 没有下载成功,这一页无法显示;已保存的设置不受影响。请刷新页面;仍不行时,请检查广告拦截插件和网络。": "{files} could not be downloaded, so this page cannot be shown. Your saved settings are unaffected. Reload the page; if that does not help, check your ad blocker and your network.",
|
||||
"确认本局信息阶段结束前可调整本局配置": "You can adjust this match's loadout until the match briefing ends",
|
||||
"开始模拟前可调整干员携带的技能与模组,干员等级不可调整": "Before the simulation starts you can change the Skills and Modules your Operators carry; Operator levels cannot be changed",
|
||||
"返回 (Esc)": "Back (Esc)",
|
||||
|
||||
@@ -24,7 +24,7 @@ import { Img, RichText, UnitThumb } from '../ui/gameComponents.js';
|
||||
import { chessAvatarUrl, chessPortraitUrl, subProfIconUrl, bondIconUrl, moduleTypeIconUrl } from '../ui/assetUrls.js';
|
||||
import { chessStatsBlock, traitText, chessTalents } from '../ui/detailPanel.js';
|
||||
import { chessLoadout } from '../ui/gameLogic.js';
|
||||
import { data, useData, localAsset } from '../data.js';
|
||||
import { data, useData, localAsset, DATA_FILES } from '../data.js';
|
||||
import { useStore } from '../store.js';
|
||||
import { PHASE } from '../../../shared/constants.js';
|
||||
import {
|
||||
@@ -394,6 +394,43 @@ const OWN_SYNC_TEXT = { ...SYNC_TEXT, locked: [N_('下一局生效'), 'is-warn']
|
||||
/** The 自选编队 tab's status line (msgids, translated where shown): out of match, like 干员持有. */
|
||||
const DIY_SYNC_TEXT = { ...OWN_SYNC_TEXT };
|
||||
|
||||
/**
|
||||
* The game-data files a tab cannot work without — its roster and its import's sanitiser: chess.json, plus backups.json
|
||||
* for 自选编队 (its slots and the picks' forms) — that did not load. useData counts a file that never arrived (a 404, a
|
||||
* blocked request, bad JSON: status 'missing') as settled like a loaded one, so the tab drew an empty roster and an
|
||||
* import sanitised every entry away (GitHub #173). Empty while the files load and once they have.
|
||||
* @param {string} tab 'loadout' | 'ownership' | 'diy'
|
||||
* @param {(name: string) => string} [status] the data store's status (data.status)
|
||||
* @returns {string[]} data file names
|
||||
*/
|
||||
export function missingGameData(tab, status = data.status) {
|
||||
return (tab === 'diy' ? ['chess', 'backups'] : ['chess']).filter((name) => status(name) === 'missing');
|
||||
}
|
||||
|
||||
/**
|
||||
* Why an import of `kind` cannot run now, as its toast — the files still loading, or the game data missing — or null.
|
||||
* Either way nothing is applied: sanitised against absent data, the 干员持有 / 自选编队 imports stored an empty list over
|
||||
* the saved one (GitHub #173).
|
||||
* @param {string} kind the dialog's tab @param {boolean} ready the screen's files are settled
|
||||
* @param {(name: string) => string} [status]
|
||||
* @returns {{ text: string, tone: 'warn'|'error' } | null}
|
||||
*/
|
||||
export function importRefusal(kind, ready, status = data.status) {
|
||||
if (!ready) return { text: t('干员数据仍在载入,请稍候再导入'), tone: 'warn' };
|
||||
if (missingGameData(kind, status).length) return { text: t('导入失败:游戏数据没有载入,未做任何改动。请刷新页面;仍不行时,请检查广告拦截插件和网络'), tone: 'error' };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 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`}`);
|
||||
return html`<div class="lo-loading lo-missing" role="alert" data-testid="loadout-data-missing">
|
||||
<${Icon} name="warn" />
|
||||
<b>${t('游戏数据没有载入')}</b>
|
||||
<p>${t('{files} 没有下载成功,这一页无法显示;已保存的设置不受影响。请刷新页面;仍不行时,请检查广告拦截插件和网络。', { files: urls })}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** The overlay screen. */
|
||||
function LoadoutScreen({ st }) {
|
||||
const ready = useData('chess', 'bonds', 'assets', 'local', 'backups');
|
||||
@@ -422,6 +459,7 @@ function LoadoutScreen({ st }) {
|
||||
const [io, setIo] = useState(null); // 导出 / 导入 dialog: { mode, text } | null
|
||||
|
||||
const tab = st.tab === 'ownership' || st.tab === 'diy' ? st.tab : 'loadout';
|
||||
const lost = ready ? missingGameData(tab) : [];
|
||||
const ownRoster = useOwnershipRoster(ready);
|
||||
const nNotOwned = notOwnedCount(st.notOwned, ownRoster);
|
||||
// 自选编队 (0.2.0 DIY): the stored picks, those the server would keep (the kit list of the last welcome)
|
||||
@@ -475,8 +513,10 @@ function LoadoutScreen({ st }) {
|
||||
try { setIo({ ...io, mode: 'import', text: await readFileText(f) }); } catch { toast(t('读取文件失败'), 'error'); }
|
||||
};
|
||||
const ioApply = () => {
|
||||
// an import before chess.json is loaded would sanitise every entry away — refuse instead of wiping the loadout
|
||||
if (!ready) { toast(t('干员数据仍在载入,请稍候再导入'), 'warn'); return; }
|
||||
// an import before chess.json is loaded, or when it never arrived, would sanitise every entry away — refuse instead
|
||||
// of wiping the loadout
|
||||
const refused = importRefusal(io.kind, ready);
|
||||
if (refused) { toast(refused.text, refused.tone); return; }
|
||||
if (ioDiy) {
|
||||
const r = parseDiyImport(ioText);
|
||||
if (!r.ok) { toast(t('导入失败:{error}', { error: t(r.error, r.params) }), 'error'); return; }
|
||||
@@ -588,7 +628,8 @@ function LoadoutScreen({ st }) {
|
||||
: tab === 'ownership'
|
||||
? html`<p class=${cx('lo-note', ownLocked && 'is-locked')}><${Icon} name="info" />${ownText}</p>`
|
||||
: html`<p class=${cx('lo-note', locked && 'is-locked')}><${Icon} name="info" />${locked ? t('本局的调配已锁定(确认本局信息后无法修改),修改将在下一局生效') : fromText}</p>`}
|
||||
${!ready ? html`<div class="lo-loading"><${Spinner} size="sm" />${t('正在载入干员数据(打开页面后仅载入一次)…')}</div>` : tab === 'diy'
|
||||
${!ready ? html`<div class="lo-loading"><${Spinner} size="sm" />${t('正在载入干员数据(打开页面后仅载入一次)…')}</div>`
|
||||
: lost.length ? html`<${DataMissing} files=${lost} />` : tab === 'diy'
|
||||
? html`<${DiyPanel} m=${m} picks=${st.diy || {}} legal=${diyLegal} kitted=${st.diyKitted} onSet=${setDiySlot} />`
|
||||
: tab === 'ownership'
|
||||
? html`<${OwnershipPanel} m=${m} roster=${ownRoster} notOwned=${st.notOwned} onToggle=${toggleOwned} />`
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// GitHub #173 「网页版游戏文件未加载」: the page runs but its game data never arrives (every /data/*.json request fails —
|
||||
// a blocked request, a proxy, a server without its data folder). useData counts a file that failed ('missing') as
|
||||
// settled like a loaded one, so 干员调配 drew an empty roster (「没有符合条件的干员」) and the 导入 of a loadout said the data
|
||||
// had no loadout usable in this version; the 干员持有 / 自选编队 imports, sanitised against the absent data, stored an
|
||||
// empty list over the saved one. Now the tab says that the game data did not load (which files, what to try) and every
|
||||
// import is refused with the same words, before anything is parsed or applied — the saved settings stay as they are.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
// the reporter's case: every data request of the page fails (the screen reads the browser data store singleton)
|
||||
globalThis.fetch = async () => ({ ok: false, status: 404, json: async () => ({}) });
|
||||
const warn = console.warn;
|
||||
console.warn = (...a) => { if (!String(a[0]).startsWith('[data]')) warn(...a); };
|
||||
|
||||
const { data } = await import('../../public/js/data.js');
|
||||
const { missingGameData, importRefusal, DataMissing } = await import('../../public/js/screens/loadout.js');
|
||||
const { setLang, setMessages } = await import('../../shared/i18n.js');
|
||||
|
||||
const TABS = ['loadout', 'ownership', 'diy'];
|
||||
const statusOf = (map, dflt = 'ready') => (name) => map[name] ?? dflt;
|
||||
const NOT_LOADED = '导入失败:游戏数据没有载入,未做任何改动。请刷新页面;仍不行时,请检查广告拦截插件和网络';
|
||||
const LOADING = '干员数据仍在载入,请稍候再导入';
|
||||
|
||||
function* walk(v) {
|
||||
if (Array.isArray(v)) { for (const x of v) yield* walk(x); return; }
|
||||
if (!v || typeof v !== 'object') return;
|
||||
yield v;
|
||||
yield* walk(v.props?.children);
|
||||
}
|
||||
const textOf = (v) => [...walk(v)].flatMap((n) => (Array.isArray(n.props?.children) ? n.props.children : [n.props?.children]))
|
||||
.filter((x) => typeof x === 'string').map((s) => s.trim()).filter(Boolean).join(' | ');
|
||||
|
||||
test('a data file that never arrived is settled like a loaded one: the tab names the files it cannot work without', async () => {
|
||||
await data.loadAll('chess', 'bonds', 'assets', 'local', 'backups'); // the screen's useData(...) files
|
||||
for (const name of ['chess', 'bonds', 'assets', 'local', 'backups']) assert.equal(data.status(name), 'missing', name);
|
||||
// (default: the browser store) chess.json for every tab, backups.json too for 自选编队 (its slots and picks' forms)
|
||||
assert.deepEqual(missingGameData('loadout'), ['chess']);
|
||||
assert.deepEqual(missingGameData('ownership'), ['chess']);
|
||||
assert.deepEqual(missingGameData('diy'), ['chess', 'backups']);
|
||||
for (const tab of TABS) {
|
||||
assert.deepEqual(missingGameData(tab, statusOf({})), [], `${tab}: loaded`);
|
||||
assert.deepEqual(missingGameData(tab, statusOf({}, 'loading')), [], `${tab}: still loading (the spinner, not this message)`);
|
||||
}
|
||||
// only the art / bond files missing: every tab still works
|
||||
for (const tab of TABS) assert.deepEqual(missingGameData(tab, statusOf({ bonds: 'missing', assets: 'missing', local: 'missing' })), []);
|
||||
const noBackups = statusOf({ backups: 'missing' });
|
||||
assert.deepEqual(TABS.map((tab) => missingGameData(tab, noBackups)), [[], [], ['backups']]);
|
||||
});
|
||||
|
||||
test('every import is refused while the files load or when the game data did not load — no other reason', () => {
|
||||
const missing = statusOf({ chess: 'missing', backups: 'missing' });
|
||||
for (const kind of TABS) {
|
||||
assert.deepEqual(importRefusal(kind, false, statusOf({})), { text: LOADING, tone: 'warn' }, `${kind}: loading`);
|
||||
assert.deepEqual(importRefusal(kind, true, missing), { text: NOT_LOADED, tone: 'error' }, `${kind}: not loaded`);
|
||||
assert.deepEqual(importRefusal(kind, true), { text: NOT_LOADED, tone: 'error' }, `${kind}: the browser store`);
|
||||
assert.equal(importRefusal(kind, true, statusOf({})), null, `${kind}: loaded`);
|
||||
}
|
||||
const noBackups = statusOf({ backups: 'missing' });
|
||||
assert.equal(importRefusal('loadout', true, noBackups), null);
|
||||
assert.equal(importRefusal('ownership', true, noBackups), null);
|
||||
assert.deepEqual(importRefusal('diy', true, noBackups), { text: NOT_LOADED, tone: 'error' });
|
||||
});
|
||||
|
||||
test('the screen: the import asks importRefusal before parsing or applying anything; a tab without its data shows DataMissing', () => {
|
||||
const src = readFileSync(path.join(ROOT, 'public/js/screens/loadout.js'), 'utf8');
|
||||
const at = src.indexOf('const ioApply = () => {');
|
||||
assert.ok(at > 0, 'ioApply');
|
||||
const body = src.slice(at, src.indexOf('\n };\n', at));
|
||||
const guard = body.indexOf('const refused = importRefusal(io.kind, ready);');
|
||||
assert.ok(guard > 0, 'the guard');
|
||||
assert.match(body.slice(guard), /^const refused = importRefusal\(io\.kind, ready\);\s*if \(refused\) \{ toast\(refused\.text, refused\.tone\); return; \}/);
|
||||
for (const call of ['parseDiyImport(', 'applyDiyImport(', 'parseOwnershipImport(', 'applyOwnershipImport(', 'parseImport(', 'applyLoadoutEntries(']) {
|
||||
assert.ok(body.indexOf(call) > guard, `${call} only after the guard`);
|
||||
}
|
||||
assert.ok(!/if \(!ready\)/.test(body), 'one guard for both cases');
|
||||
assert.match(src, /const lost = ready \? missingGameData\(tab\) : \[\];/);
|
||||
assert.match(src, /: lost\.length \? html`<\$\{DataMissing\} files=\$\{lost\} \/>` : tab === 'diy'/);
|
||||
});
|
||||
|
||||
test('the message names the files and what to try, in Chinese and in English', () => {
|
||||
const one = DataMissing({ files: ['chess'] });
|
||||
assert.equal(one.props.role, 'alert');
|
||||
assert.equal(textOf(one), '游戏数据没有载入 | /data/chess.json 没有下载成功,这一页无法显示;已保存的设置不受影响。请刷新页面;仍不行时,请检查广告拦截插件和网络。');
|
||||
assert.match(textOf(DataMissing({ files: ['chess', 'backups'] })), /\/data\/chess\.json、\/data\/backups\.json 没有下载成功/);
|
||||
const en = JSON.parse(readFileSync(path.join(ROOT, 'public/i18n/en.json'), 'utf8'));
|
||||
setMessages('en', en);
|
||||
setLang('en');
|
||||
try {
|
||||
assert.equal(textOf(DataMissing({ files: ['chess', 'backups'] })), 'The game data did not load | /data/chess.json, /data/backups.json could not be downloaded, '
|
||||
+ 'so this page cannot be shown. Your saved settings are unaffected. Reload the page; if that does not help, check your ad blocker and your network.');
|
||||
assert.equal(importRefusal('loadout', true).text, 'Import failed: the game data did not load. Nothing was changed. Reload the page; if that does not help, check your ad blocker and your network');
|
||||
} finally {
|
||||
setLang('zh');
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user