Skip to content
This repository was archived by the owner on Sep 20, 2026. It is now read-only.
This repository was archived by the owner on Sep 20, 2026. It is now read-only.

RFC: 按 Run / 战斗区间接入 Steam Timeline #103

Description

@pengx17

Context

当前 BPP 有自己的战斗回放与本地 MP4 录制链路,但 The Bazaar 本身没有使用 Steam Timeline。当前游戏安装包自带的 Steamworks.NET 和原生 Steam API 已包含 ISteamTimeline v004,游戏也已经负责 Steam API 初始化、callback pump 和 shutdown(decompiled/TheBazaarRuntime/TheBazaar.Steam/SteamModule.cs:152-212、:267-295),因此 BPP 可以在不重复管理 Steam 生命周期的前提下补充游戏定义的录像导航信息。

Dota 2 的 Steam Game Recording 设计重点是:录像按一场可理解的游戏 session 浏览,并由游戏填充少量有意义的导航标记;玩家仍可添加自己的手动标记。BPP 不需要复制 Dota 2 的击杀、团战、肉山等高密度事件,而应映射 The Bazaar 自己的天然层级:

  • 一整个 Bazaar run = Steam GamePhase
  • run 中每次英雄等级提升 = 一个低噪音 instantaneous marker(clip=None)
  • run 中每场具体 PvP 战斗 = 一个 Featured range
  • 完整一局结束 = range 终点的 Victory / Defeat 标记
  • 单局内部物品、技能、伤害、frame = 不解析、不上报

Steam 官方明确把“a single run in a roguelike”列为 GamePhase 的适用示例;GamePhase 用于把后台录像按玩家理解的 session 分行浏览,range event 则用于推荐具体剪辑区间:

本 RFC 不用 Steam 替代现有 FFmpeg 自动录制;Steam 负责用户已启用的后台/按需录像及剪辑 UI,BPP 只提供结构化 Timeline 语义。

Related to #96(战后原生 replay 的一键录制并导出入口)。

协作过程

sequenceDiagram
    participant U as 用户
    participant C as Codex
    participant Code as BPP / The Bazaar
    participant Steam as Steam Timeline docs

    U->>C: 询问能否复用 Steam 原生录制
    C->>Code: 确认游戏携带 Timeline API,但 Bazaar/BPP 均未调用
    C->>Steam: 核对 Featured range 与 GamePhase 语义
    C-->>U: 初稿:给回放添加战斗 range
    U->>C: 收敛为具体战斗区间,起点含 Day/对手,终点含完整结算
    U->>C: 借鉴 Dota 2,但不解析单局内部海量事件
    C->>Steam: 确认 roguelike run 正适合作为 GamePhase
    C-->>U: 修订为 Run → PvP Battle Range → Result End Marker
Loading

设计原则

借鉴 Dota 2 的部分

  • 时间轴首先帮助玩家回答“这是哪一场 session、哪一段值得看”。
  • 游戏标记用于导航和剪辑,不是完整 telemetry。
  • 保留玩家自己的手动 marker,不试图预测所有精彩瞬间。
  • 点击一个高价值区间即可得到合理的建议剪辑边界。

不照搬 Dota 2 的部分

  • 不解析单局内部击杀等价物、物品摧毁、技能触发、伤害、血量变化或每个 combat frame。
  • 不为“可能精彩”的内部时刻设计启发式评分。
  • 不把 Steam Timeline 变成战斗日志;详细战斗日志仍属于 BPP 自己的 UI/数据能力。
  • 不因为 Steam API 可用就自动替用户开启录像、保存 clip 或导出 MP4。

最终方案

flowchart LR
    A["Run initialized"] --> B["StartGamePhase"]
    B --> C["SetGamePhaseID(runId)"]
    C --> D["Tag: hero / mode(可用时)"]
    D -. "run 内 0..N 次" .-> U["Instant: Level N reached"]
    D --> E["Day N PvP starts"]
    E --> F["Start Featured range: vs Opponent"]
    F --> G["完整战斗画面"]
    G --> H["Update range: Victory / Defeat"]
    H --> I["Add end marker"]
    I --> J["End range"]
    J --> K{"还有下一场战斗?"}
    K -->|是| E
    K -->|Run completed/interrupted| L["Finalize phase attributes/tags"]
    L --> M["EndGamePhase"]
Loading

1. Run = GamePhase

