Skip to content

内容包与注册器

本文是 RitsuLib 注册体系的参考文档。

它主要解释:

  • CreateContentPack(...) 与底层各个注册器的关系
  • Apply() 到底做了什么
  • 什么时候该用链式构建器、清单条目、直接调用注册器,或可选的 CLR 特性
  • 固定模型身份与 ModelDb 集成是怎样建立在注册之上的
  • 生成式占位(卡牌 / 遗物 / 药水)的 API、顺序与风险说明
  • Mod 自有卡堆与顶栏按钮(ModCardPileRegistry / ModTopBarButtonRegistry,共用 mod id)

注册器总览

RitsuLib 按职责拆分了几类注册器:

注册器作用
ModContentRegistry注册角色、Act、池内卡牌/遗物/药水、能力、球体、附魔(Enchantment)、苦难(Affliction)、成就、单例、好/坏每日修正、共享卡/遗物/药水池、事件、Ancient、怪物及生成式占位等模型
ModKeywordRegistry注册可复用关键词定义
ModCardPileRegistry注册 Mod 自有卡堆(战斗/跑团 UI 卡堆;static_hover_tips 键与合格 pile id 对齐)
ModTopBarButtonRegistry注册 Mod 自有顶栏按钮(紧邻原版卡组按钮;static_hover_tips 键与合格按钮 id 对齐)
ModTimelineRegistry注册 StoryEpoch
ModUnlockRegistry注册纪元门槛与进度解锁规则

CreateContentPack(modId) 主要编排暴露在 ModContentPackContext 上的四类注册器(ContentKeywordsTimelineUnlocks)。卡堆与顶栏按钮通过 ModCardPileRegistry.For(modId)ModTopBarButtonRegistry.For(modId) 注册,或使用 STS2RitsuLib.Interop.AutoRegistration 下的可选 CLR 特性;它们与内容包共用同一 mod id,但不是 ModContentPackContext 上的字段。


CreateContentPack(...)

推荐默认使用链式构建器:

csharp
RitsuLibFramework.CreateContentPack("MyMod")
    .Character<MyCharacter>()
    .Card<MyCardPool, MyCard>()
    .Relic<MyRelicPool, MyRelic>()
    .CardKeywordOwnedByLocNamespace("brew")
    .Epoch<MyCharacterEpoch>()
    .Story<MyStory>()
    .RequireEpoch<MyLateCard, MyCharacterEpoch>()
    .Apply();

但需要明确的是,它不会:

  • 自动反射扫描内容
  • 自动替你重排注册顺序
  • 取代底层注册器的存在

它只是把一系列注册步骤按加入顺序记录下来,并在 Apply() 时顺序执行。


ModContentPackContext

Apply() 返回 ModContentPackContext,里面包含:

  • Content
  • Keywords
  • Timeline
  • Unlocks

ModCardPileRegistryModTopBarButtonRegistry 不在该结构体上;可在 Custom(...) 里调用 ModCardPileRegistry.For(ctx.ModId)ModTopBarButtonRegistry.For(ctx.ModId),或在 Mod 初始化流程中单独注册。


步骤顺序

构建器中的步骤严格按添加顺序执行。

这点在以下场景会很重要:

  • 某个 Custom(ctx => ...) 依赖前面已经注册的内容
  • 你希望日志顺序能准确反映初始化流程
  • 你在同一个 chain 中混合内容注册与自定义逻辑

构建器能做什么

构建器支持的步骤大致包括:

  • 内容模型注册
  • 关键词注册
  • 时间线注册
  • 解锁注册
  • 清单式注册
  • 任意自定义回调

一些不那么显眼,但很实用的入口包括:

  • Entry(IContentRegistrationEntry)
  • Entries(IEnumerable<IContentRegistrationEntry>)
  • Keyword(KeywordRegistrationEntry)
  • Keywords(IEnumerable<KeywordRegistrationEntry>)
  • Manifest(contentEntries, keywordEntries)
  • Custom(Action<ModContentPackContext>)
  • 生成式占位:PlaceholderCard<TPool>(...)PlaceholderRelic<TPool>(...)PlaceholderPotion<TPool>(...)
  • 扩展的单体/池类型:.Enchantment<T>().Affliction<T>().Achievement<T>().Singleton<T>().GoodModifier<T>() / .BadModifier<T>().SharedRelicPool<T>().SharedPotionPool<T>()

