summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/overnight-quality-brief.md3
-rwxr-xr-xscripts/macos-package.sh64
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs33
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")