Getting Started

Last updated:

Published on

Introduction

This guide walks through the full setup — from declaring the dependency to registering your first content.


简介

本指南覆盖从声明依赖到注册第一个内容的完整流程。


1. Declare the Dependency

Add STS2-RitsuLib to your mod_manifest.json:

json
{
  "id": "MyMod",
  "name": "My Mod",
  "dependencies": ["STS2-RitsuLib"]
}

1. 声明依赖

mod_manifest.json 中添加:

json
{
  "id": "MyMod",
  "name": "My Mod",
  "dependencies": ["STS2-RitsuLib"]
}

2. Initialize Your Mod

Use [ModInitializer] to declare the entry point. Obtain a logger, create a patcher, and register content:

csharp
using System.Reflection;
using STS2RitsuLib;
using STS2RitsuLib.Patching.Core;
using MegaCrit.Sts2.Core.Logging;
using MegaCrit.Sts2.Core.Modding;

[ModInitializer(nameof(Initialize))]
public static class MyMod
{
    public static Logger Logger { get; private set; } = null!;

    public static void Initialize()
    {
        Logger = RitsuLibFramework.CreateLogger("MyMod");
        RitsuLibFramework.EnsureGodotScriptsRegistered(Assembly.GetExecutingAssembly(), Logger);

        var patcher = RitsuLibFramework.CreatePatcher("MyMod", "core-patches");
        patcher.RegisterPatches<MyModPatches>();
        patcher.PatchAll();

        RitsuLibFramework.CreateContentPack("MyMod")
            .Character<MyCharacter>()
            .Card<MyCardPool, MyCard>()
            .Card<MyCardPool, MyOtherCard>()
            .Relic<MyRelicPool, MyRelic>()
            .Apply();
    }
}

For the full mapping of fluent methods, ModContentRegistry calls, and IContentRegistrationEntry types (enchantments, achievements, shared pools, manifests, etc.), see Content Packs & Registries.

CreatePatcher takes a patcherName used for log identification. A mod may create multiple patchers. See Patching Guide for the full patch workflow.

If your mod uses custom Godot C# scene scripts, keep EnsureGodotScriptsRegistered(...) in your initializer. See Godot Scene Authoring.


2. 初始化 Mod

使用 [ModInitializer] 声明入口方法,在其中获取 Logger、创建 Patcher 并注册内容:

csharp
using System.Reflection;
using STS2RitsuLib;
using STS2RitsuLib.Patching.Core;
using MegaCrit.Sts2.Core.Logging;
using MegaCrit.Sts2.Core.Modding;

[ModInitializer(nameof(Initialize))]
public static class MyMod
{
    public static Logger Logger { get; private set; } = null!;

    public static void Initialize()
    {
        Logger = RitsuLibFramework.CreateLogger("MyMod");
        RitsuLibFramework.EnsureGodotScriptsRegistered(Assembly.GetExecutingAssembly(), Logger);

        var patcher = RitsuLibFramework.CreatePatcher("MyMod", "core-patches");
        patcher.RegisterPatches<MyModPatches>();
        patcher.PatchAll();

        RitsuLibFramework.CreateContentPack("MyMod")
            .Character<MyCharacter>()
            .Card<MyCardPool, MyCard>()
            .Card<MyCardPool, MyOtherCard>()
            .Relic<MyRelicPool, MyRelic>()
            .Apply();
    }
}

链式方法、ModContentRegistryIContentRegistrationEntry(附魔、成就、共享池、Manifest 等)的完整对照见 内容包与注册器

CreatePatcherpatcherName 参数用于日志标识。同一个 Mod 可以创建多个 Patcher。完整补丁写法见 补丁系统

如果你的 Mod 使用了自定义 Godot C# 场景脚本,请把 EnsureGodotScriptsRegistered(...) 保留在初始化入口里。详见 Godot 场景编写说明


3. Define a Card Pool