每个真实 Bazaar run 启动一个 Steam GamePhase:

  • StartGamePhase()
  • SetGamePhaseID(stableRunId)
  • 可用时添加低基数 phase tags:英雄、模式、最终 run 结果。
  • 可用时维护 phase attributes:最终 Day、胜场数等摘要;属性只作为 session 元数据,不创建时间轴事件。
  • run 正常结束或中断时 exactly-once EndGamePhase()。

BPP 已有稳定的 RunInitializedObserved.RunId(src/BazaarPlusPlus/Core/Events/RunInitializedObserved.cs:4-7)以及 run started/ended/interrupted 生命周期(src/BazaarPlusPlus/Game/RunLifecycle/RunLifecycleModule.cs:35-50、:64-117)。实现需自行保存 active run ID:当前生命周期模块在发布离开-run 事件前会清空 CurrentServerRunId(:87-107),结束处理不能临时回读已被清空的上下文。

2. 每场 PvP = Featured range

一场具体 PvP 只贡献一个连续区间:

时机 Steam Timeline 行为 示例
完整战斗画面开始 StartRangeTimelineEvent(..., Featured) Day 8 · Battle vs Alice
range 描述 展示双方英雄和必要上下文 Vanessa vs Pygmalien · Ranked
完整战斗正常结束 UpdateRangeTimelineEvent 写入结果 Day 8 · Battle vs Alice · Victory
同一终点 添加一个 instantaneous end marker Battle ended · Victory
最后 EndRangeTimelineEvent(handle) range 边界覆盖完整战斗

现有 PvpBattleManifest 已包含 BattleId、RunId、Day/Hour、双方英雄、对手信息和结果(src/BazaarPlusPlus/Game/PvpBattles/PvpBattleManifest.cs:6-26、PvpBattleParticipants.cs:4-36、PvpBattleOutcome.cs:4-11),可用于文案和身份关联,不需要解析单局 combat frames。

3. 英雄等级提升 = 低噪音瞬时导航标记

这里的“升级”限定为 run 内英雄/玩家等级提升,不包含物品升阶、附魔或战斗中的 card-upgraded 事件。

  • 等级实际生效后添加一个 instantaneous event,例如 Level 7 reached · Day 5。
  • 该 marker 用于回看 run 进度,不默认作为精彩剪辑,因此 ePossibleClip = None;图标显示优先级保持中等,避免压过 PvP Featured range。
  • 第一版只记录新等级和可用的 Day,不展开升级奖励、选择项、获得的技能/物品等内容。
  • 以 runId + newLevel 去重;断线重连、状态 reconciliation 或重新进入 LevelUpState 不得重复标记。
  • 事件时间必须对应等级已经生效/升级展示确认开始的稳定 seam,不能只监听 Events.LevelUpVisuals:游戏在重载回 LevelUpState 时也会重新触发升级 visuals(decompiled/TheBazaarRuntime/TheBazaar.SequenceFramework.VisualSequences/TransitionInComponent.cs:446-451)。
  • 具体等级读取与完成 seam 在实现前按 decompiled 状态流确认;禁止把 CardUpgradedSimEvent 或 pedestal item upgrades 混入该 marker。

4. 边界必须对应可见画面

  • range 起点是玩家看到完整战斗开始的时刻,不能直接使用 simulation 消息到达;simulation 在视觉播放前已经包含全部 frames 和 winner(src/BazaarPlusPlus/Patches/Combat/CombatSimulationPatches.cs:13-21)。
  • range 终点是完整战斗画面/结算播放完成,不能延长到玩家之后点击 Continue、进入商店或离开战后界面。
  • 具体 native start/end seam 必须在实现前基于 decompiled 游戏调用链确认,并用主路径结构化日志实机校准;不创建独立 probe 工程。
  • 中止/失败时关闭 range,并通过 UpdateRangeTimelineEvent(..., clip=None) 取消 Featured 推荐;不生成虚假的 Victory/Defeat end marker。

5. 回放与真实战斗分开

第一阶段以真实 run 中的 PvP 战斗为权威 Timeline,因为这最接近 Dota 2 的“实际 match/session”体验,也能直接利用 Steam 后台录像。

BPP 历史/导入 replay 的 Timeline range 放到第二阶段:

  • 必须在标题中明确写 Replay · Day N · vs Opponent。
  • 不把 replay 冒充原始 battle 的发生时刻。
  • 不加入仍在进行的真实 run GamePhase。
  • 同一进程已经标记过该 battle 的 live range 时,是否仍标记 replay 需通过 UX 验证决定,默认避免重复污染 Timeline。

