### Install STS2.RitsuLib Package Source: https://sts2-ritsulib.ritsukage.com/guide/getting-started Add the package reference to your mod project. ```xml ``` -------------------------------- ### Create I18N instance with CreateModLocalization and example zhs.json (Chinese) Source: https://sts2-ritsulib.ritsukage.com/guide/localization-and-keywords Chinese version of the I18N creation example. The JSON file shows the expected format for the Chinese localization file (zhs.json). ```csharp var i18n = RitsuLibFramework.CreateModLocalization( modId: "MyMod", instanceName: "settings", pckFolders: ["res://MyMod/localization/settings"]); var label = i18n.Get("settings.enabled", "启用"); ``` ```json { "settings.enabled": "启用" } ``` -------------------------------- ### Register a Secondary Resource Source: https://sts2-ritsulib.ritsukage.com/guide/secondary-resources Use RitsuLibFramework.GetSecondaryResourceRegistry to declare a new combat resource. This example demonstrates setting up a resource with a maximum amount, turn-start reset policy, and custom icons. ```csharp var resources = RitsuLibFramework.GetSecondaryResourceRegistry("MyMod"); var charge = resources.Register("charge", new SecondaryResourceDefinition( defaultAmount: 0, baseMaxAmount: 3, turnStartPolicy: SecondaryResourceTurnStartPolicy.ResetToMax, persistencePolicy: SecondaryResourcePersistencePolicy.Combat, smallIconPath: "res://MyMod/assets/ui/charge_small.png", largeIconPath: "res://MyMod/assets/ui/charge_large.png") { ClampToMaxAmount = true, }); ``` -------------------------------- ### Architect dialogue example Source: https://sts2-ritsulib.ritsukage.com/guide/localization-and-keywords Example JSON for Architect dialogue with a mod character. Uses `THE_ARCHITECT.talk..*` keys. Optional `-attack` values are `None`, `Player`, `Architect`, or `Both`; optional `-visit` keys can override the visit index; explicit `-startattack` and `-endattack` keys set `StartAttackers` and `EndAttackers` respectively. ```json { "THE_ARCHITECT.talk.MY_MOD_CHARACTER_SEER.0-0.char": "The exit is yours, but the cost is mine.", "THE_ARCHITECT.talk.MY_MOD_CHARACTER_SEER.0-1.ancient": "Then pay precisely.", "THE_ARCHITECT.talk.MY_MOD_CHARACTER_SEER.0-attack": "Both" } ``` -------------------------------- ### Architect dialogue with startattack/endattack/visit Source: https://sts2-ritsulib.ritsukage.com/guide/localization-and-keywords Example JSON for Architect dialogue with explicit `-startattack`, `-endattack`, and `-visit` keys. `-startattack` sets `StartAttackers`, `-endattack` sets `EndAttackers`, and `-visit` overrides the visit index. ```json { "THE_ARCHITECT.talk.MY_MOD_CHARACTER_SEER.1-startattack": "Player", "THE_ARCHITECT.talk.MY_MOD_CHARACTER_SEER.1-endattack": "Both", "THE_ARCHITECT.talk.MY_MOD_CHARACTER_SEER.1-visit": "3" } ``` -------------------------------- ### Manage Looping Audio Handles Source: https://sts2-ritsulib.ritsukage.com/guide/fmod-and-audio Use PlayLoop to start looping audio and retain the returned handle to adjust parameters or stop playback later. ```csharp var loop = GameAudioService.Shared.PlayLoop( AudioSource.Event("event:/MyMod/ambience/engine"), new AudioPlaybackOptions { Routing = new AudioRoutingOptions(Channel: "my_mod_ambience"), Scope = AudioLifecycleScope.Run, }); loop?.TrySetParameter("danger", 0.5f); loop?.TryStop(); ``` -------------------------------- ### Mod ancient dialogue example Source: https://sts2-ritsulib.ritsukage.com/guide/localization-and-keywords Example JSON for a mod ancient talking to a mod character. Uses `MY_MOD_ANCIENT_MIRROR` and `MY_MOD_CHARACTER_SEER` entries, with a repeating dialogue using `r` after the line index. `ModAncientEventTemplate` reads `{ancient}.talk.firstVisitEver.*` for the first visit ever, `{ancient}.talk..*` for a specific character, and `{ancient}.talk.ANY.*` for character-agnostic fallback. ```json { "MY_MOD_ANCIENT_MIRROR.talk.MY_MOD_CHARACTER_SEER.0-0.ancient": "You brought a future with you.", "MY_MOD_ANCIENT_MIRROR.talk.MY_MOD_CHARACTER_SEER.0-0.next": "Continue", "MY_MOD_ANCIENT_MIRROR.talk.MY_MOD_CHARACTER_SEER.0-1.char": "Then I should spend it carefully.", "MY_MOD_ANCIENT_MIRROR.talk.ANY.0-0r.ancient": "Again, a face in the glass.", "MY_MOD_ANCIENT_MIRROR.talk.ANY.0-1r.char": "Again, a choice." } ``` -------------------------------- ### HarmonyAsyncIl.RedirectAwaitedCalls example Source: https://sts2-ritsulib.ritsukage.com/guide/patching-guide Use for async state-machine transpilers. RedirectAwaitedCalls only redirects calls directly awaited by the state machine and requires the replacement to preserve stack shape and exact awaitable return type. The example validates the rewrite with RequireSucceeded, RequireExactly(1), and InstructionsChecked. ```csharp var rewriter = HarmonyIlRewriter.From(instructions); var report = HarmonyAsyncIl.RedirectAwaitedCalls( rewriter, "Redirect awaited OnPlay", originalOnPlayMethod, wrapperMethod, code => code.Any(instruction => HarmonyIl.IsCallTo(instruction, wrapperMethod))); report.RequireSucceeded(); report.RequireExactly(1); return rewriter.InstructionsChecked("Redirect awaited OnPlay"); ``` -------------------------------- ### Architect dialogue example (Chinese) Source: https://sts2-ritsulib.ritsukage.com/guide/localization-and-keywords Example JSON for Architect dialogue with a mod character (Chinese version). Uses `THE_ARCHITECT.talk..*` keys. Optional `-attack` values are `None`, `Player`, `Architect`, or `Both`; optional `-visit` keys can override the visit index; explicit `-startattack` and `-endattack` keys set `StartAttackers` and `EndAttackers` respectively. ```json { "THE_ARCHITECT.talk.MY_MOD_CHARACTER_SEER.0-0.char": "出口属于你,但代价由我承担。", "THE_ARCHITECT.talk.MY_MOD_CHARACTER_SEER.0-1.ancient": "那就精确地支付。", "THE_ARCHITECT.talk.MY_MOD_CHARACTER_SEER.0-attack": "Both" } ``` -------------------------------- ### Create I18N instance with CreateModLocalization and example eng.json Source: https://sts2-ritsulib.ritsukage.com/guide/localization-and-keywords Creates an I18N instance for key-value JSON files outside the game's model tables. The JSON file shows the expected format for the fallback language (eng.json). ```csharp var i18n = RitsuLibFramework.CreateModLocalization( modId: "MyMod", instanceName: "settings", pckFolders: ["res://MyMod/localization/settings"]); var label = i18n.Get("settings.enabled", "Enabled"); ``` ```json { "settings.enabled": "Enabled" } ``` -------------------------------- ### Mod ancient dialogue example (Chinese) Source: https://sts2-ritsulib.ritsukage.com/guide/localization-and-keywords Example JSON for a mod ancient talking to a mod character (Chinese version). Uses `MY_MOD_ANCIENT_MIRROR` and `MY_MOD_CHARACTER_SEER` entries, with a repeating dialogue using `r`. `ModAncientEventTemplate` reads `{ancient}.talk.firstVisitEver.*` for the first visit ever, `{ancient}.talk..*` for a specific character, and `{ancient}.talk.ANY.*` for character-agnostic fallback. ```json { "MY_MOD_ANCIENT_MIRROR.talk.MY_MOD_CHARACTER_SEER.0-0.ancient": "你带来了一份未来。", "MY_MOD_ANCIENT_MIRROR.talk.MY_MOD_CHARACTER_SEER.0-0.next": "继续", "MY_MOD_ANCIENT_MIRROR.talk.MY_MOD_CHARACTER_SEER.0-1.char": "那我应该谨慎地使用它。", "MY_MOD_ANCIENT_MIRROR.talk.ANY.0-0r.ancient": "又一次,镜中映出一张脸。", "MY_MOD_ANCIENT_MIRROR.talk.ANY.0-1r.char": "又一次,一个选择。" } ``` -------------------------------- ### Get content and keyword registries and register a card and keyword Source: https://sts2-ritsulib.ritsukage.com/guide/content-packs-and-registries Shows how to obtain the content and keyword registries for a mod ID and use them to register a card and a keyword. The keyword registration uses RegisterCardKeywordOwnedByLocNamespace. ```csharp var content = RitsuLibFramework.GetContentRegistry("MyMod"); content.RegisterCard(); var keywords = RitsuLibFramework.GetKeywordRegistry("MyMod"); keywords.RegisterCardKeywordOwnedByLocNamespace("bleeding"); ``` -------------------------------- ### 添加共享 Tooltip 到动态变量(中文示例) Source: https://sts2-ritsulib.ritsukage.com/guide/card-dynamic-var-toolkit Chinese version of the shared tooltip example. The JSON keys provide localized title and description text for the tooltip. Use this when the game UI is in Chinese. ```csharp ModCardVars.Int("heat", Heat) .WithSharedTooltip("MY_MOD_HEAT", "res://MyMod/images/ui/heat.png"); ``` ```json { "MY_MOD_HEAT.title": "热量", "MY_MOD_HEAT.description": "部分卡牌会根据当前热量改变效果。" } ``` -------------------------------- ### Event key construction with ModEventTemplate (C# and JSON) Source: https://sts2-ritsulib.ritsukage.com/guide/localization-and-keywords Shows the C# helper methods for building localization keys and a JSON example for event entry MY_MOD_EVENT_QUIET_DOOR. The keys combine the event entry, page name, and option name. Keep page and option names stable after release because they are part of the player's localization and event-choice history. ```csharp protected string ModOptionKey(string pageName, string optionName); protected string InitialOptionKey(string optionName); protected LocString PageDescription(string pageName); ``` ```json { "MY_MOD_EVENT_QUIET_DOOR.pages.INITIAL.description": "A quiet door waits in the wall.", "MY_MOD_EVENT_QUIET_DOOR.pages.INITIAL.options.OPEN": "[Open] Step through.", "MY_MOD_EVENT_QUIET_DOOR.pages.DONE.description": "The room is quiet again." } ``` -------------------------------- ### Example entry ID: MY_MOD_CARD_MY_STRIKE Source: https://sts2-ritsulib.ritsukage.com/guide/content-authoring-toolkit Example of the entry ID for a card named MyStrike in mod MyMod. The entry is used by saves, model IDs, localization keys, asset defaults, unlock rules, and cross-mod references, and should be treated as stable after release. ```text MY_MOD_CARD_MY_STRIKE ``` -------------------------------- ### Register FMOD Banks and GUID Mappings Source: https://sts2-ritsulib.ritsukage.com/guide/fmod-and-audio Register banks and GUID mappings before attempting to use their associated events. ```csharp FmodStudioDeferredBankRegistration.RegisterBank("res://MyMod/audio/MyMod.bank"); FmodStudioDeferredBankRegistration.RegisterStudioGuidMappings("res://MyMod/audio/guid_map.json"); ``` -------------------------------- ### Successful response JSON Source: https://sts2-ritsulib.ritsukage.com/guide/telemetry-backend Successful response example. Return HTTP 200 or 202 with this JSON body. ```json { "ok": true, "accepted": 2, "rejected": 0, "request_id": "optional-log-correlation-id" } ``` -------------------------------- ### Ancient dialogue with .sfx FMOD event Source: https://sts2-ritsulib.ritsukage.com/guide/localization-and-keywords Example JSON showing how to attach an FMOD event to a dialogue line by appending `.sfx` to the line key. The value is the FMOD event path. ```json { "MY_MOD_ANCIENT_MIRROR.talk.ANY.0-0r.ancient.sfx": "event:/sfx/ui/enchant_simple" } ``` -------------------------------- ### Register Card, Relic, and Character with Attributes Source: https://sts2-ritsulib.ritsukage.com/guide/content-authoring-toolkit Shows attribute registration on concrete model types. Abstract classes are skipped. The example registers a card, a relic, and a character using the corresponding attributes. ```csharp [RegisterCard(typeof(MyCardPool))] public sealed class MyStrike : ModCardTemplate(1, CardType.Attack, CardRarity.Common, TargetType.SingleEnemy) { } [RegisterRelic(typeof(MyRelicPool))] public sealed class MyStarterRelic : ModRelicTemplate { } [RegisterCharacter] public sealed class MyCharacter : ModCharacterTemplate { } ``` -------------------------------- ### Chinese LocString JSON with DynamicVars placeholder Source: https://sts2-ritsulib.ritsukage.com/guide/loc-string-placeholder-resolution Chinese localization example for the same card, keeping the {damage} placeholder in the description. ```json { "MY_MOD_CARD_MY_STRIKE.title": "精准打击", "MY_MOD_CARD_MY_STRIKE.description": "造成 {damage} 点伤害。" } ``` -------------------------------- ### Set lobby values before run start Source: https://sts2-ritsulib.ritsukage.com/guide/persistence-guide Writes lobby values before a run starts via the slot's Lobby accessor. Lobby values are committed into the run snapshot when the new run begins. If SyncLobbyOnChange is enabled, Set and Modify also push the local contribution in multiplayer. ```csharp public static void SetLobbyChallenge(StartRunLobby lobby, string challengeId) { ChallengeData.Lobby.Modify(lobby, data => { data.ChallengeId = challengeId; }); } public static void SetLocalLoadout(StartRunLobby lobby, string loadoutId) { PlayerData.Lobby.Modify(lobby, lobby.NetService.NetId, data => { data.LoadoutId = loadoutId; }); } ``` -------------------------------- ### LocString JSON with DynamicVars placeholder Source: https://sts2-ritsulib.ritsukage.com/guide/loc-string-placeholder-resolution Example of a game localization JSON entry using the LocString pipeline. The description keeps the {damage} placeholder for a DynamicVars value. ```json { "MY_MOD_CARD_MY_STRIKE.title": "Measured Strike", "MY_MOD_CARD_MY_STRIKE.description": "Deal {damage} damage." } ``` -------------------------------- ### Read and Write Data Store Settings Source: https://sts2-ritsulib.ritsukage.com/guide/persistence-guide Use this snippet to read and write settings in a data store. Get returns the live object, Modify mutates it, and Save() persists changes explicitly unless another layer calls Save() for you. ```csharp var store = RitsuLibFramework.GetDataStore("MyMod"); var settings = store.Get("settings"); store.Modify("settings", data => { data.Volume = 60; }); store.Save("settings"); ``` -------------------------------- ### HarmonyIlRewriter TryInsertAfterFirst with pattern Source: https://sts2-ritsulib.ritsukage.com/guide/patching-guide Use for transpilers that must edit IL. The example inserts a call to overrideMethod after the first occurrence of a pattern, then validates the rewrite with report expectations and InstructionsChecked. ```csharp var rewriter = HarmonyIlRewriter.From(instructions); var pattern = HarmonyIlPattern.Sequence( HarmonyIl.IsLdstr("prefix"), HarmonyIl.IsLdloc(), HarmonyIl.IsCall(concatMethod), HarmonyIl.IsStloc()); var report = rewriter.TryInsertAfterFirst( "MyPatch insert override", pattern, [ HarmonyIl.Ldarg(0), HarmonyIl.Call(overrideMethod), ], code => code.Any(instruction => HarmonyIl.IsCallTo(instruction, overrideMethod))); report.RequireSucceeded(); if (report.Applied > 0) report.RequireExactly(1); return rewriter.InstructionsChecked("MyPatch insert override"); ``` -------------------------------- ### Batch payload JSON example (ritsulib.telemetry.batch.v1) Source: https://sts2-ritsulib.ritsukage.com/guide/telemetry-backend Shows the batch payload structure with schema, applicant_id, and events. The same JSON appears in both English and Chinese sections; backends should index properties first and store the full payload as JSON/blob. The payload may contain base_payload, private_contributions, shared_contributions, and applicant_payload; private contributions come from the applicant's own mod, shared contributions from another mod only after explicit consent. ```json { "schema": "ritsulib.telemetry.batch.v1", "applicant_id": "author.some-mod", "events": [ { "schema": "ritsulib.telemetry.v1", "applicantId": "author.some-mod", "eventName": "exception", "requestId": "diagnostics", "category": "Diagnostics", "timestampUtc": "2026-05-19T00:00:00Z", "properties": { "anonymous_install_id": "stable-anonymous-id", "session_id": "process-session-id", "ritsulib_version": "0.0.0", "applicant_id": "author.some-mod", "owner_mod_id": "author.some-mod", "payload_kind": "exception", "exception_type": "System.Exception" }, "payload": { "applicant_payload": { "exception": { "type": "System.Exception", "message": "example", "stack_trace": "..." } } } } ] } ``` ```json { "schema": "ritsulib.telemetry.batch.v1", "applicant_id": "author.some-mod", "events": [ { "schema": "ritsulib.telemetry.v1", "applicantId": "author.some-mod", "eventName": "exception", "requestId": "diagnostics", "category": "Diagnostics", "timestampUtc": "2026-05-19T00:00:00Z", "properties": { "anonymous_install_id": "stable-anonymous-id", "session_id": "process-session-id", "ritsulib_version": "0.0.0", "applicant_id": "author.some-mod", "owner_mod_id": "author.some-mod", "payload_kind": "exception", "exception_type": "System.Exception" }, "payload": { "applicant_payload": { "exception": { "type": "System.Exception", "message": "example", "stack_trace": "..." } } } } ] } ``` -------------------------------- ### Error response JSON Source: https://sts2-ritsulib.ritsukage.com/guide/telemetry-backend Error response example. Use a stable machine-readable error string. ```json { "error": "invalid_schema", "message": "schema must be ritsulib.telemetry.batch.v1" } ``` -------------------------------- ### 事件 Key 构造示例 (C# 与 JSON) Source: https://sts2-ritsulib.ritsukage.com/guide/localization-and-keywords Chinese version of the event key example. Uses the same C# helper methods and the same event entry MY_MOD_EVENT_QUIET_DOOR, but with Chinese localization values. Page and option names must remain stable after release because they are part of the localization contract and may affect event-choice records. ```csharp protected string ModOptionKey(string pageName, string optionName); protected string InitialOptionKey(string optionName); protected LocString PageDescription(string pageName); ``` ```json { "MY_MOD_EVENT_QUIET_DOOR.pages.INITIAL.description": "墙上有一扇安静的门。", "MY_MOD_EVENT_QUIET_DOOR.pages.INITIAL.options.OPEN": "[打开] 走进去。", "MY_MOD_EVENT_QUIET_DOOR.pages.DONE.description": "房间再次安静下来。" } ``` -------------------------------- ### Literal Single Energy Icon with energyIcons(1) (Chinese) Source: https://sts2-ritsulib.ritsukage.com/guide/card-dynamic-var-toolkit Chinese localization of the literal single energy icon example. Uses the same vanilla extra argument energyIcons(1) to render a fixed single energy icon following the card pool color. ```json { "MY_CARD.description": "下一张牌耗能变为0{energyPrefix:energyIcons(1)}。" } ``` -------------------------------- ### Computed Energy, Star, and Power Icon Counts with Localization (Chinese) Source: https://sts2-ritsulib.ritsukage.com/guide/card-dynamic-var-toolkit Chinese localization of the same computed icon variables example. The C# block is identical to the English version; the JSON block uses the Chinese description text with the same formatters. ```csharp public override DynamicVarSet DynamicVars => new() { ModCardVars.ComputedEnergy("EnergyGain", 1, card => ResolveEnergyGain(card)), ModCardVars.ComputedStars("StarGain", 1, card => ResolveStarGain(card)), ModCardVars.ComputedPower("StrengthPower", 2, card => ResolveStrength(card)), }; ``` ```json { "MY_CARD.description": "获得{EnergyGain:energyIcons()}。\n获得{StarGain:starIcons()}。\n获得{StrengthPower:diff()}点力量。" } ``` -------------------------------- ### Override AssetProfile with VisualCueSetBuilder Source: https://sts2-ritsulib.ritsukage.com/guide/creature-visuals-and-animation Use this override to define a VisualCueSet with the builder, allowing a still frame to hold briefly (e.g., 0.5 seconds for "attack") before the state machine returns to idle. The example defines "idle" and "attack" cues with a duration for the attack cue. ```csharp public override CharacterAssetProfile AssetProfile => new() { VisualCues = VisualCueSetBuilder.Create() .Single("idle", "res://MyMod/images/character/idle.png") .Single("attack", "res://MyMod/images/character/attack.png", 0.5f) .Build() }; ``` -------------------------------- ### Override AssetProfile with VisualCueSet dictionary Source: https://sts2-ritsulib.ritsukage.com/guide/creature-visuals-and-animation Use this override to define a VisualCueSet with a dictionary mapping cue names to texture paths. This is suitable for non-Spine characters that can be described by named stills or frame sequences. The example maps "idle" and "hit" cues to PNG textures. ```csharp public override CharacterAssetProfile AssetProfile => new() { VisualCues = new VisualCueSet( texturePathByCue: new Dictionary { ["idle"] = "res://MyMod/images/character/idle.png", ["hit"] = "res://MyMod/images/character/hit.png", }) }; ``` -------------------------------- ### Implement ICustomTypeTextCard.GetTypeModifiers Source: https://sts2-ritsulib.ritsukage.com/guide/content-authoring-toolkit Implement ICustomTypeTextCard on a card to modify its own type plaque. Returns a collection of LocString entries; the example adds a localized string keyed to "my_mod_ui" and "card_type.cursed". RitsuLib discovers this implementation through the normal run/combat hook-listener order. ```csharp IEnumerable ICustomTypeTextCard.GetTypeModifiers() { return [ new LocString("my_mod_ui", "card_type.cursed"), ]; } ``` -------------------------------- ### Register starter content using a content pack Source: https://sts2-ritsulib.ritsukage.com/guide/character-and-unlock-scaffolding Use a content pack for a centralized way to review and register starter lists, cards, and relics. ```csharp RitsuLibFramework.CreateContentPack("MyMod") .Character(c => c .AddStartingRelic(1, order: 0) .AddStartingCard(4, order: 10) .AddStartingCard(4, order: 20)) .Card() .Card() .Relic() .Apply(); ``` -------------------------------- ### Register Starter Card with Order Source: https://sts2-ritsulib.ritsukage.com/guide/content-authoring-toolkit Registers a starter card with an explicit Order. Starter lists are resolved by Order, then by registration order. Lower Order values run earlier within the same phase. ```csharp [RegisterCard(typeof(MyCardPool))] [RegisterCharacterStarterCard(typeof(MyCharacter), 4, Order = 10)] public sealed class MyStrike : ModCardTemplate(1, CardType.Attack, CardRarity.Common, TargetType.SingleEnemy) { } ``` -------------------------------- ### Register character and starter content with attributes Source: https://sts2-ritsulib.ritsukage.com/guide/character-and-unlock-scaffolding Register the character, starter cards, and starter relics using attributes. Ensure character names and pronouns are defined in characters.json. ```csharp [RegisterCharacter] public sealed class MyCharacter : ModCharacterTemplate { } [RegisterCard(typeof(MyCardPool))] [RegisterCharacterStarterCard(typeof(MyCharacter), 4, Order = 10)] public sealed class MyStrike : ModCardTemplate(1, CardType.Attack, CardRarity.Common, TargetType.SingleEnemy) { } [RegisterRelic(typeof(MyRelicPool))] [RegisterCharacterStarterRelic(typeof(MyCharacter), Order = 0)] public sealed class MyStarterRelic : ModRelicTemplate { } ``` -------------------------------- ### Batch Registration with Content Pack Source: https://sts2-ritsulib.ritsukage.com/guide/getting-started Use RitsuLibFramework.CreateContentPack to register multiple content types in a single batch. ```csharp RitsuLibFramework.CreateContentPack("MyMod") .Card() .Relic() .Apply(); ``` -------------------------------- ### Create and apply a content pack with RitsuLibFramework.CreateContentPack Source: https://sts2-ritsulib.ritsukage.com/guide/content-authoring-toolkit Use a content pack when a batch is more readable than scattered attributes. Registers cards, relics, potions, powers, act events, and a keyword owned by a localization namespace. Do not register the same model through both attributes and a content pack unless you intentionally accept duplicate handling being skipped; pick one source of truth per content family. ```csharp RitsuLibFramework.CreateContentPack("MyMod") .Card() .Relic() .Potion() .Power() .ActEvent() .CardKeywordOwnedByLocNamespace("bleeding") .Apply(); ``` -------------------------------- ### Register Data with BeginModDataRegistration Source: https://sts2-ritsulib.ritsukage.com/guide/persistence-guide Register a settings class inside BeginModDataRegistration so initialization happens after the batch is complete. Use classes rather than primitive values so you can add fields later without changing the storage slot. ```csharp public sealed class MySettings { public bool Enabled { get; set; } = true; public int Volume { get; set; } = 80; } using (RitsuLibFramework.BeginModDataRegistration("MyMod")) { var store = RitsuLibFramework.GetDataStore("MyMod"); store.Register( key: "settings", fileName: "settings.json", scope: SaveScope.Global, defaultFactory: () => new MySettings(), autoCreateIfMissing: true); } ``` -------------------------------- ### SetupCustomCombatAnimationStateMachine with ModAnimStateMachines.Standard Source: https://sts2-ritsulib.ritsukage.com/guide/creature-visuals-and-animation Use when explicit animation state transitions are needed. Return null to let the normal vanilla animation path run. Merchant and rest-site state machines receive the same combat trigger names (Idle, Dead, Hit, Attack, Cast, Relaxed, Revive); world animation names are normalized before reaching the state machine. ```csharp protected override ModAnimStateMachine? SetupCustomCombatAnimationStateMachine( Node visualsRoot, CharacterModel character) { return ModAnimStateMachines.Standard( CompositeBackendFactory.FromNode(visualsRoot)); } ``` -------------------------------- ### Initialize RitsuLib in Mod Entry Source: https://sts2-ritsulib.ritsukage.com/guide/getting-started Register your assembly for RitsuLib discovery using the ModInitializer attribute. EnsureGodotScriptsRegistered is only required if your mod uses C# scripts attached to .tscn scenes. ```csharp using System.Reflection; using MegaCrit.Sts2.Core.Logging; using MegaCrit.Sts2.Core.Modding; using STS2RitsuLib; using STS2RitsuLib.Interop; using STS2RitsuLib.Patching.Core; [ModInitializer(nameof(Initialize))] public static class MyModEntry { public const string ModId = "MyMod"; public static Logger Logger { get; private set; } = null!; public static void Initialize() { var assembly = Assembly.GetExecutingAssembly(); Logger = RitsuLibFramework.CreateLogger(ModId); ModTypeDiscoveryHub.RegisterModAssembly(ModId, assembly); RitsuLibFramework.EnsureGodotScriptsRegistered(assembly, Logger); var patcher = RitsuLibFramework.CreatePatcher(ModId, "main"); patcher.RegisterPatches(); RitsuLibFramework.ApplyRequiredPatcher(patcher, DisableMod); } private static void DisableMod() { // Mark your own mod disabled when a required patch cannot apply. } } ``` -------------------------------- ### Generate Initial Options with InitialOptionKey Source: https://sts2-ritsulib.ritsukage.com/guide/custom-events Defines initial event options using a namespaced key. Keep page and option names stable after release to maintain the localization contract. ```csharp protected override IReadOnlyList GenerateInitialOptions() { return [ new EventOption(this, OpenDoor, InitialOptionKey("OPEN")) ]; } ``` -------------------------------- ### Open log viewer from dev console Source: https://sts2-ritsulib.ritsukage.com/guide/debug-log-viewer Execute this command in the dev console to open the current session's log viewer. It is also available from the settings page. ```text openlogviewer ``` -------------------------------- ### Register Story and Epoch with Content Pack Source: https://sts2-ritsulib.ritsukage.com/guide/timeline-and-unlocks Registers the story and epoch using a content pack. Chains Story, Epoch, StoryEpoch, and ModEpochAutoTimelineSlotAfterColumn methods, then calls Apply(). ```csharp RitsuLibFramework.CreateContentPack("MyMod") .Story() .Epoch() .StoryEpoch() .ModEpochAutoTimelineSlotAfterColumn(EpochEra.Act1) .Apply(); ``` -------------------------------- ### Implement Repeatable Payments Source: https://sts2-ritsulib.ritsukage.com/guide/secondary-resources Use SpendExtra to buy as many full stacks as possible for a repeatable payment. ```csharp card.SecondaryResourceUses() .Require("seven_stars", stars.Id, 7) .SpendExtra( "seven_stars_bonus", stars.Id, perStackAmount: 2, maxStacks: null); ``` -------------------------------- ### Cached Access with CreateCache Source: https://sts2-ritsulib.ritsukage.com/guide/persistence-guide Use this snippet to create a reusable accessor that invalidates itself after profile changes or data reloads. Do not keep a Get result as a long-lived cache because profile reloads may replace the root object. ```csharp private static ModDataStoreCache Settings = RitsuLibFramework.GetDataStore("MyMod").CreateCache("settings"); var settings = Settings.Value; ``` -------------------------------- ### Implement IPatchMethod patch class with Harmony Postfix Source: https://sts2-ritsulib.ritsukage.com/guide/patching-guide Implements IPatchMethod for a strongly typed patch class. Declares PatchId, Description, IsCritical, targets, and a Harmony Postfix. Use new ModPatchTarget(type, methodName, parameterTypes) for overload disambiguation and ignoreIfMissing: true only for optional compatibility targets. ```csharp public sealed class MyCombatPatch : IPatchMethod { public static string PatchId => "my_mod_combat_patch"; public static string Description => "Adjust combat start behavior"; public static bool IsCritical => true; public static ModPatchTarget[] GetTargets() => [ new(typeof(CombatRoom), "OnEnter"), ]; public static void Postfix(CombatRoom __instance) { // Harmony postfix body. } } ``` -------------------------------- ### Declare and use SavedAttachedState Source: https://sts2-ritsulib.ritsukage.com/guide/persistence-guide Use SavedAttachedState only for model objects that already pass through the game's SavedProperties serialization. The example declares a static readonly state with a default value of 0 and assigns a value to a card instance. ```csharp private static readonly SavedAttachedState BonusDamage = new("bonus_damage", () => 0); BonusDamage[card] = 3; ``` -------------------------------- ### Play One-Shot Audio with GameAudioService Source: https://sts2-ritsulib.ritsukage.com/guide/fmod-and-audio Use GameAudioService.Shared.PlayOneShot to trigger audio events with specific volume, parameters, and lifecycle scope. ```csharp using STS2RitsuLib.Audio; GameAudioService.Shared.PlayOneShot( AudioSource.Event("event:/MyMod/ui/click"), new AudioPlaybackOptions { Volume = 0.8f, Parameters = FmodParameterMap.Set(("intensity", 1f)), Scope = AudioLifecycleScope.Room, }); ``` -------------------------------- ### Registering content with manifest arrays Source: https://sts2-ritsulib.ritsukage.com/guide/content-packs-and-registries Use manifest arrays when content is generated or split across multiple files. This approach allows for organized registration of cards, relics, and other content types. ```csharp IContentRegistrationEntry[] content = [ new CardRegistrationEntry(), new RelicRegistrationEntry(), ]; RitsuLibFramework.CreateContentPack("MyMod") .ContentManifest(content) .KeywordManifest(keywordEntries) .PackManifest(timelineAndUnlockEntries) .Apply(); ``` -------------------------------- ### Register and use RunSavedData slots with lobby sync and preparing event Source: https://sts2-ritsulib.ritsukage.com/guide/persistence-guide Register run-scoped saved data slots early and keep keys stable. Use RunSavedData for a shared value and PlayerRunSavedData for per-player values keyed by net id. Access lobby values via the Lobby accessor; with SyncLobbyOnChange enabled, Set and Modify also push local contributions in multiplayer. Subscribe to RunSavedDataPreparingEvent to fill defaults before the new run snapshot is exported. ```csharp using MegaCrit.Sts2.Core.Runs; using MegaCrit.Sts2.Core.Multiplayer.Game.Lobby; using STS2RitsuLib.RunData; public sealed class ChallengeRunState { public string? ChallengeId { get; set; } public int ElitesKilled { get; set; } } public sealed class PlayerRunState { public string? LoadoutId { get; set; } } private static RunSavedData ChallengeData = null!; private static PlayerRunSavedData PlayerData = null!; using (RitsuLibFramework.BeginModDataRegistration("MyMod")) { var runStore = RitsuLibFramework.GetRunSavedDataStore("MyMod"); ChallengeData = runStore.Register( key: "challenge", defaultFactory: () => new ChallengeRunState(), options: new RunSavedDataOptions { WritePolicy = RunSavedDataWritePolicy.WhenNonDefault, SyncLobbyOnChange = true, }); PlayerData = runStore.RegisterPerPlayer( key: "player", defaultFactory: () => new PlayerRunState(), options: new RunSavedDataOptions { SyncLobbyOnChange = true, }); } ``` ```csharp public static void RecordEliteKilled(RunState runState) { ChallengeData.Modify(runState, data => { data.ElitesKilled++; }); } public static string? GetPlayerLoadout(RunState runState, ulong netId) { return PlayerData.TryGet(runState, netId, out var data) ? data.LoadoutId : null; } ``` ```csharp public static void SetLobbyChallenge(StartRunLobby lobby, string challengeId) { ChallengeData.Lobby.Modify(lobby, data => { data.ChallengeId = challengeId; }); } public static void SetLocalLoadout(StartRunLobby lobby, string loadoutId) { PlayerData.Lobby.Modify(lobby, lobby.NetService.NetId, data => { data.LoadoutId = loadoutId; }); } ``` ```csharp RitsuLibFramework.SubscribeLifecycle(evt => { ChallengeData.Modify(evt.RunState, data => { data.ChallengeId ??= "standard"; }); }); ``` -------------------------------- ### Register a mod update check with Steam Workshop skip using SkipModUpdateCheckWhenLoadedFromSteamWorkshop Source: https://sts2-ritsulib.ritsukage.com/guide/update-checks Use for mods also distributed through Steam Workshop. Wrap the options with SkipModUpdateCheckWhenLoadedFromSteamWorkshop and pass the mod's Workshop item id. This keeps local/manual installs on the external update source, while that exact Workshop item is left to Steam's Workshop update flow. ```csharp RitsuLibFramework.RegisterModUpdateCheck( RitsuLibFramework.SkipModUpdateCheckWhenLoadedFromSteamWorkshop(new() { ModId = MyModConst.ModId, DisplayName = MyModConst.Name, CurrentVersion = MyModConst.Version, ManifestUri = new("https://example.com/my-mod/update.json"), ReleasePageUri = new("https://example.com/my-mod/releases"), }, typeof(MyModPlugin).Assembly, 1234567890)); ``` -------------------------------- ### Register Trash Heap Content with Content-Pack Builder Source: https://sts2-ritsulib.ritsukage.com/guide/custom-events Use the RitsuLibFramework content-pack builder to register your custom cards and relics to the Trash Heap. ```csharp RitsuLibFramework.CreateContentPack("MyMod") .TrashHeapCard() .TrashHeapRelic() .Apply(); ``` -------------------------------- ### Subscribe and Dispose CombatStartingEvent subscription Source: https://sts2-ritsulib.ritsukage.com/guide/lifecycle-events Use this C# snippet to subscribe to a lifecycle event and dispose the subscription within the handler when the handler is temporary. The handler calls PrepareForCombat with the run state and then disposes the subscription. ```csharp RitsuLibFramework.SubscribeLifecycle((evt, sub) => { PrepareForCombat(evt.RunState); sub.Dispose(); }); ``` -------------------------------- ### Define CharacterAssetProfile Source: https://sts2-ritsulib.ritsukage.com/guide/asset-profiles-and-fallbacks Configures character assets by grouping them into Scenes, UI, and Audio categories. Use explicit res:// paths for mod-specific assets. ```csharp public override CharacterAssetProfile AssetProfile => new() { Scenes = new() { VisualsPath = "res://MyMod/scenes/characters/my_character_visuals.tscn", EnergyCounterPath = "res://MyMod/scenes/ui/my_energy_counter.tscn", }, Ui = new() { CharacterSelectIconPath = "res://MyMod/images/character/select_icon.png", MapMarkerPath = "res://MyMod/images/character/map_marker.png", }, Audio = new() { AttackSfx = "event:/MyMod/character_attack", }, }; ``` -------------------------------- ### ComputedDynamicVar with preview factory Source: https://sts2-ritsulib.ritsukage.com/guide/card-dynamic-var-toolkit Use preview logic when card preview, target preview, or upgrade preview should show a value different from the current live card value. The preview factory is passed as the fourth argument to ComputedDynamicVar. ```csharp ModCardVars.Computed( "damage", Damage, (card, target) => ResolveDamage(card, target), (card, mode, target, runGlobalHooks) => ResolvePreviewDamage(card, mode, target)); ``` -------------------------------- ### Resolve Shortfalls with Backup Sources Source: https://sts2-ritsulib.ritsukage.com/guide/secondary-resources Use a resolver in RequireAllowingShortfall to cover missing resources from an alternative source. ```csharp card.SecondaryResourceUses() .RequireAllowingShortfall( "seven_stars", stars.Id, 7, resolveShortfall: ctx => { var backup = SecondaryResourceCmd.Get(ctx.Player, backupStars.Id); if (backup < ctx.Shortfall) return SecondaryResourceShortfallResolution.None; return SecondaryResourceShortfallResolution.Cover( ctx.Shortfall, async commit => { await SecondaryResourceCmd.Spend( commit.Player, backupStars.Id, commit.CoveredShortfall, commit.Card, commit.Source); }); }, onShortfall: async ctx => { // Runs only for any remaining amount not covered by the replacement payment. await ApplyShortfallPenalty(ctx.Card, ctx.Shortfall); }); ``` -------------------------------- ### 本地化卡牌关键词标题和描述(含能量图标语法) Source: https://sts2-ritsulib.ritsukage.com/guide/localization-and-keywords 中文版:卡牌关键词文本写入 card_keywords。描述可以使用跟随当前角色的标准能量图标语法 {energyPrefix:energyIcons(1)}。关键词悬浮提示不绑定具体卡牌实例,因此不能使用 {Damage}、{Block} 等卡牌动态变量。 ```json { "MY_MOD_KEYWORD_BLEEDING.title": "流血", "MY_MOD_KEYWORD_BLEEDING.description": "回合结束时失去生命。" } ``` ```json { "MY_MOD_KEYWORD_BLEEDING.description": "下一张牌的耗能为 0{energyPrefix:energyIcons(1)}。" } ``` -------------------------------- ### Context factory for ComputedDamage with static ctx Source: https://sts2-ritsulib.ritsukage.com/guide/card-dynamic-var-toolkit Use the context overload when one value depends on several pieces of card, run, combat, preview, or target state. The same ComputedDynamicVarContext is used for live values and previews. Prefer static factories because dynamic variables are cloned with their cards and a static factory cannot accidentally retain the card instance from which the variable was created. ```csharp public override DynamicVarSet DynamicVars => new() { ModCardVars.Int("Heat", Heat), ModCardVars.ComputedDamage( "Damage", static ctx => { var heat = ctx.GetCardIntOrDefault("Heat"); var targetBonus = ctx.HasTarget && ctx.IsInCombat ? ResolveTargetBonus(ctx.CombatState, ctx.Target) : 0m; return ctx.BaseValue + heat + targetBonus + (ctx.IsUpgraded ? 3m : 0m); }, baseValue: 6), }; ``` -------------------------------- ### Computed Energy, Star, and Power Icon Counts with Localization (English) Source: https://sts2-ritsulib.ritsukage.com/guide/card-dynamic-var-toolkit Defines computed icon variables for energy, stars, and power in a card's DynamicVars, then uses them in the English localization with the corresponding icon formatters. ComputedPower keeps the typed PowerVar shape and does not run power-amount hooks by default; use ComputedPowerAmountGiven when the computed amount should follow the vanilla preview hook path. ```csharp public override DynamicVarSet DynamicVars => new() { ModCardVars.ComputedEnergy("EnergyGain", 1, card => ResolveEnergyGain(card)), ModCardVars.ComputedStars("StarGain", 1, card => ResolveStarGain(card)), ModCardVars.ComputedPower("StrengthPower", 2, card => ResolveStrength(card)), }; ``` ```json { "MY_CARD.description": "Gain {EnergyGain:energyIcons()}.\nGain {StarGain:starIcons()}.\nGain {StrengthPower:diff()} Strength." } ``` -------------------------------- ### Register Combat UI with Feedback Source: https://sts2-ritsulib.ritsukage.com/guide/secondary-resources Registers a custom combat UI node and defines update and change handlers to provide visual feedback when resource amounts change. ```csharp resources.RegisterCombatUi( "charge_feedback", parent => new MyChargeFeedbackNode(), update: ctx => ctx.Node.Refresh(ctx.Player, ctx.VisibleDefinitions), changed: ctx => { if (ctx.Definition.Id == charge.Id && ctx.Delta > 0) ctx.Node.PlayGainFeedback(ctx.Delta, ctx.Source); }); ``` -------------------------------- ### Create custom card pile with CardPileOwned and ExtraHand style Source: https://sts2-ritsulib.ritsukage.com/guide/shell-theme Creates a custom card pile with an ExtraHand style. Use LayoutResolver for per-card transforms and OnCardVisualCreated/OnCardArrived for custom animations. FlightTargetPositionResolver and FlightStartPositionResolver customize vanilla flight endpoints. When an extra-hand card is manually played, RitsuLib bridges it through the backend vanilla hand; canceled targeting or queued actions restore the card to its source pile. Use stable local stems and add generated pile id .title, .description, and .empty entries to static_hover_tips.json. ```csharp RitsuLibFramework.CreateContentPack("MyMod") .CardPileOwned("archive", new ModCardPileSpec { Style = ModCardPileUiStyle.ExtraHand, Anchor = ModCardPileAnchor.AtCenter(new Vector2(260f, 520f)), CardShouldBeVisible = true, ExtraHand = new ModCardPileExtraHandSpec { Direction = ModExtraHandLayoutDirection.Vertical, Spacing = 86f, CardScale = Vector2.One * 0.55f, HoverScale = Vector2.One, ShowPlayableGlow = true, AllowCardPlay = true, }, }) .Apply(); ``` -------------------------------- ### Define Optional Resource Uses Source: https://sts2-ritsulib.ritsukage.com/guide/secondary-resources Use SpendIfAvailable to define optional payments that do not block card play. ```csharp card.SecondaryResourceUses() .SpendIfAvailable("bonus_charge", charge.Id, 2); ``` -------------------------------- ### ComputedDamage with preview base factory Source: https://sts2-ritsulib.ritsukage.com/guide/card-dynamic-var-toolkit Pass a preview base factory when the preview base amount differs from the live amount. The result still goes through normal damage or block hooks. ```csharp ModCardVars.ComputedDamage( "damage", 6, (card, target) => ResolveDamage(card, target), (card, mode, target, runGlobalHooks) => ResolvePreviewDamageBase(card, mode, target)); ``` -------------------------------- ### Declare Runtime Dependency (Legacy String Form) Source: https://sts2-ritsulib.ritsukage.com/guide/getting-started Use this legacy string form for older game API branches where dependency objects may fail. ```json { "dependencies": [ "STS2-RitsuLib" ] } ``` -------------------------------- ### Subscribe to GameReadyEvent with RitsuLibFramework.SubscribeLifecycle Source: https://sts2-ritsulib.ritsukage.com/guide/lifecycle-events Use this C# snippet to subscribe to a lifecycle event when a mod needs game timing but does not need to own a Harmony patch. The handler receives the event and logs the game name. Dispose the returned subscription when the handler is temporary. ```csharp var subscription = RitsuLibFramework.SubscribeLifecycle(evt => { Logger.Info( $"Game ready: {evt.Game.Name}"); }); ``` -------------------------------- ### Create ModSettingsValueBinding and ProjectedModSettingsValueBinding Source: https://sts2-ritsulib.ritsukage.com/guide/mod-settings Creates a settings binding for a mod data store and a projected binding for a sub-value. The projected binding wraps the parent binding to edit the 'volume' field. ```csharp var settingsBinding = new ModSettingsValueBinding( "MyMod", "settings", SaveScope.Global, s => s, (_, value) => value); var volumeBinding = new ProjectedModSettingsValueBinding( settingsBinding, "volume", s => s.Volume, (s, value) => { s.Volume = value; return s; }); ``` -------------------------------- ### Read dynamic variables safely with GetIntOrDefault, HasPositiveValue, GetRequired, EvaluateValueOrDefault Source: https://sts2-ritsulib.ritsukage.com/guide/card-dynamic-var-toolkit Shows safe extension helpers for reading dynamic variables from another card. Use GetIntOrDefault for optional integers, HasPositiveValue for boolean checks, GetRequired when absence is a content error, and EvaluateValueOrDefault to accept fixed or computed variables. The ...OrDefault and Try... helpers do not throw for missing keys; required helpers throw descriptive exceptions. ```csharp var amount = card.DynamicVars.GetIntOrDefault("damage"); var hasHeat = card.DynamicVars.HasPositiveValue("heat"); var damageVar = card.DynamicVars.GetRequired("Damage"); var displayedValue = card.DynamicVars.EvaluateValueOrDefault("ComputedDamage", target: target); ``` -------------------------------- ### Register Data Store with Migrations Source: https://sts2-ritsulib.ritsukage.com/guide/persistence-guide Use this snippet to register a data store with migration support before publishing a breaking data shape. Keep fileName and key stable after release; change the schema version when the JSON shape changes in a way old files cannot deserialize directly. ```csharp store.Register( "settings", "settings.json", SaveScope.Global, defaultFactory: () => new MySettings(), migrationConfig: new ModDataMigrationConfig( currentDataVersion: 2, minimumSupportedDataVersion: 1), migrations: [ new SettingsV1ToV2Migration(), ]); ``` -------------------------------- ### Show basic toast notifications Source: https://sts2-ritsulib.ritsukage.com/guide/shell-theme Use RitsuToastService to display info, warning, or error messages that do not interrupt gameplay. ```csharp RitsuToastService.ShowInfo("Settings saved.", "My Mod"); RitsuToastService.ShowWarning("Optional bank failed to load.", "My Mod"); RitsuToastService.ShowError("Required setup failed.", "My Mod"); ```