Use TypeListCardPoolModel for pool visuals and metadata (frame, energy color, etc.). Each card that belongs in the pool must be registered via .Card<MyCardPool, MyCard>(), CardRegistrationEntry<…>, or an equivalent step so ModContentRegistry records ownership and fixed ModelId.Entry, and ModHelper.AddModelToPool runs.

The base class already exposes a default empty CardTypes sequence and marks it [Obsolete]: new mods should not override CardTypes (no need to write => [] either). Match section 2 and keep the content pack / manifest as the single source of truth for pool cards.

csharp
using Godot;

public class MyCardPool : TypeListCardPoolModel
{
    public override string Title => "My Pool";
    public override string EnergyColorName => "orange";
    public override string CardFrameMaterialPath => "card_frame_orange";
    public override Color DeckEntryCardColor => new("d2a15a");
    public override bool IsColorless => false;
}

Legacy mods that still override CardTypes with a type list will get CS0618, and pairing that with pack registration for the same pool + card still duplicates AllCards—migrate to pack-only registration or add #pragma warning disable CS0618 for that override. Listing CardTypes only (no card registration) generally skips RitsuLib fixed entries and ownership—avoid it.

Generated placeholders: If you need stable ModelId values before authoring each card type (rewards, unlocks, etc.), use PlaceholderCard<TPool>(...) and the relic/potion equivalents. Full API, examples, and required warnings (save entry stability, multiplayer ModelIdSerializationCache hash, no gameplay effects) are in the “Generated placeholder content” section of Content Packs & Registries.


3. 定义卡池

使用 TypeListCardPoolModel 承载池的视觉与元数据(边框、能量色等)。属于该池的每张牌必须在内容包里通过 .Card<MyCardPool, MyCard>()CardRegistrationEntry<…> 或等价步骤登记,这样才会写入 ModContentRegistry 归属与固定 ModelId.Entry,并走 ModHelper.AddModelToPool

基类已为 CardTypes 提供默认空序列,并已标记 [Obsolete]新 Mod 不必覆写 CardTypes,也不必再写 => []。与第 2 节一致,以链式 / Manifest 为卡牌清单的唯一来源即可。

csharp
using Godot;

public class MyCardPool : TypeListCardPoolModel
{
    public override string Title => "My Pool";
    public override string EnergyColorName => "orange";
    public override string CardFrameMaterialPath => "card_frame_orange";
    public override Color DeckEntryCardColor => new("d2a15a");
    public override bool IsColorless => false;
}

若旧工程仍覆写 CardTypes 并在其中列举类型,会收到 CS0618,且若同时对同一池、同一张牌做了内容包注册,AllCards 仍会重复拼接;此时应迁移为「仅内容包注册」或仅为该覆写添加 #pragma warning disable CS0618。仅 CardTypes、不做卡牌注册时,通常拿不到 RitsuLib 固定 Entry 与归属,不建议。

生成式占位:若尚未为每张牌编写 CLR 类型,但需要稳定 ModelId 让奖励、解锁等流程先跑通,可使用 PlaceholderCard<TPool>(...) 及遗物/药水对应 API。完整说明、示例与必读警告(存档 entry、联机 ModelIdSerializationCache Hash、无玩法效果等)见 内容包与注册器 中的「生成式占位内容」一节。


4. Define a Card

Inherit from ModCardTemplate and pass base properties in the primary constructor:

