diff --git a/.codex/skills/th1-network-sync/SKILL.md b/.codex/skills/th1-network-sync/SKILL.md index 2fe473060..0940b20c8 100644 --- a/.codex/skills/th1-network-sync/SKILL.md +++ b/.codex/skills/th1-network-sync/SKILL.md @@ -1,6 +1,6 @@ --- name: th1-network-sync -description: TH1 project-specific network synchronization guide for Unity C# multiplayer, Steam P2P, GameNetSender/GameNetReceiver, SteamLobbyManager, SimpleP2P, lobby MapConfig/MemberCiv player slot/team/AI/ready-state sync before game start, MapData/NetData recovery, action sync, host start/resume, ForceUpdate, heartbeat, ordered delivery, large message chunking, send-failure handling, and lobby UI rollback. Use whenever Codex works on TH1 networking, multiplayer rooms, member civ/ready/team/AI slot config, multiplayer saves, deterministic sync, P2P queueing, MapData broadcasts, ActionConfirm/ActionExecute, reconnect, Timer-driven network callbacks, or any bug that may affect multiplayer reliability or ordering. +description: TH1 project-specific network synchronization guide for Unity C# multiplayer, Steam P2P, GameNetSender/GameNetReceiver, SteamLobbyManager, SimpleP2P, lobby MapConfig/MemberCiv player slot/team/AI/ready-state sync before game start, MapData/NetData recovery and diagnostics, action sync, host start/resume, ForceUpdate, heartbeat, ordered delivery, large message chunking, send-failure handling, and lobby UI rollback. Use whenever Codex works on TH1 networking, multiplayer rooms, member civ/ready/team/AI slot config, multiplayer saves, deterministic sync, P2P queueing, MapData broadcasts or MapDataDebug diff tools, ActionConfirm/ActionExecute, reconnect, Timer-driven network callbacks, or any bug that may affect multiplayer reliability or ordering. --- # TH1 Network Sync @@ -24,7 +24,7 @@ Do not let one peer advance local game state unless the matching network send/re - `Unity/Assets/Scripts/TH1_Logic/Core/Main.cs` - `Unity/Assets/Scripts/TH1_Logic/Action/ActionLogic.cs` - `Unity/Assets/Scripts/TH1_Instance/Timer.cs` -- `Unity/Assets/Scripts/TH1_Logic/Editor/NetworkStressEditorWindow.cs` when changing or interpreting network stress tests +- `Unity/Assets/Scripts/TH1_Logic/Editor/NetworkStressEditorWindow.cs` when changing or interpreting network stress tests or the `Tools/Steam MapData一致性诊断` editor window For the current network contract summary, read `references/network-contract.md`. For the one-click Steam P2P stress tool, report fields, and current healthy baseline, read `references/network-stress.md`. @@ -89,6 +89,12 @@ For the one-click Steam P2P stress tool, report fields, and current healthy base - Keep per-run `RunId` isolation so stale packets or ACKs from a previous test cannot pollute a new report. - The host should export after collecting client reports or after the configured report wait timeout; clients should retry report sends briefly after test completion. +10. Use `MapDataDebugMessage` for pre-reconnect divergence captures. + - `P2PMsgType.MapDataDebug` carries current `MapData` only for diagnostics: member-to-host or host-broadcast. + - The receiver compares the incoming map against local `Main.MapData` and logs `[MapDataDebug]`; it must not call `NetResumeMatch`, change `GameState`, request `ForceUpdate`, or mutate gameplay state. + - The editor entry is `Tools/Steam MapData一致性诊断`; use it when normal `ForceUpdate` diff logs are too late because the host has already advanced. + - Keep the send path on `GameNetSender` / lobby queues so large-map diagnostics still cover ordered P2P chunking. + ## Checks Before Finishing Run: @@ -113,6 +119,7 @@ For network-heavy changes, inspect these risks explicitly: - Could code reintroduce the old assumption that `MultiCivs.Count == current lobby member count`? - Could `MemberCiv.Index`, `PlayerId`, or `TeamId` diverge between host and clients before `GameStart`? - Could `MapData` deserialize with missing core fields and still be used? +- Could `MapDataDebugMessage` accidentally trigger reconnect, ForceUpdate, or any map mutation? - Could a timer callback fire after the target UI/object state is gone? - Could a retry loop or ordered gap wait forever? - If stress tooling or queue throughput changed, does the one-click two-machine report meet the baseline in `references/network-stress.md`? @@ -128,3 +135,4 @@ For network-heavy changes, inspect these risks explicitly: - Do not derive player position from lobby member order after slots exist; use `MemberCiv.Index`. - Do not start the host game from client-local optimistic ready state. - Do not call `GC.Collect()` in match entry paths as a networking fix. +- Do not use `MapDataDebugMessage` as a repair/sync mechanism; it is logging-only diagnostics. diff --git a/.codex/skills/th1-network-sync/agents/openai.yaml b/.codex/skills/th1-network-sync/agents/openai.yaml index cc8ed5f6c..761e82194 100644 --- a/.codex/skills/th1-network-sync/agents/openai.yaml +++ b/.codex/skills/th1-network-sync/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "TH1 Network Sync" - short_description: "TH1 multiplayer sync, P2P, and lobby slot rules" - default_prompt: "Use the TH1 Network Sync skill for TH1 multiplayer, P2P, GameNet, lobby player slot/team config, save resume, reconnect, or deterministic sync work." + short_description: "TH1 multiplayer sync, P2P, MapData diagnostics, and lobby slot rules" + default_prompt: "Use the TH1 Network Sync skill for TH1 multiplayer, P2P, GameNet, lobby player slot/team config, MapData diagnostics, save resume, reconnect, or deterministic sync work." diff --git a/.codex/skills/th1-network-sync/references/network-contract.md b/.codex/skills/th1-network-sync/references/network-contract.md index 452b772fc..3b1c57dd2 100644 --- a/.codex/skills/th1-network-sync/references/network-contract.md +++ b/.codex/skills/th1-network-sync/references/network-contract.md @@ -31,6 +31,7 @@ This reference summarizes the multiplayer contract after the May 2026 pre-releas - `ActionConfirm` must return send success. - `ActionExecute` must return broadcast success. - `ForceUpdate` and full `MapData` sends must validate multiplayer map data before sending. +- `MapDataDebugMessage` sends current `MapData` for diagnostics only. Member-to-host and host-broadcast sends should use `GameNetSender` and the normal lobby queue path, but callers must not treat it as authoritative sync. - Heartbeat send timestamps should only update after send acceptance. ## GameNetReceiver @@ -43,6 +44,7 @@ This reference summarizes the multiplayer contract after the May 2026 pre-releas - `NetStartGame` and `NetResumeMatch` return `bool`; UI should only close/hide after success. - `ForceUpdate` should restore previous game state if resume fails. - `MapConfirm` must guard null maps, missing actions, and null action payloads. +- `MapDataDebugMessage` should compare incoming `MapData` with local `Main.MapData` and log `[MapDataDebug]` hash/action/diff details only. It must not call `NetResumeMatch`, change `GameState`, request `ForceUpdate`, or replace local map data. ## Network Stress Tool @@ -54,6 +56,19 @@ This reference summarizes the multiplayer contract after the May 2026 pre-releas - Host export should contain one report per lobby member when clients are reachable. If a report is late, host may update the same export file after receipt. - Current default test is a one-minute flow: 50 seconds of traffic and up to 10 seconds of report collection. +## MapData Debug Tool + +- Tool path: `Unity/Assets/Scripts/TH1_Logic/Editor/NetworkStressEditorWindow.cs`. +- Menu: `Tools/Steam MapData一致性诊断`. +- Diagnostics message: `MapDataDebugMessage` / `P2PMsgType.MapDataDebug`. +- Use when `MapConfirm`/`ForceUpdate` logs are too late to reveal the original divergence. The tool captures the sender's current `MapData` at button click time. +- Buttons: + - Members send current `MapData` to host. + - Host broadcasts current `MapData` to all members. +- Receiver logs whether local and remote maps match, map hashes, action counts, first action mismatch, and diff details. +- The tool is read-only from a gameplay perspective. It must not mutate `Main.MapData`, request reconnect, enter `ForceUpdating`, or hide/show gameplay UI. +- It should continue to use `GameNetSender` / lobby P2P queues so full-map diagnostics exercise ordered delivery and large-message chunking. + ## Lobby MapConfig / Ready State - Pre-game room settings and member state live in `Main.Instance.MapConfig`. diff --git a/Tools/PlayerBugViewer/Data/0.7.1/76561199887084970/1779986308860-81bcf8ae96a6.zip b/Tools/PlayerBugViewer/Data/0.7.1/76561199887084970/1779986308860-81bcf8ae96a6.zip new file mode 100644 index 000000000..0554064f8 Binary files /dev/null and b/Tools/PlayerBugViewer/Data/0.7.1/76561199887084970/1779986308860-81bcf8ae96a6.zip differ diff --git a/Tools/PlayerBugViewer/Data/0.7.1/76561199887084970/1779989621201-79035b05d816.zip b/Tools/PlayerBugViewer/Data/0.7.1/76561199887084970/1779989621201-79035b05d816.zip new file mode 100644 index 000000000..f1a8b9425 Binary files /dev/null and b/Tools/PlayerBugViewer/Data/0.7.1/76561199887084970/1779989621201-79035b05d816.zip differ diff --git a/Tools/PlayerBugViewer/Data/0.7.1/76561199887084970/1780038824886-6bd790349c9e.zip b/Tools/PlayerBugViewer/Data/0.7.1/76561199887084970/1780038824886-6bd790349c9e.zip new file mode 100644 index 000000000..3a2e24012 Binary files /dev/null and b/Tools/PlayerBugViewer/Data/0.7.1/76561199887084970/1780038824886-6bd790349c9e.zip differ diff --git a/Tools/PlayerBugViewer/README.md b/Tools/PlayerBugViewer/README.md index db89f160c..4ece8c76d 100644 --- a/Tools/PlayerBugViewer/README.md +++ b/Tools/PlayerBugViewer/README.md @@ -5,9 +5,9 @@ ## 使用 1. 运行 `启动玩家Bug查看器.bat`。 -2. 填写 OSS `AccessKey ID` / `AccessKey Secret`,确认 `Endpoint` 和 `Bucket`。 +2. 工具会自动读取 Unity OSS 编辑器保存的 `AccessKey ID` / `AccessKey Secret`,确认 `Endpoint` 和 `Bucket`。 3. 点击「更新内容」拉取 `bugreport/` 下的 zip。 4. 选择条目查看玩家自述、版本、SteamID、CrashSight 设备 ID、设备信息、附带存档。 5. 点击「一键替换到本地存档」会删除目标目录下所有 `map_archive_*.dat` / `.bak`,再写入该汇报中的 `start + continue/end` 存档。 -配置会保存到 `config.local.json`,该文件应只保留在本机。 +也可以用 `config.local.json` 或环境变量覆盖 OSS 配置,该文件应只保留在本机。 diff --git a/Tools/PlayerBugViewer/config.local.json b/Tools/PlayerBugViewer/config.local.json new file mode 100644 index 000000000..306c85425 --- /dev/null +++ b/Tools/PlayerBugViewer/config.local.json @@ -0,0 +1,8 @@ +{ + "access_key_id": "LTAI5t7x5WJDtoFNpxDq4MWh", + "access_key_secret": "fBDtyz7Z38B7rvRXHNzqVflw6LThXX", + "endpoint": "oss-cn-shanghai.aliyuncs.com", + "bucket": "th1-oss", + "save_config_dir": "C:\\Users\\wuwenbo\\AppData\\LocalLow\\Remilia Command\\Config", + "skip_existing_downloads": true +} \ No newline at end of file diff --git a/Tools/PlayerBugViewer/player_bug_viewer.py b/Tools/PlayerBugViewer/player_bug_viewer.py index eb5fd2760..4a261e102 100644 --- a/Tools/PlayerBugViewer/player_bug_viewer.py +++ b/Tools/PlayerBugViewer/player_bug_viewer.py @@ -5,6 +5,7 @@ import hmac import json import os import shutil +import sys import threading import urllib.parse import urllib.request @@ -17,6 +18,12 @@ from tkinter import ttk APP_DIR = Path(__file__).resolve().parent +TOOLS_DIR = APP_DIR.parent +if str(TOOLS_DIR) not in sys.path: + sys.path.insert(0, str(TOOLS_DIR)) + +from oss_viewer_config import merge_default_oss_config, merge_local_oss_config + DATA_DIR = APP_DIR / "Data" CONFIG_PATH = APP_DIR / "config.local.json" OSS_PREFIX = "bugreport/" @@ -39,10 +46,10 @@ def default_config() -> dict: def load_config() -> dict: - cfg = default_config() + cfg = merge_default_oss_config(default_config()) if CONFIG_PATH.exists(): try: - cfg.update(json.loads(CONFIG_PATH.read_text(encoding="utf-8"))) + cfg = merge_local_oss_config(cfg, json.loads(CONFIG_PATH.read_text(encoding="utf-8"))) except Exception: pass return cfg diff --git a/Tools/PlayerMultilingualReportViewer/README.md b/Tools/PlayerMultilingualReportViewer/README.md index d2411d70e..7f1362d94 100644 --- a/Tools/PlayerMultilingualReportViewer/README.md +++ b/Tools/PlayerMultilingualReportViewer/README.md @@ -5,8 +5,8 @@ ## 使用 1. 运行 `启动多语言汇报查看器.bat`。 -2. 填写 OSS `AccessKey ID` / `AccessKey Secret`,确认 `Endpoint` 和 `Bucket`。 +2. 工具会自动读取 Unity OSS 编辑器保存的 `AccessKey ID` / `AccessKey Secret`,确认 `Endpoint` 和 `Bucket`。 3. 点击「更新内容」拉取 `multilingualreport/` 下的 zip。 4. 选择条目查看多语言 ID、玩家选择的字符串、玩家自述、当前解析文本、版本、SteamID、CrashSight 设备 ID 和设备信息。 -配置会保存到 `config.local.json`,本地下载缓存放在 `Data/`,两者都只应保留在本机。 +也可以用 `config.local.json` 或环境变量覆盖 OSS 配置。本地下载缓存放在 `Data/`,两者都只应保留在本机。 diff --git a/Tools/PlayerMultilingualReportViewer/config.local.json b/Tools/PlayerMultilingualReportViewer/config.local.json new file mode 100644 index 000000000..95696d039 --- /dev/null +++ b/Tools/PlayerMultilingualReportViewer/config.local.json @@ -0,0 +1,7 @@ +{ + "access_key_id": "LTAI5t7x5WJDtoFNpxDq4MWh", + "access_key_secret": "fBDtyz7Z38B7rvRXHNzqVflw6LThXX", + "endpoint": "oss-cn-shanghai.aliyuncs.com", + "bucket": "th1-oss", + "skip_existing_downloads": true +} \ No newline at end of file diff --git a/Tools/PlayerMultilingualReportViewer/player_multilingual_report_viewer.py b/Tools/PlayerMultilingualReportViewer/player_multilingual_report_viewer.py index 20ee0fe44..6b4a0b47b 100644 --- a/Tools/PlayerMultilingualReportViewer/player_multilingual_report_viewer.py +++ b/Tools/PlayerMultilingualReportViewer/player_multilingual_report_viewer.py @@ -4,6 +4,7 @@ import hashlib import hmac import json import os +import sys import threading import urllib.parse import urllib.request @@ -16,6 +17,12 @@ from tkinter import ttk APP_DIR = Path(__file__).resolve().parent +TOOLS_DIR = APP_DIR.parent +if str(TOOLS_DIR) not in sys.path: + sys.path.insert(0, str(TOOLS_DIR)) + +from oss_viewer_config import merge_default_oss_config, merge_local_oss_config + DATA_DIR = APP_DIR / "Data" CONFIG_PATH = APP_DIR / "config.local.json" OSS_PREFIX = "multilingualreport/" @@ -32,10 +39,10 @@ def default_config() -> dict: def load_config() -> dict: - cfg = default_config() + cfg = merge_default_oss_config(default_config()) if CONFIG_PATH.exists(): try: - cfg.update(json.loads(CONFIG_PATH.read_text(encoding="utf-8"))) + cfg = merge_local_oss_config(cfg, json.loads(CONFIG_PATH.read_text(encoding="utf-8"))) except Exception: pass return cfg diff --git a/Tools/oss_viewer_config.py b/Tools/oss_viewer_config.py new file mode 100644 index 000000000..1772ed991 --- /dev/null +++ b/Tools/oss_viewer_config.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +import os +from typing import Any + + +UNITY_EDITOR_PREFS_REG_PATH = r"Software\Unity Technologies\Unity Editor 5.x" + +UNITY_OSS_PREF_KEYS = { + "access_key_id": "OssDownload_AccessKeyId", + "access_key_secret": "OssDownload_AccessKeySecret", + "endpoint": "OssDownload_Endpoint", + "bucket": "OssDownload_Bucket", +} + +ENV_OSS_KEYS = { + "access_key_id": ( + "TH1_OSS_ACCESS_KEY_ID", + "OSS_ACCESS_KEY_ID", + "ALIYUN_ACCESS_KEY_ID", + "ALIBABA_CLOUD_ACCESS_KEY_ID", + ), + "access_key_secret": ( + "TH1_OSS_ACCESS_KEY_SECRET", + "OSS_ACCESS_KEY_SECRET", + "ALIYUN_ACCESS_KEY_SECRET", + "ALIBABA_CLOUD_ACCESS_KEY_SECRET", + ), + "endpoint": ("TH1_OSS_ENDPOINT", "OSS_ENDPOINT"), + "bucket": ("TH1_OSS_BUCKET", "OSS_BUCKET"), +} + + +def _clean_string(value: str) -> str: + return value.replace("\x00", "").strip() + + +def _decode_editor_prefs_value(value: Any) -> str: + if isinstance(value, bytes): + raw = value.split(b"\x00", 1)[0] + for encoding in ("utf-8", "mbcs", "latin-1"): + try: + return _clean_string(raw.decode(encoding)) + except Exception: + pass + return "" + if isinstance(value, str): + return _clean_string(value) + return "" + + +def _read_windows_unity_pref(pref_key: str) -> str: + try: + import winreg + except Exception: + return "" + + try: + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, UNITY_EDITOR_PREFS_REG_PATH) as key: + index = 0 + while True: + try: + name, value, _value_type = winreg.EnumValue(key, index) + except OSError: + break + index += 1 + if name == pref_key or name.startswith(f"{pref_key}_h"): + decoded = _decode_editor_prefs_value(value) + if decoded: + return decoded + except OSError: + return "" + + return "" + + +def load_unity_oss_editor_prefs() -> dict[str, str]: + config: dict[str, str] = {} + for field, pref_key in UNITY_OSS_PREF_KEYS.items(): + value = _read_windows_unity_pref(pref_key) + if value: + config[field] = value + return config + + +def load_env_oss_config() -> dict[str, str]: + config: dict[str, str] = {} + for field, env_names in ENV_OSS_KEYS.items(): + for env_name in env_names: + value = os.environ.get(env_name, "").strip() + if value: + config[field] = value + break + return config + + +def merge_default_oss_config(config: dict[str, Any]) -> dict[str, Any]: + merged = dict(config) + merged.update(load_unity_oss_editor_prefs()) + merged.update(load_env_oss_config()) + return merged + + +def merge_local_oss_config(config: dict[str, Any], local_config: dict[str, Any]) -> dict[str, Any]: + merged = dict(config) + for key, value in local_config.items(): + if key in UNITY_OSS_PREF_KEYS and isinstance(value, str) and not value.strip(): + continue + merged[key] = value + return merged