什么时候直接使用注册器

默认优先使用 CreateContentPack(...)

但以下情况直接使用注册器更合适:

  • 注册逻辑拆分在多个模块里
  • 你希望在自己的前置库里再包装一层 API
  • 你不想把所有注册都塞进一条长链
  • 你要程序化生成注册项

典型写法如下:

csharp
var content = RitsuLibFramework.GetContentRegistry("MyMod");
content.RegisterCharacter<MyCharacter>();

var timeline = RitsuLibFramework.GetTimelineRegistry("MyMod");
timeline.RegisterEpoch<MyEpoch>();

内容注册器的职责

ModContentRegistry 主要负责:

  • 记录某个模型类型归属于哪个 Mod
  • 校验重复注册与冲突
  • 为 ModelDb 补丁与其它集成点提供数据
  • 为已注册类型生成固定公开 ModelId.Entry

固定公开身份

对于通过 RitsuLib 注册的模型,公开 ModelId.Entry 会被强制成稳定格式:

<MODID>_<CATEGORY>_<TYPENAME>

这不是靠改你源码里的类型名实现的,而是通过 ModelDb 身份补丁在公开入口上统一的。


ModelDb 集成

仅仅完成注册还不够,游戏本身还必须"看得到"这些内容。

RitsuLib 通过对 ModelDb 及相关访问点打补丁来完成这件事,包括:

  • 追加已注册的角色、Act、能力、球体、事件、Ancient、共享池等
  • 将已注册卡牌/遗物/药水等与目标池绑定
  • 对已注册模型类型强制固定公开条目标识
  • 在缓存锁定前引导动态 Act 内容补丁

Freeze 行为

几个关键注册器都会在早期初始化后冻结:

  • 内容注册冻结
  • 时间线注册冻结
  • 解锁注册冻结

冻结之后再注册会直接抛异常。


Manifest 与 Entry 对象

如果你希望把注册描述成数据,可以使用注册条目对象:

csharp
var contentEntries = new IContentRegistrationEntry[]
{
    new CharacterRegistrationEntry<MyCharacter>(),
    new CardRegistrationEntry<MyCardPool, MyCard>(),
};

var keywordEntries = new[]
{
    KeywordRegistrationEntry.OwnedCardByLocNamespace("MyMod", "brew"),
};

RitsuLibFramework.CreateContentPack("MyMod")
    .Manifest(contentEntries, keywordEntries)
    .Apply();

CLR 特性注册(可选)

STS2RitsuLib.Interop.AutoRegistration 下的特性(例如 [RegisterSharedCardPool][RegisterCard(typeof(MyPool))])最终会调用与链式构建器、清单和直接注册器相同的底层 API。

它们在 RitsuLib 的早期 Mod 类型发现 阶段执行。类型必须能解析到某个 mod 身份(通常由 manifest 映射到程序集);否则可在类型上使用 [RitsuLibOwnedBy("modId")]

AutoRegistrationAttribute.Inherit

特性默认只作用于其标注的类型。Inherit 默认为 false。在基类上将某特性设为 Inherit = true 时,具体子类会按「若子类自身也写了同一条特性」的方式处理。


内容模型注册速查表

