内容包与注册器
本文是 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 | 注册 Story 与 Epoch |
ModUnlockRegistry | 注册纪元门槛与进度解锁规则 |
CreateContentPack(modId) 主要编排暴露在 ModContentPackContext 上的四类注册器(Content、Keywords、Timeline、Unlocks)。卡堆与顶栏按钮通过 ModCardPileRegistry.For(modId)、ModTopBarButtonRegistry.For(modId) 注册,或使用 STS2RitsuLib.Interop.AutoRegistration 下的可选 CLR 特性;它们与内容包共用同一 mod id,但不是 ModContentPackContext 上的字段。
CreateContentPack(...)
推荐默认使用链式构建器:
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,里面包含:
ContentKeywordsTimelineUnlocks
ModCardPileRegistry 与 ModTopBarButtonRegistry 不在该结构体上;可在 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
- 你不想把所有注册都塞进一条长链
- 你要程序化生成注册项
典型写法如下:
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 对象
如果你希望把注册描述成数据,可以使用注册条目对象:
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>(...) 等 |
示例
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,建议这样组织:
- 在初始化入口中创建一个内容包
- 在其中注册所有内容、关键词、时间线节点与解锁规则
Custom(...)保持小而显式- 不要把注册拖到运行期 hook 再做
- 使用
TypeListCardPoolModel时,用.Card<池, 牌>()或CardRegistrationEntry登记池内牌;不要覆写已过时的CardTypes