6. 文案与数据降级

字段 首选 逐级降级
Phase ID stable run ID 无稳定 ID 时不启动 phase
Phase tag groups Hero、Mode、Run Result 只添加已验证且低基数的字段
Phase attributes Final Day、Wins 只显示最终摘要,不生成额外事件
Battle title Day N · Battle vs <OpponentName> Day N · PvP Battle → PvP Battle
Battle description <PlayerHero> vs <OpponentHero> · Mode 只显示存在的非敏感字段
Level title Level N reached Level up
Level description Day N · <Hero> 只显示存在字段
End marker Battle ended · Victory/Defeat Battle ended

Account ID、Steam ID、内部 message ID、完整 battle payload 等不得进入用户可见 Timeline 或日志。

7. 图标与本地化 Label

Steam 的 range、instantaneous event 和 phase tag 都能携带文字与图标:

  • range/event:title、description、icon、显示 priority、clip priority;
  • phase tag:tagName、tagIcon、tagGroup;
  • phase attribute:attributeGroup、attributeValue。

在不依赖 The Bazaar 官方协作的前提下,第一版只考虑 Steam 官方内置图标。下表是用于实机验证的候选映射,最终选择需根据 Timeline UI 的实际尺寸、拥挤规则和可读性再确认:

Timeline 语义 图标 Title / Label 示例 Description 示例
Run hero tag steam_person Vanessa(group: Hero) —
Run mode tag steam_flag Ranked(group: Mode) —
Run completed steam_completed Run completed Day 12 · 10 wins
Hero level-up steam_[level](0–99) Level 7 reached Day 5 · Vanessa
PvP battle range steam_combat Day 8 · Battle vs Alice Vanessa vs Pygmalien · Ranked
Battle victory steam_trophy Battle ended · Victory vs Alice
Battle defeat steam_death Battle ended · Defeat vs Alice
Interrupted/unknown steam_caution Battle interrupted 不生成胜负结论
Replay(第二阶段) steam_view Replay · Day 8 · vs Alice 原始 battle metadata
  • 数字图标由 Steam 原生支持 steam_0–steam_99;等级超出范围或图标不可用时回退 steam_plus。
  • 事件 Title 保持短、可扫描;Description 承载对阵/模式等次级信息,不把所有字段堆入标题。
  • Steam API 要求传入 Steam UI 语言对应的本地化字符串。adapter/workflow 应读取 Steam UI language,映射到 BPP LocalizedTextSet;未支持语言回退英文。
  • 第一版至少覆盖英文、简体中文;其余沿用 BPP 已支持语言与 fallback 规则。
  • BPP 不能把运行时读取到的 Unity Sprite、Texture2D、本地 PNG 路径或 URL 直接传给 Timeline;Steam API 的 icon 参数只接受内置图标名或该产品预先上传的自定义图标名。
  • 不承诺逐事件自定义颜色;Steam 仅通过 SetTimelineGameMode 控制时间轴模式区段颜色,具体事件视觉由 Steam UI 决定。

当前落地边界

  • 第一阶段不依赖 The Bazaar 官方提供 Steamworks 权限,也不把自定义图标作为交付条件。
  • 不在运行时导出、转换或上传游戏资产;BPP 读取到的英雄/遭遇 Sprite 仍只用于游戏内 Unity UI。
  • 自定义游戏图标仅保留为未来可能性;如果没有官方协作,功能应长期使用 Steam 内置图标正常工作。

暂留方案问题

以下问题在首次 Steam UI 实机验证前不做过早定案:

  • steam_combat、steam_trophy、steam_death 等候选图标在拥挤时间轴中的辨识度是否足够,哪些需要调整 priority 或统一回退 steam_marker?
  • 数字图标 steam_[level] 在 Timeline 中是否比 steam_plus 更易理解,是否会与 Day 数字混淆?
  • Hero、Mode、Run Result 应放入 phase tag、attribute 还是事件 Description,怎样避免同一信息重复出现?
  • 动态游戏名称与固定 UI 文案分别从哪里本地化;Steam UI language 与游戏当前语言不一致时如何组合和降级?
  • 对手昵称是否默认进入用户可见 Timeline,还是仅显示对手英雄以减少隐私与异常字符问题?
  • 如果未来获得官方协作,应该使用完整英雄头像、专门裁切的小头像还是英雄徽记;资产授权、Steamworks 上传、图标名版本管理和内置图标 fallback 如何约定?

Steam 边界与降级

  • Steam Timeline 是附加输出,不成为 run、战斗、回放或本地录制的前置条件。
  • 不调用 SteamAPI.Init / Shutdown,不额外 pump callbacks;复用游戏生命周期。
  • 调用失败限制在 adapter 内并记录结构化诊断,不能向 event bus 订阅者抛出。
  • 只使用 Steam 内置 steam_* 图标;BPP 无权上传 The Bazaar 自定义 Steamworks 图标或设置商店页 Timeline feature flag。
  • TimelineEventHandle_t 只在当前进程有效;battle range handle 不持久化。
  • run ID 可用于 SetGamePhaseID,但必须先确认是否允许暴露到本机 Steam 元数据;如有隐私/稳定性疑虑,使用不可逆的本地派生 ID。
  • 玩家未开启 Steam Game Recording 时可以发送 Timeline 语义,但不会产生可剪辑视频。

实施清单

0. 设计与实机前置

  • 以 decompiled 游戏代码确认真实 PvP 可见画面的精确 start/end seam,区分 simulation、动画播放、结算和 ReplayState。
  • 确认 Online/PTR、macOS/Windows 游戏包中的 Steamworks.NET 与原生 Timeline 版本。
  • 通过临时主路径结构化日志校准 start/end 与 Steam 录像画面的偏移,不创建独立 probe。
  • 对修订后的 Run → Battle Range 生命周期做独立、只读 red-team 评审,以 file:line 证据修订,并在实现前交回确认。

1. Steam 运行时边界

  • 在主插件 csproj 显式引用游戏自带 com.rlabrecque.steamworks.net.dll,避免复制或打包第二份。
  • 在 GameInterop/SteamTimeline/ 建立薄 adapter:phase、range、instant event、title/description/icon/priority、可用性检查和 no-op 降级。
  • 不重复初始化、关闭或 pump Steam API;版本或接口缺失时安全禁用。

2. Run GamePhase

  • 新增 timeline workflow,消费 RunInitializedObserved 与 RunLifecycleChanged,持有自己的 active run ID/phase 状态。
  • 处理 run-start 与 run-ID 乱序、状态 reconciliation、重复事件、completed/interrupted、插件禁用和游戏退出。
  • 只添加已验证的低基数 tags/attributes,并为文案提供本地化与缺失值降级。
  • 保证任意路径最多一个 active GamePhase,结束 exactly-once。

3. PvP Battle range

  • 为 live PvP 建立稳定 battle ID 与当前 phase 的关联;禁止用“latest battle”猜测。
  • 可见战斗开始时创建 Day/Opponent Featured range。
  • 正常结束时更新结果、添加一个 Battle ended marker,并关闭 range。
  • 中止/失败时取消 Featured、关闭或移除 range,不生成胜负标记。
  • 处理连续战斗、重连、重复 start/end、场景卸载、run 先结束和 late callback。
  • 明确禁止解析或发布战斗内部物品/技能/伤害/frame 事件。

4. 英雄等级提升标记

  • 基于稳定等级状态/消息确定“新等级已生效”的 seam,不单独依赖可重复播放的 visuals。
  • 每个 runId + newLevel 最多创建一个 Level N reached instantaneous marker。
  • marker 使用 clip=None,只包含等级与可用 Day,不解析升级奖励内容。
  • 断线重连、重载 LevelUpState、状态 reconciliation、插件重启恢复时不重复标记。
  • 明确排除 item/card upgrade、附魔和 pedestal upgrade 事件。

5. 回放第二阶段

  • 在 live Timeline 实机稳定后,再评估 BPP replay range。
  • Replay 标题必须显式区分,且不得加入 live run phase。
  • 定义同一 battle 的 live range/replay range 去重策略。

6. 设置与可观察性

  • 增加独立 Steam Timeline 开关,默认值在实现前确认。
  • 建立可替换的 icon/label candidate mapping;通过实机 UI 验证上述待决问题后再固定最终映射。根据 Steam UI language 选择本地化 Title、Description、tag group 和 attribute label,未支持语言回退英文。
  • 结构化日志覆盖 phase/range 生命周期、可用性和降级 reason,不记录账号身份或敏感字段。
  • Timeline 开关只控制 Steam 标记;现有 RecordVideo 独立控制本地 MP4。

