================================================================
OpenDance Studio -- Windows 客户端 v0.2.4  使用说明（zip 为 LZMA 压缩，需用 7-Zip/WinRAR 解压）
================================================================

摄像头跳舞评分系统的核心体验端：接收 Android 辅助 App 通过 WebSocket
推送的 33 点骨骼关键点，驱动 VRM 角色，并与 Web 服务端下发的参考动作
做实时 DTW 评分。

本客户端只接收关键点坐标，不接收、不存储任何视频画面。

v0.2.4 修订
  1. 编码取整与契约参考实现对齐: 本端 encode() 原用 GDScript roundi（半数远离零），
     而参考实现与生产发送方（Android 的 Math.round）是「半数朝 +∞」—— -2.5 分别是
     -3 与 -2。本端是接收方（encode 只用于自测），故对产品无影响，但跨实现一致性
     只能停留在"差异仅限取整边界"的宽松口径。现改为 floor(x + 0.5)，并把验收口径
     收紧为「编码差异必须为 0」。
  2. 产物不再夹带开发/验收工具: export_filter="all_resources" 且 exclude_filter
     没有 build/*，导致探针脚本（含故意不合规的坏服务端夹具）与向量文件被打进 pck ——
     pck 从 145,400 B 涨到 315,464 B（54% 是测试夹具）。现已排除 build/*，
     并新增产物级残留检查（accept-release.sh 会断言「无开发残留」）。
  3. 无头测试 785 项断言（原 774）。

v0.2.3 修订
  1. 补 Motion.thumbnail_url: 服务端 catalog/export 从 data-models 0.6.0 起一直
     在发 thumbnailUrl，客户端类型里却缺这个字段（Model/Scene 都有），等于默默丢掉。
  2. ApiClient 不再对所有响应无条件做 UTF-8 解码: 原来下载每个文件（模型 / GLB /
     缩略图）都会打印一条 "Unicode parsing error ... Invalid UTF-8 leading byte"
     噪声，并白做一份 String 拷贝；二进制路径现在直接返回原始字节。
  3. 新增跨端集成验收 build/accept-integration.sh: 用发布包里的客户端打真实 Web
     服务端，逐项比对契约与场景物理参数，并跑通真实 VMD → 内部标准 33 点 → 客户端
     解析 2901 帧的闭环。
  4. 测试规模 820 项断言（含真实服务端在线的 API 集成组）。

v0.2.2 修订
  1. 修复发布包 exe 的版本元数据: v0.2.1 的包在 Windows「右键 -> 属性 ->
     详细信息」里显示的是 文件版本 0.1.0 / 产品版本 0.1.0, 而产物名与 Git tag
     都是 v0.2.1。根因是 export_presets.cfg 的 application/file_version 与
     application/product_version 被硬编码成 0.1.0, 而它们不会跟随 project.godot
     的 application/config/version 自动更新。
  2. 新增 build/sync-export-version.sh(导出前同步版本号到预设)与
     build/check-pe-version.sh(直接读 exe 的 PE 版本资源复核: VS_FIXEDFILEINFO
     的 FileVersion 与 StringFileInfo 的 FileVersion/ProductVersion 两处都查)。
     check-version.sh 增加预设校验, package-release.sh 在打包前与解包后各校验一次,
     verify.sh 增加步骤 1/5, tests/test_about.gd 增加对应单测。
  3. 现在本包显示: 文件版本 0.2.2 / 产品版本 0.2.2。

v0.2.1 修订
  1. 场景包围盒不再用 global_transform: SceneRuntime._compute_aabb() 原先在
     _assemble() 里调用, 而那时节点还没入树 -> Godot 会打一行 ERROR
     (Condition "!is_inside_tree()" is true), 且算出来只是各网格的局部 AABB,
     导致重力/风区域与兜底地面算偏位(风力可能作用不到物体). 现改为沿树累乘
     Node3D.transform。
  2. 新增 21 项无头"真机等价"测试: 手写合法 GLB 真实加载(三种 colliders 取值、
     重力/风数值、带位移网格的包围盒) + 17 秒保活对照(只发 ping 的客户端不被
     断开、完全不发的客户端被 1001 断开)。
  3. 新增 build/check-version.sh: 强制 project.godot / project.yaml / 发布版本
     三者一致(已接入打包与验证脚本, 并做过反向验证)。
  4. 无头测试规模 751 项断言全部通过。

--
一、快速开始
--

1. 解压后得到：
     OpenDance.exe       主程序
     OpenDance.pck       资源包（必须与 exe 同目录）
     README.txt          本文件
     README.md           同内容 Markdown 版
     THIRD_PARTY.md      开源声明
     contracts/          契约快照（只读；客户端不直连数据库）

2. 先启动 Web 管理服务端（opendance-web），确保能访问其 API。

3. 双击 OpenDance.exe 启动。

4. 在主界面确认「服务端地址」（默认 http://127.0.0.1:3000），
   点击「保存并测试」；顶部应显示「服务端正常 | DB...」。

5. 点击「刷新目录」，左侧会出现已发布的曲目、动作、模型与场景。

6. 在 Android 手机上启动「OpenDance 辅助 App」，填入本机显示在
   主界面上的接收地址（形如 ws://<本机局域网地址>:8765/pose）并连接。
   连接成功后主界面会提示「已连接 N 台设备」。

7. 依次选择曲目 -> 动作 -> 模型 -> 场景，点击「开始跟练」。
   场景可以留空不选：服务端没有可用场景时会自动回退到内置空场景，
   界面会提示但不影响开始跟练。

--
二、初次部署（从零跑起来）
--

本节面向第一次拿到源码、要从零把客户端跑起来的人；只运行已打包好的
OpenDance.exe 的用户看第一节即可。

1. 前置条件

   - 操作系统：Windows 10/11(x86_64)。开发与导出可在 Windows 或 Linux。
   - Godot 4.7.x 标准版（GDScript）。不需要 .NET / C# 版。
   - 渲染后端为 GL Compatibility，运行需 OpenGL 3.3 级别的显卡与驱动。
   - 导出模板：仅打包导出时需要，必须与编辑器完全同版本。
   - 磁盘：编辑器 + 导出模板数百 MB；导出产物 exe 约 104 MB + pck 约 107 KB。
   - 网络：能访问 Web 服务端 API（默认 TCP 3000）；与手机同一局域网，
     并放行本端 TCP 8765 入站。

2. 获取代码

   git clone https://gitea.kasugano.cn/zijunchiang/opendance-windows
   cd opendance-windows

3. 编辑器内首次运行

   - Godot 项目管理器 -> 导入 -> 选择 project.godot。
   - 首次导入会执行资源导入（生成 .godot/），可能等待数十秒到数分钟；
     不要在导入中途关闭编辑器。
   - 主场景已配置为 res://scenes/main.tscn，按 F5 运行。
   - 首次运行会生成：
       %APPDATA%\Godot\app_userdata\OpenDance Studio\opendance.cfg
       %APPDATA%\Godot\app_userdata\OpenDance Studio\logs\godot.log

4. 配置服务端地址

   主界面顶部「服务端地址」填好后点「保存并测试」，配置字段名为 api_base_url:

     http://<服务器地址>:3000      <- 你们自己部署的 Web 服务端地址(不是被脱敏的域名)
     http://192.0.2.10:3000

   注意：客户端只访问 API 端口（默认 3000）；管理端页面在管理端口
   （默认 3001），客户端不使用。保存后点「刷新目录」。

5. 与另两端的联通

   - Android 辅助 App -> 本端 WebSocket：本端是服务端，默认监听
     0.0.0.0:8765，路径 /pose，子协议 opendance.pose.v1。Android 端填写
     本端主界面显示的「接收地址」，形如 ws://<本机局域网地址>:8765/pose。
     本端可在 opendance.cfg 改 listen_host / listen_port / ws_subprotocol,
     改后点「重新连接接收服务」生效。
   - Web 服务端：需先在服务端发布曲目 / 动作 / 模型，动作还须完成关键点
     预处理，客户端才有可选内容。

6. 打包导出

   # Linux（推荐）
   GODOT_BIN=/path/to/godot bash build/export-windows.sh release

   # Windows（兜底）
   powershell -ExecutionPolicy Bypass -File build\export-windows.ps1

   产物在 build/windows/: OpenDance.exe（约 104 MB, PE 头 MZ 校验通过）
   + OpenDance.pck（约 107 KB，必须与 exe 同目录）；可能附带
   OpenDance.console.exe。

   打成发布包：

     bash build/package-release.sh 0.2.4

   得到 dist-release/ 下两个资产：

     opendance-windows-v0.2.4.zip     <- 单个完整包，禁止 .partN 分卷
     SHA256SUMS                       <- 裸文件名，可直接 sha256sum -c

   包里含 OpenDance.exe、OpenDance.pck、THIRD_PARTY.md、README.txt、
   README.md、contracts/。打包脚本会解压自检（exe + pck 必须同目录、
   exe 的 PE 头为 MZ、pck 魔数为 GDPC、exe 的 PE 版本资源 = 发布版本）
   并打印包体与上传上限的对比。

   注意：本项目 Gitea 前置 IIS 的默认上传上限是 30,000,000 字节
   （约 28.6 MiB），超过会返回 HTTP 413。这时不要改成分卷，
   请让运维放宽上限（做法见 opendance-web/deploy/proxy/README.md 第九节）。
   v0.2.0/v0.2.1/v0.2.2 该上限都尚未放开，整包（deflate 约 38 MB）改用
   LZMA 压缩到约 26 MB 才传上去（打包时用 ZIP_METHOD=lzma）。
   **因此这几个版本的 zip 需要 7-Zip / WinRAR / PeaZip 解压 ——
   Windows 10 资源管理器不支持 LZMA 的 zip**。

   如果你能把包放到别处分发（例如 Web 服务端的 /static，不受 IIS 那个
   上限约束），建议改用资源管理器可直接双击解压的 deflate 包：

     ZIP_METHOD=deflate bash build/package-release.sh 0.2.4   # 约 38 MB

   上限一旦放开，Release 资产也应换回 deflate 包。

7. 自检清单

   0) 发布包级验收（对**发布包**而不是源码；文档 §7.5）
      GODOT_BIN=/path/to/godot bash build/accept-release.sh dist-release/opendance-windows-v0.2.4.zip
      预期末行「发布包级验收全部通过」。它会校验 sha256、解压、核对文件清单、
      读 exe 的 PE 版本资源、用 Godot 直接加载包内 pck 跑 180 帧、并跑包内运行时
      探针（包内版本号 / 「关于」清单显示值 / 主场景 / autoload / 关键资源 /
      包内契约表与 contracts/VERSION 快照是否一致）。

   1) 源码一键验证：GODOT_BIN=/path/to/godot bash build/verify.sh
      预期末行「验证全部通过」。
   2) 无头测试：$GODOT --headless --path . --script res://tests/run_tests.gd
      预期「== 结果：751 通过 / 0 失败 ==」与「全部通过」（缺 VRM 素材或
      服务端不可达时相关用例跳过，数字会减少）。
   3) 启动冒烟：$GODOT --headless --path . --quit-after 180
      预期无 SCRIPT ERROR / ERROR:，退出码 0。
   4) 日志：%APPDATA%\Godot\app_userdata\OpenDance Studio\logs\godot.log
      预期无 SCRIPT ERROR。
   5) 服务端联通：主界面点「保存并测试」后顶部显示「服务端正常 | DB ...」。

8. 部署相关常见问题

   Q: 模型加载失败：缺少骨骼
   A: 模型无 Skeleton3D，或骨骼命名不在映射表内；换用标准 VRM(VRoid
      Studio 导出）。人形覆盖率低于 60% 会提示「骨骼覆盖偏低」。

   Q: 连不上服务端 / 显示「服务端不可达」
   A: 确认 Web 服务端已启动，api_base_url 指向 http://<服务器地址>:3000
      （不要填管理端口 3001）；访问 http://<服务器地址>:3000/health 应返回 200。

   Q: 导出失败：未找到导出模板
   A: 安装与编辑器同版本的 export templates。Linux 放
      ~/.local/share/godot/export_templates/<版本>.stable/;
      Windows 放 %APPDATA%\Godot\export_templates\<版本>.stable\。

   Q: 接收服务启动失败（端口 8765 可能被占用）
   A: 改 opendance.cfg 的 listen_port 后点「重新连接接收服务」，
      并同步修改 Android 端填写的接收地址。

--
三、界面说明
--

主界面
  - 四个选择器（2x2 栅格）：曲目 / 动作（仅列出已完成预处理的动作）/
    模型（VRM 优先）/ 场景（GLB + 物理参数）
  - 服务端地址：可修改并即时测试
  - 接收地址：Android 端要连接的 WebSocket 地址
  - 「刷新目录」重新拉取已发布资源（含场景）
  - 底部左侧展示所选动作的来源信息（来源类型、名称、作者、许可、链接）
  - 底部中间展示所选场景的缩略图与物理参数摘要
    （重力 / 摩擦 / 弹性 / 碰撞体来源 / 风）；未选场景时说明将使用内置空场景

游戏界面
  - 左侧 3D 视图：VRM 角色落在场景出生点并跟随关键点实时动作，
    相机按场景配置取景（follow 跟随 / fixed 固定 / orbit 环绕）
  - 场景物理在本端落地：重力（Area3D 覆盖，不改全局 ProjectSettings）、
    风（每物理帧施加风力，阵风按正弦叠加）、地面摩擦与弹性、
    GLB 碰撞体（mesh 三角网 / auto 命名约定 *_col、*_colonly）、环境光与雾
  - 右侧 HUD：实时得分、等级、连击、动作进度、连接状态、来源信息、场景参数

结算界面
  - 等级（S/A/B/C/D）、平均分与最终分、动作完成度、最高单帧、最长连击
  - 「再来一次」/「返回主菜单」/「开源声明」

关于页面
  - 「清单」逐行显示：客户端版本（vX.Y.Z）/ 运行引擎 / 契约版本 /
    接收协议 / 接收地址 / 服务端地址 / 隐私说明
  - 版本号唯一来源是 project.godot 的 application/config/version，与
    project.yaml、导出产物名、Git tag、界面展示完全一致
  - 完整可滚动的第三方开源项目声明

--
四、配置文件
--

首次运行会在以下位置生成配置文件，可直接用文本编辑器修改：

  Windows: %APPDATA%\Godot\app_userdata\OpenDance Studio\opendance.cfg

可配置项：

  api_base_url      Web 服务端地址(默认 http://127.0.0.1:3000)
  listen_host       WebSocket 监听地址(默认 0.0.0.0)
  listen_port       WebSocket 监听端口(默认 8765)
  ws_subprotocol    子协议(默认 opendance.pose.v1)
  dtw_band          DTW 带宽，单位帧（默认 15 ~ 0.5 秒抖动容忍）
  score_tolerance   评分容差：平均每步代价达到该值即 0 分(默认 0.25)
  mirror_preview    镜像预览

--
五、防火墙与网络
--

本客户端需要接受 Android 设备的入站连接：

  - Windows 防火墙需放行 OpenDance.exe 的 **TCP 8765** 入站
  - 手机与电脑需在同一局域网
  - 手机端填写主界面显示的「接收地址」

若端口被占用，可修改 opendance.cfg 中的 listen_port 后重启。

--
六、评分说明
--

- 每帧特征 = 33 点 x (x,y,z) + 9 个关节角余弦 = 108 维
  （与 Web 服务端预处理产物、Android 端推流结构完全一致）
- 置信度低于 0.5 的关键点不参与代价计算，避免遮挡导致误判
- 采用带约束 DTW(|i-j| <= band)对齐，能吸收一定的节奏快慢差异
- 得分 = clamp(1 - 平均代价 / 容差, 0, 1) x 100，并做指数平滑
- 等级：S >= 95, A >= 85, B >= 70, C >= 55, D 其余

--
七、从源码构建
--

前置：Godot 4.7.x + 对应版本的 export templates

  # Linux 上直接导出 Windows 可执行文件（推荐）
  GODOT_BIN=/path/to/godot bash build/export-windows.sh release

  # Windows 上兜底导出
  powershell -ExecutionPolicy Bypass -File build\export-windows.ps1

产物位于 build/windows/: OpenDance.exe + OpenDance.pck
（脚本会校验产物存在且为合法 PE 文件）

运行测试：

  GODOT=/path/to/godot
  $GODOT --headless --path. --import
  $GODOT --headless --path. --script res://tests/run_tests.gd
  $GODOT --headless --path. --quit-after 180

--
八、目录结构
--

  project.godot            工程配置（autoload / 渲染 / 窗口）
  export_presets.cfg       导出预设（必须在项目根目录）
  project.yaml             任务清单与状态
  DSH.md                   本项目 DSH 执行约束
  tasks/                   单任务说明书(T-WIN-001 ~ 015)
  contracts/               只读契约快照(来源 opendance-web；六项契约各自独立版本)
  scenes/main.tscn         根节点
  src/autoload/            AppConfig / CatalogStore / PoseReceiver / ScoreService
  src/net/                 239 字节编解码、WebSocket 服务、REST 客户端
  src/model/               VRM 加载、humanoid 骨骼映射
  src/anim/                关键点 -> 骨骼重定向
  src/scene/               场景实例化与物理落地(重力/风/碰撞/环境/机位)
  src/score/               特征提取、DTW、评分规则
  src/ui/                  主界面 / 游戏界面 / 结算 / 关于 / 来源展示 / 场景参数
  tests/                   无头测试框架与用例
  build/                   导出脚本

--
九、常见问题
--

Q: 主界面显示「服务端不可达」
A: 检查 Web 服务端是否已启动，以及「服务端地址」是否正确（含端口）。

Q: 目录里没有可用的动作
A: 动作必须先在管理端完成「预处理」（BVH -> 关键点序列）并「审核通过」，
   才会出现在客户端。

Q: 模型加载失败 / 动作不完整
A: 游戏界面会显示「人形覆盖率」。低于 60% 说明模型的骨骼命名不被识别，
   请换用标准 VRM(VRoid Studio 导出)模型。

Q: 连上了手机但分数一直是 0
A: 确认手机端已开始推流；主界面的「查看接收状态」会显示累计帧数与丢帧数。

Q: 手机端反复重连 / 「已连接」数忽大忽小
A: 本端有 15 秒静默超时（契约 v0.2.0 1.1），并在 welcome/config 里声明
   silentTimeoutMs=15000 与 heartbeatIntervalMs=5000；Android 端会按后者发
   应用层 ping。若仍频繁断开，用「查看接收状态」看每台设备的状态与断开原因
   （close code）：1001 表示本端主动踢人或服务重启，0/1006 表示网络抖动。
   同一设备的重复连接会按 deviceId 去重（旧连接以 1000 关闭，对端不重连），
   所以「已连接 N 台」显示的是去重后的设备数，不是连接数。

Q: 选场景时提示「场景缺少 GLB 文件」或「已回退内置空场景」
A: 服务端该场景没有可下载的 GLB（转换失败或未发布）。这不影响开始跟练，
   会使用内置空场景与默认物理参数；请让管理员在管理端「场景管理」重新上传。

Q: 场景里的角色一直往下掉
A: 场景没有可用碰撞体且配置为 colliders=auto/mesh。本端会自动生成兜底地面
   （日志会记录「已生成兜底地面」）；若场景显式配置 colliders=none，则按场景
   作者意图不生成任何碰撞体，此时角色不会被地面接住。

Q: 关于页面的开源声明
A: 与 THIRD_PARTY.md、contracts/open-source-credits.md 三处保持一致。

--
十、许可
--

本客户端代码以 MIT 许可发布。第三方开源项目声明见 THIRD_PARTY.md。
未使用 SMPL 模型及其衍生拓扑，统一采用 MediaPipe 33 点拓扑。