csharp
public class MyCard : ModCardTemplate(
    baseCost: 1,
    type: CardType.Attack,
    rarity: CardRarity.Common,
    target: TargetType.SingleEnemy)
{
    public override string Title => "Strike";
    public override string Description => 
quot;Deal
{Damage} damage.";
// Optional custom portrait public override string? CustomPortraitPath => "res://MyMod/art/strike.png"; public override void Use(ICombatContext ctx, ICreatureState user, ICreatureState? target) { ctx.DealDamage(user, target, Damage); } }

4. 定义卡牌

继承 ModCardTemplate,在主构造函数中传入基础属性:

csharp
public class MyCard : ModCardTemplate(
    baseCost: 1,
    type: CardType.Attack,
    rarity: CardRarity.Common,
    target: TargetType.SingleEnemy)
{
    public override string Title => "打击";
    public override string Description => 
quot;造成
{Damage} 点伤害。";
// 可选:自定义立绘路径 public override string? CustomPortraitPath => "res://MyMod/art/strike.png"; public override void Use(ICombatContext ctx, ICreatureState user, ICreatureState? target) { ctx.DealDamage(user, target, Damage); } }

5. Localization Keys

The ModelId.Entry for any RitsuLib-registered model is derived as:

<MODID>_<CATEGORY>_<TYPENAME>

All segments are normalized to UPPER_SNAKE_CASE.

Mod IdC# TypeCategoryEntry
MyModMyCardcardMY_MOD_CARD_MY_CARD
MyModMyRelicrelicMY_MOD_RELIC_MY_RELIC
MyModMyCharactercharacterMY_MOD_CHARACTER_MY_CHARACTER

Localization file example:

json
{
  "MY_MOD_CARD_MY_CARD.title": "Strike",
  "MY_MOD_CARD_MY_CARD.description": "Deal {damage} damage."
}

5. 本地化 Key

RitsuLib 注册的所有模型,其 ModelId.Entry 由以下规则推导(各字段规范化为全大写下划线格式):

<MODID>_<CATEGORY>_<TYPENAME>
Mod IdC# 类型类别Entry
MyModMyCardcardMY_MOD_CARD_MY_CARD
MyModMyRelicrelicMY_MOD_RELIC_MY_RELIC
MyModMyCharactercharacterMY_MOD_CHARACTER_MY_CHARACTER

本地化文件示例:

json
{
  "MY_MOD_CARD_MY_CARD.title": "打击",
  "MY_MOD_CARD_MY_CARD.description": "造成 {damage} 点伤害。"
}

6. Subscribe to Lifecycle Events

csharp
// Runs once after game is ready
RitsuLibFramework.SubscribeLifecycle<GameReadyEvent>(evt =>
{
    Logger.Info("Game ready.");
});

// On every combat start
RitsuLibFramework.SubscribeLifecycle<CombatStartingEvent>(evt =>
{
    // evt.RunState, evt.CombatState
});

Replayable events (IReplayableFrameworkLifecycleEvent) fire immediately upon late subscription if the event has already occurred.


6. 订阅生命周期事件

csharp
// 游戏就绪后执行一次
RitsuLibFramework.SubscribeLifecycle<GameReadyEvent>(evt =>
{
    Logger.Info("游戏已就绪。");
});

// 每次战斗开始时
RitsuLibFramework.SubscribeLifecycle<CombatStartingEvent>(evt =>
{
    // evt.RunState, evt.CombatState
});

可重放事件(IReplayableFrameworkLifecycleEvent)即使在事件已发生后订阅也会立即回调,无需关心订阅时机。


7. Persistent Data

Use BeginModDataRegistration for batch key registration. Persistent entries are class-based and need both a registry key and a file name:

csharp
public sealed class CounterData
{
    public int Value { get; set; }
}

using (RitsuLibFramework.BeginModDataRegistration("MyMod"))
{
    var store = RitsuLibFramework.GetDataStore("MyMod");
    store.Register<CounterData>(
        key: "my_counter",
        fileName: "counter.json",
        scope: SaveScope.Profile,
        defaultFactory: () => new CounterData());
}

See Persistence Guide for scopes, reload timing, and migrations.


7. 数据持久化

使用 BeginModDataRegistration 批量注册存档数据键。持久化条目以类为单位注册,同时需要注册键和文件名:

csharp
public sealed class CounterData
{
    public int Value { get; set; }
}

using (RitsuLibFramework.BeginModDataRegistration("MyMod"))
{
    var store = RitsuLibFramework.GetDataStore("MyMod");
    store.Register<CounterData>(
        key: "my_counter",
        fileName: "counter.json",
        scope: SaveScope.Profile,
        defaultFactory: () => new CounterData());
}

关于作用域、重载时机和迁移机制,可继续阅读 持久化设计


Next Steps

继续阅读