diff options
| -rw-r--r-- | docs/overnight-quality-brief.md | 3 | ||||
| -rwxr-xr-x | scripts/macos-package.sh | 64 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/PrototypeTests.fs | 33 |
3 files changed, 95 insertions, 5 deletions
diff --git a/docs/overnight-quality-brief.md b/docs/overnight-quality-brief.md index 71266d4..cd0c3ae 100644 --- a/docs/overnight-quality-brief.md +++ b/docs/overnight-quality-brief.md @@ -19,6 +19,9 @@ 每轮将已验证commit、远程hash、测试、真实图像路径、基准结果和阻塞写入docs/overnight-status.md。早晨进度必须区分完成、在做、未通过,不伪造完成率,不以busy或PASS单行替代证据。用户无需夜间逐项确认,常规取舍自行解决;涉及付费/凭据/扩大权限停在明确边界并完成其他安全工作。 +## 进行中(Hermes 已派发,tick 别重复派单) +M3 批验改版单 **M3-1** 已派发到 writer `ses_f3c25c8eaffelgpRBxEYvI6aOM`(go-b/deepseek-v4.1-flash,新 worktree `/tmp/opencode/lv-m3` 分支 `m3-observability`):逐世界流式输出 + 50×20 缩尺门槛 + `--cost-probe` 成本量化 + 长局成本只测不改。等它 READY_FOR_LEADER_REVIEW 后由 Hermes 验收;tick 不要另派同名任务或覆盖 lv-m3 工作树。 + ## 用户提出三大可玩性升级方向(2026-09-21 早晨拍板,用户措辞) 1. 美术:需接近星露谷物语水准的精美美术与动画效果(不复制其素材),角色动画帧、昼夜光照、场景细节差是目前主要差距。 2. 界面:需精美启动动画、开始界面布局;游戏内HUD大幅升级,禁止大黑块,参考星露谷风格布局(不复制其素材)。 diff --git a/scripts/macos-package.sh b/scripts/macos-package.sh index 3255898..ebe9e2a 100755 --- a/scripts/macos-package.sh +++ b/scripts/macos-package.sh @@ -48,6 +48,27 @@ sha256() { printf '%s\n' "${output%% *}" } +utf8_char_count() { + # 不依赖 locale 的 UTF-8 码点计数:只数每个码点的首字节 + # (ASCII <128,或 110xxxxx/1110xxxx/11110xxx 前导字节),续字节 10xxxxxx 忽略。 + printf '%s' "$1" | od -An -t u1 | awk ' + { for (i = 1; i <= NF; i++) { b = $i; if (b < 128 || (b >= 192 && b < 248)) n++ } } + END { print n + 0 }' +} + +png_dimensions() { + # 从 PNG IHDR 读取宽高(大端 4 字节,偏移 16/20),只依赖 od。 + local bytes size + bytes="$(od -An -t u1 -j 16 -N 8 "$1" | tr -s ' ' '\n' | sed '/^$/d' | paste -sd' ')" + read -r b0 b1 b2 b3 b4 b5 b6 b7 <<< "$bytes" + if [[ -z "${b7:-}" ]]; then + return 1 + fi + printf '%s %s\n' \ + "$(( b0 * 16777216 + b1 * 65536 + b2 * 256 + b3 ))" \ + "$(( b4 * 16777216 + b5 * 65536 + b6 * 256 + b7 ))" +} + write_launcher() { local launcher_path="$1" cat > "$launcher_path" <<'EOF' @@ -276,17 +297,50 @@ for rid in osx-arm64 osx-x64; do exit 1 fi done + # CJK 清单不再硬编码行数:与图集网格自洽(16 列,行数覆盖字符数且无空行), + # 并校验非空、每行恰 1 个字符、字符唯一,避免图集增长后打包悄悄过期。 + if [[ ! -s "$app_dir/Contents/MacOS/Assets/cjk-glyphs.txt" ]]; then + printf 'BLOCKED: packaged CJK manifest for %s is empty\n' "$rid" >&2 + exit 1 + fi mapfile -t cjk_manifest_lines < "$app_dir/Contents/MacOS/Assets/cjk-glyphs.txt" - if [[ "${#cjk_manifest_lines[@]}" -ne 188 ]]; then - printf 'BLOCKED: packaged CJK manifest for %s contains %s lines; expected 188\n' "$rid" "${#cjk_manifest_lines[@]}" >&2 + if [[ "${#cjk_manifest_lines[@]}" -eq 0 ]]; then + printf 'BLOCKED: packaged CJK manifest for %s is empty\n' "$rid" >&2 exit 1 fi - for cjk_manifest_line in "${cjk_manifest_lines[@]}"; do - if [[ ${#cjk_manifest_line} -ne 1 ]]; then - printf 'BLOCKED: packaged CJK manifest for %s contains a line that is not one character\n' "$rid" >&2 + declare -A cjk_seen_characters=() + for index in "${!cjk_manifest_lines[@]}"; do + cjk_manifest_line="${cjk_manifest_lines[$index]}" + if [[ "$(utf8_char_count "$cjk_manifest_line")" -ne 1 ]]; then + printf 'BLOCKED: packaged CJK manifest for %s line %s is not one character\n' "$rid" "$((index + 1))" >&2 exit 1 fi + if [[ "${cjk_seen_characters[$cjk_manifest_line]+present}" ]]; then + printf 'BLOCKED: packaged CJK manifest for %s has a duplicate character on line %s\n' "$rid" "$((index + 1))" >&2 + exit 1 + fi + cjk_seen_characters[$cjk_manifest_line]=1 done + cjk_character_count="${#cjk_manifest_lines[@]}" + cjk_cell_pixels=24 + cjk_expected_columns=16 + if ! read -r cjk_atlas_width cjk_atlas_height < <(png_dimensions "$app_dir/Contents/MacOS/Assets/cjk-glyph-atlas.png"); then + printf 'BLOCKED: cannot read packaged CJK atlas dimensions for %s\n' "$rid" >&2 + exit 1 + fi + cjk_columns=$(( cjk_atlas_width / cjk_cell_pixels )) + cjk_rows=$(( cjk_atlas_height / cjk_cell_pixels )) + cjk_capacity=$(( cjk_columns * cjk_rows )) + if (( cjk_columns != cjk_expected_columns )); then + printf 'BLOCKED: packaged CJK atlas for %s has %s columns; expected %s\n' \ + "$rid" "$cjk_columns" "$cjk_expected_columns" >&2 + exit 1 + fi + if (( cjk_capacity < cjk_character_count || cjk_capacity - cjk_character_count >= cjk_columns )); then + printf 'BLOCKED: packaged CJK atlas grid %sx%s (%s cells) is inconsistent with manifest %s characters for %s\n' \ + "$cjk_columns" "$cjk_rows" "$cjk_capacity" "$cjk_character_count" "$rid" >&2 + exit 1 + fi if ! file -b "$app_dir/Contents/MacOS/LivingVillage.Desktop" >> "$mach_o_report"; then printf 'BLOCKED: failed to inspect bundled apphost for %s\n' "$rid" >&2 diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs index d5602e8..eeafb56 100644 --- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs +++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs @@ -1018,3 +1018,36 @@ type PrototypeTests () = |> List.filter (fun relativePath -> not (File.Exists(Path.Combine(AppContext.BaseDirectory, relativePath)))) Assert.AreEqual<string list>([], missing, sprintf "Missing Jiangnan PNG assets: %s" (String.concat ", " missing)) + + [<TestMethod>] + member _.CjkGlyphManifestIsUniqueAndMatchesAtlasGrid () = + // 防复发:图集用字增长时,清单必须非空、每行恰 1 个字符、字符唯一, + // 且与 16 列图集网格自洽(打包脚本不再硬编码行数)。 + let assets = Path.Combine(AppContext.BaseDirectory, "Assets") + let manifestPath = Path.Combine(assets, "cjk-glyphs.txt") + let atlasPath = Path.Combine(assets, "cjk-glyph-atlas.png") + Assert.IsTrue(File.Exists manifestPath, "missing cjk-glyphs.txt") + Assert.IsTrue(File.Exists atlasPath, "missing cjk-glyph-atlas.png") + + let lines = File.ReadAllLines manifestPath + Assert.IsTrue(lines.Length > 0, "CJK manifest must not be empty") + for index in 0 .. lines.Length - 1 do + Assert.AreEqual<int>(1, lines.[index].Length) + Assert.AreEqual<int>(lines.Length, (Set.ofArray lines).Count) + + let header = File.ReadAllBytes atlasPath + let be32 offset = + (int header.[offset] <<< 24) + ||| (int header.[offset + 1] <<< 16) + ||| (int header.[offset + 2] <<< 8) + ||| int header.[offset + 3] + let cell = 24 + let width = be32 16 + let height = be32 20 + Assert.AreEqual<int>(0, width % cell) + Assert.AreEqual<int>(0, height % cell) + let columns = width / cell + let capacity = columns * (height / cell) + Assert.AreEqual<int>(16, columns) + Assert.IsTrue(capacity >= lines.Length, "CJK atlas grid must cover every manifest character") + Assert.IsTrue(capacity - lines.Length < columns, "CJK atlas must not keep an extra empty row") |
