Skip to content

自定义事件

本文说明如何通过 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>

例如 MyModMyFirstEvent

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().NameId.Entry 不同,会导致部分文本查找落在不同的键前缀上。

ModEventTemplateModAncientEventTemplate 通过 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();