自定义事件
本文说明如何通过 RitsuLib 将自定义事件接入游戏的事件管线。
它覆盖三类注册:
- 共享事件:
SharedEvent<TEvent>() - Act 专属事件:
ActEvent<TAct, TEvent>() - Ancient:
SharedAncient<TAncient>()/ActAncient<TAct, TAncient>()
游戏原版事件管线
游戏中事件的生成与执行涉及以下环节:
| 阶段 | 游戏类型 | 职责 |
|---|---|---|
| 候选生成 | ActModel.GenerateRooms(...) | 从 Act 本地事件池和 ModelDb.AllSharedEvents 共享池构建候选列表 |
| 过滤 | RoomSet.EnsureNextEventIsValid(...) | 按 IsAllowed(runState) 与已访问记录过滤 |
| 进入 | EventRoom.Enter(...) | 预加载资源、创建可变实例、搭建事件界面 |
| 资源 | EventModel.GetAssetPaths(...) | 提供进入事件前需要准备的资源路径 |
RitsuLib 的注册机制
RitsuLib 不替换上述流程,而是在注册阶段把 Mod 事件补充进原版已有的事件入口:
- 共享事件追加到
ModelDb.AllSharedEvents - Act 事件追加到对应 Act 的事件列表
- Ancient 追加到对应的共享或 Act 本地 Ancient 列表
最小普通事件
推荐继承 ModEventTemplate,而不是直接继承原版 EventModel。
csharp
using MegaCrit.Sts2.Core.Events;
using STS2RitsuLib.Scaffolding.Content;
public sealed class MyFirstEvent : ModEventTemplate
{
protected override IReadOnlyList<EventOption> GenerateInitialOptions()
{
return
[
new EventOption(this, Accept, InitialOptionKey("ACCEPT")),
new EventOption(this, Leave, InitialOptionKey("LEAVE")),
];
}
private Task Accept()
{
SetEventFinished(L10NLookup($"{Id.Entry}.pages.ACCEPT.description"));
return Task.CompletedTask;
}
private Task Leave()
{
SetEventFinished(L10NLookup($"{Id.Entry}.pages.LEAVE.description"));
return Task.CompletedTask;
}
}注册方式
共享事件
csharp
RitsuLibFramework.CreateContentPack("MyMod")
.SharedEvent<MyFirstEvent>()
.Apply();Act 专属事件
csharp
RitsuLibFramework.CreateContentPack("MyMod")
.ActEvent<MyAct, MyFirstEvent>()
.Apply();Ancient
csharp
RitsuLibFramework.CreateContentPack("MyMod")
.SharedAncient<MyAncient>()
.Apply();csharp
RitsuLibFramework.CreateContentPack("MyMod")
.ActAncient<MyAct, MyAncient>()
.Apply();本地化键
通过 RitsuLib 注册后,事件的 ModelId.Entry 采用固定格式:
<MODID>_EVENT_<TYPENAME>例如 MyMod 与 MyFirstEvent:
MY_MOD_EVENT_MY_FIRST_EVENT最小普通事件的本地化块示例:
json
{
"MY_MOD_EVENT_MY_FIRST_EVENT.title": "陌生的泉眼",
"MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.description": "你在路边发现了一口发光的泉眼。",
"MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.options.ACCEPT.title": "饮下泉水",
"MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.options.ACCEPT.description": "也许会有好事发生。",
"MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.options.LEAVE.title": "离开",
"MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.options.LEAVE.description": "你决定不冒险。",
"MY_MOD_EVENT_MY_FIRST_EVENT.pages.ACCEPT.description": "你感觉精神好了很多。",
"MY_MOD_EVENT_MY_FIRST_EVENT.pages.LEAVE.description": "你转身离开。"
}为什么要用 ModEventTemplate
原版 EventModel.InitialOptionKey(...) 使用 GetType().Name(经 Slugify 处理)拼接键前缀,而事件标题、页面描述等使用 Id.Entry。
对原版事件,这两者通常一致。但对通过 RitsuLib 注册的事件,GetType().Name 和 Id.Entry 不同,会导致部分文本查找落在不同的键前缀上。
ModEventTemplate 和 ModAncientEventTemplate 通过 protected new 隐藏了基类的 InitialOptionKey,统一基于最终注册后的 Id.Entry 生成选项键。
给指定 Ancient 追加可条件化选项
csharp
using MegaCrit.Sts2.Core.Events;
using MegaCrit.Sts2.Core.Models.Events;
using STS2RitsuLib.Scaffolding.Ancients.Options;
RitsuLibFramework.CreateContentPack("MyMod")
.AncientOption<Neow>(
new ModAncientOptionRule(ancient =>
[
new EventOption(
ancient,
() =>
{
ancient.SetEventFinished(ancient.L10NLookup("NEOW.pages.DONE.description"));
return Task.CompletedTask;
},
"NEOW.pages.INITIAL.options.MYMOD_BONUS")
])
{
Condition = ancient => ancient.Owner?.Character is MyCharacter,
Priority = 100,
})
.Apply();