Skip to content

[Bug] RegisterBadge 100% 必抛 NullReferenceException:徽章模板不是 AbstractModel #121

Closed

Description

@Twelve-eight

模块范围

Public API(公开 API)

严重程度

Broken feature(功能不可用)

发生了什么?

ModContentRegistry.RegisterBadge(Type) 这个公开 API 100% 必抛 NullReferenceException,任何 mod 第一次调用它就崩溃。

调用链

1. 入口无条件调用 PrimeOwnedType(src/Content/ModContentRegistry.cs:1057-1060)

public void RegisterBadge(Type badgeType)
{
    EnsureBadgeType(badgeType, nameof(badgeType));
    EnsureMutable($"register badge '{badgeType.Name}'");
    PrimeOwnedType(badgeType);          // ← 无 try/catch,无前置守卫

2. 一路走到 ModelDb.GetId(:2230-2232)

private void PrimeOwnedType(Type type)
{
    var previousId = ModelDb.GetId(type);   // ← 这里抛

3. 引擎侧沿基类链查找,遇到非 AbstractModel 派生类型就 NRE(MegaCrit.Sts2.Core.Models/ModelDb.cs:520-527)

public static Type GetCategoryType(Type type)
{
    Type type2 = type;
    while (type2.BaseType != typeof(AbstractModel))
    {
        type2 = type2.BaseType;      // 走到 object 后 BaseType == null,
    }                                // 下一轮 null.BaseType → NullReferenceException
    return type2;
}

4. 而 EnsureBadgeType 只校验到 ModBadgeTemplate,不校验 AbstractModel(:2083-2090)

徽章模板的基类是 public abstract class ModBadgeTemplate(src/Scaffolding/Content/ModBadgeTemplate.cs:12),不继承 AbstractModel。所以第 1 步的守卫放行,第 2 步必炸。

为什么连"重复注册"都救不了

PrimeOwnedType 在 RegisteredBadges.Add 之前:

PrimeOwnedType(badgeType);              // :1060  ← 先炸

lock (SyncRoot)
{
    if (!RegisteredBadges.Add(badgeType))   // :1065  ← 永远到不了
    {
        _logger.Debug($"...Skipping duplicate badge registration...");
        return;
    }
    RememberOwner(badgeType);
}

也就是说 RegisteredBadges 永远加不进任何类型。两条观察通道都是空的,但它们读的不是同一份数据:

方法 位置 数据源 结果
GetRegisteredBadgeTypes() :1836-1844 RegisteredBadges 永远为空——Add 在 :1065,被 :1060 的异常挡住
GetRegisteredTypeSnapshots() :1560 RegisteredTypeOwners 也看不到徽章——该集合由 RememberOwner(:2234)填充,同样在异常之后

这个 API 完全不可用,不是"某些情况下有 bug"。

复现步骤

// 任意 mod 初始化期间
var registry = ModContentRegistry.For("MyModId");   // 没有 Instance 静态属性,入口是 For(modId)
registry.RegisterBadge(typeof(MyBadge));
// MyBadge : ModBadgeTemplate  → 必抛 NullReferenceException

期望行为

RegisterBadge 应能正常登记 ModBadgeTemplate 派生类型,并在 GetRegisteredTypeSnapshots 中可见。

实际行为

必抛 NullReferenceException,RegisteredBadges 永远为空。

影响面:走官方推荐写法就会踩

这不是"只有直接调底层 API 才会中招"。ModContentPackBuilder.Badge<TBadge>()(src/Scaffolding/Content/ModContentPackBuilder.cs:172-175)是给 mod 作者用的便捷入口,它的实现就是:

public ModContentPackBuilder Badge<TBadge>() where TBadge : ModBadgeTemplate
{
    return AddStep(ctx => ctx.Content.RegisterBadge<TBadge>());   // ← 走同一条必抛链
}

所以按官方文档的方式注册徽章,一样必抛。这提高了本条的严重度:它不是边角 API 的缺陷,而是"徽章"这一整类内容无法被任何 mod 使用。

建议修法

RegisterBadge 注册的是"徽章",而不是"模型",因此不该走 PrimeOwnedType(那条路径是给 AbstractModel 派生的内容用的,需要 ModelDb 分配 id)。

public void RegisterBadge(Type badgeType)
{
    EnsureBadgeType(badgeType, nameof(badgeType));
    EnsureMutable($"register badge '{badgeType.Name}'");
    // 徽章不是 AbstractModel,不参与 ModelDb 的 id 分配。
    // 若确实需要记归属,改用 RememberOwner 而非 PrimeOwnedType。

    lock (SyncRoot)
    {
        if (!RegisteredBadges.Add(badgeType))
        {
            _logger.Debug($"[Content] Skipping duplicate badge registration: {badgeType.Name}");
            return;
        }
        RememberOwner(badgeType);
    }
    ...
}

如果 PrimeOwnedType 对徽章确有别的用途(例如冲突检测),那它必须先判断 typeof(AbstractModel).IsAssignableFrom(type) 再决定是否走 ModelDb。

补充

  • 引擎 GetCategoryType 本身没有防御:传入任何非 AbstractModel 派生类型都会 NRE。这属于引擎侧的设计假设(调用方保证传入正确类型),所以责任在 RitsuLib 这一侧——它在没有该保证时就调用了。
  • 我是从静态审查发现的,没有实机触发。但三步链条(守卫放行 → GetId → 无终止条件的 while)全部可逐行核对,不依赖运行时行为。

版本

  • 复核基准: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/p1High priority regression or high-impact work.
    severity/brokenA feature path is effectively unusable.
    on Oct 19, 2026
  2. added a commit that references this issue on Oct 20, 2026
    f73e344
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/p1High priority regression or high-impact work.severity/brokenA feature path is effectively unusable.status/needs-triageNeeds maintainer or analyzer triage.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions