Skip to content

[Bug] ModelCapabilities 读路径 no-op、写路径抛异常,Get() 成功后首次修改可能崩溃 #120

Closed

Description

@Twelve-eight

模块范围

Public API(公开 API)

严重程度

Minor(轻微)

发生了什么?

ModelCapabilities 的读路径和写路径对「persistence 尚未注册」这一前置条件处理不一致,导致 API 处于半可用状态。

读路径(CreateCollection,src/Models/Capabilities/ModelCapabilities.cs:90-97)把它当 no-op:

private static ModelCapabilitySet CreateCollection(AbstractModel model)
{
    var collection = new ModelCapabilitySet(model);
    collection.Load(null);
    if (IsInitialized)                    // ← 未初始化就跳过,不抛
        ModelSavedDataRuntime.GetBag(model).Set(SavedDataSlotKey, new ModelCapabilitySaveDocument(), false);
    return collection;
}

写路径(MarkSavedDataDirty,同文件 :72-79)直接抛:

internal static void MarkSavedDataDirty(AbstractModel model)
{
    if (!IsInitialized)
        throw new InvalidOperationException(
            "ModelCapabilities persistence has not been registered. Register a model capability or default capability modifier during mod initialization before mutating model capabilities.");
    ModelSavedDataRuntime.GetBag(model).Set(SavedDataSlotKey, new ModelCapabilitySaveDocument());
}

后果:调用方可以在 mod 初始化早期成功 Get() 到 capability set,然后在第一次修改能力时崩溃。

触发条件与真实调用方(已核实)

EnsureInitialized() 由 ModContentRegistry 的 3 处调用(:887 RegisterModelCapability、:923、:941)。也就是说:

  • 只要 mod 注册过任意一个 model capability,IsInitialized 就为 true,本条不触发。
  • 触发条件是:mod 先对某个 model 调 ModelCapabilities.Get() / Get<T>() / 尝试修改,之后才注册第一个 capability(或者永远不注册)。

真实调用方确实存在(git grep 确认):

位置 用途
src/Interactions/RightClick/ModRightClickRegistry.cs:736 右键交互取能力集
src/Diagnostics/DebugTools/RitsuDebugCapabilityActions.cs:304、:356 调试工具
src/Diagnostics/DebugTools/RitsuDebugStatePresetActions.cs:580 状态预设
src/Models/Capabilities/ModelCapabilities.cs:140、:150、:160 公开 API 包装

所以这是一个顺序依赖缺陷,不是"必崩"——但它让 API 在早期初始化阶段的表现不可预测,且失败信息("persistence has not been registered")没有指出"你应该先注册 capability"。

复现步骤

  1. 在一个 mod 的早期初始化阶段(ModContentRegistry 注册 capability 之前)
  2. 对任意 AbstractModel 调用 ModelCapabilities.Get(model) → 成功返回
  3. 对该 set 做任意修改,触发 MarkSavedDataDirty → InvalidOperationException

期望行为

两条路径对同一前置条件行为一致。要么都抛(明确「必须先注册」),要么都 no-op(先允许内存态存在,注册后再补持久化)。

从 CreateCollection 的写法看,作者的意图似乎是后者。

实际行为

读成功、写崩溃。

建议修法

若采纳 no-op 语义:

internal static void MarkSavedDataDirty(AbstractModel model)
{
    if (!IsInitialized)
        return;                          // 与 CreateCollection 保持一致
    ModelSavedDataRuntime.GetBag(model).Set(SavedDataSlotKey, new ModelCapabilitySaveDocument());
}

版本

  • 复核基准:main @ 2332d9d0(v0.6.7,2026-10-08)
  • dev 分支另有 2 个未发布提交(d99863e6、1fc1bff9),仅改 NModExtraHand.cs / FreePlayBindingRegistry.cs / RitsuLibFramework.cs,与本问题无关

Activity

  1. added
    area/apiPublic API, contracts, or compatibility surface.
    priority/p3Low priority or backlog.
    severity/minorMinor defect, polish issue, or low-impact bug.
    on Oct 19, 2026
  2. added a commit that references this issue on Oct 20, 2026
    1c91505
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

    area/apiPublic API, contracts, or compatibility surface.priority/p3Low priority or backlog.severity/minorMinor defect, polish issue, or low-impact bug.status/needs-triageNeeds maintainer or analyzer triage.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions