Repository navigation
Description
模块范围
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
fix(content): register badges without model database identity