0.2.6-dev: merge claude/bw-l01 — host bandwidth layers 0 and 1 (no BGM download while the music is silent, WebSocket permessage-deflate, lossless WebP 玩法说明 pages, traffic counters, content-hashed asset URLs with immutable caching, brotli q5) on top of the 0.2.5 release; its note becomes DESIGN §31 in docs/history/0.2.6.md, CHANGELOG 0.2.6-dev, version 0.2.6-dev; golden: unchanged
This commit is contained in:
commit
3d5d6b380d
47 files changed
+2325
-136
No files matched your search
@@ -77,7 +77,7 @@ so old imports keep working: `public/js/ui/gameLogic.js` (`public/js/ui/gameLogi
|
||||
| path | what |
|
||||
|---|---|
|
||||
| `server/index.js` | the process entry (`npm start`); `startServer()` wires `server/http/` |
|
||||
| `server/http/` | `config.js` (environment), `websocket.js` (sessions, `/ws`), `static.js` (the mounts), `media.js`, `files.js` (MIME, gzip, ETag, ranges), `buildTag.js`, `routes.js` (`/healthz`), `common.js`, `boot.js` (a pending update package first, banner, shutdown) |
|
||||
| `server/http/` | `config.js` (environment), `websocket.js` (sessions, `/ws`, permessage-deflate), `static.js` (the mounts, the `~<hash>` asset routes), `media.js`, `files.js` (MIME, brotli / gzip, ETag, ranges), `buildTag.js`, `routes.js` (`/healthz`), `traffic.js` (bytes sent by category and on the WebSocket, `/healthz?traffic=1`), `assetIndex.js` (the content hashes of `public/assets/` and `public/fonts/`, cached in `.cache/asset-hashes.json`), `assetViews.js` (the served `/data/assets.json` and `/data/local-assets.json`: every art URL with its hash segment; ASSETS.md), `common.js`, `boot.js` (a pending update package first, banner, shutdown) |
|
||||
| `server/net.js` | sessions and reconnect tokens, rate limits, message validation |
|
||||
| `server/lobby.js` | rooms, seats, AI seats, spectators; starts a `Match` |
|
||||
| `server/data.js` | loads `data/*.json` once (frozen) |
|
||||
@@ -131,7 +131,8 @@ data/*.json ──────────┬─▶ server/data.js ────
|
||||
├─▶ public/js/data.js ──────────▶ the UI
|
||||
└─▶ public/js/battle/runner.js ─▶ the browser's battles (setSimData)
|
||||
data/i18n/en.json ──────▶ public/js/data.js ──────────▶ the English game texts
|
||||
public/assets/ ─────────▶ public/js/render/, public/js/audio.js (URLs listed in data/assets.json)
|
||||
public/assets/ ─────────▶ public/js/render/, public/js/audio.js (URLs listed in data/assets.json,
|
||||
served with content-hash segments: server/http/assetViews.js)
|
||||
```
|
||||
|
||||
- **Game data.** `tools/build-data.mjs` downloads the official zh_CN tables into `.cache/gamedata/` (or reads them
|
||||
|
||||
+35
-2
@@ -211,11 +211,11 @@ The `stem` of a Spine model is the upstream file name. Two examples: `char_107_l
|
||||
- **Fonts:** OTF/TTF files are converted to WOFF2 by a built-in encoder (`tools/assets/woff2.mjs`: Brotli with null transforms).
|
||||
- Its output was verified lossless against Google's reference `woff2` decoder.
|
||||
- `fonts.css` lists WOFF2 first and falls back to the original file.
|
||||
- Images stay PNG. WebP conversion is not done: it would need a native dependency. The local-client board textures are the exception: `tools/local-extract/extract.py` (Python, where Pillow is already a dependency) writes WebP copies of the 12 textures the 3D board downloads (`WEBP`: colour maps lossy at quality 95 with the alpha and the RGB under transparent texels kept, normal and data maps lossless) and `data/local-assets.json` lists the copies; the PNGs stay beside them for `tools/crop-board-atlas.mjs`. `extract.py --webp` adds the copies to an existing extraction without the client.
|
||||
- Images stay PNG. WebP conversion is not done: it would need a native dependency. The local-client board textures are the exception: `tools/local-extract/extract.py` (Python, where Pillow is already a dependency) writes WebP copies of the 12 textures the 3D board downloads (`WEBP`: colour maps lossy at quality 95 with the alpha and the RGB under transparent texels kept, normal and data maps lossless) and `data/local-assets.json` lists the copies; the PNGs stay beside them for `tools/crop-board-atlas.mjs`. The 19 local 玩法说明 pages get a WebP copy too (`GUIDE_WEBP_MODE`, lossless: 25.6 MB → 17.4 MB on the CN client's pages; lossy quality 90 would be 4.2 MB and waits for the maintainer's side-by-side look), listed in place of the PNG, which stays beside it; the mirror copies under `ui/guide/` stay PNG. `extract.py --webp` adds the copies to an existing extraction without the client.
|
||||
|
||||
## Manifest schema (`data/assets.json`)
|
||||
|
||||
All paths are URL paths relative to the site root, for example `/assets/char/avatar/char_002_amiya.png`. **Only entries whose files exist on disk are emitted.** When a key is missing, the client should use its fallback (research 07 §5.6).
|
||||
All paths are URL paths relative to the site root, for example `/assets/char/avatar/char_002_amiya.png` — in the committed file; a server hands them out with a content-hash segment ("The committed manifest and the served manifests" below). **Only entries whose files exist on disk are emitted.** When a key is missing, the client should use its fallback (research 07 §5.6).
|
||||
|
||||
```js
|
||||
{
|
||||
@@ -470,6 +470,39 @@ Other renderer rules from research 07 §5.4–5.5:
|
||||
- **Missing unit SFX:** use `audio.sfx.battle.enemyHit` or a WebAudio blip (research 07 §6.4).
|
||||
- **Bond icons:** if the real glyph ever fails, the stored file is the nation camp logo.
|
||||
|
||||
## The committed manifest and the served manifests
|
||||
|
||||
`data/assets.json` in git (and as `tools/fetch-assets.mjs` writes it) and `data/local-assets.json` (as
|
||||
`tools/local-extract` writes it) hold plain URLs. Since 0.2.6 (host bandwidth, layer 1) the server hands out a view of
|
||||
each (`server/http/assetIndex.js`, `server/http/assetViews.js`):
|
||||
|
||||
- Shortly after the start (1 s) the server hashes every file under `public/assets/` and `public/fonts/` in the
|
||||
background: SHA-256, the first 16 hex digits. The hashes are kept in `.cache/asset-hashes.json` by path + size +
|
||||
mtime, so a restart only stats the files and reads none of the unchanged ones (12,691 files / 724 MB on the
|
||||
maintainer's machine: 1.3 s for the first build, 0.25 s after that). A file whose size or mtime changed is hashed
|
||||
again; a re-extraction, `setup --force` or a Docker rebuild then costs the players only the files whose bytes changed.
|
||||
- `GET /data/assets.json` and `GET /data/local-assets.json` answer with every art URL carrying its file's hash as the
|
||||
first segment: `/assets/char/avatar/char_003_kalts.png` → `/assets/~398f32e2ff78d9e6/char/avatar/char_003_kalts.png`,
|
||||
`/fonts/~<hash>/…`. Audio keeps its `/assets/~<hash>/audio/….mp3` form; `public/js/media.js` asks for it as
|
||||
`/media/~<hash>/…` (the download-manager route, `shared/media.js`).
|
||||
- A Spine model's skeleton, atlas and pages carry one hash, the model's (SHA-256 of the files' hashes): pixi-spine finds
|
||||
the atlas by the skeleton's name and the pages next to the atlas, so the URLs share a folder. In `data/assets.json`
|
||||
every `{ skel, atlas, textures }` object is a model; in `data/local-assets.json` every `spine/…` group (the files a
|
||||
`spineLocal` record names).
|
||||
- The server strips the segment and serves the file at the plain path. When the segment names the file's current bytes
|
||||
(its own hash, or its model's) the answer is `Cache-Control: public, max-age=31536000, immutable`; any other segment —
|
||||
a page loaded before an update, a URL derived from another file's — gets the current file with `no-cache`, so an old
|
||||
page keeps working. The ETag of every indexed file is its content hash (`"398f32e2ff78d9e6"`, plus `-br` / `-gz` when
|
||||
compressed), on the plain URL too.
|
||||
- A path segment, never `?v=`: `assets.js validSpine` matches `/\.skel$/`, `audio.js SKILL_MODE_FILE` the `.mp3` ending,
|
||||
pixi-spine takes the atlas name from the skeleton's basename and resolves the pages against the atlas's folder — a
|
||||
query string survives none of that.
|
||||
- A URL the index has no hash for (the file is missing, or not hashed yet) stays plain; plain URLs work as before
|
||||
(`public, max-age=86400`, revalidated after a day).
|
||||
- Until the index is ready, with `SP_ASSET_INDEX=0`, or on a static host without this server, the manifests go out as
|
||||
committed. The views are rebuilt in memory when a manifest or the hashes change; neither file is ever written by the
|
||||
server, and the `hash` field is the committed one. The index lives in `.cache/` only and is never packaged.
|
||||
|
||||
## Verification
|
||||
|
||||
`node --test test/assets.test.js` covers the pure helpers: the resolver, the atlas normalizer, the format sniffers, WOFF2, audio banks, the plan id sets, the downloader against a fake network, and the self-heal of corrupt skeletons.
|
||||
|
||||
+12
-2
@@ -9,7 +9,7 @@
|
||||
|---|---|
|
||||
| 服务器 CPU | 战斗在各玩家浏览器里模拟(DESIGN §14),服务器只负责回合、经济和校验:**每个房间每个作战回合约 1 ms CPU**。AI 队友 / 掉线玩家的战场由服务器模拟:作战开始时 3 个 AI 战场在开发机上约 0.2–0.5 s CPU,小主机上可能要几秒(分成 8 ms 小片执行,不会卡住其他房间)。`SP_VERIFY=all` 会复算每个真人战场,CPU 明显增加,小主机建议保持 `off` 或 `sample`。 |
|
||||
| 服务器内存 | 空闲约 100 MB,每个进行中的对局再增加几 MB。 |
|
||||
| 网络 | 4 人对局中服务器每回合下行约 0.25 MB(DESIGN §14 实测)。首次进入游戏时浏览器要从主机下载所需的图片 / Spine 模型 / 音频(按需加载,之后走浏览器缓存),公网隧道带宽小时第一次会慢一些。 |
|
||||
| 网络 | 4 人对局中服务器每回合下行约 0.25 MB(DESIGN §14 实测;0.2.6 起 1 KB 以上的 WebSocket 消息用 permessage-deflate 压缩,实测一局单人对局的服务器消息从 173 KB 降到 60 KB,约为原来的三分之一)。首次进入游戏时浏览器要从主机下载所需的图片 / Spine 模型 / 音频(按需加载,之后走浏览器缓存),公网隧道带宽小时第一次会慢一些。0.2.6 起服务器启动后在后台给素材算内容哈希(第一次约 1–20 秒,看机器和硬盘;之后只检查大小和修改时间),下发的素材清单里每个地址带上哈希:浏览器把图片、模型和音频缓存一年,服务器更新、重新解压或 Docker 重建之后,玩家只重新下载内容真的变了的文件。哈希存在 `.cache/asset-hashes.json`,删掉会重新计算;目录不可写(例如只读的容器)时只在内存里算。 |
|
||||
| 磁盘 | 素材约 550 MB(`public/assets`,含中文约 65 MB、日文约 85 MB 两套干员语音)+ 依赖约 125 MB(`node_modules`;整合包只带运行依赖,约 65 MB);可选的本地提取约 40 MB(`.venv-extract`)+ 70 MB 贴图(见第 6 节)。完整包解压后约 710 MB。 |
|
||||
| 玩家设备 | 支持 WebGL 的现代浏览器(Chrome / Edge / Firefox / Safari 最新版),电脑或手机平板(横屏)。老旧设备可在设置里调低画质或访问 `/?board=2d`。 |
|
||||
|
||||
@@ -170,6 +170,8 @@ cloudflared tunnel --url http://localhost:3000
|
||||
|
||||
把输出的 `https://xxxx.trycloudflare.com` 发给朋友。页面是 https 时客户端自动改用 `wss://`,不需要任何配置;服务器会通过隧道转发的 `CF-Connecting-IP` 识别真实来源(`TRUST_PROXY=auto`)。临时隧道每次启动地址都不同,且没有可用性保证;需要固定地址请使用 Cloudflare 账号 + 自己域名的「命名隧道」。
|
||||
|
||||
**固定访问地址能省很多流量**:浏览器的缓存、设置、统计和断线令牌都按地址(域名或 IP + 端口)分开保存。地址一换,对玩家来说就是一个新网站:每个人进游戏都要把图片、模型和音频重新下载一遍(第一局约 20–50 MB),设置和本机统计也看不到了。临时隧道每次重启都换域名,所以固定朋友圈子长期开服时,建议用命名隧道、Tailscale(2.1)或固定的公网 IP / 局域网 IP(1.3),让大家一直用同一个地址。
|
||||
|
||||
### 2.3 路由器端口转发
|
||||
|
||||
仅当你有**公网 IPv4**(很多宽带是运营商级 NAT,没有公网 IP,此时请用 2.1 / 2.2):
|
||||
@@ -219,7 +221,11 @@ server {
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP/2**:客户端由数百个小脚本模块组成(0.2.0 起进入对局时约 340 个),隔着公网时建议让代理以 HTTP/2 提供页面:所有模块走同一条连接,远距离玩家首次进入对局明显更快。Caddy 默认就是 HTTP/2;Nginx 见上面的 `http2`。服务器本身只说 HTTP/1.1,局域网或本机游玩不受影响。
|
||||
**HTTP/2**:客户端由数百个小脚本模块组成(0.2.0 起进入对局时约 340 个),隔着公网时建议让代理以 HTTP/2 提供页面:所有模块走同一条连接,远距离玩家首次进入对局明显更快。Caddy 默认就是 HTTP/2;Nginx 见上面的 `http2`。服务器本身只说 HTTP/1.1,局域网或本机游玩不受影响。HTTP/2 省的是往返等待,不减少字节数。
|
||||
|
||||
**压缩**:服务器自己压缩脚本、样式、数据和 Spine 的 `.skel` / `.atlas`:浏览器支持 brotli 时用 brotli(0.2.6 起,质量 5,比 gzip 快且更小:实测 `.skel` 小约 27%、游戏数据 JSON 小约 26%、前端库小约 14%、脚本小约 5%),否则用 gzip;图片和音频本来就是压缩格式,原样发送。浏览器只在 HTTPS 页面(以及 `localhost`)上声明支持 brotli,所以通过代理或隧道的 https 地址访问时用 brotli,局域网里的 `http://192.168.x.x:3000` 这类地址用 gzip。代理不需要另开压缩(Caddy 的 `encode`、Nginx 的 `gzip` 遇到已经压缩的响应会跳过)。
|
||||
|
||||
**Cloudflare 缓存**(命名隧道或自己的域名经过 Cloudflare 时):Cloudflare 默认只缓存常见扩展名,`.skel`、`.atlas` 和没有扩展名的音频地址 `/media/…` 都不在其中,每个请求都会回到开服的电脑。在 Cloudflare 控制台的「缓存规则」里加一条:主机名是你的游戏域名、且 URI 路径以 `/assets/`、`/fonts/` 或 `/media/` 开头时「符合缓存条件」,边缘 TTL 选「使用源站的 Cache-Control」。0.2.6 起素材清单里的地址带内容哈希(`/assets/~<哈希>/…`),服务器给这类地址一年的 `immutable`,换了内容的文件会换地址,所以长期缓存是安全的;`/data/`、页面和脚本不要加进规则(它们要随服务器更新)。注意:边缘公开缓存官方素材会增加被投诉的风险,大量媒体文件也要留意 Cloudflare 的服务条款;只给自己人玩时不加这条规则也可以。
|
||||
|
||||
https / wss 说明:页面通过 https 打开时客户端自动连接 `wss://同一域名/ws`;http 时用 `ws://`。服务器本身只提供 http,证书由代理 / 隧道负责。代理与服务器在同一台机器或内网时,`TRUST_PROXY=auto` 会信任它的 `X-Forwarded-For` / `X-Real-IP`;代理在公网另一台机器上时设 `TRUST_PROXY=1`(同时确保游戏端口只对代理开放)。
|
||||
|
||||
@@ -289,6 +295,8 @@ services:
|
||||
| 表情显示成默认图标、「玩法说明」只有文字要点 | 素材没下载完整:重新运行 `node tools/setup.mjs`(表情和教程图随其他素材一起从公开镜像下载,不需要客户端);缺失明细在 `.cache/assets-report.json` |
|
||||
| 本地提取失败 | 游戏照常运行,只是第 6 节表格里的几样换成替代样式。确认客户端已下载全部资源;Python 版本太新导致依赖安装失败时,安装 Python 3.12 后删除 `.venv-extract` 再运行 `node tools/setup.mjs --local` |
|
||||
| 3D 棋盘没出现 | 需要本地提取的棋盘贴图(`node tools/doctor.mjs` 会显示「3D 棋盘可用」),以及支持 WebGL2 的浏览器。没有客户端的服务器可以从同一版本的整合包复制本地素材(第 6 节) |
|
||||
| 素材地址里的 `~` 加一串字母数字 | `/assets/~<哈希>/…` 是 0.2.6 起的带版本地址(见第 0 节「网络」),浏览器可以长期缓存,属于正常现象。怀疑它导致问题时,可以用 `SP_ASSET_INDEX=0` 启动服务器临时关闭(清单改回普通地址),并请在 issue 里说明 |
|
||||
| 想知道开服电脑上传了多少流量 | 浏览器打开 `http://localhost:3000/healthz?traffic=1`(换成你的端口):`traffic.http` 是服务器启动以来各类内容(`assets` 图片和模型、`audio` 音频、`code` 页面和脚本、`data` 游戏数据和素材清单)发出的字节数和请求数(压缩后的实际字节,`notModified` 是只回了「没变」的 304),`traffic.ws` 是 WebSocket 的线上字节(`wireOut` / `wireIn`,压缩后)和压缩前的消息字节(`payloadOut` / `payloadIn`)。数字只在内存里,重启后从 0 开始。玩家那边:设置 →「问题反馈」复制的诊断里有一行 `traffic:`,是这个页面从打开起下载的字节数(`cached` 是浏览器缓存直接给的,不占流量;`external` 是其他网站,例如 Google 字体) |
|
||||
| 断线 | 同盟模拟 10 分钟内、独立模拟 24 小时内(`config.constants.singleReconnectTime`)用同一浏览器重新打开页面,自动回到原座位。同盟掉线期间按原阵容自动作战、到时自动准备(不会代为购买;想让 AI 代打请用「离开模拟 → 暂离(AI 托管)」);独立模拟不计时,等你回来 |
|
||||
|
||||
## 6. 本地客户端素材(可选)
|
||||
@@ -315,6 +323,8 @@ services:
|
||||
|
||||
**3D 棋盘贴图的下载量**:每位玩家进入对局时都要从开服的电脑下载 3D 棋盘的 12 张贴图。提取时会给这 12 张各写一份 WebP(颜色贴图有损、质量 95,法线和数据贴图无损),清单里列的是 WebP,同名 PNG 留在旁边给裁切工具和 setup 用。这部分下载量从约 6.7 MB 降到约 2 MB,网速慢的远程联机最明显。只有 PNG 的本地素材(例如在这一改动之前提取的)可以用提取时的 Python 环境运行 `tools/local-extract/extract.py --webp` 就地补上,只需要 Pillow,不需要客户端。
|
||||
|
||||
**「玩法说明」教程图的下载量**(0.2.6 起):本地提取的 19 页教程图每页 1.2–1.5 MB,玩家每翻一页都要从开服的电脑下载。提取时同样给每页写一份无损 WebP,清单列 WebP,PNG 留在旁边;19 页合计从约 25.6 MB 降到约 17.4 MB,画面完全不变。看说明时只预载下一页(以前前后两页都预载)。已经提取过的同样用 `tools/local-extract/extract.py --webp` 补上。
|
||||
|
||||
## 7. 打包发布(维护者)
|
||||
|
||||
Releases 的 zip(完整包、精简包,0.2.1 起还有更新包)由 `tools/package.mjs` 生成,在**源码仓库**里运行(整合包里没有这个工具):
|
||||
|
||||
+2
-1
@@ -4,7 +4,7 @@ This is the **single source of truth** for every implementer. Research lives in
|
||||
|
||||
Language: player-facing text is **Simplified Chinese** by default, with an English switch since 0.2.0 (§25.2, docs/I18N.md: UI strings through `t('中文')`, game texts from the official Chinese data or the official EN client's). Code, comments and identifiers are English.
|
||||
|
||||
Versions: the first public release was **0.1.0** (2026-10-02, the state of §0–§20.15); the releases after it add the player feedback and GitHub reports — 0.1.1 §21, 0.1.2 §22, 0.1.3 §23, 0.1.4 §24 — and **0.2.0** (2026-10-07; `CHANGELOG.md`) the maintainability refactor, two languages, 补位, 自选编队 and their fidelity work — §25 — **0.2.1** (2026-10-07) the 联防 battlefield restored, full potential and the GitHub fixes after it — §26 — and **0.2.2** (2026-10-09) per-operator 潜能 / 练度, Japanese voices, the statistics page, 失衡, whole-frame attack timing and the GitHub fixes after 0.2.1 — §27. **0.2.3** (2026-10-10; `package.json`, `shared/constants.js APP_VERSION`) 克莱门莎 and new modules, community fixes, text size, per-operator voices, installation, the opening reroll and local match recovery — §28. **0.2.4** (2026-10-11) the official shared card pool and item stock, Raid revival and #475, and the community fixes after 0.2.3 — §29. **0.2.5** (2026-10-12) fixed gifts and already displayed operator offers under the shared stock, random boss pairing, the official battlefield scenes on the 3D board (optional local art: the full bundle includes them when extracted, a missing scene keeps the metal board; §15, §30.21), the official damage-number option, the 驭械术师 / hit-tile / bot ports and the community fixes after 0.2.4 — §30, the state described by this document; its rules supersede the corresponding 0.2.4 ones. The labels v1 / v2 / v2.1–v2.5.2 in §0, §14–§20 and in the BALANCE / SIM comparisons name the design generations and the private playtest builds that came before it; they are kept as history.
|
||||
Versions: the first public release was **0.1.0** (2026-10-02, the state of §0–§20.15); the releases after it add the player feedback and GitHub reports — 0.1.1 §21, 0.1.2 §22, 0.1.3 §23, 0.1.4 §24 — and **0.2.0** (2026-10-07; `CHANGELOG.md`) the maintainability refactor, two languages, 补位, 自选编队 and their fidelity work — §25 — **0.2.1** (2026-10-07) the 联防 battlefield restored, full potential and the GitHub fixes after it — §26 — and **0.2.2** (2026-10-09) per-operator 潜能 / 练度, Japanese voices, the statistics page, 失衡, whole-frame attack timing and the GitHub fixes after 0.2.1 — §27. **0.2.3** (2026-10-10; `package.json`, `shared/constants.js APP_VERSION`) 克莱门莎 and new modules, community fixes, text size, per-operator voices, installation, the opening reroll and local match recovery — §28. **0.2.4** (2026-10-11) the official shared card pool and item stock, Raid revival and #475, and the community fixes after 0.2.3 — §29. **0.2.5** (2026-10-12) fixed gifts and already displayed operator offers under the shared stock, random boss pairing, the official battlefield scenes on the 3D board (optional local art: the full bundle includes them when extracted, a missing scene keeps the metal board; §15, §30.21), the official damage-number option, the 驭械术师 / hit-tile / bot ports and the community fixes after 0.2.4 — §30; its rules supersede the corresponding 0.2.4 ones. **0.2.6-dev** adds the host-bandwidth layers 0 and 1 (no BGM download while the music is silent, WebSocket compression, content-hashed asset URLs with long caching, brotli) — §31, the current development state described by this document. The labels v1 / v2 / v2.1–v2.5.2 in §0, §14–§20 and in the BALANCE / SIM comparisons name the design generations and the private playtest builds that came before it; they are kept as history.
|
||||
|
||||
---
|
||||
|
||||
@@ -37,3 +37,4 @@ normative lines it rewrote).
|
||||
| §28 | [history/0.2.3.md](history/0.2.3.md) | 0.2.3 — after the 0.2.2 release (2026-10-10) |
|
||||
| §29 | [history/0.2.4.md](history/0.2.4.md) | 0.2.4 — after the 0.2.3 release (2026-10-11) |
|
||||
| §30 | [history/0.2.5.md](history/0.2.5.md) | 0.2.5 — after the 0.2.4 release (2026-10-12) |
|
||||
| §31 | [history/0.2.6.md](history/0.2.6.md) | 0.2.6-dev — after the 0.2.5 release |
|
||||
+1
-1
@@ -184,7 +184,7 @@
|
||||
|
||||
标题画面页脚的「项目仓库」在新标签页打开本项目的 [GitHub 仓库](https://github.com/sganggs/Stronghold-Protocol)。
|
||||
|
||||
遇到报错、卡住或结果不对时,打开「设置」→「问题反馈」,点「复制诊断信息」,再到 [GitHub issue](https://github.com/sganggs/Stronghold-Protocol/issues) 里描述发生了什么并粘贴进去。诊断信息包括游戏版本、浏览器和设备、当前所在的阶段与回合,以及打开页面以来记录到的错误(最多保留最近 40 条)。战斗中复制时,默认附上这场战斗的数据,开发者可以用它重现这场战斗;不想附上可以关掉「附上本场战斗」。诊断信息只在你的浏览器里生成,不会自动上传;你和其他玩家的名字、房间号会被替换成 `<me>`、`<player2>`、`<room>` 这样的占位符。剪贴板不可用时(部分 App 内置浏览器),内容会显示在下方的文本框里,手动全选复制即可。
|
||||
遇到报错、卡住或结果不对时,打开「设置」→「问题反馈」,点「复制诊断信息」,再到 [GitHub issue](https://github.com/sganggs/Stronghold-Protocol/issues) 里描述发生了什么并粘贴进去。诊断信息包括游戏版本、浏览器和设备、当前所在的阶段与回合、打开页面以来按类别统计的下载量(图片和模型、音频、代码、数据),以及打开页面以来记录到的错误(最多保留最近 40 条)。战斗中复制时,默认附上这场战斗的数据,开发者可以用它重现这场战斗;不想附上可以关掉「附上本场战斗」。诊断信息只在你的浏览器里生成,不会自动上传;你和其他玩家的名字、房间号会被替换成 `<me>`、`<player2>`、`<room>` 这样的占位符。剪贴板不可用时(部分 App 内置浏览器),内容会显示在下方的文本框里,手动全选复制即可。
|
||||
|
||||
## 13. 统计(本机)
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ view.resize() ; view.destroy()
|
||||
|
||||
`createFieldView(host, { …, signal })` takes an `AbortSignal` (0.2.2, GitHub PR #302 by @siruimei07): an abort during the build — the Pixi load, the manifest, the fonts, the board art and the 3D board waits — destroys what the attempt made (the canvas, the ticker, the listeners, the observers) and rejects, a failure while it is built releases the same partial resources, and `view.destroy()` is idempotent. `ui/fieldHost.js mountFieldView(host, { signal })` aborts that signal when the engine fails, times out (12 s) or the owner unmounts, disposes a view a factory returns after that, and never clears the other children of `host` — the simplified view and a newer mount share it.
|
||||
|
||||
Asset URLs (0.2.6, host bandwidth layer 1): the client takes every art, model and audio URL from `data/assets.json` and `data/local-assets.json` and builds none itself, so the content-hash segment the server puts in front (`/assets/~<hash>/…`, docs/ASSETS.md "The committed manifest and the served manifests") needs nothing on this side: `validSpine` still sees `.skel`, pixi-spine finds the atlas and its pages under the same segment (a model shares one hash), audio is fetched as `/media/~<hash>/…`. The two URLs the client derives from another file's — the 3D board's `tiles.json` beside the atlas and the token death sound beside `charDie` — are served `no-cache` (the board) or drop the segment (`shared/assetPaths.js unhashedUrl`, the sound).
|
||||
|
||||
Visuals: oblique perspective board (tilted ~30°, trapezoid tiles), raised highland blocks with side faces, road/floor/forbidden styles per research 07 §7, pulsing red gates, blue objective, dashed hand pads. Units: Spine (Front/Back rule, feet anchored — flying units hover `FLY_HOVER` 1.3 tiles up, the client's 0.35 in character space, research 12, §25.13.5 — an enemy flyer from the road whatever tile it crosses (GitHub #277) —, shadow sprite on the ground), elite gold outline glow, tier chip, HP bar (green ally / red enemy), SP bar (skill ready glow; while an ammo skill runs it is the magazine instead — a yellow cell per round, emptying from the right, `b.snap ammo`, and the skill draws no standing aura, UnitInfo `ammoSkill`), 斩业星熊's 我执 pool as a red bar over her drained HP bar (`b.snap neg`), 伺夜's 狼群 count as a row of diamonds under the bars, the lit ones the 狼影 left (`b.snap wolves`; 0.2.2, GitHub PR #303 / #378 / #383), status icons, the element gauge row under the bars — icon + white bar of the remaining 元素值, element-coloured refill during a 爆发冷却 (§18.3, §20.8); enemies drawn × their official prefab factor (enemies.json `modelScale`, §20.4); an enemy's mode — the `form` of a sim fx (`shared/protocol.js fxForm`), or UnitInfo `form` for a view built later through `render/app/info.js renderInfo` — switches clip sets (`render/units.js FORMS`: 掠海漂移体 crawl, 暴鸰 bombed, 转译基底's forms, the 逐火 embers, the leaders' 重生, 守墓石像, the 孤岛风云 prisoners' warning and 解放; closing clips timed from the fx `dur`), a one-off cast (`PROJ[kind].once`) plays once, and a local-only model's web alias is tinted (`ALIAS_TINT`: 灼热 / 炽焰源石虫, §21.14); damage numbers (phys orange-white, arts purple, true white, heal green); projectiles drawn per kind, hit sparks and melee slashes, skill activation bursts and auras (§17.3); death fade for enemies, summons and devices — a knocked-out operator stays on its tile in its held Die pose under a redeploy ring (§18.3); leak flash at the objective. Fallback when Spine missing/failed: avatar in a rarity-coloured diamond with bob/lunge tweens — a failed or timed-out model is loaded again (bounded), and a view re-resolves its model and avatar when the asset manifest arrives late or the tab is shown again (§21.27). 60 fps target with ≤ 120 units (a crowd draws its Spine units through the cached atlas, `render/impostor.js`, each refreshed every k-th frame: the refresh turns follow this frame's update order — `ctx.impostorSlot()`, a counter reset at the start of every frame — so a frame refreshes ⌊n/k⌋ or ⌈n/k⌉ of n units instead of whatever random per-unit phases gave, and a unit not refreshed for 2k frames is refreshed regardless; GitHub PR #334 by @Cloudnyco, issue #327 step 1 — its step 2, a time budget by priority, is not done; a model whose pose cannot change — `SpineActor.poseHeld()`: a knocked-out operator in the held end of its Die clip, a frozen model — is neither re-posed nor redrawn, only its clock runs, until a new clip, a revive, a wind-up, a form's closing clip, a clipping switch or a zoom past the impostor's rescale threshold; the redeploy rings draw one shared rasterised label per number, `render/textures.js downLabel`, the same pixels as a Text of their own). Picking (which unit a press / hover / dropped item is on) and the drag target follow one rule, `render/pick.js` — the tile under the pointer (§18.1); the Spine store never hands out a skeleton whose unload is in flight (§17.1).
|
||||
|
||||
---
|
||||
@@ -62,7 +64,7 @@ its board-space position and facing, including on a mirrored boss half. They can
|
||||
from this preview. The inspection affordance does not make the preview a combat unit; the battle sim still creates
|
||||
the selected variant at battle start (§30.22).
|
||||
|
||||
Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer), per-unit attack/hit SFX (throttled; an impact plays for a hostile attack's phys / arts / true damage only, never an operator's skill-mode file for a normal attack, ≤ 2 copies of one battle sound — §18.4; a skill cue that reaches the client before its unit — the sim emits a deploy-tick cast ahead of the unit's `['spawn']` — is held and sounds once when the unit is tracked, `AudioManager.pendingSkill`, GitHub PR #292 by @LimitlessHPPK; entering a field late hands the sound the buffered `spawn` tuples only, `screens/game/early.js audioEarly`, so no old death, deploy or cast replays), boss music in R14/15.
|
||||
Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer), per-unit attack/hit SFX (throttled; an impact plays for a hostile attack's phys / arts / true damage only, never an operator's skill-mode file for a normal attack, ≤ 2 copies of one battle sound — §18.4; a skill cue that reaches the client before its unit — the sim emits a deploy-tick cast ahead of the unit's `['spawn']` — is held and sounds once when the unit is tracked, `AudioManager.pendingSkill`, GitHub PR #292 by @LimitlessHPPK; entering a field late hands the sound the buffered `spawn` tuples only, `screens/game/early.js audioEarly`, so no old death, deploy or cast replays), boss music in R14/15. While the music is silent — muted, or the BGM volume at 0 — no BGM is downloaded: the track the screen wants is remembered, a different one still playing stops, and the wanted track is fetched once the music is audible again (`AudioManager.bgmSilent`; about 14 MB a match for a player who turned the music off).
|
||||
|
||||
---
|
||||
|
||||
@@ -73,7 +75,7 @@ Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer
|
||||
- `scene/<stageId>/` and `scene/tex/` — the official scene of a battlefield (§15 "Official battlefield scenes", DESIGN §30.21): `scene.json` (materials, key light, chunk records), `scene.bin` (the meshes in board space), the lightmap pages `lm<i>`, and the materials' albedo textures shared by the scenes. Read from the scene bundles `scenes/activities/<season>/level_<stage>/…`, which the client carries only while the season runs: `extract.py --only scene` takes them from the client or from `--scenes <dir>` / `.cache/official-scenes/*` (`tools/local-extract/fetch_scenes.py` downloads them from the official CDN's in-season version). The 0.2.5 full release zip includes the extracted files when the maintainer extracted them before packaging; a lite zip and a source install may fetch and extract them (`node tools/setup.mjs --scenes`). The repository never stores the bundles or the extracted files.
|
||||
- `ui/{common,battle,outer}/*` — every sprite of the three official autochess UI bundles (tier chips I–VI, bond disc rings, frames, banners, HUD pieces, badges, backgrounds, mode art).
|
||||
- `emoticon/<dir>/<picId>.png` — the 36 official battle emotes, 6 themes (`shared/constants.js → EMOTE_THEMES / EMOTES`, `emoteArtPath`; data/emotes.json); also downloaded from the mirror (§22.5).
|
||||
- `guide/autochess_{home,shop,handbook}_N.png` — the 19 official tutorial pages; stored squashed to 1024² — **display at 16:9** (the mirror copies too, §22.5).
|
||||
- `guide/autochess_{home,shop,handbook}_N.png` — the 19 official tutorial pages; stored squashed to 1024² — **display at 16:9** (the mirror copies too, §22.5). `extract.py` writes a lossless WebP copy beside each page and the manifest lists the copy (`GUIDE_WEBP_MODE`; 25.6 MB → 17.4 MB); the viewer preloads only the next page (`ui/guide.js guidePreloadUrl`), not the previous one too.
|
||||
- `projectiles/*` — battle projectile sprites.
|
||||
- `spine/enemy/<id>/` — enemy models no web dump carries (灼热 / 炽焰源石虫, `extract.py ENEMY_SPINES`): manifest `enemies[id].spineLocal` (metadata from the committed `tools/assets/local-enemy-spines.json`); drawn instead of the tinted web alias when every file is listed (§21.14).
|
||||
- `spine/token/<id>/` — token (summon) models no web dump carries (40: most 自选 summons, 凯瑟琳's 爬行号·防护单元, 凛御银灰's 风雪之眼; `extract.py TOKEN_SPINES`, the Front renderer's skeleton of each battle token prefab): manifest `tokens[id].spineLocal` (metadata from the committed `tools/assets/local-token-spines.json`); drawn instead of the avatar diamond when every file is listed (docs/ASSETS.md "Token models from the local client").
|
||||
|
||||
@@ -4,7 +4,7 @@ Part of [DESIGN.md](../DESIGN.md) (the index; section numbers are global).
|
||||
|
||||
## 8. Network protocol (shared/protocol.js is normative)
|
||||
|
||||
Transport: one WebSocket per tab at `/ws`, JSON text frames `{ t: '<type>', ...payload }`. Client requests may carry `rid` (request id); the server's direct reply echoes `rid` (`ok` or `error`). Server pushes have no `rid`. Max frame 64 KB inbound; rate limit 40 msgs/s per connection (excess dropped with `error RATE`).
|
||||
Transport: one WebSocket per tab at `/ws`, JSON text frames `{ t: '<type>', ...payload }`. Client requests may carry `rid` (request id); the server's direct reply echoes `rid` (`ok` or `error`). Server pushes have no `rid`. Max frame 64 KB inbound (a compressed frame counts its inflated size); rate limit 40 msgs/s per connection (excess dropped with `error RATE`). Frames from 1 KB up are compressed with permessage-deflate (RFC 7692; `server/http/websocket.js WS_DEFLATE`: no server context takeover, a 2 KB server window, zlib memLevel 7 — about 72 KB of zlib state per connection); smaller frames go out plain, and a client that offers no compression, or caps the server window below 2 KB, gets plain frames. A measured solo session (rounds 3–5, `/healthz?traffic=1`): 173 KB of server frames → 60 KB on the wire.
|
||||
|
||||
### 8.1 Session & lobby
|
||||
C→S: `hello {name, token?, version}` · `room.create {mode:'solo'|'coop', difficulty}` · `room.join {code}` · `room.leave` · `room.ready {ready}` · `room.setDifficulty {difficulty}` (host) · `room.setAiPicksLast {on}` (host, co-op, before the match — `ROOM_STARTED` during it, `BAD_TARGET` in a solo room; un-readies the other humans like a difficulty change; the match takes it as `opts.aiPicksLast`: humans draft before AI seats, off by default, no extra random draw) · `room.addBot` / `room.removeBot {seat}` (host) · `room.kick {seat, playerId}` (host, before the match: another human leaves the room and gets `room.closed {reason:'kicked'}`; may join again; `playerId` = the player the host confirmed, a seat that changed hands meanwhile → `BAD_TARGET`) · `room.start` (host) · `room.loadout {entries, ops?}` (§16: any time in the lobby / room and during INFO_CHECK, then `WRONG_PHASE`; heavy rate-limit bucket; the match exposes it as `m.private.loadout`; `ops` — 0.2.2 — the per-operator 潜能 / 练度 `{ [charId]: { potential?: 1–6, cultivate?: 0–3 } }` of the 干员调配 roster and the 自选 owned pool, ≤ 256, checked strictly with the entries, `m.private.ops`; absent = none set: every operator 潜能 6, 精英2 Lv.60) · `room.ownership {notOwned}` (0.2.0 补位, the 干员持有 tab: the base chess ids marked as not owned — ≤ 160 ids, kept leniently: anything but a droppable NORMAL chess is dropped; stored on the session and the seat, a match takes the seat's list when it starts and never changes it — during a match `ROOM_STARTED` 'stored for the next match'; heavy bucket; the match exposes it as `m.private.standIns`) · `room.diy {picks}` (0.2.0 自选编队: `{ [slotBaseId]: { charId, skillIndex?, uniEquipId? } | null }` for the four DIY slots — ≤ 8 entries, kept leniently: shared/protocol.js `checkDiyPicks` drops an illegal pick (an operator without a kit, another tier's prototype, a prototype off its locked skill, a module of another game mode (集成战略 / 生息演算), an owned operator's second slot, the same operator twice in a tier, an unknown slot), only malformed picks are `BAD_MSG`; stored on the session and the seat like the not-owned list, a match takes the seat's picks when it starts — during a match `ROOM_STARTED` 'stored for the next match'; heavy bucket; the match exposes them as `m.private.diy`) · `ping {c}`
|
||||
|
||||
+10
-4
@@ -42,12 +42,18 @@ server/
|
||||
index.js process entry: startServer() wires server/http/ (plain node:http + ws); boots when run directly
|
||||
http/
|
||||
config.js ROOT, served dirs, env (PORT, HOST, TRUST_PROXY, DEBUG), options handed to net.js / lobby.js
|
||||
websocket.js session wiring (SessionRegistry → Lobby → Network), WebSocket upgrade at /ws (64 KB frames)
|
||||
static.js mounts / → public, /data/, /shared/, /sim/ (.js only), the /data.js browser stand-in, path guards
|
||||
websocket.js session wiring (SessionRegistry → Lobby → Network), WebSocket upgrade at /ws (64 KB frames,
|
||||
permessage-deflate from 1 KB)
|
||||
static.js mounts / → public, /data/, /shared/, /sim/ (.js only), the /data.js browser stand-in, path guards,
|
||||
the ~<hash> segment of /assets/ /fonts/ /media/ URLs stripped (immutable when it names the file)
|
||||
media.js /media/… extension-less audio → public/assets/audio
|
||||
files.js one file → response: MIME, gzip (.skel/.atlas/.json/.js/.css …), ETag / 304, cache policy, ranges
|
||||
files.js one file → response: MIME, brotli q5 / gzip (.skel/.atlas/.json/.js/.css …), ETag (content hash when known) / 304,
|
||||
cache policy, ranges; serveBuffer for the served manifests
|
||||
buildTag.js build tag of the served browser runtime (/healthz build, public/js/ui/buildGuard.js)
|
||||
routes.js request listener: 414 / 400 / 405 guards, GET /healthz, then static files
|
||||
routes.js request listener: 414 / 400 / 405 guards, GET /healthz (?traffic=1: the counters), then static files
|
||||
traffic.js traffic counters: HTTP body bytes by category (assets / audio / code / data), WebSocket wire / payload
|
||||
assetIndex.js content hashes (SHA-256, 16 hex) of public/assets + public/fonts, background build, .cache/asset-hashes.json
|
||||
assetViews.js the served /data/assets.json + /data/local-assets.json: art URLs as /assets/~<hash>/… (immutable when it matches)
|
||||
common.js security headers, URL split, error page, JSON replies, bare 400 for unparseable requests
|
||||
boot.js boot banner (Local / LAN / tunnel URLs), port-in-use hint, graceful SIGINT / SIGTERM
|
||||
net.js session registry, send helpers, per-connection rate limit, message validation (uses shared/protocol.js)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# DESIGN §31 — 0.2.6-dev
|
||||
|
||||
Part of [DESIGN.md](../DESIGN.md). Development after the 0.2.5 release.
|
||||
|
||||
## 31. Host bandwidth, layers 0 and 1
|
||||
|
||||
Unreleased. The maintainer's decision of 2026-10-12 on the bandwidth study (its measurements: a new player's first match costs the
|
||||
host about 52 MB, audio ≈ 40 % of it; returning players re-download after a re-extraction, a `setup --force`, a Docker
|
||||
rebuild or a changed tunnel address): do layer 0 (small fixes) and layer 1 (content-hashed URLs, long caching, brotli)
|
||||
first. Later layers (WebP / Opus variants, an asset-only service worker, a desktop client mode, importing an asset pack in
|
||||
the browser) are agreed and not part of this note. Nothing here touches the battle sim, the protocol messages or the
|
||||
committed data.
|
||||
|
||||
### 31.1 Layer 0 — small fixes
|
||||
|
||||
- **No BGM while the music is silent.** Muted, or the BGM volume at 0, `AudioManager.playBgm` only remembers the wanted
|
||||
track (a different one still playing stops); `setVolumes` fetches and starts it once the music is audible again. SFX
|
||||
and voices were already gated. Normative line: design/client.md Audio.
|
||||
- **WebSocket permessage-deflate** (`server/http/websocket.js WS_DEFLATE`): frames from 1 KB, no server context
|
||||
takeover, a 2 KB server window, zlib memLevel 7 (about 72 KB of zlib state per connection). [ASSUMED] the parameters
|
||||
come from measured solo sessions (the server counters: 173 KB of frames → 60 KB on the wire; replaying 216 captured
|
||||
frames, windows above 2 KB gained nothing); co-op messages are the same kinds. A client that offers a narrower server
|
||||
window than 2 KB, which ws would refuse with a 400, connects uncompressed instead (`narrowDeflateOffer`). Normative
|
||||
line: design/network.md §8 Transport.
|
||||
- **玩法说明 pages as WebP.** `tools/local-extract/extract.py` writes a WebP copy beside each of the 19 local pages and
|
||||
the manifest lists it (`GUIDE_WEBP_MODE`); the guide preloads only the next page instead of both neighbours. The
|
||||
copies are lossless (25.6 MB → 17.4 MB on the CN client's pages). Lossy quality 90 would make them 4.2 MB; per the
|
||||
decision that tutorial pages may go lossy only after a side-by-side comparison, that switch (one constant, then
|
||||
`extract.py --webp`) is left to the maintainer. Normative lines: design/client.md §13, ASSETS.md Post-processing.
|
||||
- **Traffic counters.** `GET /healthz?traffic=1` adds `traffic` (HTTP body bytes and requests by category — assets,
|
||||
audio, code, data, other — and 304s; WebSocket wire bytes after compression and payload bytes before it) and
|
||||
`assetHashes` (the index state). The 问题反馈 diagnostics gain a `traffic:` line: the page's Resource Timing
|
||||
`transferSize` by the same categories (`shared/assetPaths.js trafficCategory`), other sites apart. Nothing is stored
|
||||
or uploaded.
|
||||
- **DEPLOY.md**: a stable address keeps every player's cache, settings and reconnect token (a quick tunnel's new domain
|
||||
is a new site to the browser); HTTP/2 through the proxy saves round trips, not bytes; browsers offer brotli on HTTPS
|
||||
pages and localhost only; a Cloudflare cache rule for `/assets/`, `/fonts/`, `/media/` (with the risk note).
|
||||
|
||||
### 31.2 Layer 1 — content-hashed URLs, long caching, brotli
|
||||
|
||||
- **Content-hash index** (`server/http/assetIndex.js`): 1 s after the start, SHA-256 (16 hex) of every file under
|
||||
`public/assets/` and `public/fonts/` (dot names skipped, linked folders followed once), computed off the main thread,
|
||||
cached in `.cache/asset-hashes.json` by path + size + mtime (12,691 files / 724 MB here: 1.3 s the first time, 0.25 s
|
||||
after). A file whose size or mtime differs from its record is never vouched for; serving it asks for a rescan (at most
|
||||
one per 10 s). [ASSUMED] the 1 s delay and the 10 s rescan gap. A name that differs from a file's only in case finds
|
||||
that file (the manifest's `/assets/module/wah-y.png` is `WAH-Y.png` on this machine). `SP_ASSET_INDEX=0` turns it off.
|
||||
- **Served manifests** (`server/http/assetViews.js`): `/data/assets.json` and `/data/local-assets.json` go out with every
|
||||
art URL carrying its file's hash as the first segment (`/assets/~<hash>/…`, `/fonts/~<hash>/…`; audio stays
|
||||
`/assets/~<hash>/audio/….mp3`). A Spine model — a `{ skel, atlas, textures }` object of data/assets.json, a `spine/…`
|
||||
group of data/local-assets.json — carries one hash for all its files, since pixi-spine finds the atlas and the pages
|
||||
next to the skeleton. `no-cache` with the view's own ETag; rebuilt in memory when a manifest or the hashes change; the
|
||||
committed files are never written. Until the index is ready, without it, or on a static host: the files as committed.
|
||||
- **The `~hash` routes**: `static.js` strips the segment for `/assets/`, `/fonts/` and `/media/`; a hash that names the
|
||||
file's current bytes (its own or its model's) answers `public, max-age=31536000, immutable`, any other the current
|
||||
file with `no-cache` (an old page keeps working). Every indexed file's ETag is its content hash, on plain URLs too.
|
||||
Path segments, never `?v=`: `validSpine`'s `/\.skel$/`, `SKILL_MODE_FILE`'s `.mp3`, pixi-spine's basename and folder
|
||||
resolution all need the URL to end in the file name.
|
||||
- **Media**: `public/js/media.js` turns `/assets/~<hash>/audio/x.mp3` into `/media/~<hash>/x`; `server/http/media.js`
|
||||
resolves it like `/media/x`. The token death sound derived next to `charDie` drops `charDie`'s hash (`unhashedUrl`).
|
||||
- **Brotli** (`server/http/files.js`): quality 5 on the fly with the same memory cache (one entry per file and
|
||||
encoding), for clients that name `br` at least as high as gzip; gzip for the rest (`*` alone never picks brotli
|
||||
[ASSUMED]); `-br` / `-gz` ETag suffixes. Measured against gzip-6 on this repository: `.skel` −27 %, `data/*.json`
|
||||
−26 %, vendor −14 %, `public/js` and `server/sim` −5 %, `.atlas` −7 %.
|
||||
|
||||
### 31.3 Measured sample session
|
||||
|
||||
Measured sample session (headless Chrome, a fresh profile, `test/e2e/fastServer.mjs` solo match from round 3 to the
|
||||
result after round 5, host bytes only, response headers included; the shop and the enemies vary between runs):
|
||||
|
||||
| session | code | data | assets | audio | WebSocket (server → client) | host total |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| before, music on | 3.16 MB | 0.67 MB | 9.0–9.7 MB | 7.9 MB | 163–179 KB (no compression) | 20.8–21.4 MB |
|
||||
| after, music on | 2.95 MB | 0.62 MB | 9.5 MB | 8.0 MB | 60 KB on the wire (173 KB payload) | 21.1 MB |
|
||||
| before, muted | 3.16 MB | 0.67 MB | 9.6 MB | 7.3 MB (BGM) | 165 KB | 20.7 MB |
|
||||
| after, muted | 2.95 MB | 0.62 MB | 9.5 MB | 0 | 57 KB on the wire | 13.1 MB |
|
||||
|
||||
A first visit with music on saves little (brotli on code and data, the WebSocket); a muted player saves the BGM. The
|
||||
caching gains are for returning players: within a day both versions answer from the browser cache, but after that the
|
||||
old version revalidated every asset of the session (≈ 340 requests here) and re-downloaded everything whose mtime had
|
||||
changed, while a hashed URL is never asked for again until its bytes change.
|
||||
|
||||
### 31.4 Open items and tests
|
||||
|
||||
Open: `/fonts/fonts.css` (linked by index.html) and the woff2 files it names keep plain URLs (content ETags only); the
|
||||
optional versioned vendor URLs (layer 1.5) are not done; lossy tutorial pages wait for the comparison.
|
||||
|
||||
Tests: `test/ui/audio.test.js` (BGM gating; hashed audio through `/media/~hash/`), `test/ws-compression.test.js`,
|
||||
`test/local-extract.test.js` (guide WebP), `test/ui/localArt.test.js` (next-page preload), `test/traffic.test.js`,
|
||||
`test/asset-paths.test.js`, `test/diag.test.js` (traffic line), `test/asset-index.test.js` (index, views, `~hash`
|
||||
routes), `test/lobby.test.js` (brotli negotiation, streamed brotli, content ETags), `test/media-url.test.js`,
|
||||
`test/content/tokens_summons.test.js`; the browser suites `test/render/browser.test.js` (Spine through hashed URLs,
|
||||
`RENDER_E2E=1`), `test/ui/bond-audio.e2e.test.js` (`SP_E2E=1`, the cue through `/media/~hash/`) and
|
||||
`test/ui/real.e2e.test.js` (`SP_REAL_E2E=1`: the co-op run loads Spine, audio and art through hashed URLs). The e2e
|
||||
servers wait for the asset index before the first page (`test/e2e/client.mjs startRealServer`, the in-process ones in
|
||||
their `before`), so the browser suites see the URLs a player sees. Golden: unchanged.
|
||||
Reference in new issue
Block a user