内容链式注册器Manifest 条目
角色.Character<T>()RegisterCharacter<T>()CharacterRegistrationEntry<T>
Act.Act<T>()RegisterAct<T>()ActRegistrationEntry<T>
池内卡牌.Card<TPool,TCard>(...)RegisterCard<TPool,TCard>(...)CardRegistrationEntry<TPool,TCard>
池内遗物.Relic<TPool,TRelic>(...)RegisterRelic<TPool,TRelic>(...)RelicRegistrationEntry<TPool,TRelic>
池内药水.Potion<TPool,TPotion>(...)RegisterPotion<TPool,TPotion>(...)PotionRegistrationEntry<TPool,TPotion>
能力.Power<T>()RegisterPower<T>()PowerRegistrationEntry<T>
球体.Orb<T>()RegisterOrb<T>()OrbRegistrationEntry<T>
附魔.Enchantment<T>()RegisterEnchantment<T>()EnchantmentRegistrationEntry<T>
苦难.Affliction<T>()RegisterAffliction<T>()AfflictionRegistrationEntry<T>
成就.Achievement<T>()RegisterAchievement<T>()AchievementRegistrationEntry<T>
单例.Singleton<T>()RegisterSingleton<T>()SingletonRegistrationEntry<T>
每日修正(好).GoodModifier<T>()RegisterGoodModifier<T>()GoodModifierRegistrationEntry<T>
每日修正(坏).BadModifier<T>()RegisterBadModifier<T>()BadModifierRegistrationEntry<T>
共享卡池.SharedCardPool<T>()RegisterSharedCardPool<T>()SharedCardPoolRegistrationEntry<T>
共享遗物池.SharedRelicPool<T>()RegisterSharedRelicPool<T>()SharedRelicPoolRegistrationEntry<T>
共享药水池.SharedPotionPool<T>()RegisterSharedPotionPool<T>()SharedPotionPoolRegistrationEntry<T>
共享事件.SharedEvent<T>()RegisterSharedEvent<T>()SharedEventRegistrationEntry<T>
怪物(无链式封装)RegisterMonster<T>()MonsterRegistrationEntry<T>
占位卡牌/遗物/药水.PlaceholderCard<...>(...)RegisterPlaceholderCard<...>(...)PlaceholderCardRegistrationEntry<...>

生成式占位内容

用于在尚未为每张牌 / 每个遗物 / 每个药水编写独立 CLR 类型时,仍能注册进池子并获得稳定、可预测的公开 ModelId.Entry,以便奖励表、解锁、存档引用等流程先跑通。

API 概要

场景推荐入口
链式内容包PlaceholderCard<TPool>(stableEntryStem, PlaceholderCardDescriptor)PlaceholderRelic<TPool>(...)PlaceholderPotion<TPool>(...)
直接注册器ModContentRegistry.RegisterPlaceholderCard<TPool>(...)

示例

csharp
using MegaCrit.Sts2.Core.Entities.Cards;
using STS2RitsuLib.Content;

RitsuLibFramework.CreateContentPack("MyMod")
    .Manifest(contentEntries, keywordEntries)
    .Custom(ctx =>
    {
        ctx.Content.RegisterPlaceholderCard<MyCardPool>("wip_reward_attack",
            new PlaceholderCardDescriptor(
                BaseCost: 1,
                Type: CardType.Attack,
                Rarity: CardRarity.Common,
                Target: TargetType.AnyEnemy));
    })
    .Apply();

警告(请务必阅读)

存档与 Entry 稳定性:占位一旦进入存档或解锁数据,其 ModelId.Entry 即成为长期契约。改名 / 改 stem / 改 FromFullPublicEntry 字符串可能导致旧档、旧解锁引用失效。

无玩法效果:占位不会替你实现伤害、抽牌、遗物触发等。仅保证模型存在、池子能展开。

联机与 ModelIdSerializationCache.Hash:生成类型不会出现在游戏原生的 AllAbstractModelSubtypes 扫描结果中。加载的 Mod 组合不同 → Hash 不同 → 与未使用占位/未使用相同 Mod 列表的客户端可能无法联机


推荐注册模式

对大多数 Mod,建议这样组织:

  1. 在初始化入口中创建一个内容包
  2. 在其中注册所有内容、关键词、时间线节点与解锁规则
  3. Custom(...) 保持小而显式
  4. 不要把注册拖到运行期 hook 再做
  5. 使用 TypeListCardPoolModel 时,用 .Card<池, 牌>()CardRegistrationEntry 登记池内牌;不要覆写已过时的 CardTypes