7. 测试与实机验证

  • 为 phase/range 纯状态机、乱序/重复事件、图标/本地化文案映射、隐私过滤和中止降级增加 Unity-free 测试。
  • 运行相关 RunLifecycle/CombatReplay 测试与主项目构建。
  • 通过 Steam 启动,在 macOS/Windows 分别验证后台录制、按需录制中、录像关闭和 Overlay 不可用。
  • 验证一个 run 显示为单一 phase;多场 PvP 各自是 Day/Opponent range,顺序和结果正确。
  • 验证 range 只覆盖完整战斗,不包含 simulation 等待、战前加载或战后停留。
  • 点击 Featured range 后可以整场战斗为边界创建、调整并导出 clip。
  • 验证 run interrupted、战斗中断、重连、连续战斗、插件/游戏退出和 BPP 本地录制并行。

验收标准

  • 一个真实 Bazaar run 在 Steam 录像中对应且只对应一个 GamePhase。
  • phase 使用稳定、隐私安全的 ID,并显示可用的 Hero/Mode/Run Result 摘要。
  • 每次确认的英雄等级提升产生且只产生一个 Level N reached marker,使用 clip=None;重载/重连不重复。
  • 等级 marker 不包含升级奖励明细,也不混入物品/card upgrade。
  • run 中每场真实 PvP 产生且只产生一个完整 Featured range。
  • range 起点标识 Day 和具体对手(字段可用时),起止与可见战斗画面边界一致。
  • 正常结束时 range 显示最终 Victory/Defeat,并在终点产生且只产生一个结束标记。
  • 不解析、不发布任何单局内部物品、技能、伤害或 frame 事件。
  • Run tags、level marker、battle range、胜负/中断 marker 使用经实机确认的 Steam 内置图标和本地化短标签;候选映射可调整,图标缺失安全回退 steam_marker。
  • 英文和简体中文 Title/Description/tag/attribute label 正确;未知 Steam UI language 回退英文。
  • 点击 range 可进入整场战斗建议剪辑流程;玩家手动 marker 不受影响。
  • Steam 不可用或用户未开启录像时,run、战斗、回放和本地 FFmpeg 录制不受影响。
  • 不重复管理 Steam API 生命周期、不打包冲突 Steamworks.NET、不持久化 session-only battle handle。
  • 相关测试、构建及 macOS/Windows Steam 实机验证通过。

验证情况

本 issue 建立/修订时只完成了只读核对:

  • 确认当前 The Bazaar/BPP 均未调用 Timeline,但游戏托管与原生 Steam API 包含 Timeline v004。
  • 确认游戏负责 Steam API 初始化、callback pump 与 shutdown。
  • 确认 Steam 官方建议用 GamePhase 表示一场 PvP match 或 roguelike run。
  • 确认 BPP 已有 stable run ID 与 run started/ended/interrupted 观察面。
  • 确认游戏有独立 LevelUpState / LevelUp visuals,且重载进入该状态可能重复触发 visuals,因此需要按 run+level 去重。
  • 确认 battle manifest 已提供 Day、对手、双方英雄和胜负结果。
  • 确认 Steam API 支持 phase ID/tags/attributes、Featured range、range 更新/结束和瞬时终点事件。
  • 确认 Steam 官方提供 title/description/tag group/attribute label,以及 steam_combat、steam_plus、steam_trophy、steam_death、steam_caution、steam_view 和 steam_0–steam_99 内置图标。
  • 尚未在 The Bazaar 进程中调用 Timeline API 或观察实际 Steam UI。
  • 尚未确认 live PvP 可见画面的精确 start/end seam。
  • 尚未验证 Windows/PTR binding/export。

已知局限 / 后续工作

  • 第一版不会自动开启 Steam 录像、保存 clip、导出 MP4 或返回文件路径。
  • 第一版只做 Run / hero level-up / PvP battle / battle result 四类低噪音语义,不做战斗内部 highlights,也不标记物品升级。
  • 第一版聚焦真实 run;BPP 历史/导入 replay range 是第二阶段。
  • PvE 是否作为 Standard range、10-win run completion 是否增加额外 run 终点 marker,需在首版实机 UI 后单独决策。
  • 商店页声明 Timeline 支持和上传自定义图标需要 The Bazaar 开发方的 Steamworks 权限,mod 无法独立完成;这不是第一阶段的前置条件,仅作为未来可选协作方向保留。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestneeds-triageMaintainer needs to evaluate this issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions