diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 0000000..9f9d4fc --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,55 @@ +name: GitHub Pages + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + node-version: [lts/*] + os: [ubuntu-latest] + fail-fast: false + + steps: + - uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Get pnpm store directory + id: pnpm-cache + run: echo "store=$(pnpm store path --silent)" >> $GITHUB_OUTPUT + + - name: Cache pnpm store + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.store }} + key: ${{ runner.os }}-pnpm-${{ hashFiles('docs/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm- + + - name: 📦 Install Dependencies + working-directory: docs + run: pnpm install + + - name: 🌌 Build Valaxy Docs + working-directory: docs + run: pnpm build + + - name: 🪤 Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs/dist + force_orphan: true diff --git a/Docs/README.md b/Docs/README.md deleted file mode 100644 index ab92d8a..0000000 --- a/Docs/README.md +++ /dev/null @@ -1,91 +0,0 @@ -# RitsuLib — Documentation Index - -**English** → [en/](en/)  |  **中文** → [zh/](zh/) - ---- - -## English - -### Start Here - -| Document | Description | -|---|---| -| [Getting Started](en/GettingStarted.md) | Initial setup, bootstrapping, and first registered content | -| [Framework Design](en/FrameworkDesign.md) | Core architecture decisions and recommended reading order | -| [Terminology](en/Terminology.md) | Canonical terminology used across the documentation | - -### Content Authoring - -| Document | Description | -|---|---| -| [Content Authoring Toolkit](en/ContentAuthoringToolkit.md) | Identity rules, localization contracts, and asset override basics | -| [Content Packs & Registries](en/ContentPacksAndRegistries.md) | Registry model, fixed identity, and registration flow | -| [Character & Unlock Templates](en/CharacterAndUnlockScaffolding.md) | Character assembly, registration, and unlock integration | -| [Card Dynamic Variables](en/CardDynamicVarToolkit.md) | Custom card vars with tooltip support | -| [Custom Events](en/CustomEvents.md) | Event registration, localization alignment, and custom event scenes | -| [Timeline & Unlocks](en/TimelineAndUnlocks.md) | Story / epoch registration, progression rules, and compatibility bridges | -| [Asset Profiles & Fallbacks](en/AssetProfilesAndFallbacks.md) | Character placeholder fallback, content profiles, and path diagnostics | -| [Godot Scene Authoring](en/GodotSceneAuthoring.md) | Scene-script wrappers, editor caveats, and runtime script registration | -| [Creature Visuals & Animation](en/CreatureVisualsAndAnimation.md) | Creature visuals / animator factories and the non-Spine `ModAnimStateMachine` pipeline | -| [Mod Settings](en/ModSettings.md) | Settings UI architecture, bindings, supported controls, and page composition | - -### Localization - -| Document | Description | -|---|---| -| [Localization & Keywords](en/LocalizationAndKeywords.md) | `I18N`, keyword registration, and ancient dialogue localization | -| [LocString Placeholder Resolution](en/LocStringPlaceholderResolution.md) | Placeholder syntax, custom formatters, and extension points | - -### Runtime & Infrastructure - -| Document | Description | -|---|---| -| [Lifecycle Events](en/LifecycleEvents.md) | Lifecycle event reference and execution timing | -| [Patching Guide](en/PatchingGuide.md) | `ModPatcher`, `IPatchMethod`, and dynamic patch application | -| [Persistence Guide](en/PersistenceGuide.md) | Store scopes, save lifecycle, and migrations | -| [FMOD & Audio](en/FmodAndAudio.md) | `GameFmod`, `FmodServer`, banks, buses, files, and tooling | -| [Diagnostics & Compatibility](en/DiagnosticsAndCompatibility.md) | Diagnostics policy, compatibility fallbacks, and bridge patches | - ---- - -## 中文 - -### 从这里开始 - -| 文档 | 说明 | -|---|---| -| [快速入门](zh/GettingStarted.md) | 初始配置、引导流程与第一个已注册内容 | -| [框架设计](zh/FrameworkDesign.md) | 核心架构决策与推荐阅读顺序 | -| [术语表](zh/Terminology.md) | 文档中统一使用的术语及推荐译法 | - -### 内容编写 - -| 文档 | 说明 | -|---|---| -| [内容注册规则](zh/ContentAuthoringToolkit.md) | 身份规则、本地化约束与资源覆写基础 | -| [内容包与注册器](zh/ContentPacksAndRegistries.md) | 注册器模型、固定身份与注册流程 | -| [角色与解锁模板](zh/CharacterAndUnlockScaffolding.md) | 角色装配、注册与解锁集成 | -| [卡牌动态变量](zh/CardDynamicVarToolkit.md) | 带 Tooltip 支持的自定义卡牌变量 | -| [自定义事件](zh/CustomEvents.md) | 事件注册流程、本地化对齐与自定义事件场景 | -| [时间线与解锁](zh/TimelineAndUnlocks.md) | Story / Epoch 注册、进度规则与兼容桥接 | -| [资源配置与回退规则](zh/AssetProfilesAndFallbacks.md) | 角色 Placeholder、内容 Profile 与路径诊断 | -| [Godot 场景编写说明](zh/GodotSceneAuthoring.md) | 场景脚本包装、编辑器问题与运行时脚本注册 | -| [生物体视觉与动画](zh/CreatureVisualsAndAnimation.md) | 生物视觉 / animator 工厂接口与非 Spine `ModAnimStateMachine` 管线 | -| [Mod 设置界面](zh/ModSettings.md) | 设置 UI 架构、绑定方式、支持控件与页面组合 | - -### 本地化 - -| 文档 | 说明 | -|---|---| -| [本地化与关键词](zh/LocalizationAndKeywords.md) | `I18N`、关键词注册与 Ancient 对话本地化 | -| [LocString 占位符解析](zh/LocStringPlaceholderResolution.md) | 占位符语法、自定义格式化器与扩展点 | - -### 运行时与基础设施 - -| 文档 | 说明 | -|---|---| -| [生命周期事件](zh/LifecycleEvents.md) | 生命周期事件参考与触发时机 | -| [补丁系统](zh/PatchingGuide.md) | `ModPatcher`、`IPatchMethod` 与动态补丁应用 | -| [持久化设计](zh/PersistenceGuide.md) | 存储作用域、保存生命周期与迁移 | -| [FMOD 与音频](zh/FmodAndAudio.md) | `GameFmod`、`FmodServer`、Bank、Bus、散文件与工具链 | -| [诊断与兼容层](zh/DiagnosticsAndCompatibility.md) | 诊断策略、兼容回退与桥接补丁 | diff --git a/Docs/en/AssetProfilesAndFallbacks.md b/Docs/en/AssetProfilesAndFallbacks.md deleted file mode 100644 index 7d0f680..0000000 --- a/Docs/en/AssetProfilesAndFallbacks.md +++ /dev/null @@ -1,229 +0,0 @@ -# Asset Profiles & Fallbacks - -This is the reference document for asset-profile structure, placeholder fallback, and asset-path diagnostics. - -RitsuLib uses asset profiles to describe overrideable art, scenes, materials, and related resources. - -This document explains the structure behind those profiles and the fallback rules that make them safe to use. - ---- - -## Why Asset Profiles Exist - -Asset overrides could have been exposed as a long flat list of virtual properties. - -RitsuLib instead groups them into profile records because that scales better: - -- related assets stay together -- partial overrides remain readable -- fallback merging stays explicit -- migration from placeholder-based systems is possible without abandoning structure - -For characters, this is especially important because character assets span scenes, UI, VFX, audio, Spine, and multiplayer-specific textures. - ---- - -## Character Asset Profile Structure - -`CharacterAssetProfile` is split into several nested record groups: - -- `CharacterSceneAssetSet` -- `CharacterUiAssetSet` -- `CharacterVfxAssetSet` -- `CharacterSpineAssetSet` -- `CharacterAudioAssetSet` -- `CharacterMultiplayerAssetSet` - -This lets you override only one category without turning the other categories into noise. - -Example: - -```csharp -public override CharacterAssetProfile AssetProfile => new( - Scenes: new( - VisualsPath: "res://MyMod/scenes/character/my_character.tscn", - EnergyCounterPath: "res://MyMod/ui/energy/my_energy_counter.tscn"), - Ui: new( - IconTexturePath: "res://MyMod/ui/top_panel/icon.png", - MapMarkerPath: "res://MyMod/map/map_marker.png"), - Audio: new( - AttackSfx: "event:/sfx/characters/my_character/attack")); -``` - ---- - -## Placeholder Character Fallback - -`ModCharacterTemplate` now exposes: - -```csharp -public virtual string? PlaceholderCharacterId => "ironclad"; -``` - -Behavior: - -- your explicit `AssetProfile` is read first -- missing fields are filled from `CharacterAssetProfiles.FromCharacterId(PlaceholderCharacterId)` -- if `PlaceholderCharacterId` is `null`, fallback is disabled entirely - -This gives you BaseLib-style migration convenience without flattening the whole character API. - ---- - -## How Character Profile Merging Works - -RitsuLib merges character profiles category-by-category and field-by-field. - -That means: - -- providing a custom `Scenes` record does not erase `Ui` -- providing only `RestSiteAnimPath` does not erase `MerchantAnimPath` -- providing only `AttackSfx` does not erase the other default SFX entries - -This is important because character assets are rarely replaced all at once. - ---- - -## Character Asset Profile Helpers - -`CharacterAssetProfiles` provides several helper APIs: - -- `FromCharacterId(string)` -- `Ironclad()` / `Silent()` / `Defect()` / `Regent()` / `Necrobinder()` -- `Resolve(profile, placeholderCharacterId)` -- `Merge(fallback, profile)` -- `FillMissingFrom(...)` -- `WithPlaceholder(...)` -- `WithScenes(...)`, `WithUi(...)`, `WithVfx(...)`, `WithSpine(...)`, `WithAudio(...)`, `WithMultiplayer(...)` - -These helpers exist for two main use cases: - -- partial authoring of new characters -- migration from frameworks that assumed a placeholder character from the start - ---- - -## Content Asset Profiles - -RitsuLib also provides profile records for other content: - -- `CardAssetProfile` -- `RelicAssetProfile` -- `PowerAssetProfile` -- `OrbAssetProfile` -- `PotionAssetProfile` -- `AfflictionAssetProfile` -- `EnchantmentAssetProfile` -- `ActAssetProfile` - -These are intentionally much smaller because their asset surfaces are smaller. - ---- - -## Path Builder Helpers - -For common vanilla-style asset conventions, there are helper factories: - -- `CharacterAssetProfiles.FromCharacterId(...)` -- `ContentAssetProfiles.Card(...)` -- `ContentAssetProfiles.Relic(...)` -- `ContentAssetProfiles.Power(...)` -- `ContentAssetProfiles.Orb(...)` -- `ContentAssetProfiles.Potion(...)` -- `ContentAssetProfiles.Affliction(...)` -- `ContentAssetProfiles.Enchantment(...)` -- `ContentAssetProfiles.Act(...)` - -There is also `CharacterAssetPathHelper` for deriving character-related default asset paths such as visuals, energy counter, select background, and map marker. - -These helpers are most useful when your assets intentionally follow a conventional naming layout. - -If those assets are backed by custom Godot scenes, remember that scene roots and scripted child nodes often need mod-local wrapper classes for stable editor binding. See [Godot Scene Authoring](GodotSceneAuthoring.md). - ---- - -## Energy Counter vs Big Energy Icon vs Text Icon - -RitsuLib treats these as separate concerns: - -- `CustomEnergyCounterPath`: full combat UI counter scene -- `BigEnergyIconPath`: large pool-linked icon resolved through `EnergyIconHelper` -- `TextEnergyIconPath`: small icon used inside rich text - -Why this matters: - -- a scene replacement is the right abstraction for a custom counter -- a texture path is the right abstraction for a pool icon -- keeping them separate avoids overloading one API with three unrelated jobs - ---- - -## Missing Path Diagnostics - -RitsuLib now validates asset-path overrides through `AssetPathDiagnostics`. - -Current behavior: - -- empty path -> ignore override -- existing path -> use override -- missing path -> log a one-time warning and fall back to the base asset - -The warning includes: - -- the owner type -- the model entry when available -- the specific profile member name -- the missing path - -This makes broken resource wiring much easier to debug. - ---- - -## What Gets Path Validation - -Path validation covers resource-like overrides such as: - -- card textures, materials, overlays, and banners -- relic / power / orb / potion icons -- act backgrounds -- character visuals, energy counters, map assets, trail scenes, and Spine data -- pool energy icon paths - -It does not validate non-resource strings such as audio event ids. - -So character SFX override fields are still treated as plain values, not `ResourceLoader` paths. - ---- - -## Recommended Character Authoring Pattern - -For most custom characters, this pattern works well: - -1. leave `PlaceholderCharacterId` at `ironclad` or switch it to the base character you want to inherit from -2. override only the assets that are truly custom -3. use pool-level `BigEnergyIconPath` / `TextEnergyIconPath` for energy icon concerns -4. use `CustomEnergyCounterPath` only when you need a real counter scene replacement - -This keeps the authoring surface small while preserving safe fallback behavior. - ---- - -## Recommended Content Authoring Pattern - -For cards and other content: - -- use `AssetProfile` when several asset fields belong together -- use a direct `Custom...Path` override only for one-off exceptions -- prefer helper factories like `ContentAssetProfiles.Card(...)` when your resource layout matches the helper's expectations - -The profile approach is especially good for keeping portrait, frame, overlay, and banner decisions in one place. - ---- - -## Related Documents - -- [Character & Unlock Templates](CharacterAndUnlockScaffolding.md) -- [Content Authoring Toolkit](ContentAuthoringToolkit.md) -- [Godot Scene Authoring](GodotSceneAuthoring.md) -- [Diagnostics & Compatibility](DiagnosticsAndCompatibility.md) -- [Framework Design](FrameworkDesign.md) diff --git a/Docs/en/CardDynamicVarToolkit.md b/Docs/en/CardDynamicVarToolkit.md deleted file mode 100644 index 8d1efda..0000000 --- a/Docs/en/CardDynamicVarToolkit.md +++ /dev/null @@ -1,134 +0,0 @@ -# Card Dynamic Var Toolkit - -This document describes how RitsuLib creates card dynamic variables, how tooltip binding works, and how values are injected when a card is hovered. - ---- - -## Vanilla DynamicVar System - -> The following describes the game engine’s own dynamic variable system. RitsuLib builds convenience constructors on top of it. - -The game’s `DynamicVar` system lets cards carry values that can change at runtime. Each `DynamicVar` subclass may carry extra metadata for formatters (for example `DamageVar` for highlighting, `EnergyVar` for colors). For the full list of subclasses, see [LocString Placeholder Resolution](LocStringPlaceholderResolution.md). - ---- - -## RitsuLib Capabilities - -On top of the vanilla system, RitsuLib provides: - -- **`ModCardVars`** — convenient variable constructors -- **`DynamicVarExtensions`** — each variable can bind its own tooltip independently -- **Automatic injection** — on card hover, all bound tooltips are appended automatically (implemented via patches; no extra setup) - ---- - -## Variable Construction - -Create variables with `ModCardVars` and add them to the card’s `DynamicVarSet`: - -```csharp -public class MyCard : ModCardTemplate(1, CardType.Attack, CardRarity.Common, TargetType.SingleEnemy) -{ - private static readonly DynamicVar _charges = - ModCardVars.Int("charges", amount: 3) - .WithSharedTooltip("my_mod_charges"); - - private static readonly DynamicVar _label = - ModCardVars.String("flavor", value: "wine"); - - public override DynamicVarSet CreateDynamicVars() => - new DynamicVarSet().Add(_charges).Add(_label); -} -``` - -| Method | Description | -|---|---| -| `ModCardVars.Int(name, amount)` | Creates a numeric variable (`decimal`) | -| `ModCardVars.String(name, value)` | Creates a string variable | -| `ModCardVars.Computed(...)` | Creates a computed variable | - -RitsuLib does not assign gameplay semantics to these variables. Their meaning is entirely defined by the content author. - ---- - -## Tooltip Binding - -Bind tooltips at definition time via chained extension methods: - -### Shared tooltip (recommended) - -Reads keys from the `static_hover_tips` table: - -```csharp -var myVar = ModCardVars.Int("my_var", 2) - .WithSharedTooltip("my_mod_my_var"); -// Resolves: -// static_hover_tips["my_mod_my_var.title"] -// static_hover_tips["my_mod_my_var.description"] -``` - -### Explicit table / key - -```csharp -var myVar = ModCardVars.Int("my_var", 2) - .WithTooltip( - titleTable: "card_keywords", - titleKey: "my_mod_my_var.title", - iconPath: "res://MyMod/art/kw.png"); -``` - -### Custom factory - -```csharp -var myVar = ModCardVars.Int("my_var", 2) - .WithTooltip(var => new HoverTip( - new LocString("my_table", "my_var.title"), - new LocString("my_table", "my_var.description"))); -``` - ---- - -## Localization Example - -When using `WithSharedTooltip("my_mod_charges")`, provide entries in your `static_hover_tips` localization file: - -```json -{ - "my_mod_charges.title": "Charges", - "my_mod_charges.description": "Accumulated charges that deal extra damage." -} -``` - -RitsuLib does not ship built-in localization entries for these; if you use `WithSharedTooltip`, you must supply the strings yourself. - ---- - -## Card Hover Injection - -RitsuLib’s patches automatically append every dynamic variable in `CardModel.DynamicVars` that has a bound tooltip to the end of the hover-tip sequence. No extra configuration is required. - ---- - -## Clone Behavior - -When `DynamicVar.Clone()` runs, tooltip metadata bound on the source variable is copied to the clone. Upgraded or duplicated cards in combat therefore behave correctly without extra handling. - ---- - -## Reading Variable Values at Runtime - -Read values through `DynamicVarExtensions`: - -```csharp -int charges = card.DynamicVars.GetIntOrDefault("charges"); -decimal val = card.DynamicVars.GetValueOrDefault("charges"); -bool active = card.DynamicVars.HasPositiveValue("charges"); -``` - ---- - -## Related Documents - -- [Content Authoring Toolkit](ContentAuthoringToolkit.md) -- [Getting Started](GettingStarted.md) -- [LocString Placeholder Resolution](LocStringPlaceholderResolution.md) diff --git a/Docs/en/CharacterAndUnlockScaffolding.md b/Docs/en/CharacterAndUnlockScaffolding.md deleted file mode 100644 index 5cf6f31..0000000 --- a/Docs/en/CharacterAndUnlockScaffolding.md +++ /dev/null @@ -1,248 +0,0 @@ -# Character & Unlock Templates - -This document is the practical assembly guide for a character mod: character templates, content pools, epoch templates, and unlock registration, with full examples. - -Detailed fallback rules are in [Asset Profiles & Fallbacks](AssetProfilesAndFallbacks.md). Detailed timeline and progression semantics are in [Timeline & Unlocks](TimelineAndUnlocks.md). For wrapping scene scripts (visuals, rest sites, energy orbs), see [Godot Scene Authoring](GodotSceneAuthoring.md). - ---- - -## Overview - -A full character mod typically includes: - -| Content | Base Type | Example | -|---|---|---| -| Card pool | `TypeListCardPoolModel` | `MyCardPool` | -| Relic pool | `TypeListRelicPoolModel` | `MyRelicPool` | -| Potion pool | `TypeListPotionPoolModel` | `MyPotionPool` | -| Character | `ModCharacterTemplate` | `MyCharacter` | -| Story | `ModStoryTemplate` | `MyStory` | -| Epoch | `CharacterUnlockEpochTemplate` or custom | `MyEpoch2` | - ---- - -## Pools - -- **Card pools:** register members through `CreateContentPack` / manifest via `.Card()` or `CardRegistrationEntry`. `TypeListCardPoolModel` already defaults `CardTypes` to empty and marks it `[Obsolete]`—**do not override** it in new mods. -- **Relic / potion pools:** `TypeListRelicPoolModel` / `TypeListPotionPoolModel` now match card pools: `RelicTypes` / `PotionTypes` default to empty and are marked `[Obsolete]`. Register members through `CreateContentPack` / manifest via `.Relic()`, `.Potion()`, `RelicRegistrationEntry`, or `PotionRegistrationEntry` in new mods. - -```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; -} - -public class MyRelicPool : TypeListRelicPoolModel -{ -} - -public class MyPotionPool : TypeListPotionPoolModel -{ -} -``` - -**Legacy pool hooks (`CardTypes`, `RelicTypes`, `PotionTypes`):** do not override them in new mods. Legacy overrides emit **CS0618** and still duplicate pool content if pack registration covers the same pool + model. Migrate by deleting the override and relying on the content pack / manifest only. - -### Configure Card Frame Color (HSV) - -`TypeListCardPoolModel` supports directly overriding `PoolFrameMaterial`. When this property returns a non-null material, that material is used for card frame rendering and `CardFrameMaterialPath` is no longer required. - -```csharp -using Godot; -using STS2RitsuLib.Utils; - -public class MyCardPool : TypeListCardPoolModel -{ - // Register cards in CreateContentPack / manifest; do not override CardTypes - - // Generate a frame material from HSV: H=0.55, S=0.45, V=0.95 - public override Material? PoolFrameMaterial => - MaterialUtils.CreateHsvShaderMaterial(0.55f, 0.45f, 0.95f); -} -``` - -If you prefer path-based configuration, simply leave `PoolFrameMaterial` as `null` and override `CardFrameMaterialPath` instead. - -### Example: Configure Pool Energy Icons - -`TypeList*PoolModel` also exposes pooled energy icon hooks: - -- `BigEnergyIconPath`: the large icon resolved through `EnergyIconHelper` -- `TextEnergyIconPath`: the small inline icon used in rich-text card descriptions - -```csharp -public class MyCardPool : TypeListCardPoolModel -{ - public override string? BigEnergyIconPath => "res://MyMod/ui/energy/my_energy_big.png"; - public override string? TextEnergyIconPath => "res://MyMod/ui/energy/my_energy_text.png"; -} -``` - ---- - -## Character Template - -Inherit `ModCharacterTemplate` for the character itself, then register starter content additively from your content manifest / pack. - -Unspecified character assets automatically fall back to `PlaceholderCharacterId`, which defaults to `ironclad`. - -```csharp -public class MyCharacter : ModCharacterTemplate -{ - public override string? PlaceholderCharacterId => "ironclad"; - - // Asset paths (configured via AssetProfile) - public override CharacterAssetProfile AssetProfile => new( - Spine: new( - CombatSkeletonDataPath: "res://MyMod/spine/my_character.tres"), - Ui: new( - IconTexturePath: "res://MyMod/art/icon.png", - CharacterSelectBgPath: "res://MyMod/art/select_bg.tscn"), - Scenes: new( - RestSiteAnimPath: "res://MyMod/scenes/rest_site/my_character_rest_site.tscn")); -} - -var character = new CharacterRegistrationEntry() - .AddStartingCard(4) - .AddStartingCard(4) - .AddStartingCard() - .AddStartingRelic(); -``` - -Another mod can append content to that same character later with `CharacterStarterCardRegistrationEntry(count)` or `ModContentRegistry.RegisterCharacterStarterCard(count)`. These starter additions are resolved when the character model is queried, so registration order does not matter as long as everything is registered before content freeze. - -Override `PlaceholderCharacterId` with another base character such as `silent` or `defect` if you want their merchant / rest-site / map / default SFX alignment. Return `null` to disable this fallback. - -### Character-select unlock text (`{Prerequisite}`) - -Localized **`unlockText`** may use the **`{Prerequisite}`** token. Vanilla fills it in **`CharacterModel.GetUnlockText()`** from **`UnlocksAfterRunAs`** (on **`ModCharacterTemplate`**, supply the type via **`UnlocksAfterRunAsType`**): - -- If **`UnlocksAfterRunAs`** is **`null`** (the template default), the game substitutes the generic locked title (**`LOCKED.title`**, often shown as **`???`**). -- If set, the game uses the prerequisite character’s **`Title`** when that character is present in the current **`UnlockState.Characters`**; otherwise it still falls back to **`LOCKED.title`**. - -Override **`UnlocksAfterRunAsType`** so it matches the same character type you pass to **`UnlockEpochAfterWinAs`** / **`UnlockEpochAfterRunAs`** (or equivalent). That keeps the hover text consistent with the real unlock rule. - -**`UnlocksAfterRunAsType` does not perform the unlock** — **`ModUnlockRegistry`** rules and epoch progression remain authoritative. - ---- - -## Story Template - -Inherit `ModStoryTemplate` for the story id (`StoryKey` → slug). Bind epochs in registration order via `RegisterStoryEpoch()`, `TimelineColumnPackEntry<,>`, or `.StoryEpoch<,>()` — see `TimelineAndUnlocks.md`. - -```csharp -public class MyStory : ModStoryTemplate -{ - protected override string StoryKey => "my-character"; -} -``` - -### Ancient Dialogue Localization - -RitsuLib appends localization-defined ancient dialogues for registered mod characters before vanilla `AncientDialogueSet.PopulateLocKeys` runs. - -Key format matches vanilla: - -| Key component | Description | -|---|---| -| `.talk..-.ancient` | Ancient line | -| `.talk..-.char` | Character line | -| Optional suffix `.sfx` | Sound effect | -| Optional suffix `-visit` | Visit override | -| Optional suffix `-attack` | Architect attacker override | -| Optional suffix `r` | Repeat dialogue | - -If you need the helpers directly, use `STS2RitsuLib.Localization.AncientDialogueLocalization`. - ---- - -## Epoch Templates - -RitsuLib provides pre-built epoch templates for common unlock targets: - -| Template | Description | -|---|---| -| `CharacterUnlockEpochTemplate` | Epoch that unlocks the character itself | -| `CardUnlockEpochTemplate` | Epoch that unlocks extra cards | -| `RelicUnlockEpochTemplate` | Epoch that unlocks extra relics | -| `PotionUnlockEpochTemplate` | Epoch that unlocks extra potions | - -```csharp -public class MyCharacterEpoch : CharacterUnlockEpochTemplate -{ -} - -public class MyEpoch2 : CardUnlockEpochTemplate -{ - protected override IEnumerable CardTypes => - [ - typeof(MyAdvancedCard), - ]; -} -``` - ---- - -## Full Registration Example - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - // Cards (specify owning pool) - .Card() - .Card() - .Card() - .Card() - - // Relics - .Relic() - - // Character - .Character() - - // Story and epochs - .Story() - .Epoch() - .Epoch() - - // Unlock rules - .RequireEpoch() // card appears only after epoch 2 - .UnlockEpochAfterRunAs() // unlock epoch 2 after one completed run - - .Apply(); -``` - ---- - -## Model ID and Localization - -Character models follow the same fixed `ModelId.Entry` rule as all other content (see [Content Authoring Toolkit](ContentAuthoringToolkit.md)). - -Example — mod id `MyMod`, type `MyCharacter`: -- `ModelId.Entry` → `MY_MOD_CHARACTER_MY_CHARACTER` -- Localization key → `MY_MOD_CHARACTER_MY_CHARACTER.title` - -> Renaming a CLR type changes its derived entry and affects save compatibility. Avoid renaming after release. - ---- - -## Dependency Rules - -- Card / relic / potion types must be registered before runtime model lookup -- Pool types referenced by the character must already be registered -- Every model — including epoch-gated content — must be registered; unlock rules do not replace registration - ---- - -## Related Documents - -- [Content Authoring Toolkit](ContentAuthoringToolkit.md) -- [Getting Started](GettingStarted.md) -- [Timeline & Unlocks](TimelineAndUnlocks.md) -- [Asset Profiles & Fallbacks](AssetProfilesAndFallbacks.md) -- [Godot Scene Authoring](GodotSceneAuthoring.md) diff --git a/Docs/en/ContentAuthoringToolkit.md b/Docs/en/ContentAuthoringToolkit.md deleted file mode 100644 index 8d72b8a..0000000 --- a/Docs/en/ContentAuthoringToolkit.md +++ /dev/null @@ -1,153 +0,0 @@ -# Content Authoring Toolkit - -This document is the overview for content authoring: registration entry points, model identity, localization coupling, and asset override basics. - -Detailed registration mechanics live in [Content Packs & Registries](ContentPacksAndRegistries.md). Detailed asset semantics live in [Asset Profiles & Fallbacks](AssetProfilesAndFallbacks.md). - ---- - -## Registration APIs - -| API | Purpose | -|---|---| -| `RitsuLibFramework.CreateContentPack(modId)` | Recommended entry point — fluent builder | -| `RitsuLibFramework.GetContentRegistry(modId)` | Low-level content registry | -| `RitsuLibFramework.GetKeywordRegistry(modId)` | Keyword registry | -| `RitsuLibFramework.GetTimelineRegistry(modId)` | Timeline (story / epoch) registry | -| `RitsuLibFramework.GetUnlockRegistry(modId)` | Unlock rule registry | - -`CreateContentPack` wraps all of the above in a fluent builder that executes registered steps in insertion order when `Apply()` is called. - -This document keeps the overview short. For builder surface, manifests, fixed-entry ownership, and freeze behavior, see [Content Packs & Registries](ContentPacksAndRegistries.md). - ---- - -## Content Pack Builder - -All builder methods are chainable. A representative example: - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - .Character() - .Card() - .Relic() - .CardKeywordOwnedByLocNamespace("my_keyword", iconPath: "res://MyMod/art/kw.png") - .Story() - .Epoch() - .RequireEpoch() - .Custom(ctx => { /* ... */ }) - .Apply(); -``` - -`Apply()` returns `ModContentPackContext` for further access to individual registries. - ---- - -## Model ID Rule - -For any model registered through the RitsuLib content registry, `ModelId.Entry` uses: - -``` -__ -``` - -All segments are normalized to **UPPER_SNAKE_CASE**. - -### Examples (Mod id `MyMod`) - -| C# Type | Category | ModelId.Entry | -|---|---|---| -| `MyStrike` | card | `MY_MOD_CARD_MY_STRIKE` | -| `MyStarterRelic` | relic | `MY_MOD_RELIC_MY_STARTER_RELIC` | -| `MyCharacter` | character | `MY_MOD_CHARACTER_MY_CHARACTER` | - -> If two types under the same mod id and category share the same CLR name, they resolve to the same entry and must be renamed. - ---- - -## Localization Rule - -Localization keys are written directly against the fixed `ModelId.Entry`: - -```json -{ - "MY_MOD_CARD_MY_STRIKE.title": "My Strike", - "MY_MOD_CARD_MY_STRIKE.description": "Deal {damage} damage.", - "MY_MOD_RELIC_MY_STARTER_RELIC.title": "My Starter Relic" -} -``` - -`RitsuLibFramework.CreateModLocalization(...)` operates independently from the game's `LocString` pipeline. - ---- - -## Asset Override Rule - -RitsuLib applies template-based asset overrides via interface matching at render time. - -### Card Overrides - -Inherit `ModCardTemplate` and override via `AssetProfile` (recommended) or individual properties: - -```csharp -public class MyCard : ModCardTemplate(1, CardType.Attack, CardRarity.Common, TargetType.SingleEnemy) -{ - // Unified profile (recommended) - public override CardAssetProfile AssetProfile => new() - { - PortraitPath = "res://MyMod/art/my_card.png", - FramePath = "res://MyMod/art/frame.png", - FrameMaterialPath = "res://MyMod/art/frame.material", - }; - - // Or override a single property directly - public override string? CustomPortraitPath => "res://MyMod/art/my_card.png"; -} -``` - -Supported card fields include portrait, frame, portrait border, energy icon, overlay, and banner-related assets. - -### Other Content - -| Content type | Supported override fields | -|---|---| -| Relic | icon, icon outline, big icon | -| Power | icon, big icon | -| Orb | icon, visuals scene | -| Potion | image, outline | - -Override behavior: -1. The model must implement the matching override interface (directly or via `Mod*Template`) -2. The override member must return a non-empty path -3. If the resource path does not exist, RitsuLib emits a one-time warning and falls back to the base asset - -This warning behavior is especially important for character assets because the base game has almost no safe fallback for missing paths. - -For the full profile records, helper factories, placeholder behavior, and diagnostics policy, see [Asset Profiles & Fallbacks](AssetProfilesAndFallbacks.md). - ---- - -## Registration Timing - -All content registration must be completed before the framework freezes content registration (during early game boot). Additional registration after the freeze is invalid and may throw. - -The freeze is signaled by `ContentRegistrationClosedEvent`. - ---- - -## Compatibility - -The fixed-entry rule applies only to model types explicitly registered through the RitsuLib content registry, at `ModelDb.GetEntry(Type)`. Models not registered through RitsuLib are unaffected. - ---- - -## Related Documents - -- [Getting Started](GettingStarted.md) -- [Content Packs & Registries](ContentPacksAndRegistries.md) -- [Character & Unlock Templates](CharacterAndUnlockScaffolding.md) -- [Custom Events](CustomEvents.md) -- [Card Dynamic Variables](CardDynamicVarToolkit.md) -- [Localization & Keywords](LocalizationAndKeywords.md) -- [Framework Design](FrameworkDesign.md) -- [Asset Profiles & Fallbacks](AssetProfilesAndFallbacks.md) diff --git a/Docs/en/ContentPacksAndRegistries.md b/Docs/en/ContentPacksAndRegistries.md deleted file mode 100644 index 3c410e0..0000000 --- a/Docs/en/ContentPacksAndRegistries.md +++ /dev/null @@ -1,384 +0,0 @@ -# Content Packs & Registries - -This document is the reference for how RitsuLib registration is organized. - -It covers: - -- the relationship between `CreateContentPack(...)` and the underlying registries -- what `Apply()` actually does -- when to use builder steps, manifests, direct registry access, or optional CLR attributes -- how fixed model identity and ModelDb integration relate to registration -- generated placeholders for cards/relics/potions (API, ordering, and risks) - ---- - -## Registry Map - -RitsuLib keeps registration responsibilities split by concern: - -| Registry | Purpose | -|---|---| -| `ModContentRegistry` | Register models: characters, acts, pool-bound cards/relics/potions, powers, orbs, enchantments, afflictions, achievements, singletons, good/bad daily modifiers, shared card/relic/potion pools, events, ancients, monsters, and generated placeholders | -| `ModKeywordRegistry` | Register reusable keyword definitions | -| `ModTimelineRegistry` | Register stories and epochs | -| `ModUnlockRegistry` | Register epoch requirements and progression rules | - -`CreateContentPack(modId)` is the convenience layer that coordinates all four. - ---- - -## `CreateContentPack(...)` - -The fluent builder is the recommended entry point: - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - .Character() - .Card() - .Relic() - .CardKeywordOwnedByLocNamespace("brew") - .Epoch() - .Story() - .RequireEpoch() - .Apply(); -``` - -What the builder does not do: - -- it does not auto-discover content by reflection -- it does not reorder your steps for you -- it does not replace the underlying registries - -It simply records registration steps and runs them in insertion order when `Apply()` is called. - ---- - -## `ModContentPackContext` - -`Apply()` returns a `ModContentPackContext` containing: - -- `Content` -- `Keywords` -- `Timeline` -- `Unlocks` - -That means the fluent builder can be your main registration path, while still letting you access the raw registries afterward. - ---- - -## Step Ordering - -Builder steps execute in the order you add them. - -That matters when: - -- your custom step expects a registry entry to already exist -- you mix builder calls with `Custom(ctx => ...)` -- you want logs to reflect a specific setup flow - -`CreateContentPack` is intentionally explicit here. It is a sequenced registration script, not a dependency solver. - ---- - -## Builder Surface - -The builder supports several kinds of steps: - -- content model registration -- keyword registration -- timeline registration -- unlock registration -- manifest-driven registration -- arbitrary custom callbacks - -Less obvious helpers that are still useful: - -- `Entry(IContentRegistrationEntry)` -- `Entries(IEnumerable)` -- `Keyword(KeywordRegistrationEntry)` -- `Keywords(IEnumerable)` -- `Manifest(contentEntries, keywordEntries)` -- `Custom(Action)` -- generated placeholders: `PlaceholderCard(...)`, `PlaceholderRelic(...)`, `PlaceholderPotion(...)` (see “Generated placeholder content” below) -- extended standalone / pool types: `.Enchantment()`, `.Affliction()`, `.Achievement()`, `.Singleton()`, `.GoodModifier()` / `.BadModifier()`, `.SharedRelicPool()`, `.SharedPotionPool()` (see “Content model registration matrix” below) - -These are useful when you want registration declared as data instead of written inline in one long chain. - ---- - -## When To Use The Raw Registries - -Use `CreateContentPack(...)` by default. - -Use raw registries directly when: - -- registration is split across several modules -- you want to expose registration helpers from your own library layer -- you need registry access without committing to a single fluent chain -- you are generating registration entries programmatically - -Typical direct access looks like: - -```csharp -var content = RitsuLibFramework.GetContentRegistry("MyMod"); -content.RegisterCharacter(); - -var timeline = RitsuLibFramework.GetTimelineRegistry("MyMod"); -timeline.RegisterEpoch(); -``` - -The registries are first-class APIs, not implementation details. - ---- - -## What The Content Registry Owns - -`ModContentRegistry` is responsible for: - -- recording which model types belong to which mod -- validating ownership and duplicate registration -- feeding ModelDb integration: global accessors such as `AllCharacters`, acts, powers, orbs, shared events, ancients, **shared card / relic / potion pool types**, `DebugEnchantments`, `DebugAfflictions`, `Achievements`, `GoodModifiers`, `BadModifiers`, and related enumerations are extended via patches where needed; **per-pool** cards/relics/potions are merged through `ModHelper.AddModelToPool` when each pool expands `AllCards` / `AllRelics` / `AllPotions` (a different code path than those global appenders) -- generating fixed public `ModelId.Entry` values for registered types - -That owner tracking is what lets RitsuLib safely answer questions like: - -- which mod registered this type? -- what should its fixed public entry be? -- should vanilla progression/compatibility logic treat this as modded content? - ---- - -## Fixed Public Identity - -For RitsuLib-registered models, public `ModelId.Entry` is forced into a stable format: - -```text -__ -``` - -This is applied through the ModelDb identity patch, not by changing your CLR type names at source. - -Why it matters: - -- localization keys become deterministic -- default asset conventions become predictable -- model ownership remains clear across patches and saves - -The identity rule applies only to types explicitly registered through RitsuLib. - ---- - -## ModelDb Integration - -Registration alone is not enough; the game still needs to see the content. - -RitsuLib patches ModelDb and related model access points to: - -- append registered characters, acts, powers, orbs, events, ancients, shared card pools, **shared relic pools** (`AllRelicPools`), **shared potion pools** (`AllPotionPools`), **debug enchantments** (`DebugEnchantments`), **debug afflictions** (`DebugAfflictions`), **achievements** (`Achievements`), and **daily modifiers** (`GoodModifiers` / `BadModifiers`) where applicable -- attach registered cards/relics/potions to their **target pools** via `ModHelper.AddModelToPool` (concatenated when each pool materializes its `All*` sequence) -- force fixed public entries for registered model types -- inject types that live in **dynamic assemblies** (e.g. Reflection.Emit placeholders) into `ModelDb` before init completes, for every registered model category the registry tracks -- bootstrap dynamic act-content patching before caches lock in - -`MutuallyExclusiveModifiers` is **not** extended automatically; mod modifiers registered as good/bad appear only in those two lists. - -This is why registration must happen before the framework freeze points. - ---- - -## Freeze Behavior - -The relevant registries freeze after early initialization: - -- content registration freeze -- timeline registration freeze -- unlock registration freeze - -Once frozen, later registration attempts throw. - -This is intentional because the framework wants: - -- stable identity -- stable model lists -- deterministic unlock/filter behavior - -If a mod registers content late, the safest outcome is to fail early rather than let the game build partial caches. - ---- - -## Manifests And Entry Objects - -If you want registration to be declared as data, you can package it into entry objects: - -```csharp -var contentEntries = new IContentRegistrationEntry[] -{ - new CharacterRegistrationEntry(), - new CardRegistrationEntry(), -}; - -var keywordEntries = new[] -{ - KeywordRegistrationEntry.OwnedCardByLocNamespace("MyMod", "brew"), -}; - -RitsuLibFramework.CreateContentPack("MyMod") - .Manifest(contentEntries, keywordEntries) - .Apply(); -``` - -This is useful when you want a declarative registration list or want to share registration bundles across modules. - -You can mix entry types freely—for example: - -```csharp -var contentEntries = new IContentRegistrationEntry[] -{ - new CharacterRegistrationEntry(), - new CardRegistrationEntry(), - new EnchantmentRegistrationEntry(), - new PowerRegistrationEntry(), - new SharedRelicPoolRegistrationEntry(), -}; -``` - ---- - -## Attribute-based registration (optional) - -CLR attributes in `STS2RitsuLib.Interop.AutoRegistration` (for example `[RegisterSharedCardPool]`, `[RegisterCard(typeof(MyPool))]`) ultimately call the **same registry APIs** as the fluent builder, direct registries, and manifest entries. - -RitsuLib runs them during the early **mod type discovery** pass (`ModTypeDiscoveryPatch`). The built-in `AttributeAutoRegistrationTypeDiscoveryContributor` scans **concrete** CLR types in assemblies you register with **`ModTypeDiscoveryHub.RegisterModAssembly(modId, Assembly.GetExecutingAssembly())`** from your mod initializer **before** `PatchAll`. A type must resolve to a mod id (usually via the manifest-mapped assembly); if not, annotate the type with **`[RitsuLibOwnedBy("modId")]`**. - -This does **not** replace `CreateContentPack(...)`; it is an alternative authoring style. Mixing approaches is acceptable when ordering and freeze rules remain valid. - -### `Inherit` on `AutoRegistrationAttribute` - -Attributes apply to the type they annotate. **`Inherit`** defaults to **`false`**. When **`Inherit = true`** on an attribute declared on a **base class**, **concrete derived types** are handled as if the same attribute were declared on each subclass (the registry still receives the **subclass** `Type`). If a subclass already has a **direct** attribute that would produce the **same registration signature**, the inherited duplicate is skipped. Abstract base types are skipped by the scan; only concrete types are registered. - ---- - -## Content model registration matrix - -Every row below is **one conceptual kind of content**. You can register it in **three** primary equivalent ways below, plus the optional attribute path in the previous section (unless noted): - -1. **Fluent** — `ModContentPackBuilder` method on `CreateContentPack(...)` -2. **Registry** — `ModContentRegistry` method from `RitsuLibFramework.GetContentRegistry(modId)` or `ctx.Content` in `Custom(...)` -3. **Manifest entry** — a type implementing `IContentRegistrationEntry` in `STS2RitsuLib.Scaffolding.Content` (use `.Entry(...)`, `.Entries(...)`, or `.Manifest(...)`) - -| Content | Fluent | Registry | Manifest entry | -|---|---|---|---| -| Character | `.Character()` | `RegisterCharacter()` | `CharacterRegistrationEntry` | -| Act | `.Act()` | `RegisterAct()` | `ActRegistrationEntry` | -| Card in pool | `.Card(...)` | `RegisterCard(...)` | `CardRegistrationEntry` | -| Relic in pool | `.Relic(...)` | `RegisterRelic(...)` | `RelicRegistrationEntry` | -| Potion in pool | `.Potion(...)` | `RegisterPotion(...)` | `PotionRegistrationEntry` | -| Power | `.Power()` | `RegisterPower()` | `PowerRegistrationEntry` | -| Orb | `.Orb()` | `RegisterOrb()` | `OrbRegistrationEntry` | -| Enchantment | `.Enchantment()` | `RegisterEnchantment()` | `EnchantmentRegistrationEntry` | -| Affliction | `.Affliction()` | `RegisterAffliction()` | `AfflictionRegistrationEntry` | -| Achievement | `.Achievement()` | `RegisterAchievement()` | `AchievementRegistrationEntry` | -| Singleton | `.Singleton()` | `RegisterSingleton()` | `SingletonRegistrationEntry` | -| Daily modifier (good) | `.GoodModifier()` | `RegisterGoodModifier()` | `GoodModifierRegistrationEntry` | -| Daily modifier (bad) | `.BadModifier()` | `RegisterBadModifier()` | `BadModifierRegistrationEntry` | -| Shared card pool | `.SharedCardPool()` | `RegisterSharedCardPool()` | `SharedCardPoolRegistrationEntry` | -| Shared relic pool | `.SharedRelicPool()` | `RegisterSharedRelicPool()` | `SharedRelicPoolRegistrationEntry` | -| Shared potion pool | `.SharedPotionPool()` | `RegisterSharedPotionPool()` | `SharedPotionPoolRegistrationEntry` | -| Shared event | `.SharedEvent()` | `RegisterSharedEvent()` | `SharedEventRegistrationEntry` | -| Act encounter | `.ActEncounter()` | `RegisterActEncounter()` | `ActEncounterRegistrationEntry` | -| Act event | `.ActEvent()` | `RegisterActEvent()` | `ActEventRegistrationEntry` | -| Shared ancient | `.SharedAncient()` | `RegisterSharedAncient()` | `SharedAncientRegistrationEntry` | -| Act ancient | `.ActAncient()` | `RegisterActAncient()` | `ActAncientRegistrationEntry` | -| Monster | *(no fluent helper)* | `RegisterMonster()` | `MonsterRegistrationEntry` | -| Placeholder card / relic / potion | `.PlaceholderCard<...>(...)` etc. | `RegisterPlaceholderCard<...>(...)` etc. | `PlaceholderCardRegistrationEntry<...>` etc. | -| Archaic Tooth mapping | `.ArchaicToothTranscendence<...>()` or `.ArchaicToothTranscendence(id, type)` | `RitsuLibFramework.RegisterArchaicToothTranscendenceMapping(...)` | `ArchaicToothTranscendenceRegistrationEntry<...>` / `ArchaicToothTranscendenceByIdRegistrationEntry` | -| Touch of Orobas mapping | `.TouchOfOrobasRefinement<...>()` or `.TouchOfOrobasRefinement(id, type)` | `RitsuLibFramework.RegisterTouchOfOrobasRefinementMapping(...)` | `TouchOfOrobasRefinementRegistrationEntry<...>` / `TouchOfOrobasRefinementByIdRegistrationEntry` | - -**Enchantments:** optional authoring baseline `ModEnchantmentTemplate` plus `IModEnchantmentAssetOverrides` / `EnchantmentIntendedIconPathPatch` (see scaffolding content patches) for custom icon paths; registration in this table is still required for ownership, fixed `ModelId.Entry`, and dynamic-assembly injection like other model kinds. - -**Singletons:** there is no global `ModelDb` list to patch; registration still records ownership and injects dynamic types so `ModelDb.Singleton()` resolves correctly. - ---- - -## Generated placeholder content - -Use this when you want pool entries and a **stable public `ModelId.Entry`** (via `ModelPublicEntryOptions.FromStem` / `FromFullPublicEntry`) **without authoring one CLR type per card/relic/potion**—for example so reward tables, unlocks, or saves can reference IDs while content is still WIP. RitsuLib generates sealed subclasses at runtime with **Reflection.Emit**; gameplay is intentionally **no-op** (empty `OnPlay` / `OnUse`, etc.). - -### API summary - -| Use case | Entry point | -|---|---| -| Fluent pack | `PlaceholderCard(stableEntryStem, PlaceholderCardDescriptor)`, `PlaceholderRelic(...)`, `PlaceholderPotion(...)` | -| Registry | `ModContentRegistry.RegisterPlaceholderCard(...)` (overloads accept `ModelPublicEntryOptions`, e.g. `FromFullPublicEntry`) | -| Shape | `PlaceholderCardDescriptor`, `PlaceholderRelicDescriptor`, `PlaceholderPotionDescriptor` (structs with defaults) | -| You already have a type | Two-type overload `PlaceholderCard(stem)` only pins the entry for an existing class | - -`ModPlaceholderCardTemplate` / `ModPlaceholderRelicTemplate` / `ModPlaceholderPotionTemplate` are bases for emitted types; **mods normally should not subclass them** unless you have an advanced reason. - -### Example - -```csharp -using MegaCrit.Sts2.Core.Entities.Cards; -using STS2RitsuLib.Content; - -RitsuLibFramework.CreateContentPack("MyMod") - .Manifest(contentEntries, keywordEntries) - .Custom(ctx => - { - ctx.Content.RegisterPlaceholderCard("wip_reward_attack", - new PlaceholderCardDescriptor( - BaseCost: 1, - Type: CardType.Attack, - Rarity: CardRarity.Common, - Target: TargetType.AnyEnemy)); - }) - .Apply(); -``` - -For relics, `PlaceholderRelicDescriptor.MerchantCostOverride`: **`< 0` (default `-1`)** keeps rarity-based shop pricing; **`≥ 0`** overrides `MerchantCost`. - -### Ordering - -If you combine `Manifest(...)` with placeholders, register placeholders **after** prerequisites exist (typical pattern: `.Manifest(...)` then `.Custom(ctx => ...)` calling `RegisterPlaceholder*`), so pools and other types are already registered. - ---- - -### Warnings (read carefully) - -> **Saves and entry stability** -> Once a placeholder id appears in saves or unlock data, its `ModelId.Entry` (from the stem or `FromFullPublicEntry`) is a long-lived contract. **Renaming stems or full-entry strings** can break old saves or unlock references. When shipping real content, keep the same entry or plan a migration. - -> **No gameplay effects** -> Placeholders do not implement damage, draw, relic triggers, etc. They prevent missing-model failures in some paths; **balance and UX can still be wrong** until you replace them with real types. - -> **Localization and assets** -> Placeholders still follow default loc-key and asset conventions from the entry. Missing translations or art may show raw keys or blanks—that is expected and does not mean registration failed. - -> **Multiplayer and `ModelIdSerializationCache.Hash`** -> Emitted types are **not** returned by the game’s vanilla `AllAbstractModelSubtypes` scan. RitsuLib injects dynamic-assembly models before `ModelDb.Init` and, after `ModelIdSerializationCache.Init`, **merges every model present in `ModelDb` into the net-ID tables and recomputes the hash** (same algorithm shape as vanilla). -> **Consequence**: different loaded mod sets → different hashes → clients **may not match** for multiplayer or replays. This is inherent to dynamic placeholders, not only a single-player concern. - -> **RitsuLib version coupling** -> Placeholder generation, `InjectDynamicRegisteredModels`, and serialization-cache integration follow the framework version you ship. Pin a compatible `STS2-RitsuLib` dependency and retest after upgrading the library. - ---- - -## Recommended Registration Pattern - -For most mods: - -1. create one content pack in the mod initializer -2. register all content, keywords, timeline nodes, and unlock rules there -3. keep `Custom(...)` steps small and explicit -4. avoid late registration from gameplay hooks -5. with `TypeListCardPoolModel`, register pool cards via `.Card()` or `CardRegistrationEntry`; **do not** override the obsolete `CardTypes` hook (the base already defaults to empty—see [Getting Started](GettingStarted.md)) - -If the mod grows large, keep the builder at the top level and feed it entry objects or helper methods from submodules. - ---- - -## Related Documents - -- [Content Authoring Toolkit](ContentAuthoringToolkit.md) -- [Timeline & Unlocks](TimelineAndUnlocks.md) -- [Framework Design](FrameworkDesign.md) diff --git a/Docs/en/CreatureVisualsAndAnimation.md b/Docs/en/CreatureVisualsAndAnimation.md deleted file mode 100644 index 31d905b..0000000 --- a/Docs/en/CreatureVisualsAndAnimation.md +++ /dev/null @@ -1,338 +0,0 @@ -# Creature Visuals & Animation - -This document covers the runtime-Godot factory interfaces that let mod creatures -replace vanilla `CreateVisuals` / `GenerateAnimator`, and the backend-agnostic -animation state machine (`ModAnimStateMachine`) that drives non-Spine combat -visuals (`AnimatedSprite2D`, Godot `AnimationPlayer`, or cue frame sequences) -through the same trigger protocol Spine creatures use. - -For content pack registration, see [Content Packs & Registries](ContentPacksAndRegistries.md). -For character assembly, see [Character & Unlock Templates](CharacterAndUnlockScaffolding.md). -For Harmony patch wiring in general, see [Patching Guide](PatchingGuide.md). - ---- - -## Overview - -Vanilla binds a `MonsterModel` or `CharacterModel` to combat visuals through: - -- `Model.CreateVisuals()` — returns an `NCreatureVisuals` (the scene root under - the combat creature node). -- `Model.GenerateAnimator(MegaSprite controller)` — returns a `CreatureAnimator` - wrapping a Spine skeleton with an idle / hit / attack / cast / die / relaxed - state graph. -- `NCreature.SetAnimationTrigger(trigger)` — dispatches triggers - (`Idle`, `Attack`, `Cast`, `Hit`, `Dead`, `Revive`, ...) into that animator at - runtime. - -Mods commonly need one or more of: - -- supplying `NCreatureVisuals` from code (not a path); -- replacing the Spine state graph with a mod-authored one; -- animating creatures **without** a Spine skeleton (sprite sheets, frame - sequences, Godot `AnimationPlayer`). - -RitsuLib exposes three orthogonal factory interfaces for those hooks and one -state machine abstraction for the non-Spine case. All four interfaces are -creature-agnostic (players **and** monsters) and do not require subclassing any -template. - -| Interface | Purpose | Vanilla entry point | -|---|---|---| -| `IModCreatureVisualsFactory` | Build `NCreatureVisuals` from code | `CharacterModel.CreateVisuals`, `MonsterModel.CreateVisuals` | -| `IModCreatureAnimatorFactory` | Build Spine `CreatureAnimator` from code | `CharacterModel.GenerateAnimator`, `MonsterModel.GenerateAnimator` | -| `IModNonSpineAnimationStateMachineFactory` | Build `ModAnimStateMachine` for non-Spine visuals | `NCreature.SetAnimationTrigger` (routing patch) | -| `IModCharacterMerchantAnimationStateMachineFactory` | Build `ModAnimStateMachine` for merchant / rest-site character visuals | Merchant scene setup | - -The merchant factory is character-specific because monsters never appear in -merchant / rest-site scenes; the other three apply to any -`MegaCrit.Sts2.Core.Models.AbstractModel`. - ---- - -## Creature Visuals Factory - -`IModCreatureVisualsFactory` replaces the path-based -`(Character|Monster)Model.CreateVisuals` when it returns a non-null -`NCreatureVisuals`. `null` defers to `CustomVisualsPath` / vanilla resolution. - -```csharp -public class MyCharacter : ModCharacterTemplate<...> -{ - // IModCreatureVisualsFactory is already implemented by the template, - // forwarding to this protected virtual: - protected override NCreatureVisuals? TryCreateCreatureVisuals() - { - var scene = GD.Load( - "res://MyMod/scenes/my_character/my_character_visuals.tscn"); - return scene.Instantiate(); - } -} -``` - -For mods that do not use `ModCharacterTemplate` / `ModMonsterTemplate`, implement -the interface directly on your `CharacterModel` / `MonsterModel`: - -```csharp -public class MyRawCharacter : CharacterModel, IModCreatureVisualsFactory -{ - public NCreatureVisuals? TryCreateCreatureVisuals() => ...; -} -``` - -The routing patches (`CharacterCreatureVisualsRuntimeFactoryPatch`, -`MonsterCreatureVisualsRuntimeFactoryPatch`) run at Harmony `Priority.First`, so -they take effect before the vanilla path-based loader. - ---- - -## Creature Animator Factory (Spine) - -`IModCreatureAnimatorFactory` replaces `GenerateAnimator` for Spine visuals. -Prefer `ModAnimStateMachines.Standard` to match the vanilla state shape: - -```csharp -public class MySpineCharacter : ModCharacterTemplate<...> -{ - protected override CreatureAnimator? SetupCustomCreatureAnimator(MegaSprite controller) => - ModAnimStateMachines.Standard( - controller, - idleName: "idle_loop", - deadName: "die", - hitName: "hit", - attackName: "attack", - castName: "cast", - relaxedName: "relaxed"); -} -``` - -`ModAnimStateMachines.Standard` returns a `CreatureAnimator` wired with any-state -triggers for `Idle`, `Dead`, `Hit`, `Attack`, `Cast`, `Relaxed`. Terminal states -(`Dead`) leave `NextState` unset so playback does not loop back to idle. - -The routing patches (`CharacterCreatureAnimatorRuntimeFactoryPatch`, -`MonsterCreatureAnimatorRuntimeFactoryPatch`) honour non-null factory output; -`null` defers to vanilla `GenerateAnimator`. - ---- - -## Non-Spine State Machine - -For creatures whose combat visuals are **not** Spine (no `MegaSprite` controller), -implement `IModNonSpineAnimationStateMachineFactory` and return a -`ModAnimStateMachine` bound to the visuals root. The -`ModCreatureNonSpineAnimationPlaybackPatch` routes -`NCreature.SetAnimationTrigger(trigger)` into `ModAnimStateMachine.SetTrigger`, -so the non-Spine path receives the **same trigger stream** as Spine creatures. - -### Opting in - -```csharp -public class MyWolf : ModMonsterTemplate -{ - // IModNonSpineAnimationStateMachineFactory is already implemented by the - // template, forwarding to this protected virtual: - protected override ModAnimStateMachine? SetupCustomNonSpineAnimationStateMachine( - Node visualsRoot, MonsterModel monster) - { - if (visualsRoot is not MyWolfVisuals wolfVisuals) - return null; - - var backend = new AnimatedSprite2DBackend(wolfVisuals.GetAnimatedSprite()); - - return ModAnimStateMachineBuilder.Create() - .AddState("idle", loop: true).AsInitial().Done() - .AddState("attack").WithNext("idle").Done() - .AddState("hurt").WithNext("idle").Done() - .AddState("die").Done() // terminal: no NextState - .AddAnyState("Idle", "idle") - .AddAnyState("Attack", "attack") - .AddAnyState("Hit", "hurt") - .AddAnyState("Dead", "die") - .Build(backend); - } -} -``` - -Equivalent if you do not use a template: - -```csharp -public class MyRawMonster : MonsterModel, IModNonSpineAnimationStateMachineFactory -{ - public ModAnimStateMachine? TryCreateNonSpineAnimationStateMachine(Node visualsRoot) - => /* same builder code */; -} -``` - -### Routing behaviour - -`ModCreatureNonSpineAnimationPlaybackPatch` is a prefix on -`NCreature.SetAnimationTrigger`: - -1. If the creature has a Spine animator, skip (vanilla path runs). -2. Look up the creature's model (`Entity.Player?.Character` or `Entity.Monster`). -3. If either implements `IModNonSpineAnimationStateMachineFactory` and - returns a non-null state machine, dispatch the trigger via - `ModAnimStateMachine.SetTrigger` and return. -4. Otherwise, fall back to single-shot cue playback - (`ModCreatureVisualPlayback.TryPlayFromCreatureAnimatorTrigger`). - -State machines are **cached per visuals root** with a -`ConditionalWeakTable`, so the factory runs at most once -per combat lifetime and is automatically released when the visuals node is -freed. - -### Shorthand: `ModAnimStateMachines.StandardCue` - -For visuals that follow the vanilla idle / dead / hit / attack / cast / relaxed -shape, `ModAnimStateMachines.StandardCue` builds the state graph for you. It -uses `CompositeBackendFactory` to pick the best backend per state (cue frame -sequences first, Godot `AnimationPlayer` or `AnimatedSprite2D` if they resolve -the animation id) and returns a ready-to-use `ModAnimStateMachine`. - ---- - -## Animation Backends - -`IAnimationBackend` is the uniform driver surface consumed by -`ModAnimStateMachine`. Each backend wraps a Godot animation subsystem and -reports `Started` / `Completed` / `Interrupted` events. - -| Backend | Drives | Used for | -|---|---|---| -| `AnimatedSprite2DBackend` | `AnimatedSprite2D` | Frame-based sprite animation | -| `GodotAnimationPlayerBackend` | `AnimationPlayer` | Godot `.tres` animation library | -| `CueAnimationBackend` | `VisualCueSet` (cue frame sequences, cue textures) | Per-cue static textures / sequence playback | -| `SpineAnimationBackend` | `MegaSprite` | Spine skeletal animation | -| `CompositeAnimationBackend` | Any mix | Multi-backend dispatch (one state plays via sprite, another via animation player, etc.) | - -### Event contract - -| Event | When it fires | -|---|---| -| `Started(id)` | Playback for `id` has started | -| `Completed(id)` | One-shot finished, or a loop cycle ended | -| `Interrupted(id)` | Playback was replaced before completion | - -`ModAnimState.NextState` advances on `Completed`, so backends must emit it -accurately for non-looping states (`attack -> idle` etc.). - -### Queue semantics - -`Queue(id, loop)` is semantically "play this after the currently active -animation finishes". Backends implement it differently: - -| Backend | `Queue` behaviour | -|---|---| -| `SpineAnimationBackend` | True native Spine queue (`AddAnimation` on the track) | -| `AnimatedSprite2DBackend` | Stores pending id, plays on next `animation_finished` signal | -| `GodotAnimationPlayerBackend` | Uses `AnimationPlayer.Queue` | -| `CueAnimationBackend` | Stores pending id, plays on sequence completion | - -In all cases, calling `Play` clears any pending queued animation. - -### `Stop()` and cross-backend transitions - -`IAnimationBackend.Stop()` (default interface method) halts the backend -**silently** — it neither fires `Completed` nor `Interrupted`, and clears any -queued animation. The primary consumer is `CompositeAnimationBackend` when -transitioning from one child backend to another: - -1. The new state's backend differs from the active one. -2. `Interrupted` is fired for the outgoing animation. -3. The outgoing backend's `Stop()` is called to clear its internal state. -4. The incoming backend's `Play` runs. - -Without `Stop()`, the outgoing backend could keep emitting `Completed` / -`Interrupted` events bound to its old state id and confuse the state machine. - ---- - -## Lifecycle Trigger Patches - -Vanilla `NCreature.StartDeathAnim` and `NCreature.StartReviveAnim` dispatch the -`Dead` / `Revive` triggers only when `_spineAnimator != null`. Non-Spine -creatures therefore never receive those triggers, so a custom state machine -never sees the death animation play when the run is abandoned or the player -dies. - -RitsuLib fixes this with two Postfix patches: - -- `NCreatureNonSpineDeathAnimationTriggerPatch` — dispatches `Dead` after - `StartDeathAnim`. -- `NCreatureNonSpineReviveAnimationTriggerPatch` — dispatches `Revive` after - `StartReviveAnim`. - -### Scope gate - -The patches are **opt-in**: they only fire when the creature has no Spine -animator and the model opts into the RitsuLib visuals pipeline. Specifically, -`NonSpineAnimationTriggerScope.AppliesTo(NCreature)` returns `true` only when -**one** of the following holds for the creature's model: - -| Model slot | Interface | Notes | -|---|---|---| -| `Entity.Player?.Character` | `IModNonSpineAnimationStateMachineFactory` | State machine path | -| `Entity.Monster` | `IModNonSpineAnimationStateMachineFactory` | State machine path | -| `Entity.Player?.Character` | `IModCharacterAssetOverrides` | Cue-playback fallback (player-only) | - -Vanilla creatures and mods that do not opt into RitsuLib visuals are never -affected. The gate is identical for the `Dead` and `Revive` patches. - ---- - -## Migration & Deprecation - -Two factory interfaces were originally named after the creature kind. They are -now unified and the old names marked `[Obsolete]`: - -| New (preferred) | Obsolete aliases | -|---|---| -| `IModCreatureVisualsFactory` | `IModMonsterCreatureVisualsFactory`, `IModCharacterCreatureVisualsFactory` | -| `IModCreatureAnimatorFactory` | `IModCharacterCreatureAnimatorFactory` | - -### Compatibility guarantees - -- The routing patches check both the new and the obsolete interfaces on each - call, so mods that implement only the old interface continue to work without - any code change. -- `ModCharacterTemplate` / `ModMonsterTemplate` implement **both** the new and - the obsolete aliases and forward to the same protected virtual hooks, so - external `is IModCharacterCreatureVisualsFactory` checks against a template - subclass still succeed. -- Implementing an obsolete interface emits compiler warning **CS0618** to guide - migration. No runtime warning or behavioural change. - -### Migration steps - -1. Replace the old interface name in the `: Interfaces` list and in explicit - interface implementations: - - `IModMonsterCreatureVisualsFactory` → `IModCreatureVisualsFactory` - - `IModCharacterCreatureVisualsFactory` → `IModCreatureVisualsFactory` - - `IModCharacterCreatureAnimatorFactory` → `IModCreatureAnimatorFactory` -2. The method signatures (`TryCreateCreatureVisuals()`, - `TryCreateCreatureAnimator(MegaSprite)`) are unchanged; only the declaring - interface name differs. -3. Rebuild. CS0618 warnings disappear. - -No migration is required if you only subclass the templates and override the -protected virtual hooks (`TryCreateCreatureVisuals`, -`SetupCustomCreatureAnimator`); those hooks are unchanged. - ---- - -## Summary Cheat-sheet - -```text -Goal Interface to implement ---------------------------------------------------------------------------- -Replace CreateVisuals (players or monsters) IModCreatureVisualsFactory -Replace Spine GenerateAnimator IModCreatureAnimatorFactory -Drive a non-Spine state machine IModNonSpineAnimationStateMachineFactory -Drive merchant / rest-site state machine IModCharacterMerchantAnimationStateMachineFactory -``` - -All four interfaces are honoured whether you inherit `ModCharacterTemplate` / -`ModMonsterTemplate` or implement them directly on your `CharacterModel` / -`MonsterModel`. The routing patches always run at Harmony `Priority.First` and -defer to vanilla when the factory returns `null`. diff --git a/Docs/en/CustomEvents.md b/Docs/en/CustomEvents.md deleted file mode 100644 index 10db426..0000000 --- a/Docs/en/CustomEvents.md +++ /dev/null @@ -1,264 +0,0 @@ -# Custom Events - -This document explains how to plug custom events into the game's event pipeline using RitsuLib. - -It covers three registration shapes: - -- shared events: `SharedEvent()` -- act-specific events: `ActEvent()` -- ancients: `SharedAncient()` / `ActAncient()` - ---- - -## Base-game event pipeline - -> The following is the game's own event runtime flow, to help you see where RitsuLib registration ultimately takes effect. - -Event generation and execution in the game involve these stages: - -| Stage | Game type | Role | -|---|---|---| -| Candidate generation | `ActModel.GenerateRooms(...)` | Builds the candidate list from the act-local event pool and the `ModelDb.AllSharedEvents` shared pool | -| Filtering | `RoomSet.EnsureNextEventIsValid(...)` | Filters using `IsAllowed(runState)` and visited-state records | -| Entry | `EventRoom.Enter(...)` | Preloads assets, creates the mutable instance, and builds the event UI | -| Assets | `EventModel.GetAssetPaths(...)` | Supplies asset paths that must be ready before the event opens | - ---- - -## RitsuLib registration - -RitsuLib does not replace the flow above. At registration time it adds mod events into the same entry points the base game already uses: - -- shared events are appended to `ModelDb.AllSharedEvents` -- act events are appended to the selected act's event list -- ancients are appended to the corresponding shared or act-local ancient lists - -For authors, the work boils down to two steps: - -1. define a valid `EventModel` or `AncientEventModel` subtype -2. register it before content registration freezes - ---- - -## Minimal normal event - -Prefer inheriting `ModEventTemplate` rather than subclassing the base `EventModel` directly (see below). - -```csharp -using MegaCrit.Sts2.Core.Events; -using STS2RitsuLib.Scaffolding.Content; - -public sealed class MyFirstEvent : ModEventTemplate -{ - protected override IReadOnlyList 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; - } -} -``` - -A minimal usable event should: - -- implement `GenerateInitialOptions()` -- advance or finish the event inside option callbacks -- keep localization keys aligned with the final `ModelId.Entry` - ---- - -## Registration - -### Shared event - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - .SharedEvent() - .Apply(); -``` - -### Act-specific event - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - .ActEvent() - .Apply(); -``` - -### Ancient - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - .SharedAncient() - .Apply(); -``` - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - .ActAncient() - .Apply(); -``` - ---- - -## Localization keys - -After registration through RitsuLib, the event's `ModelId.Entry` follows a fixed format: - -```text -_EVENT_ -``` - -For `MyMod` and `MyFirstEvent`: - -```text -MY_MOD_EVENT_MY_FIRST_EVENT -``` - -Example localization block for a minimal normal event: - -```json -{ - "MY_MOD_EVENT_MY_FIRST_EVENT.title": "A Strange Spring", - "MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.description": "A glowing spring waits by the roadside.", - "MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.options.ACCEPT.title": "Drink", - "MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.options.ACCEPT.description": "This might go well.", - "MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.options.LEAVE.title": "Leave", - "MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.options.LEAVE.description": "Do not risk it.", - "MY_MOD_EVENT_MY_FIRST_EVENT.pages.ACCEPT.description": "You feel renewed.", - "MY_MOD_EVENT_MY_FIRST_EVENT.pages.LEAVE.description": "You walk away." -} -``` - -The key requirement is consistency: titles, page text, and option keys should all be derived from the same final `Id.Entry`. - ---- - -## Why use `ModEventTemplate` - -> The following explains a behavioral detail of the base game's `EventModel`. - -In the base game, `EventModel.InitialOptionKey(...)` and internal option-key helpers build key prefixes from `GetType().Name` (via `Slugify`), while titles, page text, and related lookups use `Id.Entry`. - -For vanilla events those two usually match. For events registered through RitsuLib, `GetType().Name` and `Id.Entry` differ, so some text lookups use a different key prefix than the rest. - -`ModEventTemplate` and `ModAncientEventTemplate` use `protected new` to hide the base `InitialOptionKey` helpers and generate option keys from the final registered `Id.Entry`, removing that mismatch. - ---- - -## `IsAllowed` - -> The following describes the base game's event filtering mechanism. - -Override `IsAllowed(RunState runState)` when the event should only appear in some runs: - -```csharp -public override bool IsAllowed(RunState runState) -{ - return !runState.VisitedEventIds.Contains(Id); -} -``` - -At runtime, the game walks the candidate pool until it finds an event that satisfies both: - -- `IsAllowed(...)` returns `true` -- the event has not been visited in the current run yet - -`IsAllowed` expresses whether the event may appear in the current run, not registration-time setup. - ---- - -## Custom event scene - -> The following describes the base game's custom event layout mechanism. - -Return a custom layout type: - -```csharp -public override EventLayoutType LayoutType => EventLayoutType.Custom; -``` - -The game then loads: - -```text -res://scenes/events/custom/.tscn -``` - -The scene root must implement `ICustomEventNode` and provide at least `Initialize(EventModel)` and `CurrentScreenContext`. - ---- - -## Asset preloading - -> The following describes the base game's rules for event asset preloading. - -Normal events preload by default: - -- the layout scene -- `res://images/events/.png` -- optional `res://scenes/vfx/events/_vfx.tscn` - -Ancients preload by default: - -- the layout scene -- `res://scenes/events/background_scenes/.tscn` - -Override `GetAssetPaths(IRunState runState)` to append paths when you need extra assets. - ---- - -## Minimal ancient example - -```csharp -using MegaCrit.Sts2.Core.Entities.Ancients; -using MegaCrit.Sts2.Core.Events; -using STS2RitsuLib.Scaffolding.Content; - -public sealed class MyAncient : ModAncientEventTemplate -{ - protected override AncientDialogueSet DefineDialogues() - { - return new AncientDialogueSet(); - } - - public override IEnumerable AllPossibleOptions => - [ - new EventOption(this, Accept, InitialOptionKey("ACCEPT")), - ]; - - protected override IReadOnlyList GenerateInitialOptions() - { - return AllPossibleOptions.ToArray(); - } - - private Task Accept() - { - SetEventFinished(L10NLookup($"{Id.Entry}.pages.ACCEPT.description")); - return Task.CompletedTask; - } -} -``` - -The same principle applies: keep option keys, page keys, and the final registered `Id.Entry` aligned. - ---- - -## Related docs - -- [Content Authoring Toolkit](ContentAuthoringToolkit.md) -- [Content Packs & Registries](ContentPacksAndRegistries.md) -- [Localization & Keywords](LocalizationAndKeywords.md) diff --git a/Docs/en/DiagnosticsAndCompatibility.md b/Docs/en/DiagnosticsAndCompatibility.md deleted file mode 100644 index 888e67e..0000000 --- a/Docs/en/DiagnosticsAndCompatibility.md +++ /dev/null @@ -1,148 +0,0 @@ -# Diagnostics & Compatibility - -This document describes the diagnostic policy and compatibility layers that RitsuLib adds on top of the base game. - -It focuses on: - -- one-time warnings for recurring authoring errors -- debug-oriented fallbacks for missing localization and invalid unlock data -- narrow bridge patches where vanilla systems do not process mod content - ---- - -## Design Intent - -RitsuLib does not try to hide every engine limitation. It follows these rules: - -- Surface real errors as early as possible -- where vanilla offers no safe extension point, the framework may add a bridge -- if a fallback would conceal too much behavior, keep the system explicit - -This layer is deliberately narrow and only handles edge cases. - ---- - -## One-Time Warning Policy - -Some RitsuLib diagnostics warn only once per issue (or once per stable key), including: - -- Missing resource paths (`AssetPathDiagnostics`) -- Missing `LocTable` keys when the master toggle and the **LocTable missing keys** toggle are enabled (`[Localization][DebugCompat]`) -- `THE_ARCHITECT` empty-`Lines` fallback when the debug compatibility master toggle and the **THE_ARCHITECT missing dialogue** toggle are enabled (`[Ancient]`) -- Other unlock-related one-shots (for example `ModUnlockMissingRuleWarnings`) - -Each stable key or issue class logs at most once so traces stay readable. - ---- - -## Asset Path Diagnostics - -Explicit asset override paths are validated by `AssetPathDiagnostics`. - -When a path is missing: - -- A one-time warning is logged (host type, model id, member name, missing path) -- Behavior falls back to the original asset path or original behavior - -This matters especially for character assets, where vanilla has almost no safe fallback. - -See [Asset Profiles & Fallbacks](AssetProfilesAndFallbacks.md). - ---- - -## Debug Compatibility Mode - -Optional compatibility fallbacks are grouped under `debug_compatibility_mode` and per-area toggles in mod settings. - -**Default (master toggle off):** vanilla behavior for the patched systems described here. - -**Master toggle on:** the settings UI shows a **Compatibility fallbacks** section. Per-feature toggles default to **on**. Turning a toggle **off** removes only that fallback. - -| Toggle | Effect when enabled | -|---|---| -| **LocTable missing keys** | Placeholder resolution + one-time `[Localization][DebugCompat]` warnings | -| **Invalid unlock epochs** | Skip the grant + one-time `[Unlocks][DebugCompat]` warnings | -| **THE_ARCHITECT missing dialogue** | Inject empty `Lines` entries for `ModContentRegistry` characters + one-time `[Ancient]` warning | - -Except for LocTable missing-key handling, each toggle typically applies only to content registered through RitsuLib. - -**`ModUnlockMissingRuleWarnings`** (e.g. missing boss-win rule registration): separate diagnostic path from the debug compatibility toggles. - -**Released content:** ship complete localization, timeline data, and dialogue. Treat the table above as an iteration aid. - -Windows settings path: - -```text -%appdata%\SlayTheSpire2\steam\\mod_data\com.ritsukage.sts2-RitsuLib\settings.json -``` - ---- - -## Registration Conflict Diagnostics - -RitsuLib checks these conflicts explicitly: - -| Conflict | Typical cause | -|---|---| -| Model id collision | Two registered models in the same mod/category share the same CLR type name | -| Epoch id collision | Two epochs resolve to the same `Id` | -| Story id collision | Two stories resolve to the same story identity | - -When detected, the framework throws or logs errors — it does not accept ambiguous identity silently. - ---- - -## Ancient Dialogue Compatibility Layer - -Before `AncientDialogueSet.PopulateLocKeys`, the framework appends localization-defined ancient dialogue rows for registered mod characters. Authors own the keys; the framework discovers and injects them so mod characters use the same ancient-dialogue pipeline as vanilla. - -### `THE_ARCHITECT` dialogue fallback - -Gated on the debug compatibility master toggle and the **THE_ARCHITECT missing dialogue** toggle. If vanilla `TheArchitect.LoadDialogue` yields no dialogue, RitsuLib injects empty `Lines` entries for `ModContentRegistry` characters and logs **`[Ancient]`** once. - -For key format, see [Localization & Keywords](LocalizationAndKeywords.md). - ---- - -## Unlock Compatibility Bridges - -Several vanilla progression checks only iterate vanilla characters. RitsuLib applies narrow patches so registered unlock rules participate at the same checkpoints for mod characters: - -| Bridge | Description | -|---|---| -| Elite wins | Elite kill count → epoch checks | -| Boss wins | Boss kill count → epoch checks | -| Ascension 1 | Ascension 1 → epoch checks | -| Post-run character unlock | Post-run character-unlock epochs | -| Ascension reveal | Ascension reveal unlock checks | - -Bridge patches forward RitsuLib-registered rules into vanilla progression checkpoints that otherwise skip mod characters. They do not introduce a separate progression store. - -See [Timeline & Unlocks](TimelineAndUnlocks.md). - ---- - -## Freeze Errors - -If content, timeline, or unlock registration runs after freeze, RitsuLib throws. - -That is intentional: late registration often means ModelDb caches are already built, fixed identity rules are in use, and unlock filters are active. Failing fast is the safe choice. - ---- - -## Troubleshooting notes - -1. Warnings usually point to mod data or configuration (paths, keys, rules), not random engine failure. -2. Fix missing assets and localization in source data rather than relying on placeholders long term. -3. Debug compatibility fallbacks are for iteration; release builds should ship with the master toggle off, or with per-feature toggles disabled and complete data. -4. Prefer explicit registration APIs; compatibility fallbacks are not a long-term architecture substitute. - ---- - -## Related Documents - -- [Asset Profiles & Fallbacks](AssetProfilesAndFallbacks.md) -- [Localization & Keywords](LocalizationAndKeywords.md) -- [Timeline & Unlocks](TimelineAndUnlocks.md) -- [Godot Scene Authoring](GodotSceneAuthoring.md) -- [Framework Design](FrameworkDesign.md) diff --git a/Docs/en/FmodAndAudio.md b/Docs/en/FmodAndAudio.md deleted file mode 100644 index 877257d..0000000 --- a/Docs/en/FmodAndAudio.md +++ /dev/null @@ -1,215 +0,0 @@ -# FMOD & Audio - -This document describes the game's audio architecture and the layered API that RitsuLib provides on top of it. - ---- - -## Game-native audio architecture - -> The following describes Slay the Spire 2 engine's own audio pipeline, to help explain the design background of RitsuLib's audio API. - -Slay the Spire 2 plays audio through **Godot's FMOD Studio GDExtension** (`FmodServer` singleton). On the C# side this is wrapped by **`NAudioManager`**, which indirectly calls `FmodServer` via the GDScript proxy **`AudioManagerProxy`**. - -This means: - -- All vanilla audio playback ultimately goes through **`NAudioManager` → `AudioManagerProxy` → `FmodServer`** -- **`NAudioManager`** applies **`TestMode`** muting, SFX volume scaling, and related behaviour -- If a mod wants audio to **sound like the base game**, it should use the same pipeline - ---- - -## RitsuLib audio API - -RitsuLib layers the audio API so you can use the vanilla-aligned pipeline or talk to FMOD Studio directly when needed. - -### Entry selection - -| Need | Use | -|------|-----| -| Easier high-level playback, typed handles, lifecycle cleanup | **`GameFmod.Playback`** | -| Same routing / `TestMode` behaviour as vanilla | **`GameFmod.Studio`** → `NAudioManager` | -| Same guards as `SfxCmd` (non-interactive, combat ending, etc.) | **`Sts2SfxAlignedFmod`** | -| Load/unload Studio banks, check paths | **`FmodStudioServer`** | -| Fire-and-forget one-shots on `FmodServer` **without** going through `NAudioManager` | **`FmodStudioDirectOneShots`** | -| Bus volume/mute/pause, global parameters, DSP, performance data | **`FmodStudioBusAccess`**, **`FmodStudioMixerGlobals`** | -| Snapshots (`snapshot:/…`) | **`FmodStudioSnapshots`** | -| Long-lived `create_event_instance` handles | **`FmodStudioEventInstances`** | -| WAV/OGG/MP3 via plugin loaders | **`FmodStudioStreamingFiles`** | -| Cooldown / random pool helpers (no audio by themselves) | **`FmodPlaybackThrottle`**, **`FmodPathRoundRobinPool`** | - -### Direct FMOD vs vanilla pipeline - -- **`GameFmod.Studio`** and **`Sts2SfxAlignedFmod`** go through **`NAudioManager`** and share the game's GDScript proxy (including **`TestMode`**, SFX volume, etc.) -- **`FmodStudioDirectOneShots`** and most **`FmodStudio*`** helpers call **`FmodServer`** directly—good for custom banks, loose files, and bus debugging; one-shots are not guaranteed to match every subtlety of the in-game SFX bus path -- For **“sounds like vanilla”**, prefer **`GameFmod`** or **`Sts2SfxAlignedFmod`** - ---- - -## Quick examples - -**Vanilla-aligned one-shot** - -```csharp -using STS2RitsuLib.Audio; - -Sts2SfxAlignedFmod.PlayOneShot("event:/sfx/heal"); -GameFmod.Studio.PlayMusic("event:/music/menu_update"); -``` - -**Mod content bank + `guids.txt` (must match the game's FMOD Studio major version line)** - -```csharp -FmodStudioServer.TryLoadBank("res://mods/MyMod/banks/MyMod.bank"); -FmodStudioServer.TryWaitForAllLoads(); -if (!FmodStudioServer.TryLoadStudioGuidMappings("res://mods/MyMod/banks/MyMod.guids.txt")) - return; -if (FmodStudioServer.TryCheckEventPath("event:/mods/mymod/hit") is true) - GameFmod.Studio.PlayOneShot("event:/mods/mymod/hit"); -``` - -**Loose file (short SFX — loaded as sound)** - -```csharp -var sfxPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "ping.wav"); -FmodStudioStreamingFiles.TryPlaySoundFile(sfxPath, volume: 0.9f); -``` - -**Streaming music file (recommended: Playback/Handle API)** - -```csharp -var musicPath = ProjectSettings.GlobalizePath("user://mymod/loop.ogg"); -var handle = GameFmod.Playback.PlayMusic( - AudioSource.StreamingMusic(musicPath), - new AudioPlaybackOptions { Volume = 0.7f, Scope = AudioLifecycleScope.Room } -); -``` - -**Common adaptive music flow (room / combat / victory)** - -```csharp -var adaptive = GameFmod.Playback.FollowAdaptiveMusic( - AudioAdaptivePlans.FullRunOverride( - roomSource: AudioSource.StreamingMusic(roomLoopPath), - combatSource: AudioSource.StreamingMusic(combatLoopPath), - victorySource: AudioSource.StreamingMusic(victoryStingerPath) - ) -); -``` - -**Throttle rapid triggers** - -```csharp -if (FmodPlaybackThrottle.TryEnter("my_power_proc", cooldownMs: 120)) - Sts2SfxAlignedFmod.PlayOneShot("event:/sfx/buff"); -``` - -**Singleton channel: replace the current playback** - -```csharp -GameFmod.Playback.PlayMusic( - AudioSource.StreamingMusic(nextMusicPath), - new AudioPlaybackOptions - { - Volume = 0.8f, - Routing = new AudioRoutingOptions - { - Channel = "my-mod/music", - ChannelMode = AudioChannelMode.ReplaceExisting, - AllowFadeOutOnReplace = true, - }, - } -); -``` - -**Tagged group: replace an entire UI cue group** - -```csharp -GameFmod.Playback.Play( - AudioSource.File(uiCuePath), - new AudioPlaybackOptions - { - Routing = new AudioRoutingOptions - { - Tag = "my-mod/ui-tooltips", - ReplaceTaggedGroup = true, - }, - } -); -``` - ---- - -## Auxiliary types (`STS2RitsuLib.Audio`) - -| Type | Description | -|------|-------------| -| `FmodEventPath` | Lightweight wrapper for `event:/…` paths | -| `FmodStudioRouting` | Common bus path constants | -| `FmodParameterMap` | Builds parameter dictionaries for **`GameFmod.Studio`** | - -**`STS2RitsuLib.Audio.Internal`** is internal implementation and is not a stable public API. - ---- - -## Recommended external toolchain - -RitsuLib does not include the following; they are common external workflows: - -| Tool | Role | -|------|------| -| [FMOD Studio](https://www.fmod.com/) | Edit banks and events. **Match the game's FMOD Studio major version line** (see the game's `addons/fmod` directory) | -| Built-in Godot FMOD plugin in the game | Same class of integration as `utopia-rise/fmod-gdextension`; provides the **`FmodServer`** singleton at runtime | -| [sts2-fmod-tools](https://github.com/elliotttate/sts2-fmod-tools) (community) | Optional: align Studio projects/events from the game-data side | -| DAW export | Export WAV/OGG, etc.; if mixing with vanilla SFX, watch loudness and dynamic range | - -> RitsuLib wires **guids.txt-style mappings** into **`NAudioManager`** for path-based Studio calls (one-shots, loops, music, stops, parameters, **`UpdateMusicParameter`**, etc.). After your mod loads its **`.bank`** and calls **`TryLoadStudioGuidMappings`**, **`event:/…`** paths keep using the same **`NAudioManager` → AudioManagerProxy** pipeline as vanilla. Custom Harmony that replaces or bypasses that chain must coordinate with other mods. - ---- - -## Authoring an extra mod bank (recommended workflow) - -Use this workflow when you ship **only your own `.bank`** plus a **`*.guids.txt`** from the **same Studio build**. - -### 1. Bank type and naming - -- **Do not replace or overwrite** the shipped **`Master.bank`**. -- Ship a **separately named content bank** (sometimes called a sidecar / child bank). Its file name and the **Bank** name inside FMOD Studio should be **globally unique** among mods and future official banks to avoid **naming collisions**. -- That bank holds **your** events and media; the **mixer / Master routing** still comes from the game's already-loaded vanilla banks. - -### 2. Bus / Master alignment (match vanilla mixing) - -- At runtime, **`AudioManagerProxy`** expects buses such as **`bus:/master`**, **`bus:/master/sfx`**, **`bus:/master/music`**, **`bus:/master/ambience`** (consistent with the desktop bank load order). -- **For vanilla-like loudness slider and bus behaviour**, route your events to those **`bus:/…`** paths—the same hierarchy **defined by the game's Master-side data**—instead of publishing a competing top-level Master bank that replaces the official one. -- **When you must verify identifiers**: compare **Bus / VCA** paths and GUIDs against the game's **GUIDs.txt** or tools like **`sts2-fmod-tools`**. Export **GUIDs.txt** from **the same FMOD Studio build** as your **`.bank`** so text and binary never drift apart. - -### 3. Export GUIDs and ship them with the mod - -1. **Build** your bank in FMOD Studio. -2. Take **`GUIDs.txt`** from the build output (or export a GUID list). -3. Ship it as a text resource (e.g. **`YourMod.guids.txt`**): keep every **`event:/…`** line (`{guid} event:/…`, one record per line); you may keep other lines for debugging. -4. After **`TryLoadBank`** + **`TryWaitForAllLoads`**, call **`FmodStudioServer.TryLoadStudioGuidMappings("res://…/YourMod.guids.txt")`**. That fills the path → GUID table and logs success/failure; together with RitsuLib's **`NAudioManager`** Harmony prefixes, **`event:/…`** paths keep resolving through **`NAudioManager`**. - -### 4. Runtime order and stability - -- Load your mod bank **after** the game's FMOD bootstrap and **`NAudioManager`** are ready (for example from a deferred-init callback); loading too early can leave the Studio cache in a bad state for probes. -- Use **`FmodStudioServer.TryLoadBank`**: the implementation **pins** the returned **`FmodBank`** reference so it is not finalized immediately (the GDExtension **`FmodBank`** destructor calls **`unload_bank`**). - -### 5. Toolchain version and artefact pairing - -- Match the **FMOD Studio major line** to the game's **`addons/fmod`** / runtime. -- Always ship **`.bank`** and **`GUIDs.txt` slice** from the **same build**. Mixing an old bank with a new GUID file (or vice versa) breaks **`check_event_guid`** / path resolution at runtime. - ---- - -## Troubleshooting - -- **`FmodStudioServer.TryGet()` is null** — `FmodServer` not ready (scene, headless test, or extension failed to load); check the game log -- **`TryCheckEventPath` is false** — the **`.bank`** is missing or unloaded, the path is wrong, **`TryLoadStudioGuidMappings`** did not succeed, or the bank was unloaded (use **`FmodStudioServer.TryLoadBank`**, which **pins** the returned **`FmodBank`** reference) -- **No sound and no exception** — **`TestMode`** / **`NonInteractiveMode`** may suppress **`NAudioManager`**; direct **`FmodServer`** calls are not subject to those flags - ---- - -## Related documentation - -- [Diagnostics & Compatibility](DiagnosticsAndCompatibility.md) -- [Patching Guide](PatchingGuide.md) diff --git a/Docs/en/FrameworkDesign.md b/Docs/en/FrameworkDesign.md deleted file mode 100644 index 47f3578..0000000 --- a/Docs/en/FrameworkDesign.md +++ /dev/null @@ -1,152 +0,0 @@ -# Framework Design - -This document explains the architectural decisions behind RitsuLib and the constraints those decisions impose on mod code. - ---- - -## Core Goals - -RitsuLib is built around a small set of explicit design priorities: - -- explicit registration instead of opaque “magic” discovery -- fixed model identity instead of runtime name inference -- composable asset records instead of large inheritance hierarchies -- scene replacement instead of in-place mutation of vanilla assets -- compatibility fallbacks only where the base game has no safe extension point - -The framework reduces repetitive authoring work, but it does not convert the mod into an implicit runtime graph. - -Optional **attribute-based** registration is still explicit: only types in assemblies registered with `ModTypeDiscoveryHub.RegisterModAssembly` are considered, each attribute maps to ordinary registry calls, and `AutoRegistrationAttribute.Inherit` defaults to **off** so derived types do not pick up base annotations unless you opt in. Details: [Content Packs & Registries](ContentPacksAndRegistries.md#attribute-based-registration-optional). - ---- - -## Fixed Identity - -For models registered through the RitsuLib content registry, `ModelId.Entry` is deterministic: - -```text -__ -``` - -Why this matters: - -- localization keys stay stable and predictable -- refactors are easier to reason about -- content registration conflicts are easier to detect -- migration between project structures does not depend on reflection order or class discovery behavior - -The tradeoff is deliberate: renaming a published CLR type becomes a compatibility change. - ---- - -## Registration Before Use - -RitsuLib relies on explicit registration during early boot. - -`CreateContentPack(modId)` is the convenience entry point, but the underlying registries remain first-class. - -Registration is frozen during early boot to preserve: - -- stable model identity -- stable model lists -- deterministic lookup and unlock behavior - -The framework therefore fails fast instead of mutating the model graph after runtime systems have started consuming it. - -See [Content Packs & Registries](ContentPacksAndRegistries.md) for the concrete registration model. - ---- - -## Asset Profiles Instead Of Large Character Bases - -Character authoring is organized around asset profiles. - -Instead of requiring a monolithic custom-character base type with unrelated virtual members, RitsuLib groups assets into records such as: - -- `CharacterSceneAssetSet` -- `CharacterUiAssetSet` -- `CharacterVfxAssetSet` -- `CharacterAudioAssetSet` - -This keeps responsibility boundaries explicit: - -- scenes live together -- UI assets live together -- VFX tuning lives together -- audio overrides live together - -This is more verbose than a single placeholder property, but it scales better because each asset category can evolve independently. - ---- - -## Asset Safety Mechanisms - -The asset-profile system is paired with a small set of safety mechanisms: - -- character placeholder fallback for missing character resources -- separate APIs for full energy-counter scenes versus pool-linked icons -- one-time warnings when explicit resource paths are missing - -These behaviors are part of the same design: a structured asset API must remain usable during migration and partial-content development. - -See [Asset Profiles & Fallbacks](AssetProfilesAndFallbacks.md) for the detailed behavior and API surface. - ---- - -## Compatibility Layers Stay Narrow - -RitsuLib includes compatibility-oriented patches, but they are intentionally narrow. - -The framework does not hide every engine limitation behind automation. It adds fallbacks only where the game or modding surface would otherwise be unsafe or excessively repetitive. - -Examples include `LocTable` and `THE_ARCHITECT` fallbacks under `debug_compatibility_mode`, ancient dialogue key injection, and unlock bridge patches for vanilla progression checks that skip mod characters. - -See [Diagnostics & Compatibility](DiagnosticsAndCompatibility.md) for the concrete compatibility layers. - ---- - -## Why The Patching Layer Exists - -Harmony is still the underlying patch engine, but RitsuLib wraps it with: - -- typed patch declarations via `IPatchMethod` -- critical vs optional patch semantics -- ignore-if-missing targets -- grouped registration helpers -- dynamic patch application support - -The goal is not to abstract Harmony away. The goal is to standardize patch declaration and failure handling so large mods remain maintainable. - -See [Patching Guide](PatchingGuide.md) for the patching workflow. - ---- - -## Why Persistence Is Class-Based - -Persistent entries are registered as class types rather than loose primitives. - -That choice enables: - -- schema version fields -- structured migrations -- future expansion without breaking call sites -- safer serialization boundaries - -This adds some upfront structure, but avoids primitive save keys that later need to carry schema growth. - -See [Persistence Guide](PersistenceGuide.md) for the full data model. - ---- - -## Recommended Reading Order - -- [Getting Started](GettingStarted.md) -- [Content Authoring Toolkit](ContentAuthoringToolkit.md) -- [Content Packs & Registries](ContentPacksAndRegistries.md) -- [Character & Unlock Templates](CharacterAndUnlockScaffolding.md) -- [Timeline & Unlocks](TimelineAndUnlocks.md) -- [Asset Profiles & Fallbacks](AssetProfilesAndFallbacks.md) -- [Patching Guide](PatchingGuide.md) -- [Persistence Guide](PersistenceGuide.md) -- [Localization & Keywords](LocalizationAndKeywords.md) -- [Diagnostics & Compatibility](DiagnosticsAndCompatibility.md) diff --git a/Docs/en/GettingStarted.md b/Docs/en/GettingStarted.md deleted file mode 100644 index ce334d0..0000000 --- a/Docs/en/GettingStarted.md +++ /dev/null @@ -1,201 +0,0 @@ -# Getting Started - -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"] -} -``` - ---- - -## 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(); - patcher.PatchAll(); - - RitsuLibFramework.CreateContentPack("MyMod") - .Character() - .Card() - .Card() - .Relic() - .Apply(); - } -} -``` - -For the full mapping of fluent methods, `ModContentRegistry` calls, and `IContentRegistrationEntry` types (enchantments, achievements, shared pools, manifests, etc.), see [Content Packs & Registries](ContentPacksAndRegistries.md). - -`CreatePatcher` takes a `patcherName` used for log identification. A mod may create multiple patchers. See [Patching Guide](PatchingGuide.md) for the full patch workflow. - -If your mod uses custom Godot C# scene scripts, keep `EnsureGodotScriptsRegistered(...)` in your initializer. See [Godot Scene Authoring](GodotSceneAuthoring.md). - ---- - -## 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()`, `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(...)` 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](ContentPacksAndRegistries.md). - ---- - -## 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 => $"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); - } -} -``` - ---- - -## 5. Localization Keys - -The `ModelId.Entry` for any RitsuLib-registered model is derived as: - -``` -__ -``` - -All segments are normalized to UPPER_SNAKE_CASE. - -| Mod Id | C# Type | Category | Entry | -|---|---|---|---| -| `MyMod` | `MyCard` | card | `MY_MOD_CARD_MY_CARD` | -| `MyMod` | `MyRelic` | relic | `MY_MOD_RELIC_MY_RELIC` | -| `MyMod` | `MyCharacter` | character | `MY_MOD_CHARACTER_MY_CHARACTER` | - -Localization file example: - -```json -{ - "MY_MOD_CARD_MY_CARD.title": "Strike", - "MY_MOD_CARD_MY_CARD.description": "Deal {damage} damage." -} -``` - ---- - -## 6. Subscribe to Lifecycle Events - -```csharp -// Runs once after game is ready -RitsuLibFramework.SubscribeLifecycle(evt => -{ - Logger.Info("Game ready."); -}); - -// On every combat start -RitsuLibFramework.SubscribeLifecycle(evt => -{ - // evt.RunState, evt.CombatState -}); -``` - -Replayable events (`IReplayableFrameworkLifecycleEvent`) fire immediately upon late subscription if the event has already occurred. - ---- - -## 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( - key: "my_counter", - fileName: "counter.json", - scope: SaveScope.Profile, - defaultFactory: () => new CounterData()); -} -``` - -See [Persistence Guide](PersistenceGuide.md) for scopes, reload timing, and migrations. - ---- - -## Next Steps - -- [Content Authoring Toolkit](ContentAuthoringToolkit.md) -- [Character & Unlock Templates](CharacterAndUnlockScaffolding.md) -- [Card Dynamic Variables](CardDynamicVarToolkit.md) -- [Lifecycle Events](LifecycleEvents.md) -- [Patching Guide](PatchingGuide.md) -- [Persistence Guide](PersistenceGuide.md) -- [Localization & Keywords](LocalizationAndKeywords.md) -- [Framework Design](FrameworkDesign.md) -- [Content Packs & Registries](ContentPacksAndRegistries.md) -- [Godot Scene Authoring](GodotSceneAuthoring.md) -- [Timeline & Unlocks](TimelineAndUnlocks.md) -- [Asset Profiles & Fallbacks](AssetProfilesAndFallbacks.md) -- [Diagnostics & Compatibility](DiagnosticsAndCompatibility.md) diff --git a/Docs/en/GodotSceneAuthoring.md b/Docs/en/GodotSceneAuthoring.md deleted file mode 100644 index 3d72097..0000000 --- a/Docs/en/GodotSceneAuthoring.md +++ /dev/null @@ -1,134 +0,0 @@ -# Godot Scene Authoring - -This document covers two practical concerns for STS2 mods authoring Godot scenes: - -- Scene-facing game types should be subclassed in the mod first, then bound in the editor -- Godot C# scripts in the mod assembly must be registered during initialization - ---- - -## Why Mod-Local Subclasses - -> The following describes an engine behavior in the Godot Mono workflow. - -In the Godot Mono workflow used for STS2 modding, binding C# types from the game assembly directly to `.tscn` scenes is unreliable in the editor. - -Experience shows that opening, serializing, and rebinding works more reliably when the `.tscn` binds to a script type from your own mod assembly. - -Practical rule: - -- Whenever a scene node needs to behave as an in-game Godot type, add a thin mod-local subclass first, then bind the scene to that subclass - ---- - -## Wrapper Pattern - -Do not bind scenes directly to game types such as `NEnergyCounter`. Write a mod-local script: - -```csharp -using MegaCrit.Sts2.Core.Nodes.Combat; - -namespace MyMod.Scripts -{ - public partial class MyEnergyCounter : NEnergyCounter - { - } -} -``` - -Bind the `.tscn` to `MyEnergyCounter`, not to `NEnergyCounter` directly. - -The wrapper can be empty. Its purpose is to give the editor a local script type owned by your mod. - ---- - -## Common Types That Need Wrapping - -| Game type | Typical use | -|---|---| -| `NEnergyCounter` | Energy orb scene root | -| `NRestSiteCharacter` | Rest site character scene | -| `NCreatureVisuals` | Character visuals scene | -| `NSelectionReticle` | Selection reticle | -| `MegaLabel` | Label child control | - ---- - -## Generic Binding Examples - -Custom energy orb scene: - -- Root script → `MyEnergyCounter : NEnergyCounter` -- Label child → `MyCounterLabel : MegaLabel` - -Character visuals scene: - -- Root script → `MyCreatureVisuals : NCreatureVisuals` - -Rest site scene: - -- Root script → `MyRestSiteCharacter : NRestSiteCharacter` - -The point is not the class names — it is that bound scripts live in your mod assembly. - ---- - -## Editor Rule - -Whenever the Godot editor must open, serialize, or rebind a script in your mod scene, prefer a mod-local subclass. - -Even when: - -- You have no extra logic yet -- Inheritance is a single line -- The runtime type already exists in the game assembly - -The wrapper is the compatibility layer between your scene and the editor. - ---- - -## Runtime Script Registration - -If your mod uses Godot C# scene scripts, call this during initialization: - -```csharp -using System.Reflection; - -RitsuLibFramework.EnsureGodotScriptsRegistered( - Assembly.GetExecutingAssembly(), - Logger); -``` - -This lets Godot’s script bridge discover and register C# scripts from your mod assembly. - -Do this before content registration so scene scripts resolve reliably at runtime. - ---- - -## Recommended Workflow - -1. Pick the game-side base type you need -2. Add a thin mod-local `partial class` that inherits it -3. Bind the `.tscn` to that local script -4. In your entry point, call `EnsureGodotScriptsRegistered(Assembly.GetExecutingAssembly(), Logger)` - ---- - -## When You Do Not Need Wrapping - -You usually do not need extra wrapper subclasses for: - -- Plain content model classes (card / relic / power / character) -- Pure C# helpers not used as Godot scripts -- Logic classes never bound to `.tscn` resources - -This document is only about Godot scenes and script binding. - ---- - -## Related Documents - -- [Getting Started](GettingStarted.md) -- [Character & Unlock Templates](CharacterAndUnlockScaffolding.md) -- [Asset Profiles & Fallbacks](AssetProfilesAndFallbacks.md) -- [Diagnostics & Compatibility](DiagnosticsAndCompatibility.md) diff --git a/Docs/en/LifecycleEvents.md b/Docs/en/LifecycleEvents.md deleted file mode 100644 index e3b129d..0000000 --- a/Docs/en/LifecycleEvents.md +++ /dev/null @@ -1,211 +0,0 @@ -# Lifecycle Events - -This document lists all lifecycle events provided by RitsuLib, explains subscription patterns, and details replayable event behavior. - ---- - -## Subscription Patterns - -### Subscribe by Event Type (Recommended) - -```csharp -var sub = RitsuLibFramework.SubscribeLifecycle(evt => -{ - Logger.Info($"Game ready: {evt.Game}"); -}); - -// Unsubscribe -sub.Dispose(); -``` - -### Subscribe via `ILifecycleObserver` - -```csharp -public class MyObserver : ILifecycleObserver -{ - public void OnEvent(IFrameworkLifecycleEvent evt) - { - if (evt is CombatStartingEvent combat) - HandleCombatStart(combat); - else if (evt is RunEndedEvent run) - HandleRunEnd(run); - } -} - -RitsuLibFramework.SubscribeLifecycle(new MyObserver()); -``` - -> **Replayable events** (`IReplayableFrameworkLifecycleEvent`): if you subscribe after the event has already fired, the framework immediately calls your handler with the stored event instance — no timing concerns. - ---- - -## Framework Events - -Fired during framework initialization and profile service setup. - -| Event | Replayable | Payload | -|---|---|---| -| `FrameworkInitializingEvent` | — | `FrameworkModId`, `FrameworkVersion` | -| `FrameworkInitializedEvent` | ✓ | `FrameworkModId`, `IsActive` | -| `ProfileServicesInitializingEvent` | — | — | -| `ProfileServicesInitializedEvent` | ✓ | `ProfileId` | - ---- - -## Game Bootstrap Events - -Fired in sequence during game startup, from model registration through to game ready. - -| Event | Replayable | Payload | -|---|---|---| -| `EssentialInitializationStartingEvent` | — | — | -| `EssentialInitializationCompletedEvent` | ✓ | — | -| `DeferredInitializationStartingEvent` | — | — | -| `DeferredInitializationCompletedEvent` | ✓ | — | -| `ContentRegistrationClosedEvent` | ✓ | `Reason` | -| `ModelRegistryInitializingEvent` | — | — | -| `ModelRegistryInitializedEvent` | ✓ | `RegisteredModelTypeCount` | -| `ModelIdsInitializingEvent` | — | — | -| `ModelIdsInitializedEvent` | ✓ | — | -| `ModelPreloadingStartingEvent` | — | — | -| `ModelPreloadingCompletedEvent` | ✓ | — | -| `GameTreeEnteredEvent` | ✓ | `Game` | -| `GameReadyEvent` | ✓ | `Game` | - -```csharp -RitsuLibFramework.SubscribeLifecycle(_ => -{ - var id = ModelDb.GetId(); -}); -``` - ---- - -## Run Events - -| Event | Replayable | Payload | -|---|---|---| -| `RunStartedEvent` | — | `RunState`, `IsMultiplayer`, `IsDaily` | -| `RunLoadedEvent` | — | `RunState`, `IsMultiplayer`, `IsDaily` | -| `RunEndedEvent` | — | `Run`, `IsVictory`, `IsAbandoned` | - ---- - -## Room & Act Events - -| Event | Payload | -|---|---| -| `RoomEnteringEvent` | `RunState`, `Room` | -| `RoomEnteredEvent` | `RunState`, `Room` | -| `RoomExitedEvent` | `RunManager`, `Room` | -| `ActEnteringEvent` | `RunManager`, `TargetActIndex`, `DoTransition` | -| `ActEnteredEvent` | `RunState`, `CurrentActIndex` | -| `RewardsScreenContinuingEvent` | `RunManager` | - ---- - -## Combat Events - -| Event | Payload | -|---|---| -| `CombatStartingEvent` | `RunState`, `CombatState?` | -| `CombatEndedEvent` | `RunState`, `CombatState?`, `Room` | -| `CombatVictoryEvent` | `RunState`, `CombatState?`, `Room` | -| `SideTurnStartingEvent` | `CombatState`, `Side` | -| `SideTurnStartedEvent` | `CombatState`, `Side` | -| `CardPlayingEvent` | `CombatState`, `CardPlay` | -| `CardPlayedEvent` | `CombatState`, `CardPlay` | -| `CardDrawnEvent` | `CombatState`, `Card`, `FromHandDraw` | -| `CardDiscardedEvent` | `CombatState`, `Card` | -| `CardExhaustedEvent` | `CombatState`, `Card`, `CausedByEthereal` | -| `CardRetainedEvent` | `CombatState`, `Card` | -| `CardMovedBetweenPilesEvent` | `RunState`, `CombatState?`, `Card`, `PreviousPile`, `Source` | - -### Creature Events - -| Event | Payload | -|---|---| -| `CreatureDyingEvent` | `CombatState`, `Creature` | -| `CreatureDiedEvent` | `CombatState`, `Creature` | - -```csharp -RitsuLibFramework.SubscribeLifecycle(evt => -{ - if (evt.Card is MyCard myCard) - myCard.OnDrawn(evt.CombatState); -}); -``` - ---- - -## Reward Events - -| Event | Payload | -|---|---| -| `GoldGainedEvent` | `Amount` | -| `GoldLostEvent` | `Amount` | -| `PotionProcuredEvent` | `Potion` | -| `PotionDiscardedEvent` | `Potion` | -| `RelicObtainedEvent` | `Relic` | -| `RelicRemovedEvent` | `Relic` | -| `RewardTakenEvent` | `Reward` | - ---- - -## Unlock Events - -| Event | Payload | -|---|---| -| `EpochObtainedEvent` | `Epoch` | -| `EpochRevealedEvent` | `Epoch` | -| `UnlockIncrementedEvent` | `UnlockState` | - ---- - -## Save & Persistence Events - -### Profile Lifecycle - -| Event | Payload | -|---|---| -| `ProfileIdInitializedEvent` | `ProfileId` | -| `ProfileSwitchingEvent` | `OldProfileId`, `NewProfileId` | -| `ProfileSwitchedEvent` | `ProfileId` | -| `ProfileDeletingEvent` | `ProfileId` | -| `ProfileDeletedEvent` | `ProfileId` | - -### Save Writing - -| Event | Payload | -|---|---| -| `RunSavingEvent` | `RunState` | -| `RunSavedEvent` | `RunState` | -| `ProgressSavingEvent` | — | -| `ProgressSavedEvent` | — | - -### ModDataStore Data Events - -Used internally by `ModDataStore`, also available for mods to react to save state changes. - -| Event | Description | -|---|---| -| `ProfileDataReadyEvent` | Save data loaded — safe to read/write | -| `ProfileDataChangedEvent` | Save data changed | -| `ProfileDataInvalidatedEvent` | Save data invalidated (e.g. profile switch) | - ---- - -## Game Over Events - -| Event | Payload | -|---|---| -| `GameOverScreenCreatedEvent` | `Screen` | - ---- - -## Related Documents - -- [Getting Started](GettingStarted.md) -- [Content Authoring Toolkit](ContentAuthoringToolkit.md) -- [Persistence Guide](PersistenceGuide.md) -- [Timeline & Unlocks](TimelineAndUnlocks.md) diff --git a/Docs/en/LocStringPlaceholderResolution.md b/Docs/en/LocStringPlaceholderResolution.md deleted file mode 100644 index 604d3c5..0000000 --- a/Docs/en/LocStringPlaceholderResolution.md +++ /dev/null @@ -1,219 +0,0 @@ -# LocString Placeholder Resolution - -This document covers two topics: the **game-native** localization system (`LocString`, SmartFormat configuration, built-in formatters) and the **extension guide** for registering custom `IFormatter` implementations from mods. - ---- - -## Part 1: Game-native system - -> The following describes the Slay the Spire 2 engine's own localization mechanism, not RitsuLib functionality. - -### Core components - -- **`LocString`**: holds a localization table id, entry key, and variable dictionary; `GetFormattedText()` triggers formatting. -- **`LocManager.SmartFormat`**: retrieves the raw template from `LocTable`, selects `CultureInfo` based on whether the key is localized, then calls `SmartFormatter.Format(...)`. -- **`LocManager.LoadLocFormatters`**: constructs `SmartFormatter`, registers data sources and formatter extensions. - -### Variable binding - -Variables are written to `LocString` via `Add`. **Spaces in variable names are replaced with hyphens.** - -```csharp -var locString = new LocString("cards", "strike"); -locString.Add("damage", 6); -string result = locString.GetFormattedText(); -``` - -### Placeholder syntax - -Game localization JSON uses SmartFormat placeholders. - -**Variable only** — outputs the formatted value of the variable: - -``` -{VariableName} -``` - -**With formatter** — the formatter is specified after a colon using function-call syntax. The content inside `( )` is passed to the formatter as `IFormattingInfo.FormatterOptions`: - -``` -{VariableName:formatterName()} -{VariableName:formatterName(options)} -``` - -Formatters are matched by `IFormatter.Name`. The parentheses are a required part of the invocation syntax. - -**Formatters with format segments** (e.g. `show`, `choose`, `cond`) receive additional text after a second colon, split by `|`. See individual formatter notes and the advanced examples below. - -**Example:** - -```json -{ - "damage_text": "Deal {Damage:diff()} damage to all enemies.", - "energy_text": "Gain {Energy:energyIcons()} this turn." -} -``` - -### SmartFormat built-in extensions - -Standard SmartFormat extensions registered by the game (non-exhaustive): - -| Type | Role | -|------|------| -| `ListFormatter` | List formatting | -| `DictionarySource` | Keyed variable lookup | -| `ValueTupleSource` | Value tuples | -| `ReflectionSource` | Reflection-based property access | -| `DefaultSource` | Fallback source | -| `PluralLocalizationFormatter` | Locale-sensitive pluralization | -| `ConditionalFormatter` | Conditional formatting | -| `ChooseFormatter` | `choose(...)` | -| `SubStringFormatter` | Substrings | -| `IsMatchFormatter` | Regex matching | -| `LocaleNumberFormatter` | Locale number formatting | -| `DefaultFormatter` | Fallback when no formatter matches | - -### Game-specific formatters - -The game registers the following `IFormatter` types in `MegaCrit.Sts2.Core.Localization.Formatters`: - -| `IFormatter.Name` | Placeholder | `FormatterOptions` | Notes | -|-------------------|-----------|--------------------|-------| -| `abs` | `{v:abs()}` | unused | Outputs the absolute value of a number | -| `energyIcons` | `{Energy:energyIcons()}` or `{energyPrefix:energyIcons(n)}` | Required as integer icon count when `CurrentValue` is `string` | Renders a value as energy icon glyphs; see details below | -| `starIcons` | `{v:starIcons()}` | unused | Renders a value as star icon glyphs | -| `diff` | `{v:diff()}` | unused | Highlights value changes (green for upgrades); requires `DynamicVar` | -| `inverseDiff` | `{v:inverseDiff()}` | unused | Same as `diff` with inverted color direction; requires `DynamicVar` | -| `percentMore` | `{v:percentMore()}` | unused | Converts a multiplier to a percent increase, e.g. `1.25` → `25` | -| `percentLess` | `{v:percentLess()}` | unused | Converts a multiplier to a percent decrease, e.g. `0.75` → `25` | -| `show` | `{v:show:upgrade text\|normal text}` | unused (options come from the format segment split on `|`) | Conditionally shows text based on upgrade state; requires `IfUpgradedVar` | - -**`energyIcons` details** - -The source of the icon count depends on `CurrentValue`: - -- `EnergyVar`: uses `PreviewValue` and an optional color prefix. Use `{Energy:energyIcons()}`. -- `CalculatedVar` or numeric type: uses the numeric value directly. Use `{Energy:energyIcons()}`. -- `string` (e.g. the `energyPrefix` variable used in fixed-cost text): count is read from `FormatterOptions` and must be an integer literal, e.g. `{energyPrefix:energyIcons(1)}`. - -Rendering rule: counts 1–3 repeat the icon glyph; counts ≤0 or ≥4 output the digit followed by one icon. - -**`show` details** - -The format segment after `show:` is split on `|` into one or two child formats: - -- `Upgraded`: renders the first segment. -- `Normal`: renders the second segment; if only one segment is provided, nothing is rendered. -- `UpgradePreview`: renders the first segment wrapped in `[green]...[/green]`. - -### DynamicVar types - -`DynamicVar` subclasses carry metadata consumed by formatters such as `diff` and `inverseDiff`: - -| Type | Description | -|------|-------------| -| `DamageVar` | Damage value with highlight metadata | -| `BlockVar` | Block value | -| `EnergyVar` | Energy value with color information | -| `CalculatedVar` | Base class for calculated values | -| `CalculatedDamageVar` / `CalculatedBlockVar` | Calculated damage / block | -| `ExtraDamageVar` | Extra damage | -| `BoolVar` / `IntVar` / `StringVar` | Primitive types | -| `GoldVar` / `HealVar` / `HpLossVar` / `MaxHpVar` | Resource types | -| `PowerVar` | Power value (generic) | -| `StarsVar` / `CardsVar` | Stars / card references | -| `IfUpgradedVar` | Upgrade UI display state | -| `ForgeVar` / `RepeatVar` / `SummonVar` | Other card variables | - -### Formatting pipeline - -1. `LocString.GetFormattedText()` is called -2. `LocManager.SmartFormat` retrieves the raw template from `LocTable` -3. `CultureInfo` is selected based on whether the key is localized -4. `SmartFormatter.Format` evaluates placeholders and dispatches to matching formatters -5. On failure (`FormattingException` or `ParsingErrors`): error is logged and the raw template is returned - -### Advanced examples - -**Conditional** (`ConditionalFormatter`) - -```json -{ "text": "{HasRider:This card has a rider effect|This card has no rider}" } -``` - -**Choose** (`ChooseFormatter`) - -```json -{ "text": "{CardType:choose(Attack|Skill|Power):Attack text|Skill text|Power text}" } -``` - -**Nested formatters** - -```json -{ - "text": "{Violence:Deal {Damage:diff()} damage {ViolenceHits:diff()} times|Deal {Damage:diff()} damage}" -} -``` - -**BBCode color tags** - -```json -{ "text": "Gain [gold]{Gold}[/gold] gold. Current HP: [green]{Hp}[/green]." } -``` - -Common tags: `[gold]`, `[green]`, `[red]`, `[blue]`. - ---- - -## Part 2: Custom formatters (mods) - -> The following describes how to register additional formatters via the RitsuLib patching system. - -A `Postfix` patch on `LocManager.LoadLocFormatters` provides access to the `SmartFormatter` instance, which accepts additional `IFormatter` implementations. - -**Implementing `IFormatter`:** - -```csharp -public class MyCustomFormatter : IFormatter -{ - public string Name { get => "myCustom"; set { } } - public bool CanAutoDetect { get; set; } - - public bool TryEvaluateFormat(IFormattingInfo formattingInfo) - { - formattingInfo.Write($"Custom output: {formattingInfo.CurrentValue}"); - return true; - } -} -``` - -- `Name` is the formatter identifier matched in placeholder strings (the `myCustom` in `{Var:myCustom()}`). -- Access `formattingInfo.FormatterOptions` to read any text supplied inside the parentheses. - -**Registration patch:** - -```csharp -public class RegisterMyFormatterPatch : IPatchMethod -{ - public static string PatchId => "register_my_formatter"; - public static string Description => "Register custom SmartFormat formatter"; - public static bool IsCritical => true; - - public static ModPatchTarget[] GetTargets() - => [new(typeof(LocManager), "LoadLocFormatters")]; - - public static void Postfix(SmartFormatter ____smartFormatter) - => ____smartFormatter.AddExtensions(new MyCustomFormatter()); -} -``` - -Once registered, invoke the formatter in JSON as `{SomeVar:myCustom()}` or `{SomeVar:myCustom(args)}`. - ---- - -## Related documents - -- [Localization & Keywords](LocalizationAndKeywords.md) -- [Card Dynamic Variables](CardDynamicVarToolkit.md) -- [Patching Guide](PatchingGuide.md) -- [Content Authoring Toolkit](ContentAuthoringToolkit.md) diff --git a/Docs/en/LocalizationAndKeywords.md b/Docs/en/LocalizationAndKeywords.md deleted file mode 100644 index c69a46f..0000000 --- a/Docs/en/LocalizationAndKeywords.md +++ /dev/null @@ -1,207 +0,0 @@ -# Localization & Keywords - -RitsuLib separates localization into two distinct layers: - -- **The base game's `LocString` model-key pipeline** — in-game text such as model titles and descriptions -- **Framework-provided `I18N` helper localization** — auxiliary text for the mod itself - -It also provides a lightweight keyword registry to unify hover tips and keyword text. - ---- - -## Game Model Localization - -> The following describes the game engine's own localization mechanism; RitsuLib does not replace this system. - -The game reads model text through `LocString` and various localization tables, commonly including: - -- `cards` -- `relics` -- `powers` -- `characters` -- `card_keywords` - -Those keys are built on `ModelId.Entry`. - -RitsuLib's role is limited to making model identity more stable and predictable so keys are easier to author. For concrete model ID rules, see [Content Authoring Toolkit](ContentAuthoringToolkit.md). - ---- - -## `CreateLocalization` And `CreateModLocalization` - -`I18N` is RitsuLib's helper-text localization system, independent of the game's `LocString`: - -```csharp -var i18n = RitsuLibFramework.CreateModLocalization( - modId: "MyMod", - instanceName: "MyMod-I18N", - resourceFolders: ["MyMod.localization"], - pckFolders: ["res://MyMod/localization"]); -``` - -`CreateModLocalization` is a convenience wrapper over `CreateLocalization`. -If you do not provide file-system folders, it defaults to: - -```text -user://mod-configs//localization -``` - ---- - -## Source Merge Order - -`I18N` can merge translations from three source kinds: - -1. file system folders -2. embedded resources -3. PCK folders - -Merge behavior is first-wins: - -- file-system entries are loaded first -- embedded entries only fill missing keys -- PCK entries only fill keys still missing after that - -This lets local overrides take priority over packaged defaults. - ---- - -## Language Normalization - -`I18N` normalizes locale names before loading JSON files: - -| Input | Normalized | -|---|---| -| `en`, `en_us`, `eng` | `eng` | -| `zh`, `zh_cn`, `zh_hans` | `zhs` | -| `ja`, `ja_jp` | `jpn` | - -If no language can be resolved, it falls back to `eng`. - ---- - -## Runtime Reload Behavior - -`I18N` subscribes to locale changes when possible: - -- when the game language changes, helper localization reloads automatically -- `Changed` is raised after reload completes -- if the game localization manager is unavailable at that moment, `I18N` falls back to lazy detection - -This behavior is independent of base-game `LocString` resolution. - ---- - -## Debug Compatibility Mode - -`LocTable` placeholder resolution is part of RitsuLib’s debug compatibility fallbacks. See [Diagnostics & Compatibility](DiagnosticsAndCompatibility.md) for the master toggle, the **LocTable missing keys** toggle, and one-time `[Localization][DebugCompat]` warnings. - -Use this for troubleshooting, not as a substitute for authoring real keys. - ---- - -## Keyword Registry - -Use `ModKeywordRegistry` when you want reusable keyword definitions and hover tips: - -```csharp -var keywords = RitsuLibFramework.GetKeywordRegistry("MyMod"); - -keywords.RegisterCardKeywordOwnedByLocNamespace( - localKeywordStem: "brew", - iconPath: "res://MyMod/ui/keywords/brew.png"); -``` - -This creates a normalized keyword id and binds it to title / description localization keys. - ---- - -## Automatic keyword registration (optional: CLR attributes) - -If you already use `ModTypeDiscoveryHub.RegisterModAssembly(...)` to let RitsuLib scan your assemblies, you can declare keyword registration with CLR attributes: - -```csharp -using STS2RitsuLib.Interop.AutoRegistration; - -[RegisterOwnedCardKeyword("brew", LocNamespace = "my_mod", IconPath = "res://MyMod/ui/keywords/brew.png")] -public sealed class BrewKeywordMarker; -``` - -`LocNamespace` only affects the localization namespace (the `modid` portion). The keyword stem (`brew`) participates in the default rule `_`, producing: - -- `_.title` -- `_.description` - -> Compatibility note: the legacy `LocKeyPrefix` / `locKeyPrefix` historically represents the **full stem** and is easy to misread as a prefix + keyword composition, so it is now obsolete. Use `LocNamespace` for new code. - ---- - -## Using Keywords In Code - -Common helpers: - -| Method | Description | -|---|---| -| `ModKeywordRegistry.CreateHoverTip(id)` | Create hover tip | -| `ModKeywordRegistry.GetTitle(id)` | Get title | -| `ModKeywordRegistry.GetDescription(id)` | Get description | -| `keywordId.GetModKeywordCardText()` | Get card text | -| `enumerable.ToHoverTips()` | Batch-convert to hover tips | - -You can also attach runtime keywords to arbitrary objects via `ModKeywordExtensions`: - -```csharp -card.AddModKeyword("brew"); - -if (card.HasModKeyword("brew")) -{ - // ... -} -``` - -This is useful when keyword presence is driven by runtime state rather than static card text. - ---- - -## Ancient Dialogue Localization - -RitsuLib includes `AncientDialogueLocalization`. It serves two roles: - -- helper API for scanning dialogue from localization keys -- automatic append of localization-defined mod-character ancient dialogues before `AncientDialogueSet.PopulateLocKeys` runs - -The key format matches the base game: - -| Key component | Description | -|---|---| -| `.talk..-.ancient` | Ancient line | -| `.talk..-.char` | Character line | -| Optional suffix `r` | Repeated dialogue | -| Optional suffix `.sfx` | Sound effect | -| Optional suffix `-visit` | Visit override | -| Optional suffix `-attack` | Architect-only attacker override | - -Authors only need to write localization entries to add ancient dialogue for custom characters, without manually patching each `AncientDialogueSet`. - -If **no** keys exist for an ancient, vanilla may still show `PROCEED` for `THE_ARCHITECT` while `WinRun` assumes `Dialogue` is non-null. RitsuLib adds a narrow compatibility fallback (empty `Lines`, safe attackers) for `ModContentRegistry` characters **only** when the debug compatibility master toggle and the **THE_ARCHITECT missing dialogue** toggle are enabled, with a one-time `[Ancient]` warning. - ---- - -## Recommended Split - -| Use case | Tool | -|---|---| -| Game model text (titles, descriptions) | Base game `LocString` tables | -| Mod-owned auxiliary text (settings, explanations) | `I18N` | -| Reusable keyword definitions | `ModKeywordRegistry` | -| Ancient dialogue | Localization keys + `AncientDialogueLocalization` | - ---- - -## Related Documents - -- [Content Authoring Toolkit](ContentAuthoringToolkit.md) -- [Character & Unlock Templates](CharacterAndUnlockScaffolding.md) -- [Diagnostics & Compatibility](DiagnosticsAndCompatibility.md) -- [LocString Placeholder Resolution](LocStringPlaceholderResolution.md) -- [Mod Settings UI](ModSettings.md) diff --git a/Docs/en/ModSettings.md b/Docs/en/ModSettings.md deleted file mode 100644 index 17108ac..0000000 --- a/Docs/en/ModSettings.md +++ /dev/null @@ -1,384 +0,0 @@ -# Mod Settings - -RitsuLib provides a settings UI layer for player-editable values. -It is built on top of `ModDataStore`, but it does not replace the persistence model. - -Use this system when you need to expose a selected subset of persisted values, organize them into pages and sections, and localize the visible text. Settings pages are registered explicitly by design. - ---- - -## Architecture - -Keep these responsibilities separate: - -- `ModDataStore`: persistence, scopes, defaults, migrations -- `IModSettingsValueBinding`: read/write bridge between UI and stored data -- page and section builders: UI structure and ordering -- `ModSettingsText`: text source abstraction for labels and descriptions - -This separation prevents runtime state, internal metadata, and user-editable configuration from collapsing into one model. - ---- - -## Core APIs - -| API | Purpose | -|---|---| -| `RitsuLibFramework.RegisterModSettings(modId, configure, pageId?)` | Register a settings page; when `pageId` is omitted it defaults to `modId` | -| `RitsuLibFramework.GetRegisteredModSettings()` | Return all registered pages | -| `ModSettingsBindings.Global(...)` / `Profile(...)` | Bind a control to persisted data | -| `ModSettingsBindings.InMemory(...)` | Bind a control to preview-only state | -| `ModSettingsText.Literal(...)` | Plain text | -| `ModSettingsText.I18N(...)` | `I18N`-backed settings text | -| `ModSettingsText.LocString(...)` | Game-native localization text | -| `ModSettingsText.Dynamic(...)` | Re-evaluate text on UI refresh | -| `WithModDisplayName(...)` | Override the mod label shown in the sidebar | -| `WithSortOrder(...)` | Sort sibling pages within one mod | -| `AsChildOf(parentPageId)` | Register a page as a child page | -| `section.Collapsible(startCollapsed?)` | Make a section collapsible | -| `page.WithVisibleWhen(...)` / `section.WithVisibleWhen(...)` | Conditional page or section visibility | -| `AddToggle(...)`, `AddSlider(...)`, `AddIntSlider(...)`, `AddChoice(...)`, `AddEnumChoice(...)` | Standard value editors | -| `AddColor(...)`, `AddKeyBinding(...)`, `AddImage(...)` | Specialized editors and previews | -| `AddButton(...)`, `AddHeader(...)`, `AddParagraph(...)` | Structural and action entries | -| `AddSubpage(...)` | Navigate to a child page | -| `AddList(...)` | Structured list editor | -| `ModSettingsUiActionRegistry.Register*ActionAppender(...)` | Extend the actions menu for rows, list items, pages, or sections | - ---- - -## Recommended Flow - -1. Register the complete persisted model in `ModDataStore`. -2. Create bindings only for fields that players should edit. -3. Register pages and sections around those bindings. -4. Localize all visible labels, descriptions, and option names. - -The result is an explicit contract between stored data and the settings UI. - ---- - -## UI Behavior - -- Entry point: Main menu -> `Settings` -> `General`. When at least one page is registered, RitsuLib injects a `Mod Settings (RitsuLib)` row that opens `RitsuModSettingsSubmenu`. -- Sidebar: grouped by mod. One mod group is expanded at a time. The selected page also exposes section shortcuts. -- Content pane: page header, optional back navigation for child pages, and a scrollable section body. -- Save timing: dirty bindings are flushed on a debounce of about `0.35s`. Closing or hiding the submenu, leaving the tree, or changing the game locale forces an immediate flush. - -`WithVisibleWhen(...)` and row-level `visibleWhen` predicates are re-evaluated on debounced refresh. Predicates should stay cheap and should not throw. If evaluation fails, the control remains visible. - ---- - -## Auto-Mirror Policy (BaseLib / ModConfig) - -`RitsuModSettingsSubmenu` automatically tries to mirror settings from both `BaseLib` and `ModConfig`. -When your mod intentionally supports multiple settings stacks, you can control mirror behavior with assembly-level `AssemblyMetadata` directives (requires only `System.Reflection`, no `STS2RitsuLib` reference). - -Supported keys (case-insensitive): - -- `RitsuLib.ModSettingsMirror.Global.DisableSources` -- `RitsuLib.ModSettingsMirror.Global.PreferredSource` -- `RitsuLib.ModSettingsMirror.Mod..DisableSources` -- `RitsuLib.ModSettingsMirror.Mod..PreferredSource` -- `RitsuLib.ModSettingsMirror.Type..DisableSources` -- `RitsuLib.ModSettingsMirror.Type..PreferredSource` - -Value rules: - -- `DisableSources`: `baselib`, `modconfig`, `all` (multiple values can be separated by `,` / `;` / `|`) -- `PreferredSource`: `baselib` or `modconfig` - -Priority (high -> low): `Type` -> `Mod` -> `Global`. -`PreferredSource` suppresses non-preferred mirror sources, and `DisableSources` blocks specific sources directly. - -Example: - -```csharp -using System.Reflection; - -[assembly: AssemblyMetadata("RitsuLib.ModSettingsMirror.Mod.MyMod.DisableSources", "modconfig")] -[assembly: AssemblyMetadata("RitsuLib.ModSettingsMirror.Mod.MyMod.PreferredSource", "baselib")] -[assembly: AssemblyMetadata( - "RitsuLib.ModSettingsMirror.Type.MyMod.Config.AdvancedSettings.DisableSources", - "baselib")] -``` - -You can also place the same directives directly in `csproj`: - -```xml - - - - - -``` - ---- - -## Runtime Reflection Protocol (No Library Reference) - -Besides BaseLib / ModConfig mirrors, RitsuLib also supports a pure reflection protocol for settings pages. -Your mod does not need to reference `STS2RitsuLib`; you only need to explicitly declare provider types in assembly metadata: - -```xml - - - -``` - -Runtime-initiated explicit registration is also supported (for reflection-driven init flows): - -- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderType(string providerTypeFullName, string? assemblyName = null)` -- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderType(Type providerType)` -- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderTypeAndTryRegister(string providerTypeFullName, string? assemblyName = null)` -- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderTypeAndTryRegister(Type providerType)` - -Provider contract (all methods are `static`): - -- `object CreateRitsuLibSettingsSchema()` -- `object? GetRitsuLibSettingValue(string key)` -- `void SetRitsuLibSettingValue(string key, object value)` -- Optional: `void SaveRitsuLibSettings()` -- Optional: `void InvokeRitsuLibSettingAction(string key)` (for button actions) -- Optional typed overrides (preferred over object resolver): - - `bool GetRitsuLibSettingBool(string key)` / `void SetRitsuLibSettingBool(string key, bool value)` - - `int GetRitsuLibSettingInt(string key)` / `void SetRitsuLibSettingInt(string key, int value)` - - `double GetRitsuLibSettingDouble(string key)` / `void SetRitsuLibSettingDouble(string key, double value)` - - `string GetRitsuLibSettingString(string key)` / `void SetRitsuLibSettingString(string key, string value)` - -`CreateRitsuLibSettingsSchema()` can return: - -- `Dictionary` (or equivalent object) -- a JSON string (root must be an object) -- a JSON file path (file root must be an object) - -Godot paths (`res://`, `user://`) are recommended, and regular file paths are also supported. - -Structure: - -- page: `modId`, `pageId`, `title`, `description`, `sortOrder`, `sections` -- section: `id`, `title`, `description`, `entries` -- entry: - - common fields: `id`, `type`, `key`, `label`, `description`, `scope` - - `type=toggle|string|button|choice|slider|int-slider` - - `choice`: `options` (`[{ value, label }]`) - - `slider/int-slider`: `min`, `max`, `step` - - `string`: `maxLength` - - `button`: `buttonText`, `tone` - ---- - -## Minimal Example - -First register persisted data: - -```csharp -using STS2RitsuLib.Data; -using STS2RitsuLib.Utils.Persistence; - -public sealed class MyModSettings -{ - public bool EnableFancyVfx { get; set; } = true; - public double ScreenShakeScale { get; set; } = 1.0; - public MyDifficultyMode DifficultyMode { get; set; } = MyDifficultyMode.Normal; -} - -using (RitsuLibFramework.BeginModDataRegistration("MyMod")) -{ - var store = RitsuLibFramework.GetDataStore("MyMod"); - - store.Register( - key: "settings", - fileName: "settings.json", - scope: SaveScope.Global, - defaultFactory: () => new MyModSettings(), - autoCreateIfMissing: true); -} -``` - -Then create bindings and register the page: - -```csharp -using STS2RitsuLib.Settings; - -var settingsLoc = RitsuLibFramework.CreateModLocalization( - modId: "MyMod", - instanceName: "MyMod-Settings", - resourceFolders: ["MyMod.Localization.Settings"]); - -var fancyVfx = ModSettingsBindings.Global( - "MyMod", - "settings", - model => model.EnableFancyVfx, - (model, value) => model.EnableFancyVfx = value); - -var shakeScale = ModSettingsBindings.Global( - "MyMod", - "settings", - model => model.ScreenShakeScale, - (model, value) => model.ScreenShakeScale = value); - -var difficulty = ModSettingsBindings.Global( - "MyMod", - "settings", - model => model.DifficultyMode, - (model, value) => model.DifficultyMode = value); - -RitsuLibFramework.RegisterModSettings("MyMod", page => page - .WithModDisplayName(ModSettingsText.I18N(settingsLoc, "mod.display_name", "My Fancy Mod")) - .WithTitle(ModSettingsText.I18N(settingsLoc, "page.title", "Settings")) - .WithDescription(ModSettingsText.I18N(settingsLoc, "page.description", "Player-facing options for this mod.")) - .AddSection("general", section => section - .WithTitle(ModSettingsText.I18N(settingsLoc, "general.title", "General")) - .AddToggle( - "fancy_vfx", - ModSettingsText.I18N(settingsLoc, "fancy_vfx.label", "Fancy VFX"), - fancyVfx, - ModSettingsText.I18N(settingsLoc, "fancy_vfx.desc", "Enable additional visual polish.")) - .AddSlider( - "screen_shake_scale", - ModSettingsText.I18N(settingsLoc, "screen_shake.label", "Screen Shake Scale"), - shakeScale, - minValue: 0.0, - maxValue: 2.0, - step: 0.05, - valueFormatter: value => $"{value:0.00}x") - .AddEnumChoice( - "difficulty_mode", - ModSettingsText.I18N(settingsLoc, "difficulty.label", "Difficulty"), - difficulty, - value => ModSettingsText.I18N(settingsLoc, $"difficulty.{value}", value.ToString())))); -``` - -`WithModDisplayName(...)` controls the label used in the left navigation. If it is omitted, RitsuLib falls back to the manifest name and then the mod id. - ---- - -## Ordering And Navigation - -- **Mod groups**: call `WithModSidebarOrder(int)` on the page builder, or `ModSettingsRegistry.RegisterModSidebarOrder` / `RitsuLibFramework.RegisterModSettingsSidebarOrder`. Lower values appear earlier. -- **Pages within one mod**: use `WithSortOrder(int)` for sibling pages that share the same `ParentPageId`. -- **Child pages**: register the child separately with `AsChildOf(parentPageId)`, then link to it from the parent with `AddSubpage(...)`. - -### Multiple Pages And Subpages - -- **Default page id**: `RegisterModSettings("MyMod", configure)` uses `PageId == "MyMod"`. -- **Extra root pages**: call `RegisterModSettings("MyMod", configure, pageId: "audio")` and use `WithSortOrder(...)` to order multiple root pages. -- **Child page registration**: register the child in its own call and chain `AsChildOf("parentPageId")`. -- **Child UI**: child pages show a back control in the header; the sidebar tree still reflects the hierarchy. - ---- - -## Text Sources - -Use `ModSettingsText` so the page definition stays independent from how text is loaded. - -- `Literal(...)`: simple hardcoded text or quick prototypes -- `I18N(...)`: mod-owned settings text -- `LocString(...)`: text already managed by the game localization pipeline -- `Dynamic(...)`: delegate resolved on each UI rebuild - -Recommended split: - -- gameplay and content-facing names -> `LocString` -- settings-only labels and descriptions -> `I18N` - ---- - -## Supported Controls - -- `AddToggle(...)` for `bool` -- `AddSlider(...)` for `double` -- `AddIntSlider(...)` for `int` -- `AddChoice(...)` / `AddEnumChoice(...)` for option lists; optional `ModSettingsChoicePresentation`: `Stepper` or `Dropdown` -- `AddColor(...)` for color strings -- `AddKeyBinding(...)` for binding strings -- `AddImage(...)` for a `Func` preview with height -- `AddButton(...)` for custom actions -- `AddSubpage(...)` to navigate to a registered child page -- `AddList(...)` for reorderable structured collections -- `AddHeader(...)` / `AddParagraph(...)` for explanatory structure -- collapsible sections via `.Collapsible(startCollapsed: false)` on the section builder - ---- - -## Structured Lists - -`AddList(...)` is the entry point for structured list editing. - -It supports: - -- add / remove / reorder -- nested list editors -- item-level structured copy / paste / duplicate -- custom item editors via `ModSettingsListItemContext` - -If the item type is structured, provide an item adapter so copy/paste and duplication can clone and serialize reliably. - ---- - -## Page Structure - -The UI hierarchy is: - -- mod group -- page -- section -- entry - -For most mods, one root page with several sections is sufficient. Introduce additional pages only when the content represents a distinct feature area. - -Use: - -- multiple pages for large feature areas -- `AddSubpage(...)` for drill-down flows -- collapsible sections for low-frequency settings -- lists when players edit collections rather than single values - ---- - -## Scope Guidance - -Bindings preserve the scope of the underlying persisted value. - -- `SaveScope.Global`: shared across all profiles -- `SaveScope.Profile`: varies by player profile - -Typical usage: - -- `Global`: graphics, accessibility, debug toggles, machine-level defaults -- `Profile`: profile-specific gameplay preferences or campaign-adjacent options - ---- - -## What To Expose - -Good candidates for the settings UI: - -- feature toggles -- cosmetic preferences -- accessibility adjustments -- gameplay options players are expected to tune - -Poor candidates for the settings UI: - -- caches -- migration bookkeeping -- runtime mirrors -- purely internal implementation state - -The intended pattern is to persist a complete model, then expose only the user-editable subset. - ---- - -## Built-In Reference Page - -RitsuLib registers its own page as a reference implementation. It demonstrates persisted settings, preview-only bindings, collapsible sections, nested list editing, and item copy/paste workflows. - ---- - -## Related Docs - -- [Persistence Guide](PersistenceGuide.md) -- [Localization & Keywords](LocalizationAndKeywords.md) -- [Lifecycle Events](LifecycleEvents.md) -- [Patching Guide](PatchingGuide.md) (`Settings/Patches/ModSettingsUiPatches.cs` contains the menu entry and submenu injection) diff --git a/Docs/en/PatchingGuide.md b/Docs/en/PatchingGuide.md deleted file mode 100644 index 554a571..0000000 --- a/Docs/en/PatchingGuide.md +++ /dev/null @@ -1,213 +0,0 @@ -# Patching Guide - -RitsuLib uses Harmony underneath, but wraps it in a patching layer that standardizes declaration shape, registration, and failure handling. - ---- - -## Main Types - -| Type | Purpose | -|---|---| -| `RitsuLibFramework.CreatePatcher(...)` | Create a `ModPatcher` instance | -| `ModPatcher` | Register and apply patches | -| `IPatchMethod` | Static patch declaration contract | -| `IModPatches` | Group multiple patch registrations together | -| `DynamicPatchBuilder` | Build patches from runtime-discovered methods | - ---- - -## The Normal Workflow - -```csharp -var patcher = RitsuLibFramework.CreatePatcher("MyMod", "core-patches"); -patcher.RegisterPatch(); -patcher.RegisterPatches(); - -if (!patcher.PatchAll()) - throw new InvalidOperationException("Required patches failed."); -``` - -Recommended pattern: - -- create one patcher per logical patch area -- register all patches first -- call `PatchAll()` once -- treat a `false` return as a startup failure for that patcher - ---- - -## Writing A Single Patch With `IPatchMethod` - -`IPatchMethod` is the most common patch shape. - -```csharp -using STS2RitsuLib.Patching.Models; - -public class ExamplePatch : IPatchMethod -{ - public static string PatchId => "example_patch"; - public static string Description => "Log when the method runs"; - public static bool IsCritical => false; - - public static ModPatchTarget[] GetTargets() - { - return [new(typeof(SomeType), nameof(SomeType.SomeMethod))]; - } - - public static void Prefix() - { - // Harmony prefix - } -} -``` - -Important points: - -- `PatchId` must be unique within the patcher -- `GetTargets()` can return one or many targets -- `Prefix`, `Postfix`, `Transpiler`, and `Finalizer` are discovered by name -- if none of those methods exist, patch application fails - ---- - -## Grouping Patches With `IModPatches` - -When you want one type to register several patches, implement `IModPatches`: - -```csharp -using STS2RitsuLib.Patching.Core; -using STS2RitsuLib.Patching.Models; - -public class MyPatchSet : IModPatches -{ - public static void AddTo(ModPatcher patcher) - { - patcher.RegisterPatch(); - patcher.RegisterPatch(); - } -} -``` - -Then register the group with: - -```csharp -patcher.RegisterPatches(); -``` - -This is the preferred replacement for older "apply this patch bundle object" examples. - ---- - -## Critical vs Optional Patches - -Each `IPatchMethod` can declare `IsCritical`. - -- `true`: failure causes `PatchAll()` to fail and the patcher rolls back -- `false`: failure is logged, but the patcher may still succeed overall - -Use `IsCritical = true` when the mod cannot safely run without the patch. -Use `false` for cosmetic features, optional compatibility hooks, or best-effort enhancements. - ---- - -## Ignore Missing Targets - -`ModPatchTarget` supports an `ignoreIfMissing` flag: - -```csharp -public static ModPatchTarget[] GetTargets() -{ - return [new(typeof(SomeType), "SomeOptionalMethod", ignoreIfMissing: true)]; -} -``` - -Use this when: - -- a target only exists on some game versions -- a compatibility target may not be present -- the patch is optional by design - -This differs from `IsCritical = false`: - -- `ignoreIfMissing` means "missing target is expected and not an error" -- `IsCritical = false` means "target exists, but patch failure should not abort the patcher" - ---- - -## Multiple Targets In One Patch - -One `IPatchMethod` can patch several methods that share the same Harmony logic. - -RitsuLib automatically expands `GetTargets()` into multiple `ModPatchInfo` entries. -If there is more than one target, the framework appends the target name to the generated patch id. - -That lets you keep related logic together without manually duplicating patch classes. - ---- - -## Dynamic Patches - -Use `DynamicPatchBuilder` when targets are discovered at runtime. - -```csharp -using HarmonyLib; -using STS2RitsuLib.Patching.Builders; - -var builder = new DynamicPatchBuilder("my_dynamic") - .AddMethod( - targetType: typeof(SomeType), - methodName: "SomeMethod", - postfix: DynamicPatchBuilder.FromMethod(typeof(MyRuntimePatch), nameof(MyRuntimePatch.Postfix)), - isCritical: false, - description: "Runtime-discovered patch"); - -patcher.ApplyDynamic(builder, rollbackOnCriticalFailure: false); -``` - -Use dynamic patches when static `GetTargets()` is not practical, for example: - -- patching generated runtime types -- patching property getters selected from reflection scans -- patching a variable set of discovered methods - ---- - -## Logging And Patch Boundaries - -`CreatePatcher(ownerModId, patcherName, patcherLabel)` gives each patcher: - -- a stable Harmony id: `.` -- its own logger prefix -- independent registration and application lifecycle - -Splitting patchers by feature area is usually worth it because logs stay easier to read. - ---- - -## Suggested Structure - -For medium or large mods, this layout works well: - -- one patch namespace per feature area -- one `IModPatches` type per feature area -- small `IPatchMethod` classes with one clear purpose each -- optional compatibility patches marked `IsCritical = false` - -This matches how RitsuLib itself organizes its internal framework patchers. - ---- - -## Common Mistakes - -- calling `PatchAll()` before registering all patches -- marking compatibility patches as critical without a real need -- using `IsCritical = false` when `ignoreIfMissing` is the real intent -- writing an `IPatchMethod` with no `Prefix` / `Postfix` / `Transpiler` / `Finalizer` -- keeping all unrelated patches in one giant patcher with unreadable logs - ---- - -## Related Documents - -- [Getting Started](GettingStarted.md) -- [Framework Design](FrameworkDesign.md) diff --git a/Docs/en/PersistenceGuide.md b/Docs/en/PersistenceGuide.md deleted file mode 100644 index 50b0a7f..0000000 --- a/Docs/en/PersistenceGuide.md +++ /dev/null @@ -1,258 +0,0 @@ -# Persistence Guide - -RitsuLib provides a structured persistence layer for mod data, with scoped storage, profile switching support, backup fallback, and schema migrations. - ---- - -## Main APIs - -| API | Purpose | -|---|---| -| `RitsuLibFramework.BeginModDataRegistration(modId)` | Batch registration scope | -| `RitsuLibFramework.GetDataStore(modId)` | Access the mod's `ModDataStore` | -| `ModDataStore.Register(...)` | Register one persistent entry | -| `ModDataStore.Get(key)` | Read data | -| `ModDataStore.Modify(key, ...)` | Mutate data | -| `ModDataStore.Save(key)` / `SaveAll()` | Persist changes | - ---- - -## Why Data Is Registered As Classes - -Persistent entries are registered as `class` types with a parameterless constructor. - -This allows the framework to support: - -- structured JSON payloads -- future schema expansion -- versioned migration -- safer defaults and cloning - -So instead of registering a raw integer, define a small data object: - -```csharp -public sealed class CounterData -{ - public int Value { get; set; } -} -``` - ---- - -## Registering Data - -```csharp -using STS2RitsuLib.Data; -using STS2RitsuLib.Utils.Persistence; - -using (RitsuLibFramework.BeginModDataRegistration("MyMod")) -{ - var store = RitsuLibFramework.GetDataStore("MyMod"); - - store.Register( - key: "counter", - fileName: "counter.json", - scope: SaveScope.Profile, - defaultFactory: () => new CounterData(), - autoCreateIfMissing: true); -} -``` - -Parameters worth understanding: - -- `key`: lookup key inside the store -- `fileName`: file name written under the resolved mod-data path -- `scope`: `Global` or `Profile` -- `defaultFactory`: default value when no file exists or recovery is needed -- `autoCreateIfMissing`: immediately write the default file when missing - ---- - -## Global vs Profile Scope - -`SaveScope` has two values: - -- `Global`: shared across all profiles -- `Profile`: isolated per game profile - -Design intent: - -- use `Global` for mod settings or machine-wide caches -- use `Profile` for unlocks, progression, and run-adjacent player data - -Profile-scoped entries are initialized only after profile services are ready. - ---- - -## Reading And Writing - -```csharp -var store = RitsuLibFramework.GetDataStore("MyMod"); - -var counter = store.Get("counter"); - -store.Modify("counter", data => -{ - data.Value += 1; -}); - -store.Save("counter"); -``` - -Notes: - -- `Get` returns the live registered object -- `Modify` is just a convenience wrapper around that live object -- saving is explicit unless you choose to save immediately after mutation - ---- - -## Registration Timing - -`BeginModDataRegistration` is the recommended registration pattern because it lets the store defer initialization until the batch is complete. - -That helps avoid partial setup states when a mod registers several entries in one place. - -At the end of the registration scope: - -- global entries initialize immediately -- profile entries initialize when profile services are available - ---- - -## Profile Changes - -Profile-scoped entries are aware of profile switching. - -When the active profile changes, RitsuLib: - -- saves the old profile-scoped data to the old profile path -- reloads the data from the new profile path - -This is handled by the framework; mods do not need to manually rebind their profile-scoped stores. - ---- - -## Existing Data Checks - -```csharp -if (store.HasExistingData("counter")) -{ - // There was already persisted data on disk -} -``` - -This is useful when you want different startup behavior for first-time initialization vs loading an existing save. - ---- - -## Recovery And Backup Behavior - -The persistence layer tries to be defensive: - -- if the main file cannot be read, it attempts backup fallback -- if migrated backup data loads successfully, it can be written back -- if migration or parsing fails badly enough, corrupt data can be renamed with a `.corrupt` suffix -- when recovery fails, the entry falls back to default values - -This is meant to keep the mod usable even when local data is damaged. - ---- - -## Migrations - -`Register` accepts both migration config and migration steps: - -```csharp -store.Register( - key: "settings", - fileName: "settings.json", - scope: SaveScope.Global, - defaultFactory: () => new MyData(), - migrationConfig: new ModDataMigrationConfig(currentDataVersion: 2, minimumSupportedDataVersion: 1), - migrations: - [ - new SettingsV1ToV2Migration(), - ]); -``` - -Migration rules: - -- if no config is registered, data is deserialized directly -- if config exists, the framework reads the schema version field -- migrations run in version order -- data below the minimum supported version is rejected for recovery -- successfully migrated data is saved back in the new format - -Use migrations when a file format is published and later evolves. - ---- - -## AttachedState vs SavedAttachedState - -`AttachedState` is for runtime-only sidecar state on reference objects. - -Use it when: - -- the value only matters during the current process -- the key object already defines the lifetime you want -- you do not want to subclass or mutate the target type - -`SavedAttachedState` is the persisted counterpart for objects that already flow through `SavedProperties.FromInternal(...)` and `SavedProperties.FillInternal(...)`. - -Use it when: - -- the key is a model object that participates in vanilla save serialization -- the attached value should survive save/load round-trips -- the value type is already supported by `SavedProperties` - -Supported value types are: - -- `int` -- `bool` -- `string` -- `ModelId` -- enums -- `int[]` -- enum arrays -- `SerializableCard` -- `SerializableCard[]` -- `List` - -Example: - -```csharp -using STS2RitsuLib.Utils; - -private static readonly SavedAttachedState BonusDamage = - new("bonus_damage", () => 0); - -BonusDamage[model] = 4; - -var bonus = BonusDamage.GetOrCreate(model); -``` - -Notes: - -- persisted names must be globally unique after the `"{typeof(TKey).Name}_{name}"` prefix is applied -- `SavedAttachedState` is not a generic JSON sideband channel; it is intentionally limited to `SavedProperties`-compatible value types -- reward-specific `EncounterState` sideband serialization remains a special-case implementation, not the default persistence pattern - ---- - -## Recommended Usage Pattern - -- define one data class per persisted concept -- use `AttachedState` for ephemeral runtime-only object state -- use `SavedAttachedState` only for model objects that already participate in `SavedProperties` -- keep file names stable after release -- use `Profile` scope by default for progression-like data -- batch registration inside `BeginModDataRegistration` -- add schema versions before you need them, not after a breaking change has already shipped - ---- - -## Related Documents - -- [Getting Started](GettingStarted.md) -- [Framework Design](FrameworkDesign.md) diff --git a/Docs/en/Terminology.md b/Docs/en/Terminology.md deleted file mode 100644 index b100d89..0000000 --- a/Docs/en/Terminology.md +++ /dev/null @@ -1,41 +0,0 @@ -# Terminology - -This document defines the canonical terms used across the RitsuLib documentation. - ---- - -## Core Terms - -| Term | Preferred usage | Notes | -|---|---|---| -| settings UI | settings UI | Use for the mod configuration interface as a whole. | -| settings page | page | A single registered page in the settings UI. | -| section | section | A structured group within a page. | -| entry | entry | One visible row or control within a section. | -| binding | binding | The read/write link between UI and stored or in-memory state. | -| persistence | persistence | The storage layer and save lifecycle. | -| persisted | persisted | Use for values written through the persistence layer. | -| preview-only | preview-only | Use for controls or bindings that never persist data. | -| fallback | fallback | Preferred over `shim` for compatibility behavior. | -| compatibility fallback | compatibility fallback | A narrowly scoped behavior used when vanilla data or APIs are incomplete. | -| bridge patch | bridge patch | A patch that forwards mod content into vanilla logic that would otherwise skip it. | -| registry | registry | The runtime registration container for a content type. | -| content pack | content pack | The convenience entry point that writes into multiple registries. | -| builder | builder | Use for fluent page, section, or content construction APIs. | -| override | override | Use for replacing an asset path, behavior, or value source. | -| placeholder | placeholder | A temporary fallback value used when data is missing. | -| scope | scope | The storage scope of a persisted value. | -| profile | profile | Per-profile save scope. | -| global | global | Cross-profile save scope. | -| epoch | epoch | Keep the game term `epoch` in English. | -| story | story | Keep the game term `story` in English. | -| Ancient dialogue | Ancient dialogue | Use this spelling for the game system and related keys. | - ---- - -## Related Documents - -- [Framework Design](FrameworkDesign.md) -- [Mod Settings](ModSettings.md) -- [Diagnostics & Compatibility](DiagnosticsAndCompatibility.md) -- [Localization & Keywords](LocalizationAndKeywords.md) diff --git a/Docs/en/TimelineAndUnlocks.md b/Docs/en/TimelineAndUnlocks.md deleted file mode 100644 index 00e5225..0000000 --- a/Docs/en/TimelineAndUnlocks.md +++ /dev/null @@ -1,256 +0,0 @@ -# Timeline & Unlocks - -This is the reference for timeline registration and unlock semantics. - -RitsuLib splits timeline registration and unlock rules into two systems that are meant to work together. This document covers: - -- How `Story` and `Epoch` are registered -- What the template types are responsible for -- How unlock rules are evaluated -- Limitations of vanilla progression for mod characters and RitsuLib’s compatibility bridges - ---- - -## The Two Registries - -| Registry | Role | -|---|---| -| `ModTimelineRegistry` | Registers `StoryModel` and `EpochModel` | -| `ModUnlockRegistry` | Defines unlock conditions for content or epochs | - -In the fluent builder, these correspond to: - -- `.Story()`, `.Epoch()` -- `.RequireEpoch()`, `.UnlockEpochAfter...()` - -Core distinction: - -- **Timeline registration** answers “does this thing exist?” -- **Unlock registration** answers “when does it become available?” - ---- - -## Story Registration - -Use `ModStoryTemplate` for the story **type** (slug id from `StoryKey` only). Epoch **order** is not a hard-coded list on the story class; register each epoch against the story in manifest order: - -```csharp -public class MyStory : ModStoryTemplate -{ - protected override string StoryKey => "my-story"; -} - -// Fluent (order = column order): -// .StoryEpoch() -// .StoryEpoch() -// .Story() - -// Or IModContentPackEntry list (same idea as card manifest entries): -// new TimelineColumnPackEntry(c => c.Epoch()...), -// new StoryPackEntry(), -``` - -`ModStoryTemplate` is responsible for: - -- Deriving a normalized story identity from `StoryKey` -- Building `Epochs` from `ModStoryEpochBindings` (filled by `ModTimelineRegistry.RegisterStoryEpoch()`) - -`RegisterStoryEpoch` registers the epoch with vanilla discovery **and** appends it to that story’s column. Use `.Epoch()` only for epochs that are **not** part of a mod story column. - ---- - -## Epoch Registration - -You can write plain `EpochModel` subclasses, or use RitsuLib template types: - -| Template | Description | -|---|---| -| `CharacterUnlockEpochTemplate` | Epoch that unlocks the character | -| `CardUnlockEpochTemplate` | Epoch that unlocks extra cards | -| `RelicUnlockEpochTemplate` | Epoch that unlocks extra relics | -| `PotionUnlockEpochTemplate` | Epoch that unlocks extra potions | - -These templates mainly handle: - -- Enqueue logic for the timeline unlock UI -- Follow-up epochs via `ExpansionEpochTypes` - -### Character unlock epoch template - -Built-in behavior of `CharacterUnlockEpochTemplate`: - -- Queues a character unlock in `NTimelineScreen` -- Writes the pending character unlock to save progress -- If `ExpansionEpochTypes` is set, queues further epochs into the timeline expansion - -### Card / relic / potion epoch templates - -`CardUnlockEpochTemplate`, `RelicUnlockEpochTemplate`, and `PotionUnlockEpochTemplate` work similarly: - -- You declare the model types to unlock -- The template resolves types through `ModelDb` -- `UnlockText` is generated automatically -- `QueueUnlocks()` pushes into the timeline UI - ---- - -## Expansion Epochs - -All unlock epoch templates support: - -```csharp -protected virtual IEnumerable ExpansionEpochTypes => []; -``` - -When the current epoch completes, these epochs are added automatically as timeline expansions, which helps chain unlocks: - -1. Unlock the character first -2. Then reveal card unlocks -3. Then reveal relic unlocks - ---- - -## Registration Timing and Freeze - -Both the timeline and unlock registries freeze after early initialization because: - -- Story and epoch identities must stay stable -- Unlock filtering and compatibility patches need a finalized rule set - -Register `Story`, `Epoch`, and unlock rules from your initializer — not later at runtime. - ---- - -## Requiring an Epoch for Content - -When a model is registered but should only appear after an epoch is obtained, use `RequireEpoch()`. - -Typical uses: - -- Late-game cards stay out of the pool until progress is met -- Relics open only after a specific story branch -- Shared ancients / events need a timeline milestone - -RitsuLib applies the gate across multiple entry points: - -- `UnlockState.Characters` -- Unlocked card / relic / potion pool queries -- Shared ancient lists -- Events generated for acts - -This is not UI-only filtering; it changes what the game can actually offer. - -### Epoch progress vs. timeline reveal - -Vanilla `UnlockState` built from save progress mainly reflects epochs that have reached **`EpochState.Revealed`** (visible on the timeline) in **`UnlockedEpochs`**. **`SaveManager.ObtainEpoch`** can set **`Obtained`** / **`ObtainedNoSlot`** *before* the timeline slot is revealed. - -**`ModUnlockRegistry.IsUnlocked`** (used when applying **`RequireEpoch`** gating) treats the requirement as satisfied if **either**: - -- the epoch id is in **`unlockState.UnlockedEpochs`**, or -- **`SaveManager.Instance.Progress.IsEpochObtained(epochId)`** is true. - -So pool / character / event gating lines up with mod rules that call **`ObtainEpoch`**, not only with vanilla timeline reveal timing. - ---- - -## Post-Run Epoch Rules - -Common convenience APIs on `ModUnlockRegistry`: - -| Method | Description | -|---|---| -| `UnlockEpochAfterRunAs()` | Unlock after completing a run with the given character | -| `UnlockEpochAfterWinAs()` | Unlock after a win with that character | -| `UnlockEpochAfterAscensionWin(level)` | Unlock after a win at the given ascension | -| `UnlockEpochAfterRunCount(requiredRuns, requireVictory)` | Unlock after enough runs | - -These all compile to `PostRunEpochUnlockRule`. - -You can also register a custom rule: - -```csharp -unlocks.RegisterPostRunRule( - PostRunEpochUnlockRule.Create( - epochId: new MyEpoch().Id, - description: "Unlock after any abandoned ascension-5 run", - shouldUnlock: ctx => ctx.IsAbandoned && ctx.AscensionLevel >= 5)); -``` - ---- - -## Counted Progression Rules - -| Method | Description | -|---|---| -| `UnlockEpochAfterEliteVictories(count)` | Elite kill count | -| `UnlockEpochAfterBossVictories(count)` | Boss kill count | -| `UnlockEpochAfterAscensionOneWin()` | Ascension 1 win | -| `RevealAscensionAfterEpoch()` | Show ascension after the epoch | -| `UnlockCharacterAfterRunAs()` | Unlock character after using that character | - ---- - -## Compatibility Patches - -> This section explains how vanilla progression limits mod characters and how RitsuLib bridges those gaps. - -Several vanilla progression checks assume vanilla characters and do not naturally include mod characters. RitsuLib applies narrow bridge patches so registered unlock rules still apply at those checkpoints: - -- Elite kill count → epoch checks -- Boss kill count → epoch checks -- Ascension 1 → epoch checks -- Post-run character-unlock epochs -- Ascension reveal unlock checks - -These patches do not replace vanilla progression; they only add a bridge where vanilla would skip mod characters. That is why the unlock registry stores rules explicitly by `ModelId` instead of inferring all progression from the timeline graph alone. - ---- - -## Recommended Pattern - -For a story-driven character mod: - -1. Register character, pools, epochs, and story in one content pack -2. Use `CharacterUnlockEpochTemplate` for the character unlock epoch -3. Use card / relic / potion epoch templates for follow-up content -4. Use `RequireEpoch()` for late-game gates -5. Prefer a small set of clear progression rules over many overlapping ones - ---- - -## Builder Example - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - .Character() - .Card() - .Relic() - .Epoch() - .Epoch() - .Story() - .RequireEpoch() - .RequireEpoch() - .UnlockEpochAfterWinAs() - .UnlockEpochAfterAscensionWin(10) - .Apply(); -``` - ---- - -## Common Mistakes - -- Registering epochs but forgetting the story that lists those epochs -- Registering story/epochs after the timeline has frozen -- Using `RequireEpoch` without any rule that can actually unlock that epoch -- Stacking many overlapping rules for the same epoch without a clear design -- Assuming vanilla counted progression works for mod characters without registering RitsuLib unlock rules -- Leaving **`UnlocksAfterRunAsType`** at the default on a mod character while **`unlockText`** uses **`{Prerequisite}`** — the character-select hover then shows the generic locked title (often **`???`**). Set **`UnlocksAfterRunAsType`** to the same prerequisite character type as in **`UnlockEpochAfterWinAs`** / **`UnlockEpochAfterRunAs<…>`** (see [Character & Unlock Templates](CharacterAndUnlockScaffolding.md)) - ---- - -## Related Documents - -- [Character & Unlock Templates](CharacterAndUnlockScaffolding.md) -- [Content Packs & Registries](ContentPacksAndRegistries.md) -- [Diagnostics & Compatibility](DiagnosticsAndCompatibility.md) -- [Framework Design](FrameworkDesign.md) diff --git a/Docs/zh/AssetProfilesAndFallbacks.md b/Docs/zh/AssetProfilesAndFallbacks.md deleted file mode 100644 index ff70482..0000000 --- a/Docs/zh/AssetProfilesAndFallbacks.md +++ /dev/null @@ -1,229 +0,0 @@ -# 资源配置与回退规则 - -本文是资源配置结构、占位角色回退与资源路径诊断的参考文档。 - -RitsuLib 使用资源配置对象来描述可覆写的美术、场景、材质以及相关资源。 - -本文专门解释这些配置对象的结构,以及它们背后的回退规则。 - ---- - -## 为什么要有资源配置对象 - -资源覆写当然可以做成一长串平铺的虚属性。 - -但 RitsuLib 选择把它们组织成记录类型形式的资源配置,因为这样更适合长期扩展: - -- 相关资源会自然聚合在一起 -- 局部覆写时可读性更高 -- 回退合并规则更明确 -- 从默认依赖占位角色的旧框架迁移时,也不用放弃结构化设计 - -对角色尤其如此,因为角色资源横跨场景、UI、VFX、音频、Spine 和多人模式贴图。 - ---- - -## 角色资源配置结构 - -`CharacterAssetProfile` 被拆成多个嵌套记录类型: - -- `CharacterSceneAssetSet` -- `CharacterUiAssetSet` -- `CharacterVfxAssetSet` -- `CharacterSpineAssetSet` -- `CharacterAudioAssetSet` -- `CharacterMultiplayerAssetSet` - -这样你只改一个类别时,不会把其他类别也拖成噪音。 - -例如: - -```csharp -public override CharacterAssetProfile AssetProfile => new( - Scenes: new( - VisualsPath: "res://MyMod/scenes/character/my_character.tscn", - EnergyCounterPath: "res://MyMod/ui/energy/my_energy_counter.tscn"), - Ui: new( - IconTexturePath: "res://MyMod/ui/top_panel/icon.png", - MapMarkerPath: "res://MyMod/map/map_marker.png"), - Audio: new( - AttackSfx: "event:/sfx/characters/my_character/attack")); -``` - ---- - -## 占位角色回退 - -`ModCharacterTemplate` 现在提供: - -```csharp -public virtual string? PlaceholderCharacterId => "ironclad"; -``` - -它的行为是: - -- 先读取你显式写下的 `AssetProfile` -- 缺失项再从 `CharacterAssetProfiles.FromCharacterId(PlaceholderCharacterId)` 补齐 -- 如果 `PlaceholderCharacterId` 为 `null`,则彻底关闭回退 - -这让你既拥有类似 BaseLib 的迁移便利,又保留了 Ritsu 式的结构化角色 API。 - ---- - -## 角色资源配置如何合并 - -RitsuLib 对角色资源配置的合并是“按类别、按字段”进行的。 - -这意味着: - -- 你提供一个自定义 `Scenes` 记录,不会影响 `Ui` -- 你只写 `RestSiteAnimPath`,不会把 `MerchantAnimPath` 清空 -- 你只写 `AttackSfx`,不会把其余默认音效抹掉 - -这点非常重要,因为角色资源在实际开发里几乎从来不是一次性全量替换的。 - ---- - -## CharacterAssetProfiles 辅助 API - -`CharacterAssetProfiles` 提供了这些工具方法: - -- `FromCharacterId(string)` -- `Ironclad()` / `Silent()` / `Defect()` / `Regent()` / `Necrobinder()` -- `Resolve(profile, placeholderCharacterId)` -- `Merge(fallback, profile)` -- `FillMissingFrom(...)` -- `WithPlaceholder(...)` -- `WithScenes(...)`、`WithUi(...)`、`WithVfx(...)`、`WithSpine(...)`、`WithAudio(...)`、`WithMultiplayer(...)` - -它们主要服务两类场景: - -- 新角色的局部资源编写 -- 从默认假定占位角色的旧框架迁移到 RitsuLib - ---- - -## 其他内容的资源配置 - -RitsuLib 也为其他内容提供了类似的资源配置记录类型: - -- `CardAssetProfile` -- `RelicAssetProfile` -- `PowerAssetProfile` -- `OrbAssetProfile` -- `PotionAssetProfile` -- `AfflictionAssetProfile` -- `EnchantmentAssetProfile` -- `ActAssetProfile` - -这些配置对象更小,是因为它们各自的资源表面本来就更小。 - ---- - -## 路径辅助工厂 - -对于符合原版命名习惯的资源布局,RitsuLib 提供了几个常用辅助方法: - -- `CharacterAssetProfiles.FromCharacterId(...)` -- `ContentAssetProfiles.Card(...)` -- `ContentAssetProfiles.Relic(...)` -- `ContentAssetProfiles.Power(...)` -- `ContentAssetProfiles.Orb(...)` -- `ContentAssetProfiles.Potion(...)` -- `ContentAssetProfiles.Affliction(...)` -- `ContentAssetProfiles.Enchantment(...)` -- `ContentAssetProfiles.Act(...)` - -另外还有 `CharacterAssetPathHelper`,可用于推导角色相关默认路径,例如角色视觉场景、能量球、角色选择背景、小地图标记等。 - -当你的资源布局本来就遵循某种命名约定时,这些辅助方法会很省事。 - -如果这些资源背后是自定义 Godot 场景,请记得场景根节点和带脚本的子节点往往需要使用 Mod 本地包装类,编辑器绑定才会更稳定。详见 [Godot 场景编写说明](GodotSceneAuthoring.md)。 - ---- - -## 能量球场景、大能量图标、文本图标是三层能力 - -RitsuLib 明确把它们拆开: - -- `CustomEnergyCounterPath`:完整战斗能量球场景 -- `BigEnergyIconPath`:通过 `EnergyIconHelper` 解析的大图标 -- `TextEnergyIconPath`:富文本描述里的小图标 - -这样拆的原因是: - -- 场景替换才是自定义能量球的正确抽象 -- 纹理路径才是池图标的正确抽象 -- 把三件事混进一个 API 只会让职责变糊 - ---- - -## 缺失路径诊断 - -RitsuLib 现在通过 `AssetPathDiagnostics` 统一校验资源路径覆写。 - -当前行为: - -- 路径为空 -> 忽略 override -- 路径存在 -> 使用 override -- 路径不存在 -> 输出一次警告,并回退到原始资源 - -警告里会尽量带上: - -- 宿主类型 -- 若可用则带上模型条目标识 -- 对应的配置成员名 -- 缺失路径本身 - -这让资源接错线时比以前更容易定位。 - ---- - -## 哪些内容会做路径校验 - -路径校验主要覆盖这类“真正的资源路径”: - -- 卡牌贴图、材质、覆盖层、横幅 -- 遗物 / 能力 / 球体 / 药水图标 -- Act 背景 -- 角色视觉场景、能量球、小地图资源、轨迹场景、Spine 数据 -- 卡池级能量图标路径 - -而像音效事件 id 这种“不是 Godot 资源路径”的字符串不会走 `ResourceLoader` 校验。 - -所以角色 SFX 覆写字段仍然被当作普通字符串值处理,而不是资源路径。 - ---- - -## 推荐的角色资源写法 - -对大多数自定义角色,比较推荐的模式是: - -1. 保留 `PlaceholderCharacterId = "ironclad"`,或者改成你想继承风格的基础角色 -2. 只覆写真正自定义的资源 -3. 能量图标相关优先放在 pool 级 `BigEnergyIconPath` / `TextEnergyIconPath` -4. 只有真的要换完整能量球 UI 时,再使用 `CustomEnergyCounterPath` - -这样内容编写面会比较小,同时还能保留安全回退。 - ---- - -## 推荐的普通内容资源写法 - -对卡牌及其他内容: - -- 当多个资源字段属于同一个决策时,优先使用 `AssetProfile` -- 只有单点特例时,再直接覆写某个 `Custom...Path` -- 当你的资源布局与辅助方法约定一致时,优先考虑 `ContentAssetProfiles.Card(...)` 这类工厂 - -尤其是卡牌,把立绘、边框、覆盖层、横幅放在一个配置对象里通常会更清晰。 - ---- - -## 相关文档 - -- [角色与解锁模板](CharacterAndUnlockScaffolding.md) -- [内容注册规则](ContentAuthoringToolkit.md) -- [Godot 场景编写说明](GodotSceneAuthoring.md) -- [诊断与兼容层](DiagnosticsAndCompatibility.md) -- [框架设计](FrameworkDesign.md) diff --git a/Docs/zh/CardDynamicVarToolkit.md b/Docs/zh/CardDynamicVarToolkit.md deleted file mode 100644 index 55705cd..0000000 --- a/Docs/zh/CardDynamicVarToolkit.md +++ /dev/null @@ -1,134 +0,0 @@ -# 卡牌动态变量工具包 - -本文介绍 RitsuLib 提供的卡牌动态变量创建方式、悬浮提示绑定规则及其在卡牌悬停时的注入机制。 - ---- - -## 游戏原版 DynamicVar 系统 - -> 以下描述游戏引擎自身的动态变量系统,RitsuLib 在此基础上提供便捷构造器。 - -游戏的 `DynamicVar` 系统让卡牌在运行时携带可变数值。每个 `DynamicVar` 子类可携带额外元数据供格式化器读取(如 `DamageVar` 带高亮、`EnergyVar` 带颜色)。完整子类列表见 [LocString 占位符解析](LocStringPlaceholderResolution.md)。 - ---- - -## RitsuLib 提供的能力 - -在游戏原版基础上,RitsuLib 提供: - -- **`ModCardVars`** — 便捷变量构造器 -- **`DynamicVarExtensions`** — 每个变量可独立绑定悬浮提示 -- **自动注入** — 卡牌悬停时自动注入所有已绑定悬浮提示(由补丁实现,无需额外配置) - ---- - -## 变量构造 - -通过 `ModCardVars` 创建变量,并在卡牌的 `DynamicVarSet` 中使用: - -```csharp -public class MyCard : ModCardTemplate(1, CardType.Attack, CardRarity.Common, TargetType.SingleEnemy) -{ - private static readonly DynamicVar _charges = - ModCardVars.Int("charges", amount: 3) - .WithSharedTooltip("my_mod_charges"); - - private static readonly DynamicVar _label = - ModCardVars.String("flavor", value: "wine"); - - public override DynamicVarSet CreateDynamicVars() => - new DynamicVarSet().Add(_charges).Add(_label); -} -``` - -| 方法 | 说明 | -|---|---| -| `ModCardVars.Int(name, amount)` | 创建数值变量(`decimal`) | -| `ModCardVars.String(name, value)` | 创建字符串变量 | -| `ModCardVars.Computed(...)` | 创建计算变量 | - -RitsuLib 不为这些变量赋予玩法语义,变量的具体含义完全由内容作者定义。 - ---- - -## 悬浮提示绑定 - -在变量定义时通过扩展方法链式绑定: - -### 绑定共享悬浮提示(推荐) - -从 `static_hover_tips` 表读取键: - -```csharp -var myVar = ModCardVars.Int("my_var", 2) - .WithSharedTooltip("my_mod_my_var"); -// 解析: -// static_hover_tips["my_mod_my_var.title"] -// static_hover_tips["my_mod_my_var.description"] -``` - -### 绑定指定表/键 - -```csharp -var myVar = ModCardVars.Int("my_var", 2) - .WithTooltip( - titleTable: "card_keywords", - titleKey: "my_mod_my_var.title", - iconPath: "res://MyMod/art/kw.png"); -``` - -### 绑定自定义工厂方法 - -```csharp -var myVar = ModCardVars.Int("my_var", 2) - .WithTooltip(var => new HoverTip( - new LocString("my_table", "my_var.title"), - new LocString("my_table", "my_var.description"))); -``` - ---- - -## 本地化示例 - -使用 `WithSharedTooltip("my_mod_charges")` 时,需在 `static_hover_tips` 本地化文件中提供: - -```json -{ - "my_mod_charges.title": "充能", - "my_mod_charges.description": "累积的充能层数,造成额外伤害。" -} -``` - -RitsuLib 不提供内置本地化词条,使用 `WithSharedTooltip` 时词条须由作者自行提供。 - ---- - -## 卡牌悬浮提示注入 - -RitsuLib 的补丁会在卡牌悬停时自动将 `CardModel.DynamicVars` 中所有已绑定悬浮提示的变量追加到提示序列末尾,无需额外配置。 - ---- - -## 克隆行为 - -调用 `DynamicVar.Clone()` 时,绑定在原变量上的悬浮提示元数据会一并复制到克隆对象。战斗中升级或复制卡牌时行为正确,无需额外处理。 - ---- - -## 运行时读取变量值 - -通过 `DynamicVarExtensions` 扩展方法读取: - -```csharp -int charges = card.DynamicVars.GetIntOrDefault("charges"); -decimal val = card.DynamicVars.GetValueOrDefault("charges"); -bool active = card.DynamicVars.HasPositiveValue("charges"); -``` - ---- - -## 相关文档 - -- [内容注册规则](ContentAuthoringToolkit.md) -- [快速入门](GettingStarted.md) -- [LocString 占位符解析](LocStringPlaceholderResolution.md) diff --git a/Docs/zh/CharacterAndUnlockScaffolding.md b/Docs/zh/CharacterAndUnlockScaffolding.md deleted file mode 100644 index 1e96ff3..0000000 --- a/Docs/zh/CharacterAndUnlockScaffolding.md +++ /dev/null @@ -1,248 +0,0 @@ -# 角色与解锁模板 - -本文是角色 Mod 的实践搭建指南:角色模板、内容池定义、纪元模板与解锁注册,并附完整示例。 - -更细的回退规则见 [资源配置与回退规则](AssetProfilesAndFallbacks.md),更细的时间线与进度语义见 [时间线与解锁](TimelineAndUnlocks.md)。涉及角色视觉场景、休息点、能量球等场景脚本包装时,请继续看 [Godot 场景编写说明](GodotSceneAuthoring.md)。 - ---- - -## 概览 - -一个完整的角色 Mod 通常包含以下部分: - -| 内容 | 基类 | 示例 | -|---|---|---| -| 卡池 | `TypeListCardPoolModel` | `MyCardPool` | -| 遗物池 | `TypeListRelicPoolModel` | `MyRelicPool` | -| 药水池 | `TypeListPotionPoolModel` | `MyPotionPool` | -| 角色 | `ModCharacterTemplate` | `MyCharacter` | -| 故事 | `ModStoryTemplate` | `MyStory` | -| 纪元 | `CharacterUnlockEpochTemplate` 或自定义 | `MyEpoch2` | - ---- - -## 内容池定义 - -- **卡池**:`TypeListCardPoolModel` 的池成员在 `CreateContentPack` / Manifest 中通过 `.Card<卡池, 卡牌>()` / `CardRegistrationEntry` 登记;基类已提供默认空的 `CardTypes`(`[Obsolete]`),**无需覆写**。 -- **遗物池 / 药水池**:现在与卡池保持一致,`TypeListRelicPoolModel` / `TypeListPotionPoolModel` 的 `RelicTypes` / `PotionTypes` 已提供默认空实现并标记为 `[Obsolete]`。新 Mod 请通过 `CreateContentPack` / Manifest 的 `.Relic<池, 遗物>()`、`.Potion<池, 药水>()`、`RelicRegistrationEntry`、`PotionRegistrationEntry` 注册内容。 - -```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; -} - -public class MyRelicPool : TypeListRelicPoolModel -{ -} - -public class MyPotionPool : TypeListPotionPoolModel -{ -} -``` - -**旧池钩子(`CardTypes` / `RelicTypes` / `PotionTypes`):** 新 Mod 不要再覆写。旧代码若继续覆写会得到 **CS0618**,且与内容包注册叠用时仍会重复拼接池内容。迁移方式是删除覆写、仅保留内容包 / Manifest 注册。 - -### 配置卡牌边框颜色(HSV) - -`TypeListCardPoolModel` 支持直接覆盖 `PoolFrameMaterial`。当该属性返回非空材质时,会优先使用这个材质渲染卡牌边框,不再依赖 `CardFrameMaterialPath`。 - -```csharp -using Godot; -using STS2RitsuLib.Utils; - -public class MyCardPool : TypeListCardPoolModel -{ - // 卡牌在 CreateContentPack / Manifest 中注册;勿覆写 CardTypes - - // 直接用 HSV 生成边框材质:H=0.55, S=0.45, V=0.95 - public override Material? PoolFrameMaterial => - MaterialUtils.CreateHsvShaderMaterial(0.55f, 0.45f, 0.95f); -} -``` - -若你希望继续走资源路径模式,也可以不覆盖 `PoolFrameMaterial`,仅覆盖 `CardFrameMaterialPath`。 - -### 示例:配置池能量图标 - -`TypeList*PoolModel` 现在也支持统一配置能量图标: - -- `BigEnergyIconPath`:通过 `EnergyIconHelper` 解析的大图标 -- `TextEnergyIconPath`:卡牌描述富文本里使用的小图标 - -```csharp -public class MyCardPool : TypeListCardPoolModel -{ - public override string? BigEnergyIconPath => "res://MyMod/ui/energy/my_energy_big.png"; - public override string? TextEnergyIconPath => "res://MyMod/ui/energy/my_energy_text.png"; -} -``` - ---- - -## 角色模板 - -继承 `ModCharacterTemplate` 负责角色本身,然后把 starter 内容放到内容注册阶段做追加式登记。 - -未填写的角色资源会自动回退到 `PlaceholderCharacterId`,默认值为 `ironclad`。 - -```csharp -public class MyCharacter : ModCharacterTemplate -{ - public override string? PlaceholderCharacterId => "ironclad"; - - // 资源路径(使用 AssetProfile 统一配置) - public override CharacterAssetProfile AssetProfile => new( - Spine: new( - CombatSkeletonDataPath: "res://MyMod/spine/my_character.tres"), - Ui: new( - IconTexturePath: "res://MyMod/art/icon.png", - CharacterSelectBgPath: "res://MyMod/art/select_bg.tscn"), - Scenes: new( - RestSiteAnimPath: "res://MyMod/scenes/rest_site/my_character_rest_site.tscn")); -} - -var character = new CharacterRegistrationEntry() - .AddStartingCard(4) - .AddStartingCard(4) - .AddStartingCard() - .AddStartingRelic(); -``` - -别的 mod 之后也可以继续给这个角色追加内容:可以用 `CharacterStarterCardRegistrationEntry(count)`,也可以直接调用 `ModContentRegistry.RegisterCharacterStarterCard(count)`。这些 starter 内容是在角色模型被读取时统一解析的,所以只要都发生在内容冻结前,注册先后顺序不会影响结果。 - -如果你更想继承 `silent`、`defect` 等角色的商人 / 休息点 / 小地图 / 默认音效风格,可以改写 `PlaceholderCharacterId`。若你想关闭这层兜底,可返回 `null`。 - -### 选人界面解锁说明(`{Prerequisite}`) - -本地化 **`unlockText`** 可使用 **`{Prerequisite}`** 占位符。原版在 **`CharacterModel.GetUnlockText()`** 里根据 **`UnlocksAfterRunAs`** 填充;在 **`ModCharacterTemplate`** 上通过 **`UnlocksAfterRunAsType`** 指定前置角色的 CLR 类型。 - -- 若 **`UnlocksAfterRunAs`** 为 **`null`**(模板默认),游戏会用通用锁定标题(**`LOCKED.title`**,界面上常显示为 **`???`**)。 -- 若已设置,则当前 **`UnlockState.Characters`** 里**已包含**该前置角色时,用其 **`Title`**;否则仍回退到 **`LOCKED.title`**。 - -请把 **`UnlocksAfterRunAsType`** 与 **`UnlockEpochAfterWinAs`** / **`UnlockEpochAfterRunAs`** 等规则里的 **`TCharacter`** 对齐,这样悬停说明与真实解锁条件一致。 - -**说明:** 仅设置 **`UnlocksAfterRunAsType` 不会实现解锁**,权威逻辑仍在 **`ModUnlockRegistry`** 与纪元进度中。 - ---- - -## 故事模板 - -继承 `ModStoryTemplate` 提供故事标识(`StoryKey` → slug)。纪元顺序在注册阶段用 `RegisterStoryEpoch` / `TimelineColumnPackEntry` / `.StoryEpoch<,>()` 绑定,见 `TimelineAndUnlocks.md`。 - -```csharp -public class MyStory : ModStoryTemplate -{ - protected override string StoryKey => "my-character"; -} -``` - -### Ancient 对话本地化 - -RitsuLib 会在游戏原版 `AncientDialogueSet.PopulateLocKeys` 之前,自动为已注册的 Mod 角色追加基于本地化定义的 Ancient 对话。 - -键格式与原版保持一致: - -| 键组件 | 说明 | -|---|---| -| `.talk..-.ancient` | Ancient 台词 | -| `.talk..-.char` | 角色台词 | -| 可选后缀 `.sfx` | 音效 | -| 可选后缀 `-visit` | 访问覆盖 | -| 可选后缀 `-attack` | Architect 专用攻击者覆盖 | -| 可选后缀 `r` | 重复对话 | - -如果需要直接操作工具方法,可使用 `STS2RitsuLib.Localization.AncientDialogueLocalization`。 - ---- - -## 纪元模板 - -RitsuLib 提供预置的纪元模板,用于常见解锁目标: - -| 模板 | 说明 | -|---|---| -| `CharacterUnlockEpochTemplate` | 解锁角色本身的纪元 | -| `CardUnlockEpochTemplate` | 解锁额外卡牌的纪元 | -| `RelicUnlockEpochTemplate` | 解锁额外遗物的纪元 | -| `PotionUnlockEpochTemplate` | 解锁额外药水的纪元 | - -```csharp -public class MyCharacterEpoch : CharacterUnlockEpochTemplate -{ -} - -public class MyEpoch2 : CardUnlockEpochTemplate -{ - protected override IEnumerable CardTypes => - [ - typeof(MyAdvancedCard), - ]; -} -``` - ---- - -## 完整注册示例 - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - // 卡牌(指定所属池) - .Card() - .Card() - .Card() - .Card() - - // 遗物 - .Relic() - - // 角色 - .Character() - - // 故事与纪元 - .Story() - .Epoch() - .Epoch() - - // 解锁规则 - .RequireEpoch() // 纪元 2 才显示该卡 - .UnlockEpochAfterRunAs() // 完成一局后解锁纪元 2 - - .Apply(); -``` - ---- - -## 模型 ID 与本地化 - -通过 RitsuLib 注册的角色模型遵循与其他内容相同的 `ModelId.Entry` 规则(参见 [内容注册规则](ContentAuthoringToolkit.md))。 - -示例(Mod id `MyMod`,类型 `MyCharacter`): -- `ModelId.Entry` → `MY_MOD_CHARACTER_MY_CHARACTER` -- 本地化 Key → `MY_MOD_CHARACTER_MY_CHARACTER.title` - -> 重命名 CLR 类型会改变其推导出的 Entry,影响存档兼容性。发布后请勿随意重命名。 - ---- - -## 依赖规则 - -- 卡牌/遗物/药水类型必须在运行时模型查找发生前完成注册 -- 角色引用的池类型必须已经注册 -- 所有模型(包括受解锁条件限制的内容)均必须完成注册,解锁规则**不**替代注册 - ---- - -## 相关文档 - -- [内容注册规则](ContentAuthoringToolkit.md) -- [快速入门](GettingStarted.md) -- [时间线与解锁](TimelineAndUnlocks.md) -- [资源配置与回退规则](AssetProfilesAndFallbacks.md) -- [Godot 场景编写说明](GodotSceneAuthoring.md) diff --git a/Docs/zh/ContentAuthoringToolkit.md b/Docs/zh/ContentAuthoringToolkit.md deleted file mode 100644 index d46309c..0000000 --- a/Docs/zh/ContentAuthoringToolkit.md +++ /dev/null @@ -1,153 +0,0 @@ -# 内容注册规则 - -本文是内容编写的总览文档,聚焦注册入口、模型身份、本地化耦合关系以及资源覆写基础规则。 - -更详细的注册机制见 [内容包与注册器](ContentPacksAndRegistries.md),更详细的资源语义见 [资源配置与回退规则](AssetProfilesAndFallbacks.md)。 - ---- - -## 注册接口 - -| 接口 | 说明 | -|---|---| -| `RitsuLibFramework.CreateContentPack(modId)` | 推荐入口:流式内容包构建器 | -| `RitsuLibFramework.GetContentRegistry(modId)` | 底层内容注册器 | -| `RitsuLibFramework.GetKeywordRegistry(modId)` | 关键词注册器 | -| `RitsuLibFramework.GetTimelineRegistry(modId)` | Timeline(故事/纪元)注册器 | -| `RitsuLibFramework.GetUnlockRegistry(modId)` | 解锁规则注册器 | - -`CreateContentPack` 是推荐用法,将以上注册器封装为流式 API,调用 `Apply()` 时按添加顺序依次执行。 - -本文只保留总览层内容。关于构建器完整表面、清单式注册、固定条目标识归属和冻结机制,请阅读 [内容包与注册器](ContentPacksAndRegistries.md)。 - ---- - -## 内容包构建器 - -所有方法都支持链式调用,下面给出一个代表性示例: - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - .Character() - .Card() - .Relic() - .CardKeywordOwnedByLocNamespace("my_keyword", iconPath: "res://MyMod/art/kw.png") - .Story() - .Epoch() - .RequireEpoch() - .Custom(ctx => { /* 任意注册逻辑 */ }) - .Apply(); -``` - -`Apply()` 返回 `ModContentPackContext`,可用于进一步访问各注册器。 - ---- - -## 模型 ID 规则 - -通过 RitsuLib 注册的模型,其 `ModelId.Entry` 使用以下固定格式: - -``` -__ -``` - -每个字段规范化为**全大写、以下划线分隔**的标识符。 - -### 示例(Mod id `MyMod`) - -| C# 类型 | 类别 | ModelId.Entry | -|---|---|---| -| `MyStrike` | card | `MY_MOD_CARD_MY_STRIKE` | -| `MyStarterRelic` | relic | `MY_MOD_RELIC_MY_STARTER_RELIC` | -| `MyCharacter` | character | `MY_MOD_CHARACTER_MY_CHARACTER` | - -> 同一 Mod、同一类别下两个 CLR 类型名相同的模型会产生 Entry 冲突,必须通过重命名解决。 - ---- - -## 本地化规则 - -游戏本地化 Key 直接基于固定 `ModelId.Entry` 编写: - -```json -{ - "MY_MOD_CARD_MY_STRIKE.title": "我的打击", - "MY_MOD_CARD_MY_STRIKE.description": "造成 {damage} 点伤害。", - "MY_MOD_RELIC_MY_STARTER_RELIC.title": "我的起始遗物" -} -``` - -`RitsuLibFramework.CreateModLocalization(...)` 是独立的本地化工具,与游戏的 `LocString` 模型 Key 管线相互独立。 - ---- - -## 资源覆写规则 - -RitsuLib 通过接口匹配,在渲染时将默认资源替换为 Mod 提供的资源。 - -### 卡牌资源覆写 - -继承 `ModCardTemplate` 后,通过 `AssetProfile`(推荐)或单独属性覆写: - -```csharp -public class MyCard : ModCardTemplate(1, CardType.Attack, CardRarity.Common, TargetType.SingleEnemy) -{ - // 统一通过 AssetProfile 配置(推荐) - public override CardAssetProfile AssetProfile => new() - { - PortraitPath = "res://MyMod/art/my_card.png", - FramePath = "res://MyMod/art/frame.png", - FrameMaterialPath = "res://MyMod/art/frame.material", - }; - - // 或单独覆写某一项 - public override string? CustomPortraitPath => "res://MyMod/art/my_card.png"; -} -``` - -卡牌支持的覆写大致包括 portrait、frame、portrait border、energy icon、overlay 与 banner 相关资源。 - -### 其他内容资源覆写 - -| 内容类型 | 支持字段 | -|---|---| -| Relic | icon、icon outline、big icon | -| Power | icon、big icon | -| Orb | 图标、视觉场景 | -| Potion | image、outline | - -覆写行为如下: -1. 模型必须实现对应的 override 接口(直接或通过 `Mod*Template`) -2. override 成员必须返回非空路径 -3. 如果资源路径不存在,RitsuLib 会输出一次警告,并回退到原始资源 - -这点对角色资源尤其重要,因为原版游戏对缺失角色资源几乎没有安全兜底。 - -完整资源配置结构、路径工厂辅助方法、占位角色规则与诊断策略见 [资源配置与回退规则](AssetProfilesAndFallbacks.md)。 - ---- - -## 注册时机 - -所有内容注册必须在框架冻结内容注册之前完成(游戏早期引导阶段)。冻结后继续注册属于无效操作并可能抛出异常。 - -冻结时触发的事件:`ContentRegistrationClosedEvent` - ---- - -## 兼容规则 - -固定 Entry 规则**只作用于**通过 RitsuLib 内容注册器显式注册的模型类型,处理点为 `ModelDb.GetEntry(Type)`。未经 RitsuLib 注册的模型不受影响。 - ---- - -## 相关文档 - -- [快速入门](GettingStarted.md) -- [内容包与注册器](ContentPacksAndRegistries.md) -- [角色与解锁模板](CharacterAndUnlockScaffolding.md) -- [自定义事件](CustomEvents.md) -- [卡牌动态变量](CardDynamicVarToolkit.md) -- [本地化与关键词](LocalizationAndKeywords.md) -- [框架设计](FrameworkDesign.md) -- [资源配置与回退规则](AssetProfilesAndFallbacks.md) diff --git a/Docs/zh/ContentPacksAndRegistries.md b/Docs/zh/ContentPacksAndRegistries.md deleted file mode 100644 index ce1d6a4..0000000 --- a/Docs/zh/ContentPacksAndRegistries.md +++ /dev/null @@ -1,384 +0,0 @@ -# 内容包与注册器 - -本文是 RitsuLib 注册体系的参考文档。 - -它主要解释: - -- `CreateContentPack(...)` 与底层各个注册器的关系 -- `Apply()` 到底做了什么 -- 什么时候该用链式构建器、清单条目、直接调用注册器,或可选的 CLR 特性 -- 固定模型身份与 ModelDb 集成是怎样建立在注册之上的 -- 生成式占位(卡牌 / 遗物 / 药水)的 API、顺序与风险说明 - ---- - -## 注册器总览 - -RitsuLib 按职责拆分了几类注册器: - -| 注册器 | 作用 | -|---|---| -| `ModContentRegistry` | 注册角色、Act、池内卡牌/遗物/药水、能力、球体、附魔(Enchantment)、苦难(Affliction)、成就、单例、好/坏每日修正、共享卡/遗物/药水池、事件、Ancient、怪物及生成式占位等模型 | -| `ModKeywordRegistry` | 注册可复用关键词定义 | -| `ModTimelineRegistry` | 注册 `Story` 与 `Epoch` | -| `ModUnlockRegistry` | 注册纪元门槛与进度解锁规则 | - -`CreateContentPack(modId)` 就是把这四类能力打包成一个更顺手的入口。 - ---- - -## `CreateContentPack(...)` - -推荐默认使用链式构建器: - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - .Character() - .Card() - .Relic() - .CardKeywordOwnedByLocNamespace("brew") - .Epoch() - .Story() - .RequireEpoch() - .Apply(); -``` - -但需要明确的是,它不会: - -- 自动反射扫描内容 -- 自动替你重排注册顺序 -- 取代底层注册器的存在 - -它只是把一系列注册步骤按加入顺序记录下来,并在 `Apply()` 时顺序执行。 - ---- - -## `ModContentPackContext` - -`Apply()` 返回 `ModContentPackContext`,里面包含: - -- `Content` -- `Keywords` -- `Timeline` -- `Unlocks` - -也就是说,构建器可以作为主要入口,同时你在需要时仍然可以拿到原始注册器继续操作。 - ---- - -## 步骤顺序 - -构建器中的步骤严格按添加顺序执行。 - -这点在以下场景会很重要: - -- 某个 `Custom(ctx => ...)` 依赖前面已经注册的内容 -- 你希望日志顺序能准确反映初始化流程 -- 你在同一个 chain 中混合内容注册与自定义逻辑 - -`CreateContentPack` 故意保持显式,它是“顺序执行的注册脚本”,而不是“自动推断依赖关系的求解器”。 - ---- - -## 构建器能做什么 - -构建器支持的步骤大致包括: - -- 内容模型注册 -- 关键词注册 -- 时间线注册 -- 解锁注册 -- 清单式注册 -- 任意自定义回调 - -一些不那么显眼,但很实用的入口包括: - -- `Entry(IContentRegistrationEntry)` -- `Entries(IEnumerable)` -- `Keyword(KeywordRegistrationEntry)` -- `Keywords(IEnumerable)` -- `Manifest(contentEntries, keywordEntries)` -- `Custom(Action)` -- 生成式占位:`PlaceholderCard(...)`、`PlaceholderRelic(...)`、`PlaceholderPotion(...)`(详见下文「生成式占位内容」) -- 扩展的单体/池类型:`.Enchantment()`、`.Affliction()`、`.Achievement()`、`.Singleton()`、`.GoodModifier()` / `.BadModifier()`、`.SharedRelicPool()`、`.SharedPotionPool()`(详见下文「内容模型注册速查表」) - -如果你希望“注册声明本身也是数据”,这些入口会很好用。 - ---- - -## 什么时候直接使用注册器 - -默认优先使用 `CreateContentPack(...)`。 - -但以下情况直接使用注册器更合适: - -- 注册逻辑拆分在多个模块里 -- 你希望在自己的前置库里再包装一层 API -- 你不想把所有注册都塞进一条长链 -- 你要程序化生成注册项 - -典型写法如下: - -```csharp -var content = RitsuLibFramework.GetContentRegistry("MyMod"); -content.RegisterCharacter(); - -var timeline = RitsuLibFramework.GetTimelineRegistry("MyMod"); -timeline.RegisterEpoch(); -``` - -这些注册器是一等公民 API,不是构建器背后的私有实现细节。 - ---- - -## 内容注册器的职责 - -`ModContentRegistry` 主要负责: - -- 记录某个模型类型归属于哪个 Mod -- 校验重复注册与冲突 -- 为 ModelDb 补丁与其它集成点提供数据:例如向 `AllCharacters`、Act、能力、球体、共享事件、Ancient、**共享卡池 / 遗物池 / 药水池类型**、`DebugEnchantments`、`DebugAfflictions`、`Achievements`、`GoodModifiers`、`BadModifiers` 等访问器在需要时追加已注册模型;卡牌/遗物/药水进入**具体池**则通过 `ModHelper.AddModelToPool` 在池展开 `AllCards` / `AllRelics` / `AllPotions` 时合并(与上述全局追加不是同一条实现路径) -- 为已注册类型生成固定公开 `ModelId.Entry` - -这套归属跟踪很关键,因为它让 RitsuLib 可以安全回答这些问题: - -- 某个类型是谁注册的? -- 它的固定公开条目标识应该是什么? -- 某些兼容逻辑是否应该把它当作 Mod 内容处理? - ---- - -## 固定公开身份 - -对于通过 RitsuLib 注册的模型,公开 `ModelId.Entry` 会被强制成稳定格式: - -```text -__ -``` - -这不是靠改你源码里的类型名实现的,而是通过 ModelDb 身份补丁在公开入口上统一的。 - -这么做的意义在于: - -- 本地化 Key 可预测 -- 默认资源路径约定更稳定 -- 补丁、存档、兼容逻辑里都更容易识别内容归属 - -这条规则只作用于显式通过 RitsuLib 注册的类型。 - ---- - -## ModelDb 集成 - -仅仅完成注册还不够,游戏本身还必须“看得到”这些内容。 - -RitsuLib 通过对 ModelDb 及相关访问点打补丁来完成这件事,包括: - -- 追加已注册的角色、Act、能力、球体、事件、Ancient、共享卡池、**共享遗物池**(`AllRelicPools`)、**共享药水池**(`AllPotionPools`)、**调试用附魔**(`DebugEnchantments`)、**调试用苦难**(`DebugAfflictions`)、**成就**(`Achievements`)、**每日修正**(`GoodModifiers` / `BadModifiers`)等 -- 将已注册卡牌/遗物/药水等与**目标池**绑定(`ModHelper.AddModelToPool`,在对应池的 `All*` 枚举中与原版生成结果拼接) -- 对已注册模型类型强制固定公开条目标识 -- 在 `ModelDb` 初始化完成前,把注册器跟踪到的、位于**动态程序集**中的类型(例如 Reflection.Emit 占位)注入 `_contentById` -- 在缓存锁定前引导动态 Act 内容补丁 - -`MutuallyExclusiveModifiers` **不会**自动扩展;通过好/坏列表注册的 Mod 修正只会出现在上述两个列表中。 - -这也是为什么注册必须发生在框架冻结之前。 - ---- - -## Freeze 行为 - -几个关键注册器都会在早期初始化后冻结: - -- 内容注册冻结 -- 时间线注册冻结 -- 解锁注册冻结 - -冻结之后再注册会直接抛异常。 - -这是有意为之,因为框架追求的是: - -- 身份稳定 -- 模型列表稳定 -- 解锁/过滤行为稳定 - -如果某个 Mod 在太晚的时候才注册内容,最安全的结果就是尽早失败,而不是让游戏带着半成品缓存继续跑下去。 - ---- - -## Manifest 与 Entry 对象 - -如果你希望把注册描述成数据,可以使用注册条目对象: - -```csharp -var contentEntries = new IContentRegistrationEntry[] -{ - new CharacterRegistrationEntry(), - new CardRegistrationEntry(), -}; - -var keywordEntries = new[] -{ - KeywordRegistrationEntry.OwnedCardByLocNamespace("MyMod", "brew"), -}; - -RitsuLibFramework.CreateContentPack("MyMod") - .Manifest(contentEntries, keywordEntries) - .Apply(); -``` - -这对“声明式注册列表”或“跨模块复用注册清单”的场景会很方便。 - -也可以混用多种 `IContentRegistrationEntry`,例如: - -```csharp -var contentEntries = new IContentRegistrationEntry[] -{ - new CharacterRegistrationEntry(), - new CardRegistrationEntry(), - new EnchantmentRegistrationEntry(), - new PowerRegistrationEntry(), - new SharedRelicPoolRegistrationEntry(), -}; -``` - ---- - -## CLR 特性注册(可选) - -`STS2RitsuLib.Interop.AutoRegistration` 下的特性(例如 `[RegisterSharedCardPool]`、`[RegisterCard(typeof(MyPool))]`)最终会调用与链式构建器、清单和**直接注册器**相同的底层 API。 - -它们在 RitsuLib 的早期 **Mod 类型发现** 阶段执行(`ModTypeDiscoveryPatch`):内置的 `AttributeAutoRegistrationTypeDiscoveryContributor` 会扫描你已用 **`ModTypeDiscoveryHub.RegisterModAssembly(modId, Assembly.GetExecutingAssembly())`** 登记的程序集中的**具体** CLR 类型(在 `PatchAll` **之前**于 Mod 初始化器里调用)。类型必须能解析到某个 mod 身份(通常由 manifest 映射到程序集);否则可在类型上使用 **`[RitsuLibOwnedBy("modId")]`**。 - -这**不代替** `CreateContentPack(...)`,只是另一种编写方式。只要注册顺序与冻结时机仍合法,可以与链式/清单混用。 - -### `AutoRegistrationAttribute.Inherit` - -特性默认只作用于其标注的类型。**`Inherit`** 默认为 **`false`**。在**基类**上将某特性设为 **`Inherit = true`** 时,**具体子类**会按「若子类自身也写了同一条特性」的方式处理(即仍以**子类的** `Type` 调用同一套注册 API)。若子类已有**直接**声明、且会产生**相同注册签名**的特性,则不再重复应用继承来的同签名项。扫描会跳过抽象基类,仅具体类型会进入注册流程。 - ---- - -## 内容模型注册速查表 - -下表中每一行是一种**内容类别**。可主要用下面**三种**等价方式登记,另可加前一节所述的**可选特性路径**(另有注明的除外): - -1. **链式**:`CreateContentPack(...)` 上的 `ModContentPackBuilder` 方法 -2. **注册器**:`RitsuLibFramework.GetContentRegistry(modId)` 或 `Custom(ctx => ctx.Content...)` 上的 `ModContentRegistry` 方法 -3. **Manifest 条目**:`STS2RitsuLib.Scaffolding.Content` 中实现 `IContentRegistrationEntry` 的类型,经 `.Entry(...)`、`.Entries(...)` 或 `.Manifest(...)` 应用 - -| 内容 | 链式 | 注册器 | Manifest 条目 | -|---|---|---|---| -| 角色 | `.Character()` | `RegisterCharacter()` | `CharacterRegistrationEntry` | -| Act | `.Act()` | `RegisterAct()` | `ActRegistrationEntry` | -| 池内卡牌 | `.Card(...)` | `RegisterCard(...)` | `CardRegistrationEntry` | -| 池内遗物 | `.Relic(...)` | `RegisterRelic(...)` | `RelicRegistrationEntry` | -| 池内药水 | `.Potion(...)` | `RegisterPotion(...)` | `PotionRegistrationEntry` | -| 能力 | `.Power()` | `RegisterPower()` | `PowerRegistrationEntry` | -| 球体 | `.Orb()` | `RegisterOrb()` | `OrbRegistrationEntry` | -| 附魔 | `.Enchantment()` | `RegisterEnchantment()` | `EnchantmentRegistrationEntry` | -| 苦难 | `.Affliction()` | `RegisterAffliction()` | `AfflictionRegistrationEntry` | -| 成就 | `.Achievement()` | `RegisterAchievement()` | `AchievementRegistrationEntry` | -| 单例 | `.Singleton()` | `RegisterSingleton()` | `SingletonRegistrationEntry` | -| 每日修正(好) | `.GoodModifier()` | `RegisterGoodModifier()` | `GoodModifierRegistrationEntry` | -| 每日修正(坏) | `.BadModifier()` | `RegisterBadModifier()` | `BadModifierRegistrationEntry` | -| 共享卡池 | `.SharedCardPool()` | `RegisterSharedCardPool()` | `SharedCardPoolRegistrationEntry` | -| 共享遗物池 | `.SharedRelicPool()` | `RegisterSharedRelicPool()` | `SharedRelicPoolRegistrationEntry` | -| 共享药水池 | `.SharedPotionPool()` | `RegisterSharedPotionPool()` | `SharedPotionPoolRegistrationEntry` | -| 共享事件 | `.SharedEvent()` | `RegisterSharedEvent()` | `SharedEventRegistrationEntry` | -| Act 遭遇 | `.ActEncounter()` | `RegisterActEncounter()` | `ActEncounterRegistrationEntry` | -| Act 事件 | `.ActEvent()` | `RegisterActEvent()` | `ActEventRegistrationEntry` | -| 共享 Ancient | `.SharedAncient()` | `RegisterSharedAncient()` | `SharedAncientRegistrationEntry` | -| Act Ancient | `.ActAncient()` | `RegisterActAncient()` | `ActAncientRegistrationEntry` | -| 怪物 | *(无链式封装)* | `RegisterMonster()` | `MonsterRegistrationEntry` | -| 占位卡牌/遗物/药水 | `.PlaceholderCard<...>(...)` 等 | `RegisterPlaceholderCard<...>(...)` 等 | `PlaceholderCardRegistrationEntry<...>` 等 | -| Archaic Tooth 映射 | `.ArchaicToothTranscendence<...>()` 或 `.ArchaicToothTranscendence(id, type)` | `RitsuLibFramework.RegisterArchaicToothTranscendenceMapping(...)` | `ArchaicToothTranscendenceRegistrationEntry<...>` / `ArchaicToothTranscendenceByIdRegistrationEntry` | -| Touch of Orobas 映射 | `.TouchOfOrobasRefinement<...>()` 或 `.TouchOfOrobasRefinement(id, type)` | `RitsuLibFramework.RegisterTouchOfOrobasRefinementMapping(...)` | `TouchOfOrobasRefinementRegistrationEntry<...>` / `TouchOfOrobasRefinementByIdRegistrationEntry` | - -**附魔:**可选用脚手架里的 `ModEnchantmentTemplate`、`IModEnchantmentAssetOverrides` 与 `EnchantmentIntendedIconPathPatch` 自定义图标路径;上表中的注册仍负责归属、固定 `ModelId.Entry` 以及与别类模型一致的动态程序集注入。 - -**单例:**本体没有可补丁的「全局单例列表」;注册仍用于归属与动态类型注入,以便 `ModelDb.Singleton()` 能正确解析。 - ---- - -## 生成式占位内容 - -用于在**尚未为每张牌 / 每个遗物 / 每个药水编写独立 CLR 类型**时,仍能注册进池子并获得**稳定、可预测的公开 `ModelId.Entry`**(与 `ModelPublicEntryOptions.FromStem` / `FromFullPublicEntry` 一致),以便奖励表、解锁、存档引用等流程先跑通。占位模型由 RitsuLib 在运行时通过 **Reflection.Emit** 生成密封子类,逻辑上为**无效果**(卡牌 `OnPlay`、药水 `OnUse` 等为空操作)。 - -### API 概要 - -| 场景 | 推荐入口 | -|---|---| -| 链式内容包 | `PlaceholderCard(stableEntryStem, PlaceholderCardDescriptor)`、`PlaceholderRelic(...)`、`PlaceholderPotion(...)` | -| 直接注册器 | `ModContentRegistry.RegisterPlaceholderCard(...)` 等;重载可传入 `ModelPublicEntryOptions`(例如 `FromFullPublicEntry`) | -| 形状参数 | `PlaceholderCardDescriptor`、`PlaceholderRelicDescriptor`、`PlaceholderPotionDescriptor`(结构体,带默认值,按需覆盖费用、类型、稀有度、目标等) | -| 仍自带 CLR 类型时 | 保留 `PlaceholderCard(stem)` 双泛型重载:仅为已有类型固定 entry,不生成新类型 | - -框架内部的 `ModPlaceholderCardTemplate` / `ModPlaceholderRelicTemplate` / `ModPlaceholderPotionTemplate` 供生成类型继承;**一般不必在 Mod 里再继承它们**,除非你有特殊手写需求。 - -### 示例 - -```csharp -using MegaCrit.Sts2.Core.Entities.Cards; -using STS2RitsuLib.Content; - -RitsuLibFramework.CreateContentPack("MyMod") - .Manifest(contentEntries, keywordEntries) - .Custom(ctx => - { - ctx.Content.RegisterPlaceholderCard("wip_reward_attack", - new PlaceholderCardDescriptor( - BaseCost: 1, - Type: CardType.Attack, - Rarity: CardRarity.Common, - Target: TargetType.AnyEnemy)); - }) - .Apply(); -``` - -遗物描述体中的 `MerchantCostOverride`:为 **`< 0`(默认 `-1`)** 时表示沿用稀有度默认商人价;**`≥ 0`** 时覆盖 `MerchantCost`。 - -### 与初始化顺序 - -若同时使用 `Manifest(...)` 与占位注册,请把占位步骤放在**已具备池类型等前置注册之后**(常见写法是在链上 `.Manifest(...)` 之后接 `.Custom(ctx => ...)` 调用 `RegisterPlaceholder*`),避免依赖尚未注册的池或角色。 - ---- - -### 警告(请务必阅读) - -> **存档与 Entry 稳定性** -> 占位一旦进入存档或解锁数据,其 `ModelId.Entry`(由 stem 或 `FromFullPublicEntry` 决定)即成为长期契约。**改名 / 改 stem / 改 `FromFullPublicEntry` 字符串**可能导致旧档、旧解锁引用失效。正式内容落地时,要么长期保留同一 entry,要么做迁移/兼容策略。 - -> **无玩法效果** -> 占位不会替你实现伤害、抽牌、遗物触发等。仅保证模型存在、池子能展开、部分 UI/流程不因缺模型而崩溃;**平衡与体验仍可能异常**,需尽快替换为实作类型。 - -> **本地化与资源** -> 占位仍使用基于 entry 的默认本地化键与资源路径约定;若未提供对应翻译或贴图,界面可能出现键名或缺图,这属于预期现象,不等于框架未注册成功。 - -> **联机与 `ModelIdSerializationCache.Hash`** -> 生成类型**不会**出现在游戏原生的 `AllAbstractModelSubtypes` 扫描结果中。RitsuLib 会在 `ModelDb.Init` 前注入动态程序集中的已注册模型,并在 `ModelIdSerializationCache.Init` 之后**把 `ModelDb` 中实际存在的模型一并并入联机序列化表并重算 Hash**。 -> **后果**:加载的 Mod 组合不同 → Hash 不同 → 与未使用占位/未使用相同 Mod 列表的客户端**可能无法联机或回放一致**。这是使用动态占位时的固有风险,而非单机独有。 - -> **依赖 RitsuLib 版本** -> 占位、`InjectDynamicRegisteredModels`、序列化缓存补丁等行为随 RitsuLib 演进;请为 Mod 声明合适的 `STS2-RitsuLib` 依赖版本,并在升级前置库后回归测试。 - ---- - -## 推荐注册模式 - -对大多数 Mod,建议这样组织: - -1. 在初始化入口中创建一个内容包 -2. 在其中注册所有内容、关键词、时间线节点与解锁规则 -3. `Custom(...)` 保持小而显式 -4. 不要把注册拖到运行期 hook 再做 -5. 使用 `TypeListCardPoolModel` 时,用 `.Card<池, 牌>()` 或 `CardRegistrationEntry` 登记池内牌;**不要**覆写已过时的 `CardTypes`(基类已默认空序列,详见 [快速入门](GettingStarted.md)) - -如果 Mod 很大,可以保留一个顶层构建器,再由子模块提供注册条目对象或辅助方法。 - ---- - -## 相关文档 - -- [内容注册规则](ContentAuthoringToolkit.md) -- [时间线与解锁](TimelineAndUnlocks.md) -- [框架设计](FrameworkDesign.md) diff --git a/Docs/zh/CreatureVisualsAndAnimation.md b/Docs/zh/CreatureVisualsAndAnimation.md deleted file mode 100644 index a20a687..0000000 --- a/Docs/zh/CreatureVisualsAndAnimation.md +++ /dev/null @@ -1,318 +0,0 @@ -# 生物体视觉与动画 - -本文介绍一组 mod 可以接入的运行时 Godot 工厂接口(替换原版 -`CreateVisuals` / `GenerateAnimator`),以及后端无关的动画状态机 -`ModAnimStateMachine`。后者让非 Spine 的战斗视觉(`AnimatedSprite2D`、Godot -`AnimationPlayer`、cue 帧序列)通过和 Spine 生物**相同的**触发协议驱动动画。 - -内容包注册见 [内容包与注册器](ContentPacksAndRegistries.md)。 -角色装配见 [角色与解锁模板](CharacterAndUnlockScaffolding.md)。 -Harmony 补丁机制见 [补丁系统](PatchingGuide.md)。 - ---- - -## 概览 - -原版将 `MonsterModel` / `CharacterModel` 绑定到战斗视觉的三个入口: - -- `Model.CreateVisuals()` — 返回一个 `NCreatureVisuals`(战斗生物节点下的视觉 - 根场景)。 -- `Model.GenerateAnimator(MegaSprite controller)` — 返回一个 `CreatureAnimator`, - 内部封装 Spine 骨骼及 idle / hit / attack / cast / die / relaxed 状态图。 -- `NCreature.SetAnimationTrigger(trigger)` — 在运行时把触发器(`Idle`、 - `Attack`、`Cast`、`Hit`、`Dead`、`Revive` 等)派发给这个 animator。 - -Mod 常见的需求至少包括以下一种: - -- 用代码供给 `NCreatureVisuals`(而不是只用路径); -- 用 mod 自己写的状态图替换 Spine 状态图; -- 给 **没有** Spine 骨骼的生物做动画(精灵表、帧序列、Godot `AnimationPlayer`)。 - -RitsuLib 为这三种需求暴露了三个**彼此正交**的工厂接口,以及一个针对非 Spine -场景的状态机抽象。四个接口都对生物类型无感(玩家角色与怪物通用),也不要求 -继承任何模板。 - -| 接口 | 用途 | 对应原版入口 | -|---|---|---| -| `IModCreatureVisualsFactory` | 从代码构造 `NCreatureVisuals` | `CharacterModel.CreateVisuals`、`MonsterModel.CreateVisuals` | -| `IModCreatureAnimatorFactory` | 从代码构造 Spine `CreatureAnimator` | `CharacterModel.GenerateAnimator`、`MonsterModel.GenerateAnimator` | -| `IModNonSpineAnimationStateMachineFactory` | 为非 Spine 视觉构造 `ModAnimStateMachine` | `NCreature.SetAnimationTrigger`(路由补丁) | -| `IModCharacterMerchantAnimationStateMachineFactory` | 为商人 / 休息站中的角色视觉构造 `ModAnimStateMachine` | 商人场景初始化流程 | - -商人工厂专属玩家角色,因为怪物从不会出现在商人 / 休息站场景中;其余三个接口对 -任意 `MegaCrit.Sts2.Core.Models.AbstractModel` 都适用。 - ---- - -## 生物视觉工厂 - -`IModCreatureVisualsFactory` 在返回非 null 的 `NCreatureVisuals` 时,会替换 -`(Character|Monster)Model.CreateVisuals` 的原有行为;返回 `null` 则退回到 -`CustomVisualsPath` 等原版解析链路。 - -```csharp -public class MyCharacter : ModCharacterTemplate<...> -{ - // 模板已经实现了 IModCreatureVisualsFactory,并把调用转发到这个 - // protected virtual;重写它即可: - protected override NCreatureVisuals? TryCreateCreatureVisuals() - { - var scene = GD.Load( - "res://MyMod/scenes/my_character/my_character_visuals.tscn"); - return scene.Instantiate(); - } -} -``` - -如果不使用 `ModCharacterTemplate` / `ModMonsterTemplate`,直接在自己的 -`CharacterModel` / `MonsterModel` 上实现接口即可: - -```csharp -public class MyRawCharacter : CharacterModel, IModCreatureVisualsFactory -{ - public NCreatureVisuals? TryCreateCreatureVisuals() => ...; -} -``` - -路由补丁(`CharacterCreatureVisualsRuntimeFactoryPatch`、 -`MonsterCreatureVisualsRuntimeFactoryPatch`)以 Harmony `Priority.First` 运行, -在原版基于路径的加载逻辑之前生效。 - ---- - -## Spine Animator 工厂 - -`IModCreatureAnimatorFactory` 用来替换 `GenerateAnimator`,适用于 Spine 视觉。 -推荐使用 `ModAnimStateMachines.Standard` 以复用原版状态图的形状: - -```csharp -public class MySpineCharacter : ModCharacterTemplate<...> -{ - protected override CreatureAnimator? SetupCustomCreatureAnimator(MegaSprite controller) => - ModAnimStateMachines.Standard( - controller, - idleName: "idle_loop", - deadName: "die", - hitName: "hit", - attackName: "attack", - castName: "cast", - relaxedName: "relaxed"); -} -``` - -`ModAnimStateMachines.Standard` 返回一个已经布好 `Idle` / `Dead` / `Hit` / -`Attack` / `Cast` / `Relaxed` any-state 触发器的 `CreatureAnimator`。终态 -(`Dead`)不设置 `NextState`,所以播放完不会自动回到 idle。 - -路由补丁(`CharacterCreatureAnimatorRuntimeFactoryPatch`、 -`MonsterCreatureAnimatorRuntimeFactoryPatch`)接受非 null 的工厂返回值;返回 -`null` 则退回到原版 `GenerateAnimator`。 - ---- - -## 非 Spine 状态机 - -如果生物的战斗视觉**不是** Spine(没有 `MegaSprite` 控制器),实现 -`IModNonSpineAnimationStateMachineFactory` 并返回绑定到视觉根节点的 -`ModAnimStateMachine`。`ModCreatureNonSpineAnimationPlaybackPatch` 把 -`NCreature.SetAnimationTrigger(trigger)` 路由到 `ModAnimStateMachine.SetTrigger`, -因此非 Spine 生物会接收到与 Spine 生物**完全相同**的触发流。 - -### 接入方式 - -```csharp -public class MyWolf : ModMonsterTemplate -{ - // 模板已经实现了 IModNonSpineAnimationStateMachineFactory,并把调用转发 - // 到这个 protected virtual;重写它即可: - protected override ModAnimStateMachine? SetupCustomNonSpineAnimationStateMachine( - Node visualsRoot, MonsterModel monster) - { - if (visualsRoot is not MyWolfVisuals wolfVisuals) - return null; - - var backend = new AnimatedSprite2DBackend(wolfVisuals.GetAnimatedSprite()); - - return ModAnimStateMachineBuilder.Create() - .AddState("idle", loop: true).AsInitial().Done() - .AddState("attack").WithNext("idle").Done() - .AddState("hurt").WithNext("idle").Done() - .AddState("die").Done() // 终态:不设置 NextState - .AddAnyState("Idle", "idle") - .AddAnyState("Attack", "attack") - .AddAnyState("Hit", "hurt") - .AddAnyState("Dead", "die") - .Build(backend); - } -} -``` - -不使用模板时同理: - -```csharp -public class MyRawMonster : MonsterModel, IModNonSpineAnimationStateMachineFactory -{ - public ModAnimStateMachine? TryCreateNonSpineAnimationStateMachine(Node visualsRoot) - => /* 同上的 builder 代码 */; -} -``` - -### 路由行为 - -`ModCreatureNonSpineAnimationPlaybackPatch` 是 `NCreature.SetAnimationTrigger` -上的 Prefix 补丁,流程如下: - -1. 如果生物已有 Spine animator,直接跳过(原版链路继续执行)。 -2. 定位生物对应的模型(`Entity.Player?.Character` 或 `Entity.Monster`)。 -3. 如果任一侧实现了 `IModNonSpineAnimationStateMachineFactory` 且返回非 null 的 - 状态机,调用 `ModAnimStateMachine.SetTrigger` 派发触发器,然后返回。 -4. 否则退回到单次 cue 播放 - (`ModCreatureVisualPlayback.TryPlayFromCreatureAnimatorTrigger`)。 - -状态机按视觉根节点缓存在 `ConditionalWeakTable` 中, -因此工厂在一次战斗生命周期内最多执行一次,节点释放时会自动回收。 - -### 快捷方式:`ModAnimStateMachines.StandardCue` - -如果视觉遵循 vanilla 的 idle / dead / hit / attack / cast / relaxed 结构, -直接使用 `ModAnimStateMachines.StandardCue` 即可由库内构建状态图。它会通过 -`CompositeBackendFactory` 为每个状态挑选最合适的后端(优先 cue 帧序列,其次 -Godot `AnimationPlayer` 或 `AnimatedSprite2D`),返回一个可直接使用的 -`ModAnimStateMachine`。 - ---- - -## 动画后端 - -`IAnimationBackend` 是 `ModAnimStateMachine` 消费的统一驱动层。每个后端包装 -Godot 的一个动画子系统,并在对应时机发出 `Started` / `Completed` / -`Interrupted` 事件。 - -| 后端 | 驱动对象 | 适用场景 | -|---|---|---| -| `AnimatedSprite2DBackend` | `AnimatedSprite2D` | 基于帧的 sprite 动画 | -| `GodotAnimationPlayerBackend` | `AnimationPlayer` | Godot `.tres` 动画库 | -| `CueAnimationBackend` | `VisualCueSet`(cue 帧序列 / cue 贴图) | 单帧贴图或帧序列播放 | -| `SpineAnimationBackend` | `MegaSprite` | Spine 骨骼动画 | -| `CompositeAnimationBackend` | 任意组合 | 多后端派发(同一状态机内部分状态走 sprite,另一部分走 animation player 等) | - -### 事件契约 - -| 事件 | 触发时机 | -|---|---| -| `Started(id)` | `id` 对应的播放已开始 | -| `Completed(id)` | 单次播放结束,或一个循环周期结束 | -| `Interrupted(id)` | 播放被新动画抢占,尚未自然结束 | - -`ModAnimState.NextState` 在 `Completed` 时推进,因此对非循环状态(`attack -> -idle` 等),后端**必须**准确发出 `Completed`。 - -### 队列语义 - -`Queue(id, loop)` 的语义是「当前动画播完后再播这一个」。各后端实现略有差异: - -| 后端 | `Queue` 行为 | -|---|---| -| `SpineAnimationBackend` | Spine 原生队列(在 track 上 `AddAnimation`) | -| `AnimatedSprite2DBackend` | 记录待播 id,在下一次 `animation_finished` 信号时播放 | -| `GodotAnimationPlayerBackend` | 使用 `AnimationPlayer.Queue` | -| `CueAnimationBackend` | 记录待播 id,在当前序列结束时播放 | - -任何后端上调用 `Play` 都会清空已排队的动画。 - -### `Stop()` 与跨后端切换 - -`IAnimationBackend.Stop()`(默认接口方法)会**静默**停止后端——既不发 -`Completed` 也不发 `Interrupted`,并清掉排队动画。它的主要使用方是 -`CompositeAnimationBackend`,在不同子后端之间切换时: - -1. 新状态使用的后端与当前活动的后端不同。 -2. 为即将离开的动画发出 `Interrupted`。 -3. 调用离开后端的 `Stop()` 清理其内部状态。 -4. 调用新进入后端的 `Play`。 - -如果不调用 `Stop()`,离开的后端可能继续发出已失效状态 id 的 `Completed` / -`Interrupted` 事件,干扰上层状态机。 - ---- - -## 生命周期触发补丁 - -原版 `NCreature.StartDeathAnim` 和 `NCreature.StartReviveAnim` 只在 -`_spineAnimator != null` 时派发 `Dead` / `Revive` 触发器。非 Spine 生物因此 -收不到这两个触发器,自定义状态机在「弃置当前游戏」或玩家死亡时永远看不到 -死亡动画。 - -RitsuLib 通过两个 Postfix 补丁修正这一缺陷: - -- `NCreatureNonSpineDeathAnimationTriggerPatch` — 在 `StartDeathAnim` 之后派发 - `Dead`。 -- `NCreatureNonSpineReviveAnimationTriggerPatch` — 在 `StartReviveAnim` 之后 - 派发 `Revive`。 - -### 作用域收敛 - -这两个补丁是**opt-in**:只有当生物没有 Spine animator 且模型**显式**接入了 -RitsuLib 视觉链路时才会触发。具体而言,`NonSpineAnimationTriggerScope.AppliesTo(NCreature)` -在以下任一条件成立时返回 `true`: - -| 模型槽位 | 接口 | 备注 | -|---|---|---| -| `Entity.Player?.Character` | `IModNonSpineAnimationStateMachineFactory` | 状态机路径 | -| `Entity.Monster` | `IModNonSpineAnimationStateMachineFactory` | 状态机路径 | -| `Entity.Player?.Character` | `IModCharacterAssetOverrides` | cue 播放回退(仅玩家) | - -原版生物、以及未接入 RitsuLib 视觉的其他 mod 都不会被影响。`Dead` 与 `Revive` -两个补丁使用完全相同的 gate。 - ---- - -## 迁移与废弃 - -两个工厂接口最初按生物种类分别命名,现在已统一,旧名称被标记为 `[Obsolete]`: - -| 新名称(推荐) | 已废弃的别名 | -|---|---| -| `IModCreatureVisualsFactory` | `IModMonsterCreatureVisualsFactory`、`IModCharacterCreatureVisualsFactory` | -| `IModCreatureAnimatorFactory` | `IModCharacterCreatureAnimatorFactory` | - -### 兼容性保证 - -- 路由补丁在每次调用时**同时**检查新接口和废弃接口,所以只实现旧接口的 mod - 无需任何代码改动即可继续工作。 -- `ModCharacterTemplate` / `ModMonsterTemplate` **同时**实现新接口和废弃别名, - 并把调用转发到同一批 protected virtual 钩子;外部对模板子类做 - `is IModCharacterCreatureVisualsFactory` 之类的类型检查仍然成立。 -- 实现废弃接口会触发编译警告 **CS0618** 引导迁移。运行时行为不变、没有运行时 - 警告。 - -### 迁移步骤 - -1. 在 `: Interfaces` 列表和显式接口实现中把旧名替换为新名: - - `IModMonsterCreatureVisualsFactory` → `IModCreatureVisualsFactory` - - `IModCharacterCreatureVisualsFactory` → `IModCreatureVisualsFactory` - - `IModCharacterCreatureAnimatorFactory` → `IModCreatureAnimatorFactory` -2. 方法签名(`TryCreateCreatureVisuals()`、 - `TryCreateCreatureAnimator(MegaSprite)`)保持不变,变化只在声明接口的名字上。 -3. 重新编译,CS0618 警告消失。 - -如果只是继承模板并重写 protected virtual 钩子 -(`TryCreateCreatureVisuals`、`SetupCustomCreatureAnimator`),无需任何迁移; -这些钩子未变。 - ---- - -## 速查表 - -```text -目标 实现的接口 ---------------------------------------------------------------------------- -替换 CreateVisuals(玩家或怪物) IModCreatureVisualsFactory -替换 Spine GenerateAnimator IModCreatureAnimatorFactory -驱动非 Spine 状态机 IModNonSpineAnimationStateMachineFactory -驱动商人 / 休息站状态机 IModCharacterMerchantAnimationStateMachineFactory -``` - -无论你是继承 `ModCharacterTemplate` / `ModMonsterTemplate`,还是直接在 -`CharacterModel` / `MonsterModel` 上实现这些接口,路由补丁都会生效:它们以 -Harmony `Priority.First` 运行,且在工厂返回 `null` 时退回到原版行为。 diff --git a/Docs/zh/CustomEvents.md b/Docs/zh/CustomEvents.md deleted file mode 100644 index 999b72b..0000000 --- a/Docs/zh/CustomEvents.md +++ /dev/null @@ -1,264 +0,0 @@ -# 自定义事件 - -本文说明如何通过 RitsuLib 将自定义事件接入游戏的事件管线。 - -它覆盖三类注册: - -- 共享事件:`SharedEvent()` -- Act 专属事件:`ActEvent()` -- Ancient:`SharedAncient()` / `ActAncient()` - ---- - -## 游戏原版事件管线 - -> 以下是游戏引擎自身的事件运行时流程,帮助理解 RitsuLib 的注册内容最终在哪里生效。 - -游戏中事件的生成与执行涉及以下环节: - -| 阶段 | 游戏类型 | 职责 | -|---|---|---| -| 候选生成 | `ActModel.GenerateRooms(...)` | 从 Act 本地事件池和 `ModelDb.AllSharedEvents` 共享池构建候选列表 | -| 过滤 | `RoomSet.EnsureNextEventIsValid(...)` | 按 `IsAllowed(runState)` 与已访问记录过滤 | -| 进入 | `EventRoom.Enter(...)` | 预加载资源、创建可变实例、搭建事件界面 | -| 资源 | `EventModel.GetAssetPaths(...)` | 提供进入事件前需要准备的资源路径 | - ---- - -## RitsuLib 的注册机制 - -RitsuLib 不替换上述流程,而是在注册阶段把 Mod 事件补充进原版已有的事件入口: - -- 共享事件追加到 `ModelDb.AllSharedEvents` -- Act 事件追加到对应 Act 的事件列表 -- Ancient 追加到对应的共享或 Act 本地 Ancient 列表 - -对 Mod 作者来说,实际工作可以概括为两步: - -1. 定义一个合法的 `EventModel` 或 `AncientEventModel` 子类 -2. 在内容注册冻结之前将其注册 - ---- - -## 最小普通事件 - -推荐继承 `ModEventTemplate`,而不是直接继承原版 `EventModel`(原因见下文)。 - -```csharp -using MegaCrit.Sts2.Core.Events; -using STS2RitsuLib.Scaffolding.Content; - -public sealed class MyFirstEvent : ModEventTemplate -{ - protected override IReadOnlyList 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; - } -} -``` - -最小可用事件至少应满足: - -- 实现 `GenerateInitialOptions()` -- 在选项回调里推进或结束事件 -- 本地化键与最终 `ModelId.Entry` 保持一致 - ---- - -## 注册方式 - -### 共享事件 - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - .SharedEvent() - .Apply(); -``` - -### Act 专属事件 - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - .ActEvent() - .Apply(); -``` - -### Ancient - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - .SharedAncient() - .Apply(); -``` - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - .ActAncient() - .Apply(); -``` - ---- - -## 本地化键 - -通过 RitsuLib 注册后,事件的 `ModelId.Entry` 采用固定格式: - -```text -_EVENT_ -``` - -例如 `MyMod` 与 `MyFirstEvent`: - -```text -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": "你转身离开。" -} -``` - -关键要求是一致性:事件标题、页面文本和选项键都应基于同一个最终的 `Id.Entry` 生成。 - ---- - -## 为什么要用 `ModEventTemplate` - -> 以下解释涉及游戏原版 `EventModel` 的一个行为特征。 - -原版 `EventModel.InitialOptionKey(...)` 及内部 option-key 辅助方法使用 `GetType().Name`(经 `Slugify` 处理)拼接键前缀,而事件标题、页面描述等使用 `Id.Entry`。 - -对原版事件,这两者通常一致。但对通过 RitsuLib 注册的事件,`GetType().Name` 和 `Id.Entry` 不同,会导致部分文本查找落在不同的键前缀上。 - -`ModEventTemplate` 和 `ModAncientEventTemplate` 通过 `protected new` 隐藏了基类的 `InitialOptionKey`,统一基于最终注册后的 `Id.Entry` 生成选项键,从而消除这种不一致。 - ---- - -## `IsAllowed` - -> 以下描述游戏原版的事件过滤机制。 - -如果事件只应在部分跑局中出现,可以覆写 `IsAllowed(RunState runState)`: - -```csharp -public override bool IsAllowed(RunState runState) -{ - return !runState.VisitedEventIds.Contains(Id); -} -``` - -运行时,游戏会在候选事件池中轮询,直到找到同时满足以下条件的事件: - -- `IsAllowed(...)` 返回 `true` -- 当前跑局尚未访问过该事件 - -`IsAllowed` 表达的是"当前跑局是否允许出现",不是注册阶段的准备逻辑。 - ---- - -## 自定义事件场景 - -> 以下描述游戏原版的自定义事件布局机制。 - -返回自定义布局类型: - -```csharp -public override EventLayoutType LayoutType => EventLayoutType.Custom; -``` - -此时游戏会加载: - -```text -res://scenes/events/custom/.tscn -``` - -该场景根节点必须实现 `ICustomEventNode`,至少提供 `Initialize(EventModel)` 和 `CurrentScreenContext`。 - ---- - -## 资源预加载 - -> 以下描述游戏原版的事件资源预加载规则。 - -普通事件默认预加载: - -- 布局场景 -- `res://images/events/.png` -- 可选的 `res://scenes/vfx/events/_vfx.tscn` - -Ancient 默认预加载: - -- 布局场景 -- `res://scenes/events/background_scenes/.tscn` - -如需额外资源,可覆写 `GetAssetPaths(IRunState runState)` 追加路径。 - ---- - -## Ancient 最小示例 - -```csharp -using MegaCrit.Sts2.Core.Entities.Ancients; -using MegaCrit.Sts2.Core.Events; -using STS2RitsuLib.Scaffolding.Content; - -public sealed class MyAncient : ModAncientEventTemplate -{ - protected override AncientDialogueSet DefineDialogues() - { - return new AncientDialogueSet(); - } - - public override IEnumerable AllPossibleOptions => - [ - new EventOption(this, Accept, InitialOptionKey("ACCEPT")), - ]; - - protected override IReadOnlyList GenerateInitialOptions() - { - return AllPossibleOptions.ToArray(); - } - - private Task Accept() - { - SetEventFinished(L10NLookup($"{Id.Entry}.pages.ACCEPT.description")); - return Task.CompletedTask; - } -} -``` - -选项键、页面键与最终注册后的 `Id.Entry` 保持一致的原则同样适用。 - ---- - -## 相关文档 - -- [内容注册规则](ContentAuthoringToolkit.md) -- [内容包与注册器](ContentPacksAndRegistries.md) -- [本地化与关键词](LocalizationAndKeywords.md) diff --git a/Docs/zh/DiagnosticsAndCompatibility.md b/Docs/zh/DiagnosticsAndCompatibility.md deleted file mode 100644 index 94e8f36..0000000 --- a/Docs/zh/DiagnosticsAndCompatibility.md +++ /dev/null @@ -1,148 +0,0 @@ -# 诊断与兼容层 - -本文说明 RitsuLib 在游戏原版之上提供的诊断策略与兼容层。 - -重点包括: - -- 用于定位重复性数据错误的一次性警告 -- 面向调试的缺失本地化与无效解锁数据回退 -- 原版系统不处理 Mod 内容时使用的窄桥接补丁 - ---- - -## 设计意图 - -RitsuLib 不会试图隐藏所有引擎限制。它遵循以下规则: - -- 能尽早暴露真实错误,就尽早暴露 -- 原版没有安全扩展点时,框架可以补桥 -- 某个回退会掩盖过多行为时,保持系统显式 - -这层能力是刻意收敛的,只处理边缘问题。 - ---- - -## 一次性警告策略 - -RitsuLib 的部分诊断只会对同一个问题(或同一稳定键)警告一次,包括: - -- 缺失资源路径(`AssetPathDiagnostics`) -- **总开关 + LocTable 子项**开启时缺失的 `LocTable` 键(`[Localization][DebugCompat]`) -- **调试总开关 + 建筑师子项**开启时,`THE_ARCHITECT` 无对话注入占位值(`[Ancient]`) -- 其他解锁相关的一次性提示(例如 `ModUnlockMissingRuleWarnings`) - -同一稳定键或同一类问题至多记录一次,在可读的日志量下保留定位信息。 - ---- - -## 资源路径诊断 - -显式资源覆写路径由 `AssetPathDiagnostics` 校验。 - -当资源路径不存在时: - -- 输出一次警告(包含宿主类型、模型标识、配置成员名和缺失路径) -- 回退到原始资源路径或原始行为 - -这对角色资源尤其重要,因为游戏原版对缺失角色资源几乎没有安全兜底。 - -详见 [资源配置与回退规则](AssetProfilesAndFallbacks.md)。 - ---- - -## Debug 兼容模式 - -可选兼容回退由 `debug_compatibility_mode`(总开关)与设置页中的分项子开关控制。 - -**默认(总开关关):** 走原版逻辑。 - -**总开关开:** 游戏内展开 **兼容回退项**;子项默认**开启**。关闭某一子项时,仅移除对应回退。 - -| 子项 | 开启时 | -|---|---| -| **LocTable 缺键** | 占位解析 + 一次性 `[Localization][DebugCompat]` 警告 | -| **无效解锁纪元(Epoch)** | 跳过该次授予 + 一次性 `[Unlocks][DebugCompat]` 警告 | -| **建筑师缺对话** | 对 `ModContentRegistry` 角色注入空 `Lines` 条目 + 一次性 `[Ancient]` 警告 | - -除 LocTable 缺键处理外,各子项通常只作用于通过 RitsuLib 注册的内容。 - -**`ModUnlockMissingRuleWarnings`**(例如未注册 Boss 胜场规则):独立于调试兼容子开关的诊断路径。 - -**发布内容:** 应提供完整本地化、时间线与对话数据;上表仅用于迭代阶段排障。 - -Windows 下设置文件路径: - -```text -%appdata%\SlayTheSpire2\steam\\mod_data\com.ritsukage.sts2-RitsuLib\settings.json -``` - ---- - -## 注册冲突诊断 - -RitsuLib 会显式检查以下冲突: - -| 冲突类型 | 常见触发场景 | -|---|---| -| 模型 ID 冲突 | 同 Mod / 同类别下两个已注册模型的 CLR 类型名相同 | -| 纪元 ID 冲突 | 两个纪元解析出同一个 `Id` | -| 故事 ID 冲突 | 两个故事解析出同一个故事标识 | - -检测到冲突时抛异常或输出错误日志,不会静默接受模糊身份。 - ---- - -## Ancient 对话兼容层 - -框架在 `AncientDialogueSet.PopulateLocKeys` 之前为已注册 Mod 角色追加基于本地化键的 Ancient 对话条目;作者编写键,框架负责发现与注入,使 Mod 角色复用与原版相同的 Ancient 对话管线。 - -### `THE_ARCHITECT` 对话兜底 - -受调试兼容 **总开关 + 建筑师子项** 控制。若原版 `TheArchitect.LoadDialogue` 无结果,RitsuLib 对 `ModContentRegistry` 角色注入空 `Lines` 占位值并记录一次 **`[Ancient]`** 警告。 - -具体键结构见 [本地化与关键词](LocalizationAndKeywords.md)。 - ---- - -## 解锁兼容桥 - -若干原版进度检查仅针对 vanilla 角色遍历。RitsuLib 以窄补丁将已注册解锁规则挂到相同检查点,使 Mod 角色在同一节点上参与判定: - -| 桥接类型 | 说明 | -|---|---| -| 精英胜场 | 精英击杀计数的纪元判定桥接 | -| Boss 胜场 | Boss 击杀计数的纪元判定桥接 | -| 进阶 1 | 进阶 1 的纪元判定桥接 | -| 局后角色解锁 | 局后角色解锁纪元桥接 | -| 进阶显示 | 进阶显示解锁判定桥接 | - -桥接补丁会把 RitsuLib 已注册规则转发到原版会跳过 Mod 角色的进度检查点;不引入独立的进度存储。 - -详见 [时间线与解锁](TimelineAndUnlocks.md)。 - ---- - -## Freeze 异常 - -当内容、时间线或解锁在冻结之后还被注册时,RitsuLib 会直接抛异常。 - -这是诊断机制:一旦晚注册,往往意味着 ModelDb 缓存已建立、固定身份规则已被使用、解锁过滤已在运行。此时最安全的做法是尽早失败。 - ---- - -## 排查要点 - -1. 警告多表示 Mod 数据或配置问题(路径、键、规则),而非随机引擎故障。 -2. 资源与本地化应在数据源补全,而不是长期依赖占位值或兼容回退。 -3. 调试兼容回退用于迭代排障;发布构建宜关闭总开关或关闭子项并交付完整数据。 -4. 优先使用显式注册 API;兼容回退不宜作为长期架构依赖。 - ---- - -## 相关文档 - -- [资源配置与回退规则](AssetProfilesAndFallbacks.md) -- [本地化与关键词](LocalizationAndKeywords.md) -- [时间线与解锁](TimelineAndUnlocks.md) -- [Godot 场景编写说明](GodotSceneAuthoring.md) -- [框架设计](FrameworkDesign.md) diff --git a/Docs/zh/FmodAndAudio.md b/Docs/zh/FmodAndAudio.md deleted file mode 100644 index 98582b6..0000000 --- a/Docs/zh/FmodAndAudio.md +++ /dev/null @@ -1,215 +0,0 @@ -# FMOD 与音频 - -本文档说明游戏的音频架构,以及 RitsuLib 在此基础上提供的分层 API。 - ---- - -## 游戏原版音频架构 - -> 以下描述杀戮尖塔 2 引擎自身的音频管线,帮助理解 RitsuLib 音频 API 的设计背景。 - -杀戮尖塔 2 通过 **Godot 的 FMOD Studio GDExtension**(`FmodServer` 单例)播放音频。C# 侧由 `NAudioManager` 封装,它通过 GDScript 代理 `AudioManagerProxy` 间接调用 `FmodServer`。 - -这意味着: - -- 原版的音频播放最终都经过 `NAudioManager` → `AudioManagerProxy` → `FmodServer` 这条路径 -- `NAudioManager` 包含 `TestMode` 静音、SFX 音量施加等行为 -- 如果 Mod 希望音频行为"听起来和原版一样",应该走同一条管线 - ---- - -## RitsuLib 音频 API - -RitsuLib 将音频 API 分层,既能走与原版一致的管线,也能在需要时直连 FMOD Studio。 - -### 入口选择 - -| 需求 | 使用 | -|---|---| -| 更易用的高层播放、返回 handle、自动生命周期清理 | **`GameFmod.Playback`** | -| 与原版相同的路由 / `TestMode` 行为 | **`GameFmod.Studio`** → `NAudioManager` | -| 与 `SfxCmd` 相同的防护(非交互、战斗结束等) | **`Sts2SfxAlignedFmod`** | -| 加载/卸载 Studio Bank、检查路径 | **`FmodStudioServer`** | -| 在 `FmodServer` 上直接 one-shot(不经过 `NAudioManager`) | **`FmodStudioDirectOneShots`** | -| Bus 音量/静音/暂停、全局参数、DSP、性能数据 | **`FmodStudioBusAccess`**、**`FmodStudioMixerGlobals`** | -| Snapshot(`snapshot:/…`) | **`FmodStudioSnapshots`** | -| 长期持有的 `create_event_instance` | **`FmodStudioEventInstances`** | -| 通过插件加载 WAV/OGG/MP3 | **`FmodStudioStreamingFiles`** | -| 冷却、随机池(本身不发声) | **`FmodPlaybackThrottle`**、**`FmodPathRoundRobinPool`** | - -### 直连 FMOD 与原版管线的区别 - -- `GameFmod.Studio` 和 `Sts2SfxAlignedFmod` 走 `NAudioManager`,与原版游戏共享 GDScript 代理(含 `TestMode`、SFX 音量等) -- `FmodStudioDirectOneShots` 及多数 `FmodStudio*` 直接调用 `FmodServer`,适合自定义 Bank、散文件、Bus 调试;但 one-shot 不保证与游戏 SFX Bus 处理完全一致 -- 如果要"听起来和原版一样",优先使用 `GameFmod` 或 `Sts2SfxAlignedFmod` - ---- - -## 简短示例 - -**与原版一致的 one-shot** - -```csharp -using STS2RitsuLib.Audio; - -Sts2SfxAlignedFmod.PlayOneShot("event:/sfx/heal"); -GameFmod.Studio.PlayMusic("event:/music/menu_update"); -``` - -**模组内容 Bank + `guids.txt`(须与游戏 FMOD 主版本线兼容)** - -```csharp -FmodStudioServer.TryLoadBank("res://mods/MyMod/banks/MyMod.bank"); -FmodStudioServer.TryWaitForAllLoads(); -if (!FmodStudioServer.TryLoadStudioGuidMappings("res://mods/MyMod/banks/MyMod.guids.txt")) - return; -if (FmodStudioServer.TryCheckEventPath("event:/mods/mymod/hit") is true) - GameFmod.Studio.PlayOneShot("event:/mods/mymod/hit"); -``` - -**短音效文件(按 sound 加载)** - -```csharp -var sfxPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "ping.wav"); -FmodStudioStreamingFiles.TryPlaySoundFile(sfxPath, volume: 0.9f); -``` - -**流式音乐(推荐:新 Playback/Handle API)** - -```csharp -var musicPath = ProjectSettings.GlobalizePath("user://mymod/loop.ogg"); -var handle = GameFmod.Playback.PlayMusic( - AudioSource.StreamingMusic(musicPath), - new AudioPlaybackOptions { Volume = 0.7f, Scope = AudioLifecycleScope.Room } -); -``` - -**跟随游戏自动切换的常见三段式音乐(房间 / 战斗 / 胜利)** - -```csharp -var adaptive = GameFmod.Playback.FollowAdaptiveMusic( - AudioAdaptivePlans.FullRunOverride( - roomSource: AudioSource.StreamingMusic(roomLoopPath), - combatSource: AudioSource.StreamingMusic(combatLoopPath), - victorySource: AudioSource.StreamingMusic(victoryStingerPath) - ) -); -``` - -**触发过快时节流** - -```csharp -if (FmodPlaybackThrottle.TryEnter("my_power_proc", cooldownMs: 120)) - Sts2SfxAlignedFmod.PlayOneShot("event:/sfx/buff"); -``` - -**单例频道:替换当前播放** - -```csharp -GameFmod.Playback.PlayMusic( - AudioSource.StreamingMusic(nextMusicPath), - new AudioPlaybackOptions - { - Volume = 0.8f, - Routing = new AudioRoutingOptions - { - Channel = "my-mod/music", - ChannelMode = AudioChannelMode.ReplaceExisting, - AllowFadeOutOnReplace = true, - }, - } -); -``` - -**标签分组:替换整组 UI 提示音** - -```csharp -GameFmod.Playback.Play( - AudioSource.File(uiCuePath), - new AudioPlaybackOptions - { - Routing = new AudioRoutingOptions - { - Tag = "my-mod/ui-tooltips", - ReplaceTaggedGroup = true, - }, - } -); -``` - ---- - -## 辅助类型(`STS2RitsuLib.Audio`) - -| 类型 | 说明 | -|---|---| -| `FmodEventPath` | `event:/…` 路径轻量封装 | -| `FmodStudioRouting` | 常用 Bus 路径常量 | -| `FmodParameterMap` | 为 `GameFmod.Studio` 构造参数字典 | - -`STS2RitsuLib.Audio.Internal` 为内部实现,不作为稳定公共 API。 - ---- - -## 推荐外部工具链 - -RitsuLib 不包含下列工具,它们是常见的外部工作流: - -| 工具 | 作用 | -|---|---| -| [FMOD Studio](https://www.fmod.com/) | 编辑 Bank / Event。务必与游戏所用 FMOD 主版本线一致(可参考游戏目录 `addons/fmod`) | -| 游戏内置 Godot FMOD 插件 | 与 `utopia-rise/fmod-gdextension` 同类集成,运行时提供 `FmodServer` 单例 | -| [sts2-fmod-tools](https://github.com/elliotttate/sts2-fmod-tools)(社区) | 可选:从游戏数据侧辅助对齐 Studio 工程/事件 | -| DAW 导出 | 导出 WAV/OGG 等;若与原版 SFX 混播,注意响度与动态范围 | - -> RitsuLib 已对 **`NAudioManager`** 中与路径相关的 Studio 调用(OneShot / Loop / Music / Stop / SetParam / `UpdateMusicParameter` 等)接入 **guids.txt 映射**:模组在加载 **`.bank`** 后调用 **`TryLoadStudioGuidMappings`**,即可继续用 **`event:/…`** 字符串走与原版相同的 **`NAudioManager` → AudioManagerProxy** 管线。自定义替换或绕过该链路的 Harmony 补丁需自行与其它 Mod 协调。 - ---- - -## 模组附加 Bank 制作(推荐流程) - -模组仅发布 **自建 `.bank`** 与同次构建导出的 **`*.guids.txt`** 时,建议按下述方式制作与接入。 - -### 1. Bank 类型与命名 - -- **不要替换或改名覆盖**游戏自带的 **`Master.bank`**。 -- 模组应使用 **独立命名的内容 Bank**(又称「子 Bank」、Sidecar Bank),文件名与 Studio 内 **Bank 名称**在游戏内全局 **唯一**,避免与其它模组或未来的官方 Bank **重名冲突**。 -- 该 Bank 仅承载你的 Event / 采样;混音树上的 **Master / Routing** 仍依赖游戏已加载的原版 Master 管线。 - -### 2. Bus / Master 对齐(与原版混音一致) - -- 游戏里 **`AudioManagerProxy`** 使用的典型路径包括 **`bus:/master`**、**`bus:/master/sfx`**、**`bus:/master/music`**、**`bus:/master/ambience`**(与原版 `banks/desktop` 加载顺序下的 Studio 缓存一致)。 -- **若希望模组音效/音乐的分轨、衰减、音量滑条行为与原版一致**:在 FMOD Studio 中为 Event 指定的 **Routing / Output**,应落到上述 **已与原版一致的 `bus:/…`** 路径上(即路由到游戏里已经存在、GUID 由原版 Master 侧定义的 Bus),而不要自造一套与原版无关的顶层 Master Bank 去顶替官方。 -- **需要逐项对齐时**:可从官方/解包工程的 **GUIDs.txt**、`sts2-fmod-tools` 等对照 **Bus / VCA** 的路径与 GUID;同一 **FMOD Studio 主版本线** 前提下,与你的模组 Bank **同一次构建**导出 **GUIDs.txt**,避免 txt 与 `.bank` 二进制不一致。 - -### 3. 导出 GUID 并导入模组资源 - -1. 在 FMOD Studio 中 **Build** 你的模组 Bank。 -2. 在生成目录中取 **`GUIDs.txt`**(或由 Studio 导出 **GUID List**)。 -3. 拷贝为模组中的文本资源(例如 **`Evil.guids.txt`**):至少保留全部 **`event:/…`** 行(格式为 **`{xxxxxxxx-…} event:/…`**,一行一条);可按需保留与其它对象相关的行便于自查。 -4. 游戏初始化时在 **`TryLoadBank`**(或你的封装)加载 **`.bank`**、`TryWaitForAllLoads` 之后调用 **`FmodStudioServer.TryLoadStudioGuidMappings("res://…/YourMod.guids.txt")`**:框架会写入路径 → GUID 表并打日志;与 **RitsuLib** 内对 **`NAudioManager`** 的 Harmony 前缀配合后,即可用 **`event:/…`** 字符串走 **`NAudioManager`** 原版入口。 - -### 4. 运行时顺序与稳定性 - -- **在游戏的 FMOD 启动流程与 `NAudioManager` 已就绪之后**再 `TryLoadBank` 你的模组 Bank(例如在延迟初始化回调中);过早加载时 Studio 侧缓存可能尚未稳定,探测易失败。 -- 使用 **`FmodStudioServer.TryLoadBank`** 加载模组 Bank:实现会 **保留返回的 `FmodBank` 引用**,避免仅校验返回值后引用被回收导致 **引擎侧自动 unload**(见 GDExtension `FmodBank` 析构行为)。 - -### 5. 版本与产物一致性 - -- **FMOD Studio 主版本**须与游戏内 **`addons/fmod`** 所用库一致(或官方文档允许的兼容范围)。 -- **同一次 Build** 产出的 **`.bank`** 与 **`GUIDs.txt`** 必须成对发布;任意一侧来自旧构建都会导致 **`check_event_guid` / 路径解析失败**。 - ---- - -## 排错 - -- **`FmodStudioServer.TryGet()` 为 null** — `FmodServer` 未就绪(场景、无头测试或扩展加载失败),查游戏日志 -- **`TryCheckEventPath` 为 false** — 对应 **`.bank` 未加载**、路径写错、**`TryLoadStudioGuidMappings` 未成功**,或 **Bank 已被卸载**(须使用会 **pin `FmodBank` 引用** 的 `TryLoadBank` 封装) -- **无声且无异常** — `TestMode` / `NonInteractiveMode` 可能抑制 `NAudioManager`;直连 `FmodServer` 不受这些标志约束 - ---- - -## 相关文档 - -- [诊断与兼容层](DiagnosticsAndCompatibility.md) -- [补丁系统](PatchingGuide.md) diff --git a/Docs/zh/FrameworkDesign.md b/Docs/zh/FrameworkDesign.md deleted file mode 100644 index ed8893a..0000000 --- a/Docs/zh/FrameworkDesign.md +++ /dev/null @@ -1,152 +0,0 @@ -# 框架设计 - -本文说明 RitsuLib 的核心架构决策,以及这些决策对 Mod 实现方式的影响。 - ---- - -## 核心目标 - -RitsuLib 以少量明确的设计原则为核心: - -- 使用显式注册,而非不透明、无约束的「魔法」式发现 -- 使用固定模型身份,而非运行时推断名称 -- 使用可组合的资源记录,而非大型继承层级 -- 使用场景替换,而非原版资源原地修改 -- 仅在原版缺少安全扩展点时引入兼容回退 - -框架会减少重复性工作,但不会把 Mod 运行时结构隐藏为不可见行为。 - -可选的 **CLR 特性**注册仍然是显式的:只有通过 `ModTypeDiscoveryHub.RegisterModAssembly` 登记的程序集才会参与扫描;每条特性最终仍对应与普通代码相同的注册器调用;`AutoRegistrationAttribute.Inherit` 默认为 **关闭**,避免子类在未声明的情况下继承基类上的特性。说明见 [内容包与注册器](ContentPacksAndRegistries.md#clr-特性注册可选)。 - ---- - -## 固定模型身份 - -对通过 RitsuLib 内容注册器注册的模型,`ModelId.Entry` 是确定性的: - -```text -__ -``` - -这样做的好处: - -- 本地化 Key 稳定且可预测 -- 重构时更容易判断影响面 -- 内容冲突更容易定位 -- 不依赖反射顺序、自动扫描细节或类发现时机 - -这一取舍是明确的:已发布的 CLR 类型一旦改名,就属于兼容性变更。 - ---- - -## 先注册,再使用 - -RitsuLib 要求在早期引导阶段完成显式注册。 - -`CreateContentPack(modId)` 是便捷入口,但底层注册器仍然是第一层概念。 - -框架在早期引导阶段冻结注册,以保证: - -- 稳定的模型身份 -- 稳定的模型列表 -- 可预测的查找与解锁行为 - -因此,框架选择尽早失败,而不是在运行时系统已开始消费模型后继续修改模型图。 - -具体注册模型可见 [内容包与注册器](ContentPacksAndRegistries.md)。 - ---- - -## 资源配置,而不是大型角色基类 - -角色内容编写围绕结构化资源配置展开。 - -RitsuLib 不要求把所有角色资源塞进一个单体基类,而是按职责分组: - -- `CharacterSceneAssetSet` -- `CharacterUiAssetSet` -- `CharacterVfxAssetSet` -- `CharacterAudioAssetSet` - -这样可以保持职责边界清晰: - -- 场景资源放一起 -- UI 放一起 -- VFX 调整放一起 -- 音效放一起 - -这种方式确实比单一占位属性更冗长,但更利于独立扩展各类资源能力。 - ---- - -## 资源安全机制 - -资源配置体系配套了一组小范围的安全机制: - -- 角色缺失资源时的占位角色回退 -- 完整能量球场景与池级图标的分层 API -- 显式资源路径不存在时的一次性警告 - -这些行为属于同一设计目标的一部分,用于保证结构化资源 API 在迁移和未完成内容阶段仍然可用。 - -具体行为与 API 细节见 [资源配置与回退规则](AssetProfilesAndFallbacks.md)。 - ---- - -## 兼容层保持收敛 - -RitsuLib 提供兼容型补丁,但范围刻意保持收敛。 - -框架不会用自动化去覆盖所有引擎限制。只有在原版扩展点不安全,或重复劳动明显过高时,才会加入兼容回退。 - -典型例子包括:`debug_compatibility_mode` 下的 `LocTable` 与 `THE_ARCHITECT` 回退、Ancient 对话键注入,以及原版进度检查跳过 Mod 角色时使用的解锁桥接补丁。 - -具体兼容层可见 [诊断与兼容层](DiagnosticsAndCompatibility.md)。 - ---- - -## 为什么要有自己的补丁层 - -底层仍然是 Harmony,但 RitsuLib 在其上增加了一层统一约定: - -- 用 `IPatchMethod` 声明补丁 -- 区分 critical / optional -- 支持忽略缺失目标 -- 支持分组注册 -- 支持动态补丁 - -目的不是隐藏 Harmony,而是统一补丁声明、失败处理与日志行为,降低大型 Mod 的维护成本。 - -具体流程见 [补丁系统](PatchingGuide.md)。 - ---- - -## 为什么持久化按类组织 - -RitsuLib 的持久化条目是按类注册的,而不是随手塞原始值。 - -这样做可以自然支持: - -- 数据版本字段 -- 数据迁移 -- 后续扩展字段 -- 更清晰的序列化边界 - -前期会增加少量样板,但可以避免原始值存档在后期演化为复杂结构时的维护问题。 - -完整数据设计见 [持久化设计](PersistenceGuide.md)。 - ---- - -## 推荐阅读顺序 - -- [快速入门](GettingStarted.md) -- [内容注册规则](ContentAuthoringToolkit.md) -- [内容包与注册器](ContentPacksAndRegistries.md) -- [角色与解锁模板](CharacterAndUnlockScaffolding.md) -- [时间线与解锁](TimelineAndUnlocks.md) -- [资源配置与回退规则](AssetProfilesAndFallbacks.md) -- [补丁系统](PatchingGuide.md) -- [持久化设计](PersistenceGuide.md) -- [本地化与关键词](LocalizationAndKeywords.md) -- [诊断与兼容层](DiagnosticsAndCompatibility.md) diff --git a/Docs/zh/GettingStarted.md b/Docs/zh/GettingStarted.md deleted file mode 100644 index febd845..0000000 --- a/Docs/zh/GettingStarted.md +++ /dev/null @@ -1,199 +0,0 @@ -# 快速入门 - -本指南覆盖从声明依赖到注册第一个内容的完整流程。 - ---- - -## 1. 声明依赖 - -在 `mod_manifest.json` 中添加: - -```json -{ - "id": "MyMod", - "name": "My Mod", - "dependencies": ["STS2-RitsuLib"] -} -``` - ---- - -## 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(); - patcher.PatchAll(); - - RitsuLibFramework.CreateContentPack("MyMod") - .Character() - .Card() - .Card() - .Relic() - .Apply(); - } -} -``` - -链式方法、`ModContentRegistry` 与 `IContentRegistrationEntry`(附魔、成就、共享池、Manifest 等)的完整对照见 [内容包与注册器](ContentPacksAndRegistries.md)。 - -`CreatePatcher` 的 `patcherName` 参数用于日志标识。同一个 Mod 可以创建多个 Patcher。完整补丁写法见 [补丁系统](PatchingGuide.md)。 - -如果你的 Mod 使用了自定义 Godot C# 场景脚本,请把 `EnsureGodotScriptsRegistered(...)` 保留在初始化入口里。详见 [Godot 场景编写说明](GodotSceneAuthoring.md)。 - ---- - -## 3. 定义卡池 - -使用 `TypeListCardPoolModel` 承载池的视觉与元数据(边框、能量色等)。**属于该池的每张牌**必须在内容包里通过 `.Card()`、`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(...)` 及遗物/药水对应 API。完整说明、示例与**必读警告**(存档 entry、联机 `ModelIdSerializationCache` Hash、无玩法效果等)见 [内容包与注册器](ContentPacksAndRegistries.md) 中的「生成式占位内容」一节。 - ---- - -## 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 => $"造成 {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. 本地化 Key - -RitsuLib 注册的所有模型,其 `ModelId.Entry` 由以下规则推导(各字段规范化为全大写下划线格式): - -``` -__ -``` - -| Mod Id | C# 类型 | 类别 | Entry | -|---|---|---|---| -| `MyMod` | `MyCard` | card | `MY_MOD_CARD_MY_CARD` | -| `MyMod` | `MyRelic` | relic | `MY_MOD_RELIC_MY_RELIC` | -| `MyMod` | `MyCharacter` | character | `MY_MOD_CHARACTER_MY_CHARACTER` | - -本地化文件示例: - -```json -{ - "MY_MOD_CARD_MY_CARD.title": "打击", - "MY_MOD_CARD_MY_CARD.description": "造成 {damage} 点伤害。" -} -``` - ---- - -## 6. 订阅生命周期事件 - -```csharp -// 游戏就绪后执行一次 -RitsuLibFramework.SubscribeLifecycle(evt => -{ - Logger.Info("游戏已就绪。"); -}); - -// 每次战斗开始时 -RitsuLibFramework.SubscribeLifecycle(evt => -{ - // evt.RunState, evt.CombatState -}); -``` - -可重放事件(`IReplayableFrameworkLifecycleEvent`)即使在事件已发生后订阅也会立即回调,无需关心订阅时机。 - ---- - -## 7. 数据持久化 - -使用 `BeginModDataRegistration` 批量注册存档数据键。持久化条目以类为单位注册,同时需要注册键和文件名: - -```csharp -public sealed class CounterData -{ - public int Value { get; set; } -} - -using (RitsuLibFramework.BeginModDataRegistration("MyMod")) -{ - var store = RitsuLibFramework.GetDataStore("MyMod"); - store.Register( - key: "my_counter", - fileName: "counter.json", - scope: SaveScope.Profile, - defaultFactory: () => new CounterData()); -} -``` - -关于作用域、重载时机和迁移机制,可继续阅读 [持久化设计](PersistenceGuide.md)。 - ---- - -## 继续阅读 - -- [内容注册规则](ContentAuthoringToolkit.md) -- [角色与解锁模板](CharacterAndUnlockScaffolding.md) -- [卡牌动态变量](CardDynamicVarToolkit.md) -- [生命周期事件](LifecycleEvents.md) -- [补丁系统](PatchingGuide.md) -- [持久化设计](PersistenceGuide.md) -- [本地化与关键词](LocalizationAndKeywords.md) -- [框架设计](FrameworkDesign.md) -- [内容包与注册器](ContentPacksAndRegistries.md) -- [Godot 场景编写说明](GodotSceneAuthoring.md) -- [时间线与解锁](TimelineAndUnlocks.md) -- [资源配置与回退规则](AssetProfilesAndFallbacks.md) -- [诊断与兼容层](DiagnosticsAndCompatibility.md) diff --git a/Docs/zh/GodotSceneAuthoring.md b/Docs/zh/GodotSceneAuthoring.md deleted file mode 100644 index 3707cb2..0000000 --- a/Docs/zh/GodotSceneAuthoring.md +++ /dev/null @@ -1,134 +0,0 @@ -# Godot 场景编写说明 - -本文说明 STS2 Mod 在 Godot 场景编写时的两个实践性问题: - -- 面向场景的游戏类型应先在 Mod 里继承一层本地子类,再让编辑器绑定 -- Mod 程序集里的 Godot C# 脚本需要在初始化时注册 - ---- - -## 为什么需要本地子类 - -> 以下涉及 Godot Mono 工作流的一个引擎行为特征。 - -在当前 STS2 modding 使用的 Godot Mono 工作流里,来自游戏程序集的 C# 类型直接绑定到 `.tscn` 场景上时,编辑器行为并不稳定。 - -实际经验表明,只有当 `.tscn` 里绑定的是 Mod 自己程序集里的脚本类型时,编辑器的打开、序列化、重新绑定才更可靠。 - -稳定的经验法则: - -- 只要场景节点需要表现为游戏里的 Godot 类型,就先在 Mod 本地继承一个子类,再让场景绑定这个子类 - ---- - -## 包装子类模式 - -不要让场景直接绑定 `NEnergyCounter` 这种游戏类型,而是先写一个本地脚本: - -```csharp -using MegaCrit.Sts2.Core.Nodes.Combat; - -namespace MyMod.Scripts -{ - public partial class MyEnergyCounter : NEnergyCounter - { - } -} -``` - -然后在 `.tscn` 里绑定 `MyEnergyCounter`,而不是直接绑定 `NEnergyCounter`。 - -这个包装子类完全可以是空的。它存在的意义是给编辑器一个属于 Mod 自身的本地脚本类型。 - ---- - -## 常见需要包装的类型 - -| 游戏类型 | 典型用途 | -|---|---| -| `NEnergyCounter` | 能量球场景根节点 | -| `NRestSiteCharacter` | 休息点角色场景 | -| `NCreatureVisuals` | 角色视觉场景 | -| `NSelectionReticle` | 选择准星 | -| `MegaLabel` | 标签子控件 | - ---- - -## 通用绑定示例 - -自定义能量球场景: - -- 根脚本 → `MyEnergyCounter : NEnergyCounter` -- 标签子节点 → `MyCounterLabel : MegaLabel` - -角色视觉场景: - -- 根脚本 → `MyCreatureVisuals : NCreatureVisuals` - -休息点场景: - -- 根脚本 → `MyRestSiteCharacter : NRestSiteCharacter` - -重点不在于类名,而在于场景里绑定的脚本应属于 Mod 自己的程序集。 - ---- - -## 编辑器侧规则 - -只要 Godot 编辑器需要打开、序列化或重新绑定某个 Mod 场景里的脚本,就优先使用 Mod 本地子类。 - -即使: - -- 暂时没有额外逻辑 -- 只是简单继承一行 -- 运行时目标类型在游戏程序集里已存在 - -这个包装本质上是场景与编辑器之间的兼容层。 - ---- - -## 运行时脚本注册 - -如果 Mod 使用了 Godot C# 场景脚本,需在初始化阶段调用: - -```csharp -using System.Reflection; - -RitsuLibFramework.EnsureGodotScriptsRegistered( - Assembly.GetExecutingAssembly(), - Logger); -``` - -这让 Godot 的脚本桥接层发现并注册 Mod 程序集里的 C# 脚本。 - -应在内容注册之前完成此步骤,确保运行时能稳定发现场景脚本。 - ---- - -## 推荐工作流 - -1. 确定需要的游戏侧基类 -2. 在 Mod 本地创建继承它的薄包装 `partial class` -3. 让 `.tscn` 绑定这个本地脚本 -4. 在初始化入口调用 `EnsureGodotScriptsRegistered(Assembly.GetExecutingAssembly(), Logger)` - ---- - -## 不需要包装的情况 - -以下内容通常不需要额外包装子类: - -- 普通内容模型类(card / relic / power / character) -- 不作为 Godot 脚本使用的纯 C# 辅助类 -- 从不绑定到 `.tscn` 场景资源的逻辑类 - -本文只针对 Godot 场景编写与脚本绑定问题。 - ---- - -## 相关文档 - -- [快速入门](GettingStarted.md) -- [角色与解锁模板](CharacterAndUnlockScaffolding.md) -- [资源配置与回退规则](AssetProfilesAndFallbacks.md) -- [诊断与兼容层](DiagnosticsAndCompatibility.md) diff --git a/Docs/zh/LifecycleEvents.md b/Docs/zh/LifecycleEvents.md deleted file mode 100644 index f593fc4..0000000 --- a/Docs/zh/LifecycleEvents.md +++ /dev/null @@ -1,211 +0,0 @@ -# 生命周期事件参考 - -本文列出 RitsuLib 提供的全部生命周期事件,介绍订阅方式及可重放事件的行为。 - ---- - -## 订阅方式 - -### 按类型订阅(推荐) - -```csharp -var sub = RitsuLibFramework.SubscribeLifecycle(evt => -{ - Logger.Info($"游戏已就绪:{evt.Game}"); -}); - -// 取消订阅 -sub.Dispose(); -``` - -### 通过 `ILifecycleObserver` 订阅多种事件 - -```csharp -public class MyObserver : ILifecycleObserver -{ - public void OnEvent(IFrameworkLifecycleEvent evt) - { - if (evt is CombatStartingEvent combat) - HandleCombatStart(combat); - else if (evt is RunEndedEvent run) - HandleRunEnd(run); - } -} - -RitsuLibFramework.SubscribeLifecycle(new MyObserver()); -``` - -> **可重放事件(`IReplayableFrameworkLifecycleEvent`):** 若在事件已发生后才订阅,框架会立即以已存储的事件实例回调,无需关心订阅时机。 - ---- - -## 框架事件 - -框架初始化与 Profile 服务初始化阶段触发。 - -| 事件 | 可重放 | 携带数据 | -|---|---|---| -| `FrameworkInitializingEvent` | — | `FrameworkModId`、`FrameworkVersion` | -| `FrameworkInitializedEvent` | ✓ | `FrameworkModId`、`IsActive` | -| `ProfileServicesInitializingEvent` | — | — | -| `ProfileServicesInitializedEvent` | ✓ | `ProfileId` | - ---- - -## 游戏引导事件 - -游戏启动流程中依次触发,覆盖 Model 注册到游戏就绪全程。 - -| 事件 | 可重放 | 携带数据 | -|---|---|---| -| `EssentialInitializationStartingEvent` | — | — | -| `EssentialInitializationCompletedEvent` | ✓ | — | -| `DeferredInitializationStartingEvent` | — | — | -| `DeferredInitializationCompletedEvent` | ✓ | — | -| `ContentRegistrationClosedEvent` | ✓ | `Reason` | -| `ModelRegistryInitializingEvent` | — | — | -| `ModelRegistryInitializedEvent` | ✓ | `RegisteredModelTypeCount` | -| `ModelIdsInitializingEvent` | — | — | -| `ModelIdsInitializedEvent` | ✓ | — | -| `ModelPreloadingStartingEvent` | — | — | -| `ModelPreloadingCompletedEvent` | ✓ | — | -| `GameTreeEnteredEvent` | ✓ | `Game` | -| `GameReadyEvent` | ✓ | `Game` | - -```csharp -RitsuLibFramework.SubscribeLifecycle(_ => -{ - var id = ModelDb.GetId(); -}); -``` - ---- - -## 跑局事件 - -| 事件 | 可重放 | 携带数据 | -|---|---|---| -| `RunStartedEvent` | — | `RunState`、`IsMultiplayer`、`IsDaily` | -| `RunLoadedEvent` | — | `RunState`、`IsMultiplayer`、`IsDaily` | -| `RunEndedEvent` | — | `Run`、`IsVictory`、`IsAbandoned` | - ---- - -## 房间与章节事件 - -| 事件 | 携带数据 | -|---|---| -| `RoomEnteringEvent` | `RunState`、`Room` | -| `RoomEnteredEvent` | `RunState`、`Room` | -| `RoomExitedEvent` | `RunManager`、`Room` | -| `ActEnteringEvent` | `RunManager`、`TargetActIndex`、`DoTransition` | -| `ActEnteredEvent` | `RunState`、`CurrentActIndex` | -| `RewardsScreenContinuingEvent` | `RunManager` | - ---- - -## 战斗事件 - -| 事件 | 携带数据 | -|---|---| -| `CombatStartingEvent` | `RunState`、`CombatState?` | -| `CombatEndedEvent` | `RunState`、`CombatState?`、`Room` | -| `CombatVictoryEvent` | `RunState`、`CombatState?`、`Room` | -| `SideTurnStartingEvent` | `CombatState`、`Side` | -| `SideTurnStartedEvent` | `CombatState`、`Side` | -| `CardPlayingEvent` | `CombatState`、`CardPlay` | -| `CardPlayedEvent` | `CombatState`、`CardPlay` | -| `CardDrawnEvent` | `CombatState`、`Card`、`FromHandDraw` | -| `CardDiscardedEvent` | `CombatState`、`Card` | -| `CardExhaustedEvent` | `CombatState`、`Card`、`CausedByEthereal` | -| `CardRetainedEvent` | `CombatState`、`Card` | -| `CardMovedBetweenPilesEvent` | `RunState`、`CombatState?`、`Card`、`PreviousPile`、`Source` | - -### 生物事件 - -| 事件 | 携带数据 | -|---|---| -| `CreatureDyingEvent` | `CombatState`、`Creature` | -| `CreatureDiedEvent` | `CombatState`、`Creature` | - -```csharp -RitsuLibFramework.SubscribeLifecycle(evt => -{ - if (evt.Card is MyCard myCard) - myCard.OnDrawn(evt.CombatState); -}); -``` - ---- - -## 奖励事件 - -| 事件 | 携带数据 | -|---|---| -| `GoldGainedEvent` | `Amount` | -| `GoldLostEvent` | `Amount` | -| `PotionProcuredEvent` | `Potion` | -| `PotionDiscardedEvent` | `Potion` | -| `RelicObtainedEvent` | `Relic` | -| `RelicRemovedEvent` | `Relic` | -| `RewardTakenEvent` | `Reward` | - ---- - -## 解锁事件 - -| 事件 | 携带数据 | -|---|---| -| `EpochObtainedEvent` | `Epoch` | -| `EpochRevealedEvent` | `Epoch` | -| `UnlockIncrementedEvent` | `UnlockState` | - ---- - -## 存档与持久化事件 - -### Profile 生命周期 - -| 事件 | 携带数据 | -|---|---| -| `ProfileIdInitializedEvent` | `ProfileId` | -| `ProfileSwitchingEvent` | `OldProfileId`、`NewProfileId` | -| `ProfileSwitchedEvent` | `ProfileId` | -| `ProfileDeletingEvent` | `ProfileId` | -| `ProfileDeletedEvent` | `ProfileId` | - -### 存档写入 - -| 事件 | 携带数据 | -|---|---| -| `RunSavingEvent` | `RunState` | -| `RunSavedEvent` | `RunState` | -| `ProgressSavingEvent` | — | -| `ProgressSavedEvent` | — | - -### ModDataStore 数据事件 - -由 `ModDataStore` 内部使用,也可供 Mod 监听存档状态变化。 - -| 事件 | 说明 | -|---|---| -| `ProfileDataReadyEvent` | 存档数据加载完毕,可安全读写 | -| `ProfileDataChangedEvent` | 存档数据发生变更 | -| `ProfileDataInvalidatedEvent` | 存档数据失效(如切换档案) | - ---- - -## 游戏结算事件 - -| 事件 | 携带数据 | -|---|---| -| `GameOverScreenCreatedEvent` | `Screen` | - ---- - -## 相关文档 - -- [快速入门](GettingStarted.md) -- [内容注册规则](ContentAuthoringToolkit.md) -- [持久化设计](PersistenceGuide.md) -- [时间线与解锁](TimelineAndUnlocks.md) diff --git a/Docs/zh/LocStringPlaceholderResolution.md b/Docs/zh/LocStringPlaceholderResolution.md deleted file mode 100644 index 2909498..0000000 --- a/Docs/zh/LocStringPlaceholderResolution.md +++ /dev/null @@ -1,219 +0,0 @@ -# LocString 占位符解析 - -本文档分为两部分:**游戏原版机制**(`LocString`、SmartFormat 配置、内置格式化器)和**扩展指南**(Mod 如何注册自定义 `IFormatter`)。 - ---- - -## 第一部分:游戏原版机制 - -> 以下内容描述的是杀戮尖塔 2 引擎自身的本地化解析机制,不是 RitsuLib 提供的功能。 - -### 核心组件 - -- **`LocString`**:持有本地化表 id、条目键与变量字典,调用 `GetFormattedText()` 执行格式化。 -- **`LocManager.SmartFormat`**:从 `LocTable` 取原始模板,根据键是否已本地化选择 `CultureInfo`,再由 `SmartFormatter.Format(...)` 解析。 -- **`LocManager.LoadLocFormatters`**:初始化 `SmartFormatter`,注册数据源与格式化器扩展。 - -### 变量绑定 - -变量通过 `LocString.Add` 写入字典,**名称中的空格会被替换为连字符**。 - -```csharp -var locString = new LocString("cards", "strike"); -locString.Add("damage", 6); -string result = locString.GetFormattedText(); -``` - -### 占位符语法 - -游戏本地化 JSON 中使用 SmartFormat 占位符。 - -**仅变量名** — 直接输出变量值: - -``` -{VariableName} -``` - -**指定格式化器** — 格式化器以函数调用形式写在冒号后,括号内内容(`FormatterOptions`)由格式化器自行解读: - -``` -{VariableName:formatterName()} -{VariableName:formatterName(options)} -``` - -格式化器由 `IFormatter.Name` 匹配。`(` `)` 是调用语法的必要组成部分,不可省略。 - -**带额外格式段的格式化器**(如 `show`、`choose`、`cond`)在调用后通过第二个冒号传递格式文本,详见后续各格式化器说明及高级示例。 - -**示例:** - -```json -{ - "damage_text": "对所有敌人造成 {Damage:diff()} 点伤害。", - "energy_text": "本回合获得 {Energy:energyIcons()}。" -} -``` - -### SmartFormat 内置扩展 - -游戏注册的标准 SmartFormat 扩展(节选): - -| 类型 | 作用 | -|------|------| -| `ListFormatter` | 列表格式化 | -| `DictionarySource` | 按键读取变量 | -| `ValueTupleSource` | 值元组 | -| `ReflectionSource` | 反射访问属性 | -| `DefaultSource` | 默认数据源 | -| `PluralLocalizationFormatter` | 语言环境复数 | -| `ConditionalFormatter` | 条件格式化 | -| `ChooseFormatter` | `choose(...)` | -| `SubStringFormatter` | 子字符串 | -| `IsMatchFormatter` | 正则匹配 | -| `LocaleNumberFormatter` | 区域数字格式 | -| `DefaultFormatter` | 无匹配时的回退 | - -### 游戏自定义格式化器 - -游戏在 `MegaCrit.Sts2.Core.Localization.Formatters` 中注册了以下 `IFormatter`: - -| `IFormatter.Name` | 占位符写法 | `FormatterOptions` | 说明 | -|-------------------|-----------|--------------------|------| -| `abs` | `{v:abs()}` | 不使用 | 输出数值的绝对值 | -| `energyIcons` | `{Energy:energyIcons()}` 或 `{energyPrefix:energyIcons(n)}` | `CurrentValue` 为 `string` 时,必须提供整数参数作为图标个数 | 将数值渲染为能量图标,详见下方说明 | -| `starIcons` | `{v:starIcons()}` | 不使用 | 将数值渲染为星星图标 | -| `diff` | `{v:diff()}` | 不使用 | 以绿色(升级)高亮显示数值变化,需传入 `DynamicVar` | -| `inverseDiff` | `{v:inverseDiff()}` | 不使用 | 与 `diff` 相同但颜色方向相反,需传入 `DynamicVar` | -| `percentMore` | `{v:percentMore()}` | 不使用 | 将乘数转换为增加百分比,例如 `1.25` 输出 `25` | -| `percentLess` | `{v:percentLess()}` | 不使用 | 将乘数转换为减少百分比,例如 `0.75` 输出 `25` | -| `show` | `{v:show:升级文案\|普通文案}` | 不使用(选项由格式段 `|` 分隔提供) | 根据升级状态条件显示文案,需传入 `IfUpgradedVar` | - -**`energyIcons` 用法补充** - -`CurrentValue` 决定图标个数的来源: - -- `EnergyVar`:使用 `PreviewValue` 与可选颜色前缀,使用 `{Energy:energyIcons()}`。 -- `CalculatedVar` 或数值类型:直接使用数值,使用 `{Energy:energyIcons()}`。 -- `string`(如固定文本中的 `energyPrefix` 变量):个数由 `FormatterOptions` 提供,必须写 `energyIcons(n)`,例如 `{energyPrefix:energyIcons(1)}`。 - -图标渲染规则:个数 1–3 重复单独图标;个数 ≤0 或 ≥4 输出数字加单个图标。 - -**`show` 用法补充** - -`show:` 后的格式文本按 `|` 拆分为一至两段: - -- 升级状态(`Upgraded`):渲染第一段。 -- 普通状态(`Normal`):渲染第二段;若只有一段则输出空白。 -- 升级预览(`UpgradePreview`):以绿色渲染第一段。 - -### DynamicVar 类型 - -`DynamicVar` 子类携带格式化元数据,是 `diff`、`inverseDiff` 等格式化器的必要输入: - -| 类型 | 说明 | -|------|------| -| `DamageVar` | 伤害值,携带高亮元数据 | -| `BlockVar` | 格挡值 | -| `EnergyVar` | 能量值,携带颜色信息 | -| `CalculatedVar` | 计算值基类 | -| `CalculatedDamageVar` / `CalculatedBlockVar` | 计算后的伤害/格挡 | -| `ExtraDamageVar` | 额外伤害 | -| `BoolVar` / `IntVar` / `StringVar` | 基础类型 | -| `GoldVar` / `HealVar` / `HpLossVar` / `MaxHpVar` | 资源类型 | -| `PowerVar` | 能力值(泛型) | -| `StarsVar` / `CardsVar` | 星/牌引用 | -| `IfUpgradedVar` | 升级显示状态 | -| `ForgeVar` / `RepeatVar` / `SummonVar` | 其它卡牌变量 | - -### 格式化流程 - -1. 调用 `LocString.GetFormattedText()` -2. `LocManager.SmartFormat` 从 `LocTable` 取原始模板 -3. 根据键是否已本地化选择 `CultureInfo` -4. `SmartFormatter.Format` 解析占位符并调用匹配的格式化器 -5. 若格式化失败(`FormattingException` 或 `ParsingErrors`),记录错误并返回原始模板 - -### 高级示例 - -**条件格式**(`ConditionalFormatter`) - -```json -{ "text": "{HasRider:此卡有附加效果|此卡无附加效果}" } -``` - -**选择格式**(`ChooseFormatter`) - -```json -{ "text": "{CardType:choose(Attack|Skill|Power):攻击文本|技能文本|能力文本}" } -``` - -**嵌套格式化器** - -```json -{ - "text": "{Violence:造成 {Damage:diff()} 点伤害 {ViolenceHits:diff()} 次|造成 {Damage:diff()} 点伤害}" -} -``` - -**BBCode 颜色标签** - -```json -{ "text": "获得 [gold]{Gold}[/gold] 金币,当前生命 [green]{Hp}[/green]。" } -``` - -常用标签:`[gold]`、`[green]`、`[red]`、`[blue]`。 - ---- - -## 第二部分:自定义格式化器(Mod) - -> 以下内容描述如何通过 RitsuLib 补丁系统为游戏注册自定义格式化器。 - -通过对 `LocManager.LoadLocFormatters` 打 `Postfix` 补丁,可在 `SmartFormatter` 中注册额外的 `IFormatter` 实现。 - -**实现 `IFormatter`:** - -```csharp -public class MyCustomFormatter : IFormatter -{ - public string Name { get => "myCustom"; set { } } - public bool CanAutoDetect { get; set; } - - public bool TryEvaluateFormat(IFormattingInfo formattingInfo) - { - formattingInfo.Write($"自定义输出: {formattingInfo.CurrentValue}"); - return true; - } -} -``` - -- `Name` 是格式化器标识符,对应 JSON 中 `{Var:myCustom()}` 的 `myCustom` 部分。 -- 若需要参数,通过 `formattingInfo.FormatterOptions` 读取括号内的字符串。 - -**注册补丁:** - -```csharp -public class RegisterMyFormatterPatch : IPatchMethod -{ - public static string PatchId => "register_my_formatter"; - public static string Description => "Register custom SmartFormat formatter"; - public static bool IsCritical => true; - - public static ModPatchTarget[] GetTargets() - => [new(typeof(LocManager), "LoadLocFormatters")]; - - public static void Postfix(SmartFormatter ____smartFormatter) - => ____smartFormatter.AddExtensions(new MyCustomFormatter()); -} -``` - -注册后,在 JSON 中通过 `{SomeVar:myCustom()}` 或 `{SomeVar:myCustom(args)}` 调用。 - ---- - -## 相关文档 - -- [本地化与关键词](LocalizationAndKeywords.md) -- [卡牌动态变量](CardDynamicVarToolkit.md) -- [补丁系统](PatchingGuide.md) -- [内容注册规则](ContentAuthoringToolkit.md) diff --git a/Docs/zh/LocalizationAndKeywords.md b/Docs/zh/LocalizationAndKeywords.md deleted file mode 100644 index 0339b42..0000000 --- a/Docs/zh/LocalizationAndKeywords.md +++ /dev/null @@ -1,202 +0,0 @@ -# 本地化与关键词 - -RitsuLib 将本地化明确分为两层: - -- **游戏原版的 `LocString` 模型键管线** — 模型标题、描述等游戏内文本 -- **框架自带的 `I18N` 辅助本地化** — Mod 自身的辅助文本 - -同时提供轻量关键词注册器,用来统一悬浮提示和关键词文本。 - ---- - -## 游戏原版模型本地化 - -> 以下描述游戏引擎自身的本地化机制,RitsuLib 不替换此系统。 - -游戏通过 `LocString` 和各本地化表来读取模型文本,常见表包括: - -- `cards`、`relics`、`powers`、`characters`、`card_keywords` - -这些键建立在 `ModelId.Entry` 之上。 - -RitsuLib 的作用仅限于让模型身份更稳定、更可预测,从而使键更容易编写。具体的模型 ID 规则见 [内容注册规则](ContentAuthoringToolkit.md)。 - ---- - -## `CreateLocalization` 与 `CreateModLocalization` - -`I18N` 是 RitsuLib 提供的辅助文本本地化系统,独立于游戏的 `LocString`: - -```csharp -var i18n = RitsuLibFramework.CreateModLocalization( - modId: "MyMod", - instanceName: "MyMod-I18N", - resourceFolders: ["MyMod.localization"], - pckFolders: ["res://MyMod/localization"]); -``` - -`CreateModLocalization` 是 `CreateLocalization` 的便捷包装。如果不传文件系统目录,默认使用: - -```text -user://mod-configs//localization -``` - ---- - -## 资源合并顺序 - -`I18N` 支持三类来源: - -1. 文件系统目录 -2. 嵌入资源 -3. PCK 目录 - -合并策略是"先到先得": - -- 先加载文件系统目录 -- 嵌入资源只补缺失键 -- PCK 再补剩余缺失键 - -这样本地覆写可以自然优先于打包默认值。 - ---- - -## 语言代码归一化 - -`I18N` 在加载 JSON 之前会规范化语言代码: - -| 输入 | 归一化结果 | -|---|---| -| `en`、`en_us`、`eng` | `eng` | -| `zh`、`zh_cn`、`zh_hans` | `zhs` | -| `ja`、`ja_jp` | `jpn` | - -无法解析的语言默认回退到 `eng`。 - ---- - -## 运行时重载行为 - -`I18N` 会在可能的情况下订阅语言切换事件: - -- 游戏语言改变时,辅助本地化自动重载 -- 重载完成后触发 `Changed` 事件 -- 如果当前阶段拿不到游戏本地化管理器,则退回懒检测模式 - -此行为与游戏原版 `LocString` 的解析相互独立。 - ---- - -## 调试兼容模式 - -`LocTable` 占位值解析属于 RitsuLib 调试兼容回退之一:总开关、**LocTable** 子项与一次性 `[Localization][DebugCompat]` 警告见 [诊断与兼容层](DiagnosticsAndCompatibility.md)。 - -用于排障,不能代替补全真实键。 - ---- - -## 关键词注册器 - -`ModKeywordRegistry` 用于统一定义关键词及其悬浮提示: - -```csharp -var keywords = RitsuLibFramework.GetKeywordRegistry("MyMod"); - -keywords.RegisterCardKeywordOwnedByLocNamespace( - localKeywordStem: "brew", - iconPath: "res://MyMod/ui/keywords/brew.png"); -``` - -注册后会生成规范化标识,并绑定标题/描述的本地化键。 - ---- - -## 自动注册关键词(可选:CLR 特性) - -如果你已经使用 `ModTypeDiscoveryHub.RegisterModAssembly(...)` 让 RitsuLib 扫描你的程序集,也可以用特性声明关键词注册: - -```csharp -using STS2RitsuLib.Interop.AutoRegistration; - -[RegisterOwnedCardKeyword("brew", LocNamespace = "my_mod", IconPath = "res://MyMod/ui/keywords/brew.png")] -public sealed class BrewKeywordMarker; -``` - -这里 `LocNamespace` 只影响本地化键的 namespace(即 `modid` 部分)。关键词 stem(`brew`)会自动参与默认生成规则:`_`,并形成: - -- `_.title` -- `_.description` - -> 兼容性说明:旧字段 `LocKeyPrefix`/`locKeyPrefix` 历史上实际代表“完整 stem”,容易误解为 prefix + keyword,已标记为过时;新代码请使用 `LocNamespace`。 - ---- - -## 在代码里使用关键词 - -常用辅助方法: - -| 方法 | 说明 | -|---|---| -| `ModKeywordRegistry.CreateHoverTip(id)` | 创建悬浮提示 | -| `ModKeywordRegistry.GetTitle(id)` | 获取标题 | -| `ModKeywordRegistry.GetDescription(id)` | 获取描述 | -| `keywordId.GetModKeywordCardText()` | 获取卡牌文本 | -| `enumerable.ToHoverTips()` | 批量转换为悬浮提示 | - -也可以通过 `ModKeywordExtensions` 把运行时关键词挂在任意对象上: - -```csharp -card.AddModKeyword("brew"); - -if (card.HasModKeyword("brew")) -{ - // ... -} -``` - -适合"关键词是否存在由运行时状态决定"的场景。 - ---- - -## Ancient 对话本地化 - -RitsuLib 内置了 `AncientDialogueLocalization`,它有两个作用: - -- 提供从本地化键扫描对话的辅助 API -- 在游戏原版 `AncientDialogueSet.PopulateLocKeys` 之前,自动为已注册的 Mod 角色追加基于本地化定义的 Ancient 对话 - -键格式与原版保持一致: - -| 键组件 | 说明 | -|---|---| -| `.talk..-.ancient` | Ancient 台词 | -| `.talk..-.char` | 角色台词 | -| 可选后缀 `r` | 重复对话 | -| 可选后缀 `.sfx` | 音效 | -| 可选后缀 `-visit` | 访问覆盖 | -| 可选后缀 `-attack` | Architect 专用攻击者覆盖 | - -作者只需编写本地化条目,即可为自定义角色补充 Ancient 对话,无需手动为每个 `AncientDialogueSet` 添加补丁。 - -若某个 Ancient **完全没有**对应键,原版仍可能在 `THE_ARCHITECT` 显示 `PROCEED`,但 `WinRun` 会假定 `Dialogue` 非空。RitsuLib 仅在调试**总开关 + 建筑师子项**开启时,对 `ModContentRegistry` 角色注入窄范围兼容回退(空 `Lines`、安全的攻击方枚举),并记录一次 `[Ancient]` 警告。 - ---- - -## 推荐分工 - -| 用途 | 工具 | -|---|---| -| 游戏模型的文本(标题、描述) | 游戏原版 `LocString` 表 | -| Mod 自有辅助文本(设置页、说明) | `I18N` | -| 可复用关键词定义 | `ModKeywordRegistry` | -| Ancient 对话 | 本地化键 + `AncientDialogueLocalization` | - ---- - -## 相关文档 - -- [内容注册规则](ContentAuthoringToolkit.md) -- [角色与解锁模板](CharacterAndUnlockScaffolding.md) -- [诊断与兼容层](DiagnosticsAndCompatibility.md) -- [LocString 占位符解析](LocStringPlaceholderResolution.md) -- [Mod 设置界面](ModSettings.md) diff --git a/Docs/zh/ModSettings.md b/Docs/zh/ModSettings.md deleted file mode 100644 index b44ff74..0000000 --- a/Docs/zh/ModSettings.md +++ /dev/null @@ -1,383 +0,0 @@ -# Mod 设置界面 - -RitsuLib 提供一套用于玩家可编辑值的设置 UI。它构建在 `ModDataStore` 之上,但不替代底层持久化模型。 - -这套系统适合用于暴露一部分持久化字段、按页面和分区组织设置项,并统一管理界面文案。所有设置项都需要显式注册,这一限制是有意设计。 - ---- - -## 架构分层 - -建议保持以下职责分离: - -- `ModDataStore`:持久化、作用域、默认值、迁移 -- `IModSettingsValueBinding`:UI 与存储值之间的读写桥接 -- 页面 / 分区构建器:页面结构、层级与排序 -- `ModSettingsText`:标签与描述的文本来源抽象 - -这样可以避免把运行时状态、内部元数据与玩家配置混入同一个模型。 - ---- - -## 核心 API - -| API | 作用 | -|---|---| -| `RitsuLibFramework.RegisterModSettings(modId, configure, pageId?)` | 注册设置页;省略 `pageId` 时默认为 `modId` | -| `RitsuLibFramework.GetRegisteredModSettings()` | 返回当前所有已注册设置页 | -| `ModSettingsBindings.Global(...)` / `Profile(...)` | 将控件绑定到持久化数据 | -| `ModSettingsBindings.InMemory(...)` | 绑定到仅预览状态 | -| `ModSettingsText.Literal(...)` | 纯文本 | -| `ModSettingsText.I18N(...)` | 基于 `I18N` 的设置界面文本 | -| `ModSettingsText.LocString(...)` | 游戏原生本地化文本 | -| `ModSettingsText.Dynamic(...)` | 在 UI 刷新时重新求值 | -| `WithModDisplayName(...)` | 覆盖侧栏中的 Mod 名称 | -| `WithSortOrder(...)` | 控制同级页面排序 | -| `AsChildOf(parentPageId)` | 将页面注册为子页 | -| `section.Collapsible(startCollapsed?)` | 声明可折叠分区 | -| `page.WithVisibleWhen(...)` / `section.WithVisibleWhen(...)` | 按条件显示或隐藏页面、分区 | -| `AddToggle(...)`、`AddSlider(...)`、`AddIntSlider(...)`、`AddChoice(...)`、`AddEnumChoice(...)` | 标准值编辑控件 | -| `AddColor(...)`、`AddKeyBinding(...)`、`AddImage(...)` | 专用编辑控件与预览 | -| `AddButton(...)`、`AddHeader(...)`、`AddParagraph(...)` | 结构项与动作项 | -| `AddSubpage(...)` | 导航到子页 | -| `AddList(...)` | 结构化列表编辑器 | -| `ModSettingsUiActionRegistry.Register*ActionAppender(...)` | 扩展行、列表项、页面或分区的 Actions 菜单 | - ---- - -## 推荐流程 - -1. 在 `ModDataStore` 中注册完整持久化模型。 -2. 仅为需要暴露给玩家的字段创建绑定。 -3. 围绕这些绑定注册页面和分区。 -4. 补齐所有可见标签、描述与选项名称的本地化。 - -这样可以把存储结构与设置 UI 的公开范围明确分开。 - ---- - -## 界面行为 - -- **入口**:主菜单 -> `设置` -> `General`。当至少存在一个已注册页面时,RitsuLib 会注入 `Mod Settings (RitsuLib)` 入口并打开 `RitsuModSettingsSubmenu`。 -- **侧栏**:按 Mod 分组,同一时间只展开一个分组。当前页下方会显示对应分区快捷入口。 -- **内容区**:顶部显示页面标题;子页提供返回导航;正文按分区滚动显示。 -- **保存时机**:绑定被标记为脏后,约 `0.35s` 防抖保存;关闭或隐藏子菜单、退出场景树、切换游戏语言时会立即刷写。 - -`WithVisibleWhen(...)` 与行级 `visibleWhen` 谓词会在防抖刷新时重新计算。谓词应保持轻量且避免抛异常;如果求值失败,控件保持显示。 - ---- - -## 自动镜像策略(BaseLib / ModConfig) - -`RitsuModSettingsSubmenu` 会自动尝试镜像 `BaseLib` 与 `ModConfig` 的设置页。 -当你的模组同时接入多套设置源时,可以通过程序集级 `AssemblyMetadata` 指令(仅依赖 `System.Reflection`)控制镜像行为,无需引用 `STS2RitsuLib`。 - -支持的键(不区分大小写): - -- `RitsuLib.ModSettingsMirror.Global.DisableSources` -- `RitsuLib.ModSettingsMirror.Global.PreferredSource` -- `RitsuLib.ModSettingsMirror.Mod..DisableSources` -- `RitsuLib.ModSettingsMirror.Mod..PreferredSource` -- `RitsuLib.ModSettingsMirror.Type..DisableSources` -- `RitsuLib.ModSettingsMirror.Type..PreferredSource` - -值约定: - -- `DisableSources`:`baselib`、`modconfig`、`all`(可用 `,` / `;` / `|` 分隔多个值) -- `PreferredSource`:`baselib` 或 `modconfig` - -优先级(高 -> 低):`Type` -> `Mod` -> `Global`。 -`PreferredSource` 会让非首选来源不参与镜像;`DisableSources` 会直接禁用对应来源镜像。 - -示例: - -```csharp -using System.Reflection; - -[assembly: AssemblyMetadata("RitsuLib.ModSettingsMirror.Mod.MyMod.DisableSources", "modconfig")] -[assembly: AssemblyMetadata("RitsuLib.ModSettingsMirror.Mod.MyMod.PreferredSource", "baselib")] -[assembly: AssemblyMetadata( - "RitsuLib.ModSettingsMirror.Type.MyMod.Config.AdvancedSettings.DisableSources", - "baselib")] -``` - -也可以直接写在 `csproj` 中: - -```xml - - - - - -``` - ---- - -## 运行时反射协议(无库引用) - -除了 BaseLib / ModConfig 镜像外,RitsuLib 还支持“纯反射协议”注册设置页。 -模组无需引用 `STS2RitsuLib`,只需在程序集元数据中显式声明 provider 类型: - -```xml - - - -``` - -也支持在运行时主动注册 provider(适合你在初始化流程中按需反射调用): - -- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderType(string providerTypeFullName, string? assemblyName = null)` -- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderType(Type providerType)` -- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderTypeAndTryRegister(string providerTypeFullName, string? assemblyName = null)` -- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderTypeAndTryRegister(Type providerType)` - -Provider 约定(全部为 `static` 方法): - -- `object CreateRitsuLibSettingsSchema()` -- `object? GetRitsuLibSettingValue(string key)` -- `void SetRitsuLibSettingValue(string key, object value)` -- 可选:`void SaveRitsuLibSettings()` -- 可选:`void InvokeRitsuLibSettingAction(string key)`(用于 button) -- 可选强类型覆盖(优先于 object resolver): - - `bool GetRitsuLibSettingBool(string key)` / `void SetRitsuLibSettingBool(string key, bool value)` - - `int GetRitsuLibSettingInt(string key)` / `void SetRitsuLibSettingInt(string key, int value)` - - `double GetRitsuLibSettingDouble(string key)` / `void SetRitsuLibSettingDouble(string key, double value)` - - `string GetRitsuLibSettingString(string key)` / `void SetRitsuLibSettingString(string key, string value)` - -`CreateRitsuLibSettingsSchema()` 可以返回: - -- `Dictionary`(或等价对象) -- JSON 字符串(根节点必须是对象) -- JSON 文件路径(内容根节点必须是对象) - -推荐使用 Godot 路径(`res://`、`user://`),也支持普通文件路径。 - -字段结构: - -- page: `modId`, `pageId`, `title`, `description`, `sortOrder`, `sections` -- section: `id`, `title`, `description`, `entries` -- entry: - - 公共字段:`id`, `type`, `key`, `label`, `description`, `scope` - - `type=toggle|string|button|choice|slider|int-slider` - - `choice`:`options`(`[{ value, label }]`) - - `slider/int-slider`:`min`, `max`, `step` - - `string`:`maxLength` - - `button`:`buttonText`, `tone` - ---- - -## 最小示例 - -先注册持久化数据: - -```csharp -using STS2RitsuLib.Data; -using STS2RitsuLib.Utils.Persistence; - -public sealed class MyModSettings -{ - public bool EnableFancyVfx { get; set; } = true; - public double ScreenShakeScale { get; set; } = 1.0; - public MyDifficultyMode DifficultyMode { get; set; } = MyDifficultyMode.Normal; -} - -using (RitsuLibFramework.BeginModDataRegistration("MyMod")) -{ - var store = RitsuLibFramework.GetDataStore("MyMod"); - - store.Register( - key: "settings", - fileName: "settings.json", - scope: SaveScope.Global, - defaultFactory: () => new MyModSettings(), - autoCreateIfMissing: true); -} -``` - -然后创建绑定并注册设置页: - -```csharp -using STS2RitsuLib.Settings; - -var settingsLoc = RitsuLibFramework.CreateModLocalization( - modId: "MyMod", - instanceName: "MyMod-Settings", - resourceFolders: ["MyMod.Localization.Settings"]); - -var fancyVfx = ModSettingsBindings.Global( - "MyMod", - "settings", - model => model.EnableFancyVfx, - (model, value) => model.EnableFancyVfx = value); - -var shakeScale = ModSettingsBindings.Global( - "MyMod", - "settings", - model => model.ScreenShakeScale, - (model, value) => model.ScreenShakeScale = value); - -var difficulty = ModSettingsBindings.Global( - "MyMod", - "settings", - model => model.DifficultyMode, - (model, value) => model.DifficultyMode = value); - -RitsuLibFramework.RegisterModSettings("MyMod", page => page - .WithModDisplayName(ModSettingsText.I18N(settingsLoc, "mod.display_name", "My Fancy Mod")) - .WithTitle(ModSettingsText.I18N(settingsLoc, "page.title", "Settings")) - .WithDescription(ModSettingsText.I18N(settingsLoc, "page.description", "Player-facing options for this mod.")) - .AddSection("general", section => section - .WithTitle(ModSettingsText.I18N(settingsLoc, "general.title", "General")) - .AddToggle( - "fancy_vfx", - ModSettingsText.I18N(settingsLoc, "fancy_vfx.label", "Fancy VFX"), - fancyVfx, - ModSettingsText.I18N(settingsLoc, "fancy_vfx.desc", "Enable additional visual polish.")) - .AddSlider( - "screen_shake_scale", - ModSettingsText.I18N(settingsLoc, "screen_shake.label", "Screen Shake Scale"), - shakeScale, - minValue: 0.0, - maxValue: 2.0, - step: 0.05, - valueFormatter: value => $"{value:0.00}x") - .AddEnumChoice( - "difficulty_mode", - ModSettingsText.I18N(settingsLoc, "difficulty.label", "Difficulty"), - difficulty, - value => ModSettingsText.I18N(settingsLoc, $"difficulty.{value}", value.ToString())))); -``` - -`WithModDisplayName(...)` 控制左侧导航中的 Mod 标签。若未设置,RitsuLib 会回退到 manifest 名称,再回退到 mod id。 - ---- - -## 排序与导航 - -- **Mod 分组**:在页面构建器上调用 `WithModSidebarOrder(int)`,或使用 `ModSettingsRegistry.RegisterModSidebarOrder` / `RitsuLibFramework.RegisterModSettingsSidebarOrder`。数值越小越靠前。 -- **同一 Mod 内的页面**:对共享 `ParentPageId` 的兄弟页使用 `WithSortOrder(int)`。 -- **子页**:子页需单独注册,并通过 `AsChildOf(parentPageId)` 绑定父页,再在父页中使用 `AddSubpage(...)` 跳转。 - -### 多页面与子页面 - -- **默认页面 id**:`RegisterModSettings("MyMod", configure)` 的 `PageId` 默认为 `"MyMod"`。 -- **额外根页**:调用 `RegisterModSettings("MyMod", configure, pageId: "audio")`,并通过 `WithSortOrder(...)` 控制多个根页的顺序。 -- **子页注册**:子页必须单独注册,并链式调用 `AsChildOf("parentPageId")`。 -- **子页 UI**:子页标题栏提供返回控件,侧栏树仍保留完整层级。 - ---- - -## 文本来源 - -使用 `ModSettingsText`,可以让页面定义不依赖具体文本加载方式。 - -- `Literal(...)`:简单硬编码文本或快速原型 -- `I18N(...)`:Mod 自有的设置界面文本 -- `LocString(...)`:已纳入游戏本地化管线的文本 -- `Dynamic(...)`:在每次 UI 刷新时通过委托重新生成文本 - -推荐分工: - -- 游戏内容和内容名称 -> `LocString` -- 设置页专用标签与描述 -> `I18N` - ---- - -## 支持的控件类型 - -- `AddToggle(...)`:`bool` -- `AddSlider(...)`:`double` -- `AddIntSlider(...)`:`int` -- `AddChoice(...)` / `AddEnumChoice(...)`:候选列表;可选 `ModSettingsChoicePresentation`:`Stepper` 或 `Dropdown` -- `AddColor(...)`:颜色字符串 -- `AddKeyBinding(...)`:按键绑定字符串 -- `AddImage(...)`:通过 `Func` 提供图像预览 -- `AddButton(...)`:自定义动作按钮 -- `AddSubpage(...)`:跳转到已注册子页 -- `AddList(...)`:可排序结构化集合 -- `AddHeader(...)` / `AddParagraph(...)`:说明与结构辅助项 -- 可折叠分区:在分区构建器上调用 `.Collapsible(startCollapsed: false)` - ---- - -## 结构化列表 - -`AddList(...)` 是结构化列表编辑入口。 - -它支持: - -- 新增 / 删除 / 排序 -- 嵌套列表编辑 -- 列表项级复制 / 粘贴 / 创建副本 -- 通过 `ModSettingsListItemContext` 自定义列表项编辑器 - -如果列表项类型是结构化数据,建议提供 item adapter,以保证复制、粘贴和副本操作可以正确克隆与序列化。 - ---- - -## 页面结构 - -当前 UI 层级为: - -- mod 分组 -- page -- section -- entry - -对于大多数 Mod,一个根页面配多个分区就足够。只有在功能区域明确分离时,才建议拆出额外页面。 - -适合使用的场景: - -- 多页面:大型功能区分离 -- `AddSubpage(...)`:钻取式设置流 -- 可折叠 section:收纳低频选项 -- 列表:编辑集合而非单个值 - ---- - -## 作用域建议 - -绑定会保留底层持久化值的作用域。 - -- `SaveScope.Global`:所有档位共享 -- `SaveScope.Profile`:按玩家档位区分 - -常见用途: - -- `Global`:画面、辅助功能、调试开关、机器级默认项 -- `Profile`:按档位变化的玩法偏好或流程相关设置 - ---- - -## 适合暴露到设置页的内容 - -适合放入设置界面的内容: - -- 功能开关 -- 外观偏好 -- 辅助功能调整项 -- 玩家预期可调的玩法参数 - -不适合放入设置界面的内容: - -- 缓存 -- 迁移元数据 -- 运行时镜像状态 -- 纯内部实现字段 - -推荐模式是先持久化完整模型,再选择性暴露玩家真正需要调整的那部分。 - ---- - -## 内置参考页 - -RitsuLib 自身注册了一页参考设置,用于展示已持久化设置、仅预览绑定、可折叠分区、嵌套列表编辑以及列表项复制粘贴工作流。 - ---- - -## 相关文档 - -- [持久化设计](PersistenceGuide.md) -- [本地化与关键词](LocalizationAndKeywords.md) -- [生命周期事件](LifecycleEvents.md) -- [补丁系统](PatchingGuide.md)(`Settings/Patches/ModSettingsUiPatches.cs` 包含菜单入口与子菜单注入逻辑) diff --git a/Docs/zh/PatchingGuide.md b/Docs/zh/PatchingGuide.md deleted file mode 100644 index 743903d..0000000 --- a/Docs/zh/PatchingGuide.md +++ /dev/null @@ -1,220 +0,0 @@ -# 补丁系统 - -RitsuLib 底层仍然使用 Harmony,但在上面包了一层补丁系统,用来统一补丁声明形状、注册方式和失败处理。 - ---- - -## 主要类型 - -| 类型 | 作用 | -|---|---| -| `RitsuLibFramework.CreatePatcher(...)` | 创建 `ModPatcher` | -| `ModPatcher` | 注册并应用补丁 | -| `IPatchMethod` | 单个补丁的静态声明接口 | -| `IModPatches` | 用于分组注册多个补丁 | -| `DynamicPatchBuilder` | 处理运行时发现目标的方法补丁 | - ---- - -## 常规流程 - -```csharp -var patcher = RitsuLibFramework.CreatePatcher("MyMod", "core-patches"); -patcher.RegisterPatch(); -patcher.RegisterPatches(); - -if (!patcher.PatchAll()) - throw new InvalidOperationException("Required patches failed."); -``` - -推荐做法: - -- 每个逻辑区域使用一个 patcher -- 先注册完所有补丁 -- 最后统一调用一次 `PatchAll()` -- 如果返回 `false`,就把它视为该 patcher 的启动失败 - ---- - -## 用 `IPatchMethod` 编写单个补丁 - -`IPatchMethod` 是最常见的补丁形式。 - -```csharp -using STS2RitsuLib.Patching.Models; - -public class ExamplePatch : IPatchMethod -{ - public static string PatchId => "example_patch"; - public static string Description => "Log when the method runs"; - public static bool IsCritical => false; - - public static ModPatchTarget[] GetTargets() - { - return [new(typeof(SomeType), nameof(SomeType.SomeMethod))]; - } - - public static void Prefix() - { - // Harmony prefix - } -} -``` - -需要注意: - -- `PatchId` 在同一个 patcher 里必须唯一 -- `GetTargets()` 可以返回一个或多个目标 -- `Prefix`、`Postfix`、`Transpiler`、`Finalizer` 通过命名约定发现 -- 如果这些方法一个都没有,补丁会被视为失败 - ---- - -## 用 `IModPatches` 分组注册 - -如果你希望一个类型统一注册多个补丁,可以实现 `IModPatches`: - -```csharp -using STS2RitsuLib.Patching.Core; -using STS2RitsuLib.Patching.Models; - -public class MyPatchSet : IModPatches -{ - public static void AddTo(ModPatcher patcher) - { - patcher.RegisterPatch(); - patcher.RegisterPatch(); - } -} -``` - -然后这样注册: - -```csharp -patcher.RegisterPatches(); -``` - -这就是旧文档里那种“直接 apply 一个补丁集合对象”的现代替代写法。 - ---- - -## Critical 与 Optional 补丁 - -每个 `IPatchMethod` 都可以声明 `IsCritical`。 - -- `true`:失败后 `PatchAll()` 会失败,patcher 会回滚 -- `false`:失败会记录日志,但 patcher 仍可能整体成功 - -什么时候该设成 `true`: - -- 缺了这个补丁 Mod 根本无法安全运行 - -什么时候适合 `false`: - -- 纯 UI / 表现增强 -- 兼容性补丁 -- 最佳努力型功能 - ---- - -## Ignore Missing Target - -`ModPatchTarget` 支持 `ignoreIfMissing`: - -```csharp -public static ModPatchTarget[] GetTargets() -{ - return [new(typeof(SomeType), "SomeOptionalMethod", ignoreIfMissing: true)]; -} -``` - -适用场景: - -- 某个目标只在部分游戏版本存在 -- 某个兼容目标可能不存在 -- 缺失目标本来就是预期情况 - -它和 `IsCritical = false` 不是一回事: - -- `ignoreIfMissing` 表示“目标不存在也不算错误” -- `IsCritical = false` 表示“目标存在,但补丁失败不应终止整个 patcher” - ---- - -## 一个补丁作用多个目标 - -一个 `IPatchMethod` 可以同时补多个方法,只要它们共享同一套 Harmony 逻辑。 - -RitsuLib 会把 `GetTargets()` 自动展开成多个 `ModPatchInfo`。 -当目标不止一个时,框架会自动把目标名附加到补丁标识上,避免冲突。 - -这样你就能把相关逻辑放在一起,而不需要手动复制多个补丁类。 - ---- - -## 动态补丁 - -当补丁目标需要运行时发现时,可以使用 `DynamicPatchBuilder`。 - -```csharp -using HarmonyLib; -using STS2RitsuLib.Patching.Builders; - -var builder = new DynamicPatchBuilder("my_dynamic") - .AddMethod( - targetType: typeof(SomeType), - methodName: "SomeMethod", - postfix: DynamicPatchBuilder.FromMethod(typeof(MyRuntimePatch), nameof(MyRuntimePatch.Postfix)), - isCritical: false, - description: "Runtime-discovered patch"); - -patcher.ApplyDynamic(builder, rollbackOnCriticalFailure: false); -``` - -常见用途: - -- 给运行时生成的类型打补丁 -- 通过反射扫描后决定要给哪些属性读取器打补丁 -- 给一组动态发现的方法打补丁 - ---- - -## 日志与补丁边界 - -`CreatePatcher(ownerModId, patcherName, patcherLabel)` 会为每个补丁器生成: - -- 稳定的 Harmony id:`.` -- 独立的日志前缀 -- 独立的注册和应用生命周期 - -实际开发里,把补丁器按功能拆开通常非常值得,因为日志会清晰很多。 - ---- - -## 推荐结构 - -对中大型 Mod,比较建议这样组织: - -- 每个功能区一个补丁命名空间 -- 每个功能区一个 `IModPatches` 分组类型 -- 每个 `IPatchMethod` 只做一件明确的事 -- 兼容补丁默认设为 `IsCritical = false` - -这也是 RitsuLib 自己组织内部框架补丁时采用的方式。 - ---- - -## 常见错误 - -- 还没注册完补丁就调用 `PatchAll()` -- 没必要的兼容补丁却标成 critical -- 真正意图是“目标可能不存在”,却只写了 `IsCritical = false` -- `IPatchMethod` 里没有 `Prefix` / `Postfix` / `Transpiler` / `Finalizer` -- 把所有不相关补丁都塞进一个巨大 patcher,导致日志难读 - ---- - -## 相关文档 - -- [快速入门](GettingStarted.md) -- [框架设计](FrameworkDesign.md) diff --git a/Docs/zh/PersistenceGuide.md b/Docs/zh/PersistenceGuide.md deleted file mode 100644 index 8645874..0000000 --- a/Docs/zh/PersistenceGuide.md +++ /dev/null @@ -1,258 +0,0 @@ -# 持久化设计 - -RitsuLib 提供了一套结构化的 Mod 数据持久化层,支持作用域存储、档位切换、备份回退以及 schema 迁移。 - ---- - -## 主要 API - -| API | 作用 | -|---|---| -| `RitsuLibFramework.BeginModDataRegistration(modId)` | 批量注册作用域 | -| `RitsuLibFramework.GetDataStore(modId)` | 获取该 Mod 的 `ModDataStore` | -| `ModDataStore.Register(...)` | 注册一个持久化条目 | -| `ModDataStore.Get(key)` | 读取数据 | -| `ModDataStore.Modify(key, ...)` | 修改数据 | -| `ModDataStore.Save(key)` / `SaveAll()` | 持久化写盘 | - ---- - -## 为什么数据以 class 形式注册 - -RitsuLib 的持久化条目要求是带无参构造的类。 - -这么做是为了自然支持: - -- 结构化 JSON -- 后续字段扩展 -- schema 迁移 -- 更安全的默认值克隆 - -所以不要注册一个裸 `int`,而是定义一个小数据对象: - -```csharp -public sealed class CounterData -{ - public int Value { get; set; } -} -``` - ---- - -## 注册数据 - -```csharp -using STS2RitsuLib.Data; -using STS2RitsuLib.Utils.Persistence; - -using (RitsuLibFramework.BeginModDataRegistration("MyMod")) -{ - var store = RitsuLibFramework.GetDataStore("MyMod"); - - store.Register( - key: "counter", - fileName: "counter.json", - scope: SaveScope.Profile, - defaultFactory: () => new CounterData(), - autoCreateIfMissing: true); -} -``` - -这些参数的含义需要特别注意: - -- `key`:在 store 内部查找该条目的键 -- `fileName`:写入磁盘时使用的文件名 -- `scope`:`Global` 或 `Profile` -- `defaultFactory`:没有文件或需要恢复时使用的默认值 -- `autoCreateIfMissing`:文件不存在时是否立即写出默认文件 - ---- - -## Global 与 Profile 作用域 - -`SaveScope` 只有两个值: - -- `Global`:所有档位共享 -- `Profile`:按游戏档位隔离 - -设计建议: - -- Mod 设置、机器级缓存适合 `Global` -- 解锁、进度、玩家档位相关数据适合 `Profile` - -`Profile` 作用域的数据只会在档位服务准备好之后初始化。 - ---- - -## 读取与写入 - -```csharp -var store = RitsuLibFramework.GetDataStore("MyMod"); - -var counter = store.Get("counter"); - -store.Modify("counter", data => -{ - data.Value += 1; -}); - -store.Save("counter"); -``` - -几点说明: - -- `Get` 返回的是当前注册条目的活动对象 -- `Modify` 本质上只是对这个活动对象做一次包装 -- 保存默认是显式的,是否每次改完立刻写盘由作者自己决定 - ---- - -## 注册时机 - -推荐始终通过 `BeginModDataRegistration` 批量注册。 - -这样做的好处是,数据存储器可以在整个批次结束后再统一初始化,避免半注册状态。 - -作用域结束时: - -- 全局条目会立即初始化 -- 档位条目会在档位服务可用时初始化 - ---- - -## 档位切换 - -档位作用域的数据会自动感知档位切换。 - -当当前档位改变时,RitsuLib 会: - -- 先把旧档位数据保存回旧档位路径 -- 再从新档位路径重新加载 - -这部分由框架接管,Mod 不需要手写档位切换时的重绑定逻辑。 - ---- - -## 判断是否已有存档数据 - -```csharp -if (store.HasExistingData("counter")) -{ - // 磁盘上已经存在旧数据 -} -``` - -这个判断常用于区分“首次初始化”和“读取旧存档”两种启动路径。 - ---- - -## 备份与恢复行为 - -持久化层会尽量采用保守策略: - -- 主文件读取失败时尝试备份回退 -- 如果从备份成功恢复并完成迁移,可以写回主文件 -- 当迁移或解析严重失败时,损坏文件可能被重命名为 `.corrupt` -- 若恢复失败,则回退为默认值 - -目标是:即使本地数据损坏,Mod 仍尽量保持可用。 - ---- - -## 数据迁移 - -`Register` 支持同时传入迁移配置与迁移步骤: - -```csharp -store.Register( - key: "settings", - fileName: "settings.json", - scope: SaveScope.Global, - defaultFactory: () => new MyData(), - migrationConfig: new ModDataMigrationConfig(currentDataVersion: 2, minimumSupportedDataVersion: 1), - migrations: - [ - new SettingsV1ToV2Migration(), - ]); -``` - -迁移规则: - -- 没有 migration config 时,直接反序列化 -- 有 config 时,框架会先读取 schema version 字段 -- migration 会按版本顺序执行 -- 低于最小支持版本的数据会被拒绝并进入恢复路径 -- 成功迁移后的数据会回写成新格式 - -只要文件格式已经发布并且后续会演进,就建议尽早引入迁移版本号。 - ---- - -## AttachedState 与 SavedAttachedState - -`AttachedState` 用于给引用类型对象挂运行时 sidecar 状态。 - -适合场景: - -- 值只在当前进程内有效 -- 希望状态生命周期跟随 key 对象 -- 不想为目标类型做继承或直接改模型字段 - -`SavedAttachedState` 是它的可持久化版本,面向已经会经过 `SavedProperties.FromInternal(...)` 和 `SavedProperties.FillInternal(...)` 的对象。 - -适合场景: - -- key 是会参与原生存档序列化的模型对象 -- 附加值需要跨 save/load 保留 -- 值类型本身受 `SavedProperties` 支持 - -当前支持的值类型: - -- `int` -- `bool` -- `string` -- `ModelId` -- enum -- `int[]` -- enum 数组 -- `SerializableCard` -- `SerializableCard[]` -- `List` - -示例: - -```csharp -using STS2RitsuLib.Utils; - -private static readonly SavedAttachedState BonusDamage = - new("bonus_damage", () => 0); - -BonusDamage[model] = 4; - -var bonus = BonusDamage.GetOrCreate(model); -``` - -说明: - -- 持久化字段名在套用 `"{typeof(TKey).Name}_{name}"` 前缀后必须全局唯一 -- `SavedAttachedState` 不是任意 JSON sideband 通道,而是刻意限制在 `SavedProperties` 可表示的值类型范围内 -- reward 专用的 `EncounterState` sideband 序列化依然只是特例,不是默认推荐模式 - ---- - -## 推荐实践 - -- 每个持久化概念定义一个独立 class -- 纯运行时对象状态优先使用 `AttachedState` -- 只有模型对象本来就参与 `SavedProperties` 时才使用 `SavedAttachedState` -- 发布后尽量保持 `fileName` 稳定 -- 进度类数据默认优先考虑 `Profile` -- 始终在 `BeginModDataRegistration` 中批量注册 -- schema version 最好在真正需要迁移前就准备好 - ---- - -## 相关文档 - -- [快速入门](GettingStarted.md) -- [框架设计](FrameworkDesign.md) diff --git a/Docs/zh/Terminology.md b/Docs/zh/Terminology.md deleted file mode 100644 index bf0a60c..0000000 --- a/Docs/zh/Terminology.md +++ /dev/null @@ -1,41 +0,0 @@ -# 术语表 - -本文定义 RitsuLib 文档中统一使用的核心术语及推荐译法。 - ---- - -## 核心术语 - -| 英文术语 | 推荐中文 | 说明 | -|---|---|---| -| settings UI | 设置界面 | 指整体玩家配置界面。 | -| page | 页面 | 设置界面中的单个已注册页面。 | -| section | 分区 | 页面中的结构化分组。 | -| entry | 条目 | 分区中的单行可见控件或文本项。 | -| binding | 绑定 | UI 与存储值或内存状态之间的读写连接。 | -| persistence | 持久化 | 存储层与保存生命周期。 | -| persisted | 已持久化 / 会持久化 | 用于描述会写入持久化层的值。 | -| preview-only | 仅预览 | 指不会写入持久化层的控件或绑定。 | -| fallback | 回退 | 兼容或缺失数据场景下的回退行为。 | -| compatibility fallback | 兼容回退 | 优先使用该术语,避免使用“垫片”。 | -| bridge patch | 桥接补丁 | 将 Mod 内容转发到原版逻辑检查点的补丁。 | -| registry | 注册器 | 某类内容的运行时注册容器。 | -| content pack | 内容包 | 向多个注册器写入内容的便捷入口。 | -| builder | 构建器 | 用于链式构造页面、分区或内容的 API。 | -| override | 覆写 | 对资源路径、行为或值来源进行替换。 | -| placeholder | 占位值 | 数据缺失时使用的临时值。 | -| scope | 作用域 | 持久化值的存储范围。 | -| profile | 档位 | 按玩家档位区分的保存范围。 | -| global | 全局 | 跨档位共享的保存范围。 | -| epoch | 纪元(Epoch) | 中文文档首次出现可带英文,后续可简称“纪元”。 | -| story | 故事(Story) | 中文文档首次出现可带英文,后续可简称“故事”。 | -| Ancient dialogue | Ancient 对话 | 与游戏系统保持一致,不改写为其他称呼。 | - ---- - -## 相关文档 - -- [框架设计](FrameworkDesign.md) -- [Mod 设置界面](ModSettings.md) -- [诊断与兼容层](DiagnosticsAndCompatibility.md) -- [本地化与关键词](LocalizationAndKeywords.md) diff --git a/Docs/zh/TimelineAndUnlocks.md b/Docs/zh/TimelineAndUnlocks.md deleted file mode 100644 index db5f70f..0000000 --- a/Docs/zh/TimelineAndUnlocks.md +++ /dev/null @@ -1,250 +0,0 @@ -# 时间线与解锁 - -本文是时间线注册与解锁语义的参考文档。 - -RitsuLib 将时间线注册和解锁规则拆成两个系统,配合使用。本文说明: - -- `Story` / `Epoch` 的注册方式 -- 模板类型的职责 -- 解锁规则的判定机制 -- 原版进度逻辑对 Mod 角色的局限性与 RitsuLib 的兼容桥接 - ---- - -## 两个注册器 - -| 注册器 | 职责 | -|---|---| -| `ModTimelineRegistry` | 注册 `StoryModel` 和 `EpochModel` | -| `ModUnlockRegistry` | 定义内容或纪元的解锁条件 | - -在链式构建器里,对应: - -- `.Story()`、`.Epoch()` -- `.RequireEpoch()`、`.UnlockEpochAfter...()` - -核心区别: - -- **时间线注册**回答"这个东西是否存在" -- **解锁注册**回答"它什么时候可用" - ---- - -## `Story` 注册 - -故事类型仍用 `ModStoryTemplate`,只实现 `StoryKey`。栏内 **Epoch 顺序**不要在故事类里写死;按注册顺序把每个 Epoch 绑到该故事: - -```csharp -public class MyStory : ModStoryTemplate -{ - protected override string StoryKey => "my-story"; -} - -// 流式: .StoryEpoch() … .Story() -// 或 IModContentPackEntry: TimelineColumnPackEntry / StoryPackEntry -``` - -`ModStoryTemplate` 的职责: - -- 通过 `StoryKey` 自动生成规范化的故事标识 -- 通过 `ModStoryEpochBindings`(`RegisterStoryEpoch` 写入)组装 `Epochs` - -`RegisterStoryEpoch` 会注册 Epoch 并追加到该故事栏。不属于 mod 故事栏的 Epoch 可继续只用 `.Epoch()`。 - ---- - -## `Epoch` 注册 - -可以直接写原生 `EpochModel` 子类,也可以使用 RitsuLib 提供的模板类型: - -| 模板 | 说明 | -|---|---| -| `CharacterUnlockEpochTemplate` | 解锁角色本身的纪元 | -| `CardUnlockEpochTemplate` | 解锁额外卡牌的纪元 | -| `RelicUnlockEpochTemplate` | 解锁额外遗物的纪元 | -| `PotionUnlockEpochTemplate` | 解锁额外药水的纪元 | - -这些模板主要负责: - -- 生成时间线界面的解锁入队逻辑 -- 通过 `ExpansionEpochTypes` 支持后续纪元展开 - -### 角色解锁纪元模板 - -`CharacterUnlockEpochTemplate` 的内置行为: - -- 向 `NTimelineScreen` 队列一个角色解锁 -- 把待解锁角色写入进度存档 -- 若配置了 `ExpansionEpochTypes`,继续把后续纪元加入时间线展开 - -### 卡牌/遗物/药水纪元模板 - -`CardUnlockEpochTemplate`、`RelicUnlockEpochTemplate`、`PotionUnlockEpochTemplate` 的工作方式相似: - -- 声明要解锁的模型类型 -- 模板通过 `ModelDb` 解析类型 -- `UnlockText` 自动生成 -- `QueueUnlocks()` 自动推入时间线界面 - ---- - -## Expansion Epochs - -所有解锁纪元模板都支持: - -```csharp -protected virtual IEnumerable ExpansionEpochTypes => []; -``` - -当前纪元完成时会自动把这些纪元作为时间线扩展加入,用于组织解锁链: - -1. 先解锁角色 -2. 再展开卡牌解锁 -3. 再展开遗物解锁 - ---- - -## 注册时机与冻结 - -时间线和解锁两个注册器都会在早期初始化后冻结。原因是: - -- 故事/纪元标识必须稳定 -- 解锁过滤与兼容补丁需要面对最终确定的规则表 - -`Story`、`Epoch` 和解锁规则都应在初始化入口中注册,不要拖到运行期。 - ---- - -## 为内容设置 Epoch 门槛 - -当模型已注册,但应在某个纪元解锁后才出现时,使用 `RequireEpoch()`。 - -常见用途: - -- 后期卡牌在进度达成前不进入牌池 -- 遗物只在特定故事分支后开放 -- 共享 Ancient / 事件需要时间线进度门槛 - -RitsuLib 将门槛应用到多个访问入口: - -- `UnlockState.Characters` -- 卡牌/遗物/药水的已解锁池查询 -- 共享 Ancient 列表 -- Act 生成出来的事件列表 - -这不是单纯 UI 过滤,而是真正影响游戏可提供内容的规则。 - -### 纪元进度与时间线「揭示」 - -从存档生成的原版 **`UnlockState`** 里,**`UnlockedEpochs`** 主要反映已进入 **`EpochState.Revealed`**(时间线栏位已显示)的纪元。而 **`SaveManager.ObtainEpoch`** 可能先把纪元标成 **`Obtained`** / **`ObtainedNoSlot`**,时间线槽位尚未揭示。 - -应用 **`RequireEpoch`** 门槛时,**`ModUnlockRegistry.IsUnlocked`** 在以下**任一**成立时即视为已满足: - -- 该纪元 id 出现在 **`unlockState.UnlockedEpochs`** 中,或 -- **`SaveManager.Instance.Progress.IsEpochObtained(epochId)`** 为真。 - -这样,牌池 / 角色 / 事件等门槛会与通过 Mod 规则调用 **`ObtainEpoch`** 的进度一致,而不必等到原版时间线 UI 完全跟上。 - ---- - -## 局后 Epoch 规则 - -`ModUnlockRegistry` 提供的常用便捷 API: - -| 方法 | 说明 | -|---|---| -| `UnlockEpochAfterRunAs()` | 使用指定角色完成一局后解锁 | -| `UnlockEpochAfterWinAs()` | 使用指定角色胜利后解锁 | -| `UnlockEpochAfterAscensionWin(level)` | 指定进阶等级胜利后解锁 | -| `UnlockEpochAfterRunCount(requiredRuns, requireVictory)` | 累计跑局次数后解锁 | - -这些最终都转成 `PostRunEpochUnlockRule`。 - -也可以直接注册自定义规则: - -```csharp -unlocks.RegisterPostRunRule( - PostRunEpochUnlockRule.Create( - epochId: new MyEpoch().Id, - description: "在任意一次被放弃的 5 层进阶局后解锁", - shouldUnlock: ctx => ctx.IsAbandoned && ctx.AscensionLevel >= 5)); -``` - ---- - -## 累计进度型规则 - -| 方法 | 说明 | -|---|---| -| `UnlockEpochAfterEliteVictories(count)` | 精英击杀数 | -| `UnlockEpochAfterBossVictories(count)` | Boss 击杀数 | -| `UnlockEpochAfterAscensionOneWin()` | 进阶 1 胜利 | -| `RevealAscensionAfterEpoch()` | 纪元后显示进阶 | -| `UnlockCharacterAfterRunAs()` | 使用角色后解锁角色 | - ---- - -## 兼容补丁 - -> 以下解释原版进度系统对 Mod 角色的局限性,以及 RitsuLib 的桥接策略。 - -原版的若干进度检查是按原版角色设计的,不会自然支持 Mod 角色。RitsuLib 通过以下桥接补丁,让注册的解锁规则在这些检查点上生效: - -- 精英击杀计数的纪元判定桥接 -- Boss 击杀计数的纪元判定桥接 -- 进阶 1 的纪元判定桥接 -- 局后角色解锁纪元桥接 -- 进阶显示解锁判定桥接 - -这些补丁并不重写原版进度系统,只是在原版会跳过 Mod 角色的节点上补一层桥。这也是为什么解锁注册器会显式按 `ModelId` 保存规则,而不是试图仅从时间线图推断全部进度逻辑。 - ---- - -## 推荐模式 - -对故事驱动型角色 Mod: - -1. 在一个内容包里注册角色、池、纪元和故事 -2. 用 `CharacterUnlockEpochTemplate` 作为角色解锁纪元 -3. 用卡牌/遗物/药水纪元模板做后续内容展开 -4. 用 `RequireEpoch()` 给后期内容加门槛 -5. 使用少量清晰的进度规则,而不是堆叠重叠规则 - ---- - -## 构建器示例 - -```csharp -RitsuLibFramework.CreateContentPack("MyMod") - .Character() - .Card() - .Relic() - .Epoch() - .Epoch() - .Story() - .RequireEpoch() - .RequireEpoch() - .UnlockEpochAfterWinAs() - .UnlockEpochAfterAscensionWin(10) - .Apply(); -``` - ---- - -## 常见错误 - -- 注册了纪元,却忘了注册包含这些纪元的故事 -- 在时间线冻结之后才注册故事/纪元 -- 给内容设置了 `RequireEpoch`,却没有任何规则能真正解锁该纪元 -- 对同一个纪元叠很多重叠解锁规则,却没有明确设计理由 -- 误以为原版累计进度逻辑会自动兼容 Mod 角色,而没有注册 RitsuLib 解锁规则 -- Mod 角色的 **`unlockText`** 里用了 **`{Prerequisite}`**,却未覆盖 **`UnlocksAfterRunAsType`**(默认为 `null`)——选人界面悬停说明里的前置名会变成通用锁定标题(常显示为 **`???`**)。应将其设为与 **`UnlockEpochAfterWinAs`** / **`UnlockEpochAfterRunAs<…>`** 中 **`TCharacter`** 一致的前置角色类型(详见 [角色与解锁模板](CharacterAndUnlockScaffolding.md)) - ---- - -## 相关文档 - -- [角色与解锁模板](CharacterAndUnlockScaffolding.md) -- [内容包与注册器](ContentPacksAndRegistries.md) -- [诊断与兼容层](DiagnosticsAndCompatibility.md) -- [框架设计](FrameworkDesign.md) diff --git a/README.md b/README.md index a6e7827..c88a3bb 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ the bundled mods. The library exists alongside [BaseLib](https://github.com/Alchyr/BaseLib-StS2) and currently does not conflict with it. -Documentation index: [Docs/README.md](Docs/README.md) +Documentation site (Valaxy, bilingual): [docs/README.md](docs/README.md) ## Mod Settings @@ -20,7 +20,7 @@ RitsuLib includes a settings UI layer for player-editable values. - source labels and descriptions from `I18N` or game-native `LocString` - keep RitsuLib settings registration independent from BaseLib's config-page registry and file paths -Guide: [Docs/en/ModSettings.md](Docs/en/ModSettings.md) +Mod settings guide: [docs/pages/guide/mod-settings.md](docs/pages/guide/mod-settings.md) ## Debug Compatibility Mode diff --git a/README.zh.md b/README.zh.md index fbd74c8..5420be2 100644 --- a/README.zh.md +++ b/README.zh.md @@ -8,7 +8,7 @@ RitsuLib 按实际需求演进,主要服务于仓库内 Mod 的内容编写、 该库可与 [BaseLib](https://github.com/Alchyr/BaseLib-StS2) 并存,当前不存在已知冲突。 -文档入口: [Docs/README.md](Docs/README.md) +文档站(Valaxy,中英): [docs/README.md](docs/README.md) ## Mod 设置 @@ -19,7 +19,7 @@ RitsuLib 提供一套用于玩家可编辑配置的设置 UI。 - 标签与描述可来自 `I18N` 或游戏原生 `LocString` - 设置页注册与 BaseLib 的配置页注册、文件路径彼此独立 -说明文档: [Docs/zh/ModSettings.md](Docs/zh/ModSettings.md) +Mod 设置说明: [docs/pages/guide/mod-settings.md](docs/pages/guide/mod-settings.md) ## Debug 兼容模式 diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..b9a4b7e --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,23 @@ +# valaxy / vite +dist +dist-ssr +.vite-ssg-dist +.vite-ssg-temp +temp +node_modules +*.log +.DS_Store +*.local +.idea + +# valaxy generated +components.d.ts +.valaxy + +# rss (if enabled later) +public/feed.xml +public/feed.json +public/atom.xml + +# valaxy fuse +public/valaxy-fuse-list.json diff --git a/docs/.npmrc b/docs/.npmrc new file mode 100644 index 0000000..24a9302 --- /dev/null +++ b/docs/.npmrc @@ -0,0 +1,2 @@ +# Valaxy + vite-ssg resolve subpaths (e.g. unplugin-vue-router/data-loaders/*) like npm flat node_modules. +shamefully-hoist=true diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..98975d7 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,19 @@ +# RitsuLib documentation site + +Valaxy + `valaxy-theme-nova`, modeled after the DevMode docs layout. + +This project lives in the repository’s **`docs/`** directory. Handbook-style Markdown for guides is under [`pages/guide/`](pages/guide/). Bilingual pages follow Valaxy’s pattern: `## Heading{lang="en"}` / `## …{lang="zh-CN"}`, with `::: en` / `::: zh-CN` blocks (see [Valaxy i18n](https://valaxy.site/guide/i18n)). + +Local preview: + +```bash +cd docs +pnpm install +pnpm dev +``` + +Production build: + +```bash +pnpm build +``` diff --git a/docs/locales/en.yml b/docs/locales/en.yml new file mode 100644 index 0000000..dd44c4b --- /dev/null +++ b/docs/locales/en.yml @@ -0,0 +1,37 @@ +nova: + sidebar: + docs: Documentation + +nav: + home: Home + guide: Documentation + guide_hub: Index + guide_getting_started: Getting started + guide_framework_design: Framework design + guide_terminology: Terminology + guide_content_authoring: Content authoring + guide_content_packs: Content packs & registries + guide_character_unlock: Character & unlock + guide_card_dynamic_var: Card dynamic variables + guide_custom_events: Custom events + guide_timeline_unlocks: Timeline & unlocks + guide_asset_profiles: Asset profiles & fallbacks + guide_godot_scene: Godot scene authoring + guide_mod_settings: Mod settings + guide_localization: Localization & keywords + guide_loc_string: LocString placeholders + guide_lifecycle: Lifecycle events + guide_patching: Patching + guide_persistence: Persistence + guide_fmod: FMOD & audio + guide_diagnostics: Diagnostics & compatibility + +category: + guide: Documentation + +hero: + actions: + get-started: Read the docs + +tooltip: + edit_this_page: Edit this page on GitHub diff --git a/docs/locales/zh-CN.yml b/docs/locales/zh-CN.yml new file mode 100644 index 0000000..4f3328b --- /dev/null +++ b/docs/locales/zh-CN.yml @@ -0,0 +1,37 @@ +nova: + sidebar: + docs: 文档 + +nav: + home: 首页 + guide: 文档 + guide_hub: 目录 + guide_getting_started: 快速入门 + guide_framework_design: 框架设计 + guide_terminology: 术语表 + guide_content_authoring: 内容注册规则 + guide_content_packs: 内容包与注册器 + guide_character_unlock: 角色与解锁 + guide_card_dynamic_var: 卡牌动态变量 + guide_custom_events: 自定义事件 + guide_timeline_unlocks: 时间线与解锁 + guide_asset_profiles: 资源配置与回退 + guide_godot_scene: Godot 场景编写 + guide_mod_settings: Mod 设置 + guide_localization: 本地化与关键词 + guide_loc_string: LocString 占位符 + guide_lifecycle: 生命周期事件 + guide_patching: 补丁系统 + guide_persistence: 持久化 + guide_fmod: FMOD 与音频 + guide_diagnostics: 诊断与兼容 + +category: + guide: 文档 + +hero: + actions: + get-started: 阅读文档 + +tooltip: + edit_this_page: 在 GitHub 上编辑此页 diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..b2f2380 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,20 @@ +{ + "name": "ritsulib-docs", + "type": "module", + "private": true, + "packageManager": "pnpm@10.24.0", + "scripts": { + "build": "pnpm run build:ssg", + "build:spa": "valaxy build", + "build:ssg": "valaxy build --ssg", + "dev": "valaxy", + "serve": "vite preview" + }, + "dependencies": { + "valaxy": "^0.28.4", + "valaxy-theme-nova": "^0.3.1" + }, + "devDependencies": { + "typescript": "^6.0.2" + } +} diff --git a/docs/pages/guide/asset-profiles-and-fallbacks.md b/docs/pages/guide/asset-profiles-and-fallbacks.md new file mode 100644 index 0000000..a011708 --- /dev/null +++ b/docs/pages/guide/asset-profiles-and-fallbacks.md @@ -0,0 +1,578 @@ +--- +title: + en: Asset Profiles & Fallbacks + zh-CN: 资源配置与回退规则 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This is the reference document for asset-profile structure, placeholder fallback, and asset-path diagnostics. + +RitsuLib uses asset profiles to describe overrideable art, scenes, materials, and related resources. + +This document explains the structure behind those profiles and the fallback rules that make them safe to use. + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文是资源配置结构、占位角色回退与资源路径诊断的参考文档。 + +RitsuLib 使用资源配置对象来描述可覆写的美术、场景、材质以及相关资源。 + +本文专门解释这些配置对象的结构,以及它们背后的回退规则。 + +--- + +::: + +## Why Asset Profiles Exist{lang="en"} + +::: en + +Asset overrides could have been exposed as a long flat list of virtual properties. + +RitsuLib instead groups them into profile records because that scales better: + +- related assets stay together +- partial overrides remain readable +- fallback merging stays explicit +- migration from placeholder-based systems is possible without abandoning structure + +For characters, this is especially important because character assets span scenes, UI, VFX, audio, Spine, and multiplayer-specific textures. + +--- + +::: + +## 为什么要有资源配置对象{lang="zh-CN"} + +::: zh-CN + +资源覆写当然可以做成一长串平铺的虚属性。 + +但 RitsuLib 选择把它们组织成记录类型形式的资源配置,因为这样更适合长期扩展: + +- 相关资源会自然聚合在一起 +- 局部覆写时可读性更高 +- 回退合并规则更明确 +- 从默认依赖占位角色的旧框架迁移时,也不用放弃结构化设计 + +对角色尤其如此,因为角色资源横跨场景、UI、VFX、音频、Spine 和多人模式贴图。 + +--- + +::: + +## Character Asset Profile Structure{lang="en"} + +::: en + +`CharacterAssetProfile` is split into several nested record groups: + +- `CharacterSceneAssetSet` +- `CharacterUiAssetSet` +- `CharacterVfxAssetSet` +- `CharacterSpineAssetSet` +- `CharacterAudioAssetSet` +- `CharacterMultiplayerAssetSet` + +This lets you override only one category without turning the other categories into noise. + +Example: + +```csharp +public override CharacterAssetProfile AssetProfile => new( + Scenes: new( + VisualsPath: "res://MyMod/scenes/character/my_character.tscn", + EnergyCounterPath: "res://MyMod/ui/energy/my_energy_counter.tscn"), + Ui: new( + IconTexturePath: "res://MyMod/ui/top_panel/icon.png", + MapMarkerPath: "res://MyMod/map/map_marker.png"), + Audio: new( + AttackSfx: "event:/sfx/characters/my_character/attack")); +``` + +--- + +::: + +## 角色资源配置结构{lang="zh-CN"} + +::: zh-CN + +`CharacterAssetProfile` 被拆成多个嵌套记录类型: + +- `CharacterSceneAssetSet` +- `CharacterUiAssetSet` +- `CharacterVfxAssetSet` +- `CharacterSpineAssetSet` +- `CharacterAudioAssetSet` +- `CharacterMultiplayerAssetSet` + +这样你只改一个类别时,不会把其他类别也拖成噪音。 + +例如: + +```csharp +public override CharacterAssetProfile AssetProfile => new( + Scenes: new( + VisualsPath: "res://MyMod/scenes/character/my_character.tscn", + EnergyCounterPath: "res://MyMod/ui/energy/my_energy_counter.tscn"), + Ui: new( + IconTexturePath: "res://MyMod/ui/top_panel/icon.png", + MapMarkerPath: "res://MyMod/map/map_marker.png"), + Audio: new( + AttackSfx: "event:/sfx/characters/my_character/attack")); +``` + +--- + +::: + +## Placeholder Character Fallback{lang="en"} + +::: en + +`ModCharacterTemplate` now exposes: + +```csharp +public virtual string? PlaceholderCharacterId => "ironclad"; +``` + +Behavior: + +- your explicit `AssetProfile` is read first +- missing fields are filled from `CharacterAssetProfiles.FromCharacterId(PlaceholderCharacterId)` +- if `PlaceholderCharacterId` is `null`, fallback is disabled entirely + +This gives you BaseLib-style migration convenience without flattening the whole character API. + +--- + +::: + +## 占位角色回退{lang="zh-CN"} + +::: zh-CN + +`ModCharacterTemplate` 现在提供: + +```csharp +public virtual string? PlaceholderCharacterId => "ironclad"; +``` + +它的行为是: + +- 先读取你显式写下的 `AssetProfile` +- 缺失项再从 `CharacterAssetProfiles.FromCharacterId(PlaceholderCharacterId)` 补齐 +- 如果 `PlaceholderCharacterId` 为 `null`,则彻底关闭回退 + +这让你既拥有类似 BaseLib 的迁移便利,又保留了 Ritsu 式的结构化角色 API。 + +--- + +::: + +## How Character Profile Merging Works{lang="en"} + +::: en + +RitsuLib merges character profiles category-by-category and field-by-field. + +That means: + +- providing a custom `Scenes` record does not erase `Ui` +- providing only `RestSiteAnimPath` does not erase `MerchantAnimPath` +- providing only `AttackSfx` does not erase the other default SFX entries + +This is important because character assets are rarely replaced all at once. + +--- + +::: + +## 角色资源配置如何合并{lang="zh-CN"} + +::: zh-CN + +RitsuLib 对角色资源配置的合并是“按类别、按字段”进行的。 + +这意味着: + +- 你提供一个自定义 `Scenes` 记录,不会影响 `Ui` +- 你只写 `RestSiteAnimPath`,不会把 `MerchantAnimPath` 清空 +- 你只写 `AttackSfx`,不会把其余默认音效抹掉 + +这点非常重要,因为角色资源在实际开发里几乎从来不是一次性全量替换的。 + +--- + +::: + +## Character Asset Profile Helpers{lang="en"} + +::: en + +`CharacterAssetProfiles` provides several helper APIs: + +- `FromCharacterId(string)` +- `Ironclad()` / `Silent()` / `Defect()` / `Regent()` / `Necrobinder()` +- `Resolve(profile, placeholderCharacterId)` +- `Merge(fallback, profile)` +- `FillMissingFrom(...)` +- `WithPlaceholder(...)` +- `WithScenes(...)`, `WithUi(...)`, `WithVfx(...)`, `WithSpine(...)`, `WithAudio(...)`, `WithMultiplayer(...)` + +These helpers exist for two main use cases: + +- partial authoring of new characters +- migration from frameworks that assumed a placeholder character from the start + +--- + +::: + +## CharacterAssetProfiles 辅助 API{lang="zh-CN"} + +::: zh-CN + +`CharacterAssetProfiles` 提供了这些工具方法: + +- `FromCharacterId(string)` +- `Ironclad()` / `Silent()` / `Defect()` / `Regent()` / `Necrobinder()` +- `Resolve(profile, placeholderCharacterId)` +- `Merge(fallback, profile)` +- `FillMissingFrom(...)` +- `WithPlaceholder(...)` +- `WithScenes(...)`、`WithUi(...)`、`WithVfx(...)`、`WithSpine(...)`、`WithAudio(...)`、`WithMultiplayer(...)` + +它们主要服务两类场景: + +- 新角色的局部资源编写 +- 从默认假定占位角色的旧框架迁移到 RitsuLib + +--- + +::: + +## Content Asset Profiles{lang="en"} + +::: en + +RitsuLib also provides profile records for other content: + +- `CardAssetProfile` +- `RelicAssetProfile` +- `PowerAssetProfile` +- `OrbAssetProfile` +- `PotionAssetProfile` +- `AfflictionAssetProfile` +- `EnchantmentAssetProfile` +- `ActAssetProfile` + +These are intentionally much smaller because their asset surfaces are smaller. + +--- + +::: + +## 其他内容的资源配置{lang="zh-CN"} + +::: zh-CN + +RitsuLib 也为其他内容提供了类似的资源配置记录类型: + +- `CardAssetProfile` +- `RelicAssetProfile` +- `PowerAssetProfile` +- `OrbAssetProfile` +- `PotionAssetProfile` +- `AfflictionAssetProfile` +- `EnchantmentAssetProfile` +- `ActAssetProfile` + +这些配置对象更小,是因为它们各自的资源表面本来就更小。 + +--- + +::: + +## Path Builder Helpers{lang="en"} + +::: en + +For common vanilla-style asset conventions, there are helper factories: + +- `CharacterAssetProfiles.FromCharacterId(...)` +- `ContentAssetProfiles.Card(...)` +- `ContentAssetProfiles.Relic(...)` +- `ContentAssetProfiles.Power(...)` +- `ContentAssetProfiles.Orb(...)` +- `ContentAssetProfiles.Potion(...)` +- `ContentAssetProfiles.Affliction(...)` +- `ContentAssetProfiles.Enchantment(...)` +- `ContentAssetProfiles.Act(...)` + +There is also `CharacterAssetPathHelper` for deriving character-related default asset paths such as visuals, energy counter, select background, and map marker. + +These helpers are most useful when your assets intentionally follow a conventional naming layout. + +If those assets are backed by custom Godot scenes, remember that scene roots and scripted child nodes often need mod-local wrapper classes for stable editor binding. See [Godot Scene Authoring](/guide/godot-scene-authoring). + +--- + +::: + +## 路径辅助工厂{lang="zh-CN"} + +::: zh-CN + +对于符合原版命名习惯的资源布局,RitsuLib 提供了几个常用辅助方法: + +- `CharacterAssetProfiles.FromCharacterId(...)` +- `ContentAssetProfiles.Card(...)` +- `ContentAssetProfiles.Relic(...)` +- `ContentAssetProfiles.Power(...)` +- `ContentAssetProfiles.Orb(...)` +- `ContentAssetProfiles.Potion(...)` +- `ContentAssetProfiles.Affliction(...)` +- `ContentAssetProfiles.Enchantment(...)` +- `ContentAssetProfiles.Act(...)` + +另外还有 `CharacterAssetPathHelper`,可用于推导角色相关默认路径,例如角色视觉场景、能量球、角色选择背景、小地图标记等。 + +当你的资源布局本来就遵循某种命名约定时,这些辅助方法会很省事。 + +如果这些资源背后是自定义 Godot 场景,请记得场景根节点和带脚本的子节点往往需要使用 Mod 本地包装类,编辑器绑定才会更稳定。详见 [Godot 场景编写说明](/guide/godot-scene-authoring)。 + +--- + +::: + +## Energy Counter vs Big Energy Icon vs Text Icon{lang="en"} + +::: en + +RitsuLib treats these as separate concerns: + +- `CustomEnergyCounterPath`: full combat UI counter scene +- `BigEnergyIconPath`: large pool-linked icon resolved through `EnergyIconHelper` +- `TextEnergyIconPath`: small icon used inside rich text + +Why this matters: + +- a scene replacement is the right abstraction for a custom counter +- a texture path is the right abstraction for a pool icon +- keeping them separate avoids overloading one API with three unrelated jobs + +--- + +::: + +## 能量球场景、大能量图标、文本图标是三层能力{lang="zh-CN"} + +::: zh-CN + +RitsuLib 明确把它们拆开: + +- `CustomEnergyCounterPath`:完整战斗能量球场景 +- `BigEnergyIconPath`:通过 `EnergyIconHelper` 解析的大图标 +- `TextEnergyIconPath`:富文本描述里的小图标 + +这样拆的原因是: + +- 场景替换才是自定义能量球的正确抽象 +- 纹理路径才是池图标的正确抽象 +- 把三件事混进一个 API 只会让职责变糊 + +--- + +::: + +## Missing Path Diagnostics{lang="en"} + +::: en + +RitsuLib now validates asset-path overrides through `AssetPathDiagnostics`. + +Current behavior: + +- empty path -> ignore override +- existing path -> use override +- missing path -> log a one-time warning and fall back to the base asset + +The warning includes: + +- the owner type +- the model entry when available +- the specific profile member name +- the missing path + +This makes broken resource wiring much easier to debug. + +--- + +::: + +## 缺失路径诊断{lang="zh-CN"} + +::: zh-CN + +RitsuLib 现在通过 `AssetPathDiagnostics` 统一校验资源路径覆写。 + +当前行为: + +- 路径为空 -> 忽略 override +- 路径存在 -> 使用 override +- 路径不存在 -> 输出一次警告,并回退到原始资源 + +警告里会尽量带上: + +- 宿主类型 +- 若可用则带上模型条目标识 +- 对应的配置成员名 +- 缺失路径本身 + +这让资源接错线时比以前更容易定位。 + +--- + +::: + +## What Gets Path Validation{lang="en"} + +::: en + +Path validation covers resource-like overrides such as: + +- card textures, materials, overlays, and banners +- relic / power / orb / potion icons +- act backgrounds +- character visuals, energy counters, map assets, trail scenes, and Spine data +- pool energy icon paths + +It does not validate non-resource strings such as audio event ids. + +So character SFX override fields are still treated as plain values, not `ResourceLoader` paths. + +--- + +::: + +## 哪些内容会做路径校验{lang="zh-CN"} + +::: zh-CN + +路径校验主要覆盖这类“真正的资源路径”: + +- 卡牌贴图、材质、覆盖层、横幅 +- 遗物 / 能力 / 球体 / 药水图标 +- Act 背景 +- 角色视觉场景、能量球、小地图资源、轨迹场景、Spine 数据 +- 卡池级能量图标路径 + +而像音效事件 id 这种“不是 Godot 资源路径”的字符串不会走 `ResourceLoader` 校验。 + +所以角色 SFX 覆写字段仍然被当作普通字符串值处理,而不是资源路径。 + +--- + +::: + +## Recommended Character Authoring Pattern{lang="en"} + +::: en + +For most custom characters, this pattern works well: + +1. leave `PlaceholderCharacterId` at `ironclad` or switch it to the base character you want to inherit from +2. override only the assets that are truly custom +3. use pool-level `BigEnergyIconPath` / `TextEnergyIconPath` for energy icon concerns +4. use `CustomEnergyCounterPath` only when you need a real counter scene replacement + +This keeps the authoring surface small while preserving safe fallback behavior. + +--- + +::: + +## 推荐的角色资源写法{lang="zh-CN"} + +::: zh-CN + +对大多数自定义角色,比较推荐的模式是: + +1. 保留 `PlaceholderCharacterId = "ironclad"`,或者改成你想继承风格的基础角色 +2. 只覆写真正自定义的资源 +3. 能量图标相关优先放在 pool 级 `BigEnergyIconPath` / `TextEnergyIconPath` +4. 只有真的要换完整能量球 UI 时,再使用 `CustomEnergyCounterPath` + +这样内容编写面会比较小,同时还能保留安全回退。 + +--- + +::: + +## Recommended Content Authoring Pattern{lang="en"} + +::: en + +For cards and other content: + +- use `AssetProfile` when several asset fields belong together +- use a direct `Custom...Path` override only for one-off exceptions +- prefer helper factories like `ContentAssetProfiles.Card(...)` when your resource layout matches the helper's expectations + +The profile approach is especially good for keeping portrait, frame, overlay, and banner decisions in one place. + +--- + +::: + +## 推荐的普通内容资源写法{lang="zh-CN"} + +::: zh-CN + +对卡牌及其他内容: + +- 当多个资源字段属于同一个决策时,优先使用 `AssetProfile` +- 只有单点特例时,再直接覆写某个 `Custom...Path` +- 当你的资源布局与辅助方法约定一致时,优先考虑 `ContentAssetProfiles.Card(...)` 这类工厂 + +尤其是卡牌,把立绘、边框、覆盖层、横幅放在一个配置对象里通常会更清晰。 + +--- + +::: + +## Related Documents{lang="en"} + +::: en + +- [Character & Unlock Templates](/guide/character-and-unlock-scaffolding) +- [Content Authoring Toolkit](/guide/content-authoring-toolkit) +- [Godot Scene Authoring](/guide/godot-scene-authoring) +- [Diagnostics & Compatibility](/guide/diagnostics-and-compatibility) +- [Framework Design](/guide/framework-design) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [角色与解锁模板](/guide/character-and-unlock-scaffolding) +- [内容注册规则](/guide/content-authoring-toolkit) +- [Godot 场景编写说明](/guide/godot-scene-authoring) +- [诊断与兼容层](/guide/diagnostics-and-compatibility) +- [框架设计](/guide/framework-design) + +::: diff --git a/docs/pages/guide/card-dynamic-var-toolkit.md b/docs/pages/guide/card-dynamic-var-toolkit.md new file mode 100644 index 0000000..1a5be49 --- /dev/null +++ b/docs/pages/guide/card-dynamic-var-toolkit.md @@ -0,0 +1,356 @@ +--- +title: + en: Card Dynamic Var Toolkit + zh-CN: 卡牌动态变量工具包 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This document describes how RitsuLib creates card dynamic variables, how tooltip binding works, and how values are injected when a card is hovered. + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文介绍 RitsuLib 提供的卡牌动态变量创建方式、悬浮提示绑定规则及其在卡牌悬停时的注入机制。 + +--- + +::: + +## Vanilla DynamicVar System{lang="en"} + +::: en + +> The following describes the game engine’s own dynamic variable system. RitsuLib builds convenience constructors on top of it. + +The game’s `DynamicVar` system lets cards carry values that can change at runtime. Each `DynamicVar` subclass may carry extra metadata for formatters (for example `DamageVar` for highlighting, `EnergyVar` for colors). For the full list of subclasses, see [LocString Placeholder Resolution](/guide/loc-string-placeholder-resolution). + +--- + +::: + +## 游戏原版 DynamicVar 系统{lang="zh-CN"} + +::: zh-CN + +> 以下描述游戏引擎自身的动态变量系统,RitsuLib 在此基础上提供便捷构造器。 + +游戏的 `DynamicVar` 系统让卡牌在运行时携带可变数值。每个 `DynamicVar` 子类可携带额外元数据供格式化器读取(如 `DamageVar` 带高亮、`EnergyVar` 带颜色)。完整子类列表见 [LocString 占位符解析](/guide/loc-string-placeholder-resolution)。 + +--- + +::: + +## RitsuLib Capabilities{lang="en"} + +::: en + +On top of the vanilla system, RitsuLib provides: + +- **`ModCardVars`** — convenient variable constructors +- **`DynamicVarExtensions`** — each variable can bind its own tooltip independently +- **Automatic injection** — on card hover, all bound tooltips are appended automatically (implemented via patches; no extra setup) + +--- + +::: + +## RitsuLib 提供的能力{lang="zh-CN"} + +::: zh-CN + +在游戏原版基础上,RitsuLib 提供: + +- **`ModCardVars`** — 便捷变量构造器 +- **`DynamicVarExtensions`** — 每个变量可独立绑定悬浮提示 +- **自动注入** — 卡牌悬停时自动注入所有已绑定悬浮提示(由补丁实现,无需额外配置) + +--- + +::: + +## Variable Construction{lang="en"} + +::: en + +Create variables with `ModCardVars` and add them to the card’s `DynamicVarSet`: + +```csharp +public class MyCard : ModCardTemplate(1, CardType.Attack, CardRarity.Common, TargetType.SingleEnemy) +{ + private static readonly DynamicVar _charges = + ModCardVars.Int("charges", amount: 3) + .WithSharedTooltip("my_mod_charges"); + + private static readonly DynamicVar _label = + ModCardVars.String("flavor", value: "wine"); + + public override DynamicVarSet CreateDynamicVars() => + new DynamicVarSet().Add(_charges).Add(_label); +} +``` + +| Method | Description | +|---|---| +| `ModCardVars.Int(name, amount)` | Creates a numeric variable (`decimal`) | +| `ModCardVars.String(name, value)` | Creates a string variable | +| `ModCardVars.Computed(...)` | Creates a computed variable | + +RitsuLib does not assign gameplay semantics to these variables. Their meaning is entirely defined by the content author. + +--- + +::: + +## 变量构造{lang="zh-CN"} + +::: zh-CN + +通过 `ModCardVars` 创建变量,并在卡牌的 `DynamicVarSet` 中使用: + +```csharp +public class MyCard : ModCardTemplate(1, CardType.Attack, CardRarity.Common, TargetType.SingleEnemy) +{ + private static readonly DynamicVar _charges = + ModCardVars.Int("charges", amount: 3) + .WithSharedTooltip("my_mod_charges"); + + private static readonly DynamicVar _label = + ModCardVars.String("flavor", value: "wine"); + + public override DynamicVarSet CreateDynamicVars() => + new DynamicVarSet().Add(_charges).Add(_label); +} +``` + +| 方法 | 说明 | +|---|---| +| `ModCardVars.Int(name, amount)` | 创建数值变量(`decimal`) | +| `ModCardVars.String(name, value)` | 创建字符串变量 | +| `ModCardVars.Computed(...)` | 创建计算变量 | + +RitsuLib 不为这些变量赋予玩法语义,变量的具体含义完全由内容作者定义。 + +--- + +::: + +## Tooltip Binding{lang="en"} + +::: en + +Bind tooltips at definition time via chained extension methods: + +### Shared tooltip (recommended) + +Reads keys from the `static_hover_tips` table: + +```csharp +var myVar = ModCardVars.Int("my_var", 2) + .WithSharedTooltip("my_mod_my_var"); +// Resolves: +// static_hover_tips["my_mod_my_var.title"] +// static_hover_tips["my_mod_my_var.description"] +``` + +### Explicit table / key + +```csharp +var myVar = ModCardVars.Int("my_var", 2) + .WithTooltip( + titleTable: "card_keywords", + titleKey: "my_mod_my_var.title", + iconPath: "res://MyMod/art/kw.png"); +``` + +### Custom factory + +```csharp +var myVar = ModCardVars.Int("my_var", 2) + .WithTooltip(var => new HoverTip( + new LocString("my_table", "my_var.title"), + new LocString("my_table", "my_var.description"))); +``` + +--- + +::: + +## 悬浮提示绑定{lang="zh-CN"} + +::: zh-CN + +在变量定义时通过扩展方法链式绑定: + +### 绑定共享悬浮提示(推荐) + +从 `static_hover_tips` 表读取键: + +```csharp +var myVar = ModCardVars.Int("my_var", 2) + .WithSharedTooltip("my_mod_my_var"); +// 解析: +// static_hover_tips["my_mod_my_var.title"] +// static_hover_tips["my_mod_my_var.description"] +``` + +### 绑定指定表/键 + +```csharp +var myVar = ModCardVars.Int("my_var", 2) + .WithTooltip( + titleTable: "card_keywords", + titleKey: "my_mod_my_var.title", + iconPath: "res://MyMod/art/kw.png"); +``` + +### 绑定自定义工厂方法 + +```csharp +var myVar = ModCardVars.Int("my_var", 2) + .WithTooltip(var => new HoverTip( + new LocString("my_table", "my_var.title"), + new LocString("my_table", "my_var.description"))); +``` + +--- + +::: + +## Localization Example{lang="en"} + +::: en + +When using `WithSharedTooltip("my_mod_charges")`, provide entries in your `static_hover_tips` localization file: + +```json +{ + "my_mod_charges.title": "Charges", + "my_mod_charges.description": "Accumulated charges that deal extra damage." +} +``` + +RitsuLib does not ship built-in localization entries for these; if you use `WithSharedTooltip`, you must supply the strings yourself. + +--- + +::: + +## 本地化示例{lang="zh-CN"} + +::: zh-CN + +使用 `WithSharedTooltip("my_mod_charges")` 时,需在 `static_hover_tips` 本地化文件中提供: + +```json +{ + "my_mod_charges.title": "充能", + "my_mod_charges.description": "累积的充能层数,造成额外伤害。" +} +``` + +RitsuLib 不提供内置本地化词条,使用 `WithSharedTooltip` 时词条须由作者自行提供。 + +--- + +::: + +## Card Hover Injection{lang="en"} + +::: en + +RitsuLib’s patches automatically append every dynamic variable in `CardModel.DynamicVars` that has a bound tooltip to the end of the hover-tip sequence. No extra configuration is required. + +--- + +::: + +## 卡牌悬浮提示注入{lang="zh-CN"} + +::: zh-CN + +RitsuLib 的补丁会在卡牌悬停时自动将 `CardModel.DynamicVars` 中所有已绑定悬浮提示的变量追加到提示序列末尾,无需额外配置。 + +--- + +::: + +## Clone Behavior{lang="en"} + +::: en + +When `DynamicVar.Clone()` runs, tooltip metadata bound on the source variable is copied to the clone. Upgraded or duplicated cards in combat therefore behave correctly without extra handling. + +--- + +::: + +## 克隆行为{lang="zh-CN"} + +::: zh-CN + +调用 `DynamicVar.Clone()` 时,绑定在原变量上的悬浮提示元数据会一并复制到克隆对象。战斗中升级或复制卡牌时行为正确,无需额外处理。 + +--- + +::: + +## Reading Variable Values at Runtime{lang="en"} + +::: en + +Read values through `DynamicVarExtensions`: + +```csharp +int charges = card.DynamicVars.GetIntOrDefault("charges"); +decimal val = card.DynamicVars.GetValueOrDefault("charges"); +bool active = card.DynamicVars.HasPositiveValue("charges"); +``` + +--- + +::: + +## 运行时读取变量值{lang="zh-CN"} + +::: zh-CN + +通过 `DynamicVarExtensions` 扩展方法读取: + +```csharp +int charges = card.DynamicVars.GetIntOrDefault("charges"); +decimal val = card.DynamicVars.GetValueOrDefault("charges"); +bool active = card.DynamicVars.HasPositiveValue("charges"); +``` + +--- + +::: + +## Related Documents{lang="en"} + +::: en + +- [Content Authoring Toolkit](/guide/content-authoring-toolkit) +- [Getting Started](/guide/getting-started) +- [LocString Placeholder Resolution](/guide/loc-string-placeholder-resolution) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [内容注册规则](/guide/content-authoring-toolkit) +- [快速入门](/guide/getting-started) +- [LocString 占位符解析](/guide/loc-string-placeholder-resolution) + +::: diff --git a/docs/pages/guide/character-and-unlock-scaffolding.md b/docs/pages/guide/character-and-unlock-scaffolding.md new file mode 100644 index 0000000..26cf3e4 --- /dev/null +++ b/docs/pages/guide/character-and-unlock-scaffolding.md @@ -0,0 +1,584 @@ +--- +title: + en: Character & Unlock Templates + zh-CN: 角色与解锁模板 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This document is the practical assembly guide for a character mod: character templates, content pools, epoch templates, and unlock registration, with full examples. + +Detailed fallback rules are in [Asset Profiles & Fallbacks](/guide/asset-profiles-and-fallbacks). Detailed timeline and progression semantics are in [Timeline & Unlocks](/guide/timeline-and-unlocks). For wrapping scene scripts (visuals, rest sites, energy orbs), see [Godot Scene Authoring](/guide/godot-scene-authoring). + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文是角色 Mod 的实践搭建指南:角色模板、内容池定义、纪元模板与解锁注册,并附完整示例。 + +更细的回退规则见 [资源配置与回退规则](/guide/asset-profiles-and-fallbacks),更细的时间线与进度语义见 [时间线与解锁](/guide/timeline-and-unlocks)。涉及角色视觉场景、休息点、能量球等场景脚本包装时,请继续看 [Godot 场景编写说明](/guide/godot-scene-authoring)。 + +--- + +::: + +## Overview{lang="en"} + +::: en + +A full character mod typically includes: + +| Content | Base Type | Example | +|---|---|---| +| Card pool | `TypeListCardPoolModel` | `MyCardPool` | +| Relic pool | `TypeListRelicPoolModel` | `MyRelicPool` | +| Potion pool | `TypeListPotionPoolModel` | `MyPotionPool` | +| Character | `ModCharacterTemplate` | `MyCharacter` | +| Story | `ModStoryTemplate` | `MyStory` | +| Epoch | `CharacterUnlockEpochTemplate` or custom | `MyEpoch2` | + +--- + +::: + +## 概览{lang="zh-CN"} + +::: zh-CN + +一个完整的角色 Mod 通常包含以下部分: + +| 内容 | 基类 | 示例 | +|---|---|---| +| 卡池 | `TypeListCardPoolModel` | `MyCardPool` | +| 遗物池 | `TypeListRelicPoolModel` | `MyRelicPool` | +| 药水池 | `TypeListPotionPoolModel` | `MyPotionPool` | +| 角色 | `ModCharacterTemplate` | `MyCharacter` | +| 故事 | `ModStoryTemplate` | `MyStory` | +| 纪元 | `CharacterUnlockEpochTemplate` 或自定义 | `MyEpoch2` | + +--- + +::: + +## Pools{lang="en"} + +::: en + +- **Card pools:** register members through `CreateContentPack` / manifest via `.Card()` or `CardRegistrationEntry`. `TypeListCardPoolModel` already defaults `CardTypes` to empty and marks it `[Obsolete]`—**do not override** it in new mods. +- **Relic / potion pools:** `TypeListRelicPoolModel` / `TypeListPotionPoolModel` now match card pools: `RelicTypes` / `PotionTypes` default to empty and are marked `[Obsolete]`. Register members through `CreateContentPack` / manifest via `.Relic()`, `.Potion()`, `RelicRegistrationEntry`, or `PotionRegistrationEntry` in new mods. + +```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; +} + +public class MyRelicPool : TypeListRelicPoolModel +{ +} + +public class MyPotionPool : TypeListPotionPoolModel +{ +} +``` + +**Legacy pool hooks (`CardTypes`, `RelicTypes`, `PotionTypes`):** do not override them in new mods. Legacy overrides emit **CS0618** and still duplicate pool content if pack registration covers the same pool + model. Migrate by deleting the override and relying on the content pack / manifest only. + +### Configure Card Frame Color (HSV) + +`TypeListCardPoolModel` supports directly overriding `PoolFrameMaterial`. When this property returns a non-null material, that material is used for card frame rendering and `CardFrameMaterialPath` is no longer required. + +```csharp +using Godot; +using STS2RitsuLib.Utils; + +public class MyCardPool : TypeListCardPoolModel +{ + // Register cards in CreateContentPack / manifest; do not override CardTypes + + // Generate a frame material from HSV: H=0.55, S=0.45, V=0.95 + public override Material? PoolFrameMaterial => + MaterialUtils.CreateHsvShaderMaterial(0.55f, 0.45f, 0.95f); +} +``` + +If you prefer path-based configuration, simply leave `PoolFrameMaterial` as `null` and override `CardFrameMaterialPath` instead. + +### Example: Configure Pool Energy Icons + +`TypeList*PoolModel` also exposes pooled energy icon hooks: + +- `BigEnergyIconPath`: the large icon resolved through `EnergyIconHelper` +- `TextEnergyIconPath`: the small inline icon used in rich-text card descriptions + +```csharp +public class MyCardPool : TypeListCardPoolModel +{ + public override string? BigEnergyIconPath => "res://MyMod/ui/energy/my_energy_big.png"; + public override string? TextEnergyIconPath => "res://MyMod/ui/energy/my_energy_text.png"; +} +``` + +--- + +::: + +## 内容池定义{lang="zh-CN"} + +::: zh-CN + +- **卡池**:`TypeListCardPoolModel` 的池成员在 `CreateContentPack` / Manifest 中通过 `.Card<卡池, 卡牌>()` / `CardRegistrationEntry` 登记;基类已提供默认空的 `CardTypes`(`[Obsolete]`),**无需覆写**。 +- **遗物池 / 药水池**:现在与卡池保持一致,`TypeListRelicPoolModel` / `TypeListPotionPoolModel` 的 `RelicTypes` / `PotionTypes` 已提供默认空实现并标记为 `[Obsolete]`。新 Mod 请通过 `CreateContentPack` / Manifest 的 `.Relic<池, 遗物>()`、`.Potion<池, 药水>()`、`RelicRegistrationEntry`、`PotionRegistrationEntry` 注册内容。 + +```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; +} + +public class MyRelicPool : TypeListRelicPoolModel +{ +} + +public class MyPotionPool : TypeListPotionPoolModel +{ +} +``` + +**旧池钩子(`CardTypes` / `RelicTypes` / `PotionTypes`):** 新 Mod 不要再覆写。旧代码若继续覆写会得到 **CS0618**,且与内容包注册叠用时仍会重复拼接池内容。迁移方式是删除覆写、仅保留内容包 / Manifest 注册。 + +### 配置卡牌边框颜色(HSV) + +`TypeListCardPoolModel` 支持直接覆盖 `PoolFrameMaterial`。当该属性返回非空材质时,会优先使用这个材质渲染卡牌边框,不再依赖 `CardFrameMaterialPath`。 + +```csharp +using Godot; +using STS2RitsuLib.Utils; + +public class MyCardPool : TypeListCardPoolModel +{ + // 卡牌在 CreateContentPack / Manifest 中注册;勿覆写 CardTypes + + // 直接用 HSV 生成边框材质:H=0.55, S=0.45, V=0.95 + public override Material? PoolFrameMaterial => + MaterialUtils.CreateHsvShaderMaterial(0.55f, 0.45f, 0.95f); +} +``` + +若你希望继续走资源路径模式,也可以不覆盖 `PoolFrameMaterial`,仅覆盖 `CardFrameMaterialPath`。 + +### 示例:配置池能量图标 + +`TypeList*PoolModel` 现在也支持统一配置能量图标: + +- `BigEnergyIconPath`:通过 `EnergyIconHelper` 解析的大图标 +- `TextEnergyIconPath`:卡牌描述富文本里使用的小图标 + +```csharp +public class MyCardPool : TypeListCardPoolModel +{ + public override string? BigEnergyIconPath => "res://MyMod/ui/energy/my_energy_big.png"; + public override string? TextEnergyIconPath => "res://MyMod/ui/energy/my_energy_text.png"; +} +``` + +--- + +::: + +## Character Template{lang="en"} + +::: en + +Inherit `ModCharacterTemplate` for the character itself, then register starter content additively from your content manifest / pack. + +Unspecified character assets automatically fall back to `PlaceholderCharacterId`, which defaults to `ironclad`. + +```csharp +public class MyCharacter : ModCharacterTemplate +{ + public override string? PlaceholderCharacterId => "ironclad"; + + // Asset paths (configured via AssetProfile) + public override CharacterAssetProfile AssetProfile => new( + Spine: new( + CombatSkeletonDataPath: "res://MyMod/spine/my_character.tres"), + Ui: new( + IconTexturePath: "res://MyMod/art/icon.png", + CharacterSelectBgPath: "res://MyMod/art/select_bg.tscn"), + Scenes: new( + RestSiteAnimPath: "res://MyMod/scenes/rest_site/my_character_rest_site.tscn")); +} + +var character = new CharacterRegistrationEntry() + .AddStartingCard(4) + .AddStartingCard(4) + .AddStartingCard() + .AddStartingRelic(); +``` + +Another mod can append content to that same character later with `CharacterStarterCardRegistrationEntry(count)` or `ModContentRegistry.RegisterCharacterStarterCard(count)`. These starter additions are resolved when the character model is queried, so registration order does not matter as long as everything is registered before content freeze. + +Override `PlaceholderCharacterId` with another base character such as `silent` or `defect` if you want their merchant / rest-site / map / default SFX alignment. Return `null` to disable this fallback. + +### Character-select unlock text (`{Prerequisite}`) + +Localized **`unlockText`** may use the **`{Prerequisite}`** token. Vanilla fills it in **`CharacterModel.GetUnlockText()`** from **`UnlocksAfterRunAs`** (on **`ModCharacterTemplate`**, supply the type via **`UnlocksAfterRunAsType`**): + +- If **`UnlocksAfterRunAs`** is **`null`** (the template default), the game substitutes the generic locked title (**`LOCKED.title`**, often shown as **`???`**). +- If set, the game uses the prerequisite character’s **`Title`** when that character is present in the current **`UnlockState.Characters`**; otherwise it still falls back to **`LOCKED.title`**. + +Override **`UnlocksAfterRunAsType`** so it matches the same character type you pass to **`UnlockEpochAfterWinAs`** / **`UnlockEpochAfterRunAs`** (or equivalent). That keeps the hover text consistent with the real unlock rule. + +**`UnlocksAfterRunAsType` does not perform the unlock** — **`ModUnlockRegistry`** rules and epoch progression remain authoritative. + +--- + +::: + +## 角色模板{lang="zh-CN"} + +::: zh-CN + +继承 `ModCharacterTemplate` 负责角色本身,然后把 starter 内容放到内容注册阶段做追加式登记。 + +未填写的角色资源会自动回退到 `PlaceholderCharacterId`,默认值为 `ironclad`。 + +```csharp +public class MyCharacter : ModCharacterTemplate +{ + public override string? PlaceholderCharacterId => "ironclad"; + + // 资源路径(使用 AssetProfile 统一配置) + public override CharacterAssetProfile AssetProfile => new( + Spine: new( + CombatSkeletonDataPath: "res://MyMod/spine/my_character.tres"), + Ui: new( + IconTexturePath: "res://MyMod/art/icon.png", + CharacterSelectBgPath: "res://MyMod/art/select_bg.tscn"), + Scenes: new( + RestSiteAnimPath: "res://MyMod/scenes/rest_site/my_character_rest_site.tscn")); +} + +var character = new CharacterRegistrationEntry() + .AddStartingCard(4) + .AddStartingCard(4) + .AddStartingCard() + .AddStartingRelic(); +``` + +别的 mod 之后也可以继续给这个角色追加内容:可以用 `CharacterStarterCardRegistrationEntry(count)`,也可以直接调用 `ModContentRegistry.RegisterCharacterStarterCard(count)`。这些 starter 内容是在角色模型被读取时统一解析的,所以只要都发生在内容冻结前,注册先后顺序不会影响结果。 + +如果你更想继承 `silent`、`defect` 等角色的商人 / 休息点 / 小地图 / 默认音效风格,可以改写 `PlaceholderCharacterId`。若你想关闭这层兜底,可返回 `null`。 + +### 选人界面解锁说明(`{Prerequisite}`) + +本地化 **`unlockText`** 可使用 **`{Prerequisite}`** 占位符。原版在 **`CharacterModel.GetUnlockText()`** 里根据 **`UnlocksAfterRunAs`** 填充;在 **`ModCharacterTemplate`** 上通过 **`UnlocksAfterRunAsType`** 指定前置角色的 CLR 类型。 + +- 若 **`UnlocksAfterRunAs`** 为 **`null`**(模板默认),游戏会用通用锁定标题(**`LOCKED.title`**,界面上常显示为 **`???`**)。 +- 若已设置,则当前 **`UnlockState.Characters`** 里**已包含**该前置角色时,用其 **`Title`**;否则仍回退到 **`LOCKED.title`**。 + +请把 **`UnlocksAfterRunAsType`** 与 **`UnlockEpochAfterWinAs`** / **`UnlockEpochAfterRunAs`** 等规则里的 **`TCharacter`** 对齐,这样悬停说明与真实解锁条件一致。 + +**说明:** 仅设置 **`UnlocksAfterRunAsType` 不会实现解锁**,权威逻辑仍在 **`ModUnlockRegistry`** 与纪元进度中。 + +--- + +::: + +## Story Template{lang="en"} + +::: en + +Inherit `ModStoryTemplate` for the story id (`StoryKey` → slug). Bind epochs in registration order via `RegisterStoryEpoch()`, `TimelineColumnPackEntry<,>`, or `.StoryEpoch<,>()` — see `TimelineAndUnlocks.md`. + +```csharp +public class MyStory : ModStoryTemplate +{ + protected override string StoryKey => "my-character"; +} +``` + +### Ancient Dialogue Localization + +RitsuLib appends localization-defined ancient dialogues for registered mod characters before vanilla `AncientDialogueSet.PopulateLocKeys` runs. + +Key format matches vanilla: + +| Key component | Description | +|---|---| +| `.talk..-.ancient` | Ancient line | +| `.talk..-.char` | Character line | +| Optional suffix `.sfx` | Sound effect | +| Optional suffix `-visit` | Visit override | +| Optional suffix `-attack` | Architect attacker override | +| Optional suffix `r` | Repeat dialogue | + +If you need the helpers directly, use `STS2RitsuLib.Localization.AncientDialogueLocalization`. + +--- + +::: + +## 故事模板{lang="zh-CN"} + +::: zh-CN + +继承 `ModStoryTemplate` 提供故事标识(`StoryKey` → slug)。纪元顺序在注册阶段用 `RegisterStoryEpoch` / `TimelineColumnPackEntry` / `.StoryEpoch<,>()` 绑定,见 `TimelineAndUnlocks.md`。 + +```csharp +public class MyStory : ModStoryTemplate +{ + protected override string StoryKey => "my-character"; +} +``` + +### Ancient 对话本地化 + +RitsuLib 会在游戏原版 `AncientDialogueSet.PopulateLocKeys` 之前,自动为已注册的 Mod 角色追加基于本地化定义的 Ancient 对话。 + +键格式与原版保持一致: + +| 键组件 | 说明 | +|---|---| +| `.talk..-.ancient` | Ancient 台词 | +| `.talk..-.char` | 角色台词 | +| 可选后缀 `.sfx` | 音效 | +| 可选后缀 `-visit` | 访问覆盖 | +| 可选后缀 `-attack` | Architect 专用攻击者覆盖 | +| 可选后缀 `r` | 重复对话 | + +如果需要直接操作工具方法,可使用 `STS2RitsuLib.Localization.AncientDialogueLocalization`。 + +--- + +::: + +## Epoch Templates{lang="en"} + +::: en + +RitsuLib provides pre-built epoch templates for common unlock targets: + +| Template | Description | +|---|---| +| `CharacterUnlockEpochTemplate` | Epoch that unlocks the character itself | +| `CardUnlockEpochTemplate` | Epoch that unlocks extra cards | +| `RelicUnlockEpochTemplate` | Epoch that unlocks extra relics | +| `PotionUnlockEpochTemplate` | Epoch that unlocks extra potions | + +```csharp +public class MyCharacterEpoch : CharacterUnlockEpochTemplate +{ +} + +public class MyEpoch2 : CardUnlockEpochTemplate +{ + protected override IEnumerable CardTypes => + [ + typeof(MyAdvancedCard), + ]; +} +``` + +--- + +::: + +## 纪元模板{lang="zh-CN"} + +::: zh-CN + +RitsuLib 提供预置的纪元模板,用于常见解锁目标: + +| 模板 | 说明 | +|---|---| +| `CharacterUnlockEpochTemplate` | 解锁角色本身的纪元 | +| `CardUnlockEpochTemplate` | 解锁额外卡牌的纪元 | +| `RelicUnlockEpochTemplate` | 解锁额外遗物的纪元 | +| `PotionUnlockEpochTemplate` | 解锁额外药水的纪元 | + +```csharp +public class MyCharacterEpoch : CharacterUnlockEpochTemplate +{ +} + +public class MyEpoch2 : CardUnlockEpochTemplate +{ + protected override IEnumerable CardTypes => + [ + typeof(MyAdvancedCard), + ]; +} +``` + +--- + +::: + +## Full Registration Example{lang="en"} + +::: en + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + // Cards (specify owning pool) + .Card() + .Card() + .Card() + .Card() + + // Relics + .Relic() + + // Character + .Character() + + // Story and epochs + .Story() + .Epoch() + .Epoch() + + // Unlock rules + .RequireEpoch() // card appears only after epoch 2 + .UnlockEpochAfterRunAs() // unlock epoch 2 after one completed run + + .Apply(); +``` + +--- + +::: + +## 完整注册示例{lang="zh-CN"} + +::: zh-CN + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + // 卡牌(指定所属池) + .Card() + .Card() + .Card() + .Card() + + // 遗物 + .Relic() + + // 角色 + .Character() + + // 故事与纪元 + .Story() + .Epoch() + .Epoch() + + // 解锁规则 + .RequireEpoch() // 纪元 2 才显示该卡 + .UnlockEpochAfterRunAs() // 完成一局后解锁纪元 2 + + .Apply(); +``` + +--- + +::: + +## Model ID and Localization{lang="en"} + +::: en + +Character models follow the same fixed `ModelId.Entry` rule as all other content (see [Content Authoring Toolkit](/guide/content-authoring-toolkit)). + +Example — mod id `MyMod`, type `MyCharacter`: +- `ModelId.Entry` → `MY_MOD_CHARACTER_MY_CHARACTER` +- Localization key → `MY_MOD_CHARACTER_MY_CHARACTER.title` + +> Renaming a CLR type changes its derived entry and affects save compatibility. Avoid renaming after release. + +--- + +::: + +## 模型 ID 与本地化{lang="zh-CN"} + +::: zh-CN + +通过 RitsuLib 注册的角色模型遵循与其他内容相同的 `ModelId.Entry` 规则(参见 [内容注册规则](/guide/content-authoring-toolkit))。 + +示例(Mod id `MyMod`,类型 `MyCharacter`): +- `ModelId.Entry` → `MY_MOD_CHARACTER_MY_CHARACTER` +- 本地化 Key → `MY_MOD_CHARACTER_MY_CHARACTER.title` + +> 重命名 CLR 类型会改变其推导出的 Entry,影响存档兼容性。发布后请勿随意重命名。 + +--- + +::: + +## Dependency Rules{lang="en"} + +::: en + +- Card / relic / potion types must be registered before runtime model lookup +- Pool types referenced by the character must already be registered +- Every model — including epoch-gated content — must be registered; unlock rules do not replace registration + +--- + +::: + +## 依赖规则{lang="zh-CN"} + +::: zh-CN + +- 卡牌/遗物/药水类型必须在运行时模型查找发生前完成注册 +- 角色引用的池类型必须已经注册 +- 所有模型(包括受解锁条件限制的内容)均必须完成注册,解锁规则**不**替代注册 + +--- + +::: + +## Related Documents{lang="en"} + +::: en + +- [Content Authoring Toolkit](/guide/content-authoring-toolkit) +- [Getting Started](/guide/getting-started) +- [Timeline & Unlocks](/guide/timeline-and-unlocks) +- [Asset Profiles & Fallbacks](/guide/asset-profiles-and-fallbacks) +- [Godot Scene Authoring](/guide/godot-scene-authoring) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [内容注册规则](/guide/content-authoring-toolkit) +- [快速入门](/guide/getting-started) +- [时间线与解锁](/guide/timeline-and-unlocks) +- [资源配置与回退规则](/guide/asset-profiles-and-fallbacks) +- [Godot 场景编写说明](/guide/godot-scene-authoring) + +::: diff --git a/docs/pages/guide/content-authoring-toolkit.md b/docs/pages/guide/content-authoring-toolkit.md new file mode 100644 index 0000000..a9fe20e --- /dev/null +++ b/docs/pages/guide/content-authoring-toolkit.md @@ -0,0 +1,386 @@ +--- +title: + en: Content Authoring Toolkit + zh-CN: 内容注册规则 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This document is the overview for content authoring: registration entry points, model identity, localization coupling, and asset override basics. + +Detailed registration mechanics live in [Content Packs & Registries](/guide/content-packs-and-registries). Detailed asset semantics live in [Asset Profiles & Fallbacks](/guide/asset-profiles-and-fallbacks). + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文是内容编写的总览文档,聚焦注册入口、模型身份、本地化耦合关系以及资源覆写基础规则。 + +更详细的注册机制见 [内容包与注册器](/guide/content-packs-and-registries),更详细的资源语义见 [资源配置与回退规则](/guide/asset-profiles-and-fallbacks)。 + +--- + +::: + +## Registration APIs{lang="en"} + +::: en + +| API | Purpose | +|---|---| +| `RitsuLibFramework.CreateContentPack(modId)` | Recommended entry point — fluent builder | +| `RitsuLibFramework.GetContentRegistry(modId)` | Low-level content registry | +| `RitsuLibFramework.GetKeywordRegistry(modId)` | Keyword registry | +| `RitsuLibFramework.GetTimelineRegistry(modId)` | Timeline (story / epoch) registry | +| `RitsuLibFramework.GetUnlockRegistry(modId)` | Unlock rule registry | + +`CreateContentPack` wraps all of the above in a fluent builder that executes registered steps in insertion order when `Apply()` is called. + +This document keeps the overview short. For builder surface, manifests, fixed-entry ownership, and freeze behavior, see [Content Packs & Registries](/guide/content-packs-and-registries). + +--- + +::: + +## 注册接口{lang="zh-CN"} + +::: zh-CN + +| 接口 | 说明 | +|---|---| +| `RitsuLibFramework.CreateContentPack(modId)` | 推荐入口:流式内容包构建器 | +| `RitsuLibFramework.GetContentRegistry(modId)` | 底层内容注册器 | +| `RitsuLibFramework.GetKeywordRegistry(modId)` | 关键词注册器 | +| `RitsuLibFramework.GetTimelineRegistry(modId)` | Timeline(故事/纪元)注册器 | +| `RitsuLibFramework.GetUnlockRegistry(modId)` | 解锁规则注册器 | + +`CreateContentPack` 是推荐用法,将以上注册器封装为流式 API,调用 `Apply()` 时按添加顺序依次执行。 + +本文只保留总览层内容。关于构建器完整表面、清单式注册、固定条目标识归属和冻结机制,请阅读 [内容包与注册器](/guide/content-packs-and-registries)。 + +--- + +::: + +## Content Pack Builder{lang="en"} + +::: en + +All builder methods are chainable. A representative example: + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + .Character() + .Card() + .Relic() + .CardKeywordOwnedByLocNamespace("my_keyword", iconPath: "res://MyMod/art/kw.png") + .Story() + .Epoch() + .RequireEpoch() + .Custom(ctx => { /* ... */ }) + .Apply(); +``` + +`Apply()` returns `ModContentPackContext` for further access to individual registries. + +--- + +::: + +## 内容包构建器{lang="zh-CN"} + +::: zh-CN + +所有方法都支持链式调用,下面给出一个代表性示例: + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + .Character() + .Card() + .Relic() + .CardKeywordOwnedByLocNamespace("my_keyword", iconPath: "res://MyMod/art/kw.png") + .Story() + .Epoch() + .RequireEpoch() + .Custom(ctx => { /* 任意注册逻辑 */ }) + .Apply(); +``` + +`Apply()` 返回 `ModContentPackContext`,可用于进一步访问各注册器。 + +--- + +::: + +## Model ID Rule{lang="en"} + +::: en + +For any model registered through the RitsuLib content registry, `ModelId.Entry` uses: + +``` +__ +``` + +All segments are normalized to **UPPER_SNAKE_CASE**. + +### Examples (Mod id `MyMod`) + +| C# Type | Category | ModelId.Entry | +|---|---|---| +| `MyStrike` | card | `MY_MOD_CARD_MY_STRIKE` | +| `MyStarterRelic` | relic | `MY_MOD_RELIC_MY_STARTER_RELIC` | +| `MyCharacter` | character | `MY_MOD_CHARACTER_MY_CHARACTER` | + +> If two types under the same mod id and category share the same CLR name, they resolve to the same entry and must be renamed. + +--- + +::: + +## 模型 ID 规则{lang="zh-CN"} + +::: zh-CN + +通过 RitsuLib 注册的模型,其 `ModelId.Entry` 使用以下固定格式: + +``` +__ +``` + +每个字段规范化为**全大写、以下划线分隔**的标识符。 + +### 示例(Mod id `MyMod`) + +| C# 类型 | 类别 | ModelId.Entry | +|---|---|---| +| `MyStrike` | card | `MY_MOD_CARD_MY_STRIKE` | +| `MyStarterRelic` | relic | `MY_MOD_RELIC_MY_STARTER_RELIC` | +| `MyCharacter` | character | `MY_MOD_CHARACTER_MY_CHARACTER` | + +> 同一 Mod、同一类别下两个 CLR 类型名相同的模型会产生 Entry 冲突,必须通过重命名解决。 + +--- + +::: + +## Localization Rule{lang="en"} + +::: en + +Localization keys are written directly against the fixed `ModelId.Entry`: + +```json +{ + "MY_MOD_CARD_MY_STRIKE.title": "My Strike", + "MY_MOD_CARD_MY_STRIKE.description": "Deal {damage} damage.", + "MY_MOD_RELIC_MY_STARTER_RELIC.title": "My Starter Relic" +} +``` + +`RitsuLibFramework.CreateModLocalization(...)` operates independently from the game's `LocString` pipeline. + +--- + +::: + +## 本地化规则{lang="zh-CN"} + +::: zh-CN + +游戏本地化 Key 直接基于固定 `ModelId.Entry` 编写: + +```json +{ + "MY_MOD_CARD_MY_STRIKE.title": "我的打击", + "MY_MOD_CARD_MY_STRIKE.description": "造成 {damage} 点伤害。", + "MY_MOD_RELIC_MY_STARTER_RELIC.title": "我的起始遗物" +} +``` + +`RitsuLibFramework.CreateModLocalization(...)` 是独立的本地化工具,与游戏的 `LocString` 模型 Key 管线相互独立。 + +--- + +::: + +## Asset Override Rule{lang="en"} + +::: en + +RitsuLib applies template-based asset overrides via interface matching at render time. + +### Card Overrides + +Inherit `ModCardTemplate` and override via `AssetProfile` (recommended) or individual properties: + +```csharp +public class MyCard : ModCardTemplate(1, CardType.Attack, CardRarity.Common, TargetType.SingleEnemy) +{ + // Unified profile (recommended) + public override CardAssetProfile AssetProfile => new() + { + PortraitPath = "res://MyMod/art/my_card.png", + FramePath = "res://MyMod/art/frame.png", + FrameMaterialPath = "res://MyMod/art/frame.material", + }; + + // Or override a single property directly + public override string? CustomPortraitPath => "res://MyMod/art/my_card.png"; +} +``` + +Supported card fields include portrait, frame, portrait border, energy icon, overlay, and banner-related assets. + +### Other Content + +| Content type | Supported override fields | +|---|---| +| Relic | icon, icon outline, big icon | +| Power | icon, big icon | +| Orb | icon, visuals scene | +| Potion | image, outline | + +Override behavior: +1. The model must implement the matching override interface (directly or via `Mod*Template`) +2. The override member must return a non-empty path +3. If the resource path does not exist, RitsuLib emits a one-time warning and falls back to the base asset + +This warning behavior is especially important for character assets because the base game has almost no safe fallback for missing paths. + +For the full profile records, helper factories, placeholder behavior, and diagnostics policy, see [Asset Profiles & Fallbacks](/guide/asset-profiles-and-fallbacks). + +--- + +::: + +## 资源覆写规则{lang="zh-CN"} + +::: zh-CN + +RitsuLib 通过接口匹配,在渲染时将默认资源替换为 Mod 提供的资源。 + +### 卡牌资源覆写 + +继承 `ModCardTemplate` 后,通过 `AssetProfile`(推荐)或单独属性覆写: + +```csharp +public class MyCard : ModCardTemplate(1, CardType.Attack, CardRarity.Common, TargetType.SingleEnemy) +{ + // 统一通过 AssetProfile 配置(推荐) + public override CardAssetProfile AssetProfile => new() + { + PortraitPath = "res://MyMod/art/my_card.png", + FramePath = "res://MyMod/art/frame.png", + FrameMaterialPath = "res://MyMod/art/frame.material", + }; + + // 或单独覆写某一项 + public override string? CustomPortraitPath => "res://MyMod/art/my_card.png"; +} +``` + +卡牌支持的覆写大致包括 portrait、frame、portrait border、energy icon、overlay 与 banner 相关资源。 + +### 其他内容资源覆写 + +| 内容类型 | 支持字段 | +|---|---| +| Relic | icon、icon outline、big icon | +| Power | icon、big icon | +| Orb | 图标、视觉场景 | +| Potion | image、outline | + +覆写行为如下: +1. 模型必须实现对应的 override 接口(直接或通过 `Mod*Template`) +2. override 成员必须返回非空路径 +3. 如果资源路径不存在,RitsuLib 会输出一次警告,并回退到原始资源 + +这点对角色资源尤其重要,因为原版游戏对缺失角色资源几乎没有安全兜底。 + +完整资源配置结构、路径工厂辅助方法、占位角色规则与诊断策略见 [资源配置与回退规则](/guide/asset-profiles-and-fallbacks)。 + +--- + +::: + +## Registration Timing{lang="en"} + +::: en + +All content registration must be completed before the framework freezes content registration (during early game boot). Additional registration after the freeze is invalid and may throw. + +The freeze is signaled by `ContentRegistrationClosedEvent`. + +--- + +::: + +## 注册时机{lang="zh-CN"} + +::: zh-CN + +所有内容注册必须在框架冻结内容注册之前完成(游戏早期引导阶段)。冻结后继续注册属于无效操作并可能抛出异常。 + +冻结时触发的事件:`ContentRegistrationClosedEvent` + +--- + +::: + +## Compatibility{lang="en"} + +::: en + +The fixed-entry rule applies only to model types explicitly registered through the RitsuLib content registry, at `ModelDb.GetEntry(Type)`. Models not registered through RitsuLib are unaffected. + +--- + +::: + +## 兼容规则{lang="zh-CN"} + +::: zh-CN + +固定 Entry 规则**只作用于**通过 RitsuLib 内容注册器显式注册的模型类型,处理点为 `ModelDb.GetEntry(Type)`。未经 RitsuLib 注册的模型不受影响。 + +--- + +::: + +## Related Documents{lang="en"} + +::: en + +- [Getting Started](/guide/getting-started) +- [Content Packs & Registries](/guide/content-packs-and-registries) +- [Character & Unlock Templates](/guide/character-and-unlock-scaffolding) +- [Custom Events](/guide/custom-events) +- [Card Dynamic Variables](/guide/card-dynamic-var-toolkit) +- [Localization & Keywords](/guide/localization-and-keywords) +- [Framework Design](/guide/framework-design) +- [Asset Profiles & Fallbacks](/guide/asset-profiles-and-fallbacks) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [快速入门](/guide/getting-started) +- [内容包与注册器](/guide/content-packs-and-registries) +- [角色与解锁模板](/guide/character-and-unlock-scaffolding) +- [自定义事件](/guide/custom-events) +- [卡牌动态变量](/guide/card-dynamic-var-toolkit) +- [本地化与关键词](/guide/localization-and-keywords) +- [框架设计](/guide/framework-design) +- [资源配置与回退规则](/guide/asset-profiles-and-fallbacks) + +::: diff --git a/docs/pages/guide/content-packs-and-registries.md b/docs/pages/guide/content-packs-and-registries.md new file mode 100644 index 0000000..2bbefc0 --- /dev/null +++ b/docs/pages/guide/content-packs-and-registries.md @@ -0,0 +1,912 @@ +--- +title: + en: Content Packs & Registries + zh-CN: 内容包与注册器 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This document is the reference for how RitsuLib registration is organized. + +It covers: + +- the relationship between `CreateContentPack(...)` and the underlying registries +- what `Apply()` actually does +- when to use builder steps, manifests, direct registry access, or optional CLR attributes +- how fixed model identity and ModelDb integration relate to registration +- generated placeholders for cards/relics/potions (API, ordering, and risks) + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文是 RitsuLib 注册体系的参考文档。 + +它主要解释: + +- `CreateContentPack(...)` 与底层各个注册器的关系 +- `Apply()` 到底做了什么 +- 什么时候该用链式构建器、清单条目、直接调用注册器,或可选的 CLR 特性 +- 固定模型身份与 ModelDb 集成是怎样建立在注册之上的 +- 生成式占位(卡牌 / 遗物 / 药水)的 API、顺序与风险说明 + +--- + +::: + +## Registry Map{lang="en"} + +::: en + +RitsuLib keeps registration responsibilities split by concern: + +| Registry | Purpose | +|---|---| +| `ModContentRegistry` | Register models: characters, acts, pool-bound cards/relics/potions, powers, orbs, enchantments, afflictions, achievements, singletons, good/bad daily modifiers, shared card/relic/potion pools, events, ancients, monsters, and generated placeholders | +| `ModKeywordRegistry` | Register reusable keyword definitions | +| `ModTimelineRegistry` | Register stories and epochs | +| `ModUnlockRegistry` | Register epoch requirements and progression rules | + +`CreateContentPack(modId)` is the convenience layer that coordinates all four. + +--- + +::: + +## 注册器总览{lang="zh-CN"} + +::: zh-CN + +RitsuLib 按职责拆分了几类注册器: + +| 注册器 | 作用 | +|---|---| +| `ModContentRegistry` | 注册角色、Act、池内卡牌/遗物/药水、能力、球体、附魔(Enchantment)、苦难(Affliction)、成就、单例、好/坏每日修正、共享卡/遗物/药水池、事件、Ancient、怪物及生成式占位等模型 | +| `ModKeywordRegistry` | 注册可复用关键词定义 | +| `ModTimelineRegistry` | 注册 `Story` 与 `Epoch` | +| `ModUnlockRegistry` | 注册纪元门槛与进度解锁规则 | + +`CreateContentPack(modId)` 就是把这四类能力打包成一个更顺手的入口。 + +--- + +::: + +## `CreateContentPack(...)`{lang="en"} + +::: en + +The fluent builder is the recommended entry point: + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + .Character() + .Card() + .Relic() + .CardKeywordOwnedByLocNamespace("brew") + .Epoch() + .Story() + .RequireEpoch() + .Apply(); +``` + +What the builder does not do: + +- it does not auto-discover content by reflection +- it does not reorder your steps for you +- it does not replace the underlying registries + +It simply records registration steps and runs them in insertion order when `Apply()` is called. + +--- + +::: + +## `CreateContentPack(...)`{lang="zh-CN"} + +::: zh-CN + +推荐默认使用链式构建器: + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + .Character() + .Card() + .Relic() + .CardKeywordOwnedByLocNamespace("brew") + .Epoch() + .Story() + .RequireEpoch() + .Apply(); +``` + +但需要明确的是,它不会: + +- 自动反射扫描内容 +- 自动替你重排注册顺序 +- 取代底层注册器的存在 + +它只是把一系列注册步骤按加入顺序记录下来,并在 `Apply()` 时顺序执行。 + +--- + +::: + +## `ModContentPackContext`{lang="en"} + +::: en + +`Apply()` returns a `ModContentPackContext` containing: + +- `Content` +- `Keywords` +- `Timeline` +- `Unlocks` + +That means the fluent builder can be your main registration path, while still letting you access the raw registries afterward. + +--- + +::: + +## `ModContentPackContext`{lang="zh-CN"} + +::: zh-CN + +`Apply()` 返回 `ModContentPackContext`,里面包含: + +- `Content` +- `Keywords` +- `Timeline` +- `Unlocks` + +也就是说,构建器可以作为主要入口,同时你在需要时仍然可以拿到原始注册器继续操作。 + +--- + +::: + +## Step Ordering{lang="en"} + +::: en + +Builder steps execute in the order you add them. + +That matters when: + +- your custom step expects a registry entry to already exist +- you mix builder calls with `Custom(ctx => ...)` +- you want logs to reflect a specific setup flow + +`CreateContentPack` is intentionally explicit here. It is a sequenced registration script, not a dependency solver. + +--- + +::: + +## 步骤顺序{lang="zh-CN"} + +::: zh-CN + +构建器中的步骤严格按添加顺序执行。 + +这点在以下场景会很重要: + +- 某个 `Custom(ctx => ...)` 依赖前面已经注册的内容 +- 你希望日志顺序能准确反映初始化流程 +- 你在同一个 chain 中混合内容注册与自定义逻辑 + +`CreateContentPack` 故意保持显式,它是“顺序执行的注册脚本”,而不是“自动推断依赖关系的求解器”。 + +--- + +::: + +## Builder Surface{lang="en"} + +::: en + +The builder supports several kinds of steps: + +- content model registration +- keyword registration +- timeline registration +- unlock registration +- manifest-driven registration +- arbitrary custom callbacks + +Less obvious helpers that are still useful: + +- `Entry(IContentRegistrationEntry)` +- `Entries(IEnumerable)` +- `Keyword(KeywordRegistrationEntry)` +- `Keywords(IEnumerable)` +- `Manifest(contentEntries, keywordEntries)` +- `Custom(Action)` +- generated placeholders: `PlaceholderCard(...)`, `PlaceholderRelic(...)`, `PlaceholderPotion(...)` (see “Generated placeholder content” below) +- extended standalone / pool types: `.Enchantment()`, `.Affliction()`, `.Achievement()`, `.Singleton()`, `.GoodModifier()` / `.BadModifier()`, `.SharedRelicPool()`, `.SharedPotionPool()` (see “Content model registration matrix” below) + +These are useful when you want registration declared as data instead of written inline in one long chain. + +--- + +::: + +## 构建器能做什么{lang="zh-CN"} + +::: zh-CN + +构建器支持的步骤大致包括: + +- 内容模型注册 +- 关键词注册 +- 时间线注册 +- 解锁注册 +- 清单式注册 +- 任意自定义回调 + +一些不那么显眼,但很实用的入口包括: + +- `Entry(IContentRegistrationEntry)` +- `Entries(IEnumerable)` +- `Keyword(KeywordRegistrationEntry)` +- `Keywords(IEnumerable)` +- `Manifest(contentEntries, keywordEntries)` +- `Custom(Action)` +- 生成式占位:`PlaceholderCard(...)`、`PlaceholderRelic(...)`、`PlaceholderPotion(...)`(详见下文「生成式占位内容」) +- 扩展的单体/池类型:`.Enchantment()`、`.Affliction()`、`.Achievement()`、`.Singleton()`、`.GoodModifier()` / `.BadModifier()`、`.SharedRelicPool()`、`.SharedPotionPool()`(详见下文「内容模型注册速查表」) + +如果你希望“注册声明本身也是数据”,这些入口会很好用。 + +--- + +::: + +## When To Use The Raw Registries{lang="en"} + +::: en + +Use `CreateContentPack(...)` by default. + +Use raw registries directly when: + +- registration is split across several modules +- you want to expose registration helpers from your own library layer +- you need registry access without committing to a single fluent chain +- you are generating registration entries programmatically + +Typical direct access looks like: + +```csharp +var content = RitsuLibFramework.GetContentRegistry("MyMod"); +content.RegisterCharacter(); + +var timeline = RitsuLibFramework.GetTimelineRegistry("MyMod"); +timeline.RegisterEpoch(); +``` + +The registries are first-class APIs, not implementation details. + +--- + +::: + +## 什么时候直接使用注册器{lang="zh-CN"} + +::: zh-CN + +默认优先使用 `CreateContentPack(...)`。 + +但以下情况直接使用注册器更合适: + +- 注册逻辑拆分在多个模块里 +- 你希望在自己的前置库里再包装一层 API +- 你不想把所有注册都塞进一条长链 +- 你要程序化生成注册项 + +典型写法如下: + +```csharp +var content = RitsuLibFramework.GetContentRegistry("MyMod"); +content.RegisterCharacter(); + +var timeline = RitsuLibFramework.GetTimelineRegistry("MyMod"); +timeline.RegisterEpoch(); +``` + +这些注册器是一等公民 API,不是构建器背后的私有实现细节。 + +--- + +::: + +## What The Content Registry Owns{lang="en"} + +::: en + +`ModContentRegistry` is responsible for: + +- recording which model types belong to which mod +- validating ownership and duplicate registration +- feeding ModelDb integration: global accessors such as `AllCharacters`, acts, powers, orbs, shared events, ancients, **shared card / relic / potion pool types**, `DebugEnchantments`, `DebugAfflictions`, `Achievements`, `GoodModifiers`, `BadModifiers`, and related enumerations are extended via patches where needed; **per-pool** cards/relics/potions are merged through `ModHelper.AddModelToPool` when each pool expands `AllCards` / `AllRelics` / `AllPotions` (a different code path than those global appenders) +- generating fixed public `ModelId.Entry` values for registered types + +That owner tracking is what lets RitsuLib safely answer questions like: + +- which mod registered this type? +- what should its fixed public entry be? +- should vanilla progression/compatibility logic treat this as modded content? + +--- + +::: + +## 内容注册器的职责{lang="zh-CN"} + +::: zh-CN + +`ModContentRegistry` 主要负责: + +- 记录某个模型类型归属于哪个 Mod +- 校验重复注册与冲突 +- 为 ModelDb 补丁与其它集成点提供数据:例如向 `AllCharacters`、Act、能力、球体、共享事件、Ancient、**共享卡池 / 遗物池 / 药水池类型**、`DebugEnchantments`、`DebugAfflictions`、`Achievements`、`GoodModifiers`、`BadModifiers` 等访问器在需要时追加已注册模型;卡牌/遗物/药水进入**具体池**则通过 `ModHelper.AddModelToPool` 在池展开 `AllCards` / `AllRelics` / `AllPotions` 时合并(与上述全局追加不是同一条实现路径) +- 为已注册类型生成固定公开 `ModelId.Entry` + +这套归属跟踪很关键,因为它让 RitsuLib 可以安全回答这些问题: + +- 某个类型是谁注册的? +- 它的固定公开条目标识应该是什么? +- 某些兼容逻辑是否应该把它当作 Mod 内容处理? + +--- + +::: + +## Fixed Public Identity{lang="en"} + +::: en + +For RitsuLib-registered models, public `ModelId.Entry` is forced into a stable format: + +```text +__ +``` + +This is applied through the ModelDb identity patch, not by changing your CLR type names at source. + +Why it matters: + +- localization keys become deterministic +- default asset conventions become predictable +- model ownership remains clear across patches and saves + +The identity rule applies only to types explicitly registered through RitsuLib. + +--- + +::: + +## 固定公开身份{lang="zh-CN"} + +::: zh-CN + +对于通过 RitsuLib 注册的模型,公开 `ModelId.Entry` 会被强制成稳定格式: + +```text +__ +``` + +这不是靠改你源码里的类型名实现的,而是通过 ModelDb 身份补丁在公开入口上统一的。 + +这么做的意义在于: + +- 本地化 Key 可预测 +- 默认资源路径约定更稳定 +- 补丁、存档、兼容逻辑里都更容易识别内容归属 + +这条规则只作用于显式通过 RitsuLib 注册的类型。 + +--- + +::: + +## ModelDb Integration{lang="en"} + +::: en + +Registration alone is not enough; the game still needs to see the content. + +RitsuLib patches ModelDb and related model access points to: + +- append registered characters, acts, powers, orbs, events, ancients, shared card pools, **shared relic pools** (`AllRelicPools`), **shared potion pools** (`AllPotionPools`), **debug enchantments** (`DebugEnchantments`), **debug afflictions** (`DebugAfflictions`), **achievements** (`Achievements`), and **daily modifiers** (`GoodModifiers` / `BadModifiers`) where applicable +- attach registered cards/relics/potions to their **target pools** via `ModHelper.AddModelToPool` (concatenated when each pool materializes its `All*` sequence) +- force fixed public entries for registered model types +- inject types that live in **dynamic assemblies** (e.g. Reflection.Emit placeholders) into `ModelDb` before init completes, for every registered model category the registry tracks +- bootstrap dynamic act-content patching before caches lock in + +`MutuallyExclusiveModifiers` is **not** extended automatically; mod modifiers registered as good/bad appear only in those two lists. + +This is why registration must happen before the framework freeze points. + +--- + +::: + +## ModelDb 集成{lang="zh-CN"} + +::: zh-CN + +仅仅完成注册还不够,游戏本身还必须“看得到”这些内容。 + +RitsuLib 通过对 ModelDb 及相关访问点打补丁来完成这件事,包括: + +- 追加已注册的角色、Act、能力、球体、事件、Ancient、共享卡池、**共享遗物池**(`AllRelicPools`)、**共享药水池**(`AllPotionPools`)、**调试用附魔**(`DebugEnchantments`)、**调试用苦难**(`DebugAfflictions`)、**成就**(`Achievements`)、**每日修正**(`GoodModifiers` / `BadModifiers`)等 +- 将已注册卡牌/遗物/药水等与**目标池**绑定(`ModHelper.AddModelToPool`,在对应池的 `All*` 枚举中与原版生成结果拼接) +- 对已注册模型类型强制固定公开条目标识 +- 在 `ModelDb` 初始化完成前,把注册器跟踪到的、位于**动态程序集**中的类型(例如 Reflection.Emit 占位)注入 `_contentById` +- 在缓存锁定前引导动态 Act 内容补丁 + +`MutuallyExclusiveModifiers` **不会**自动扩展;通过好/坏列表注册的 Mod 修正只会出现在上述两个列表中。 + +这也是为什么注册必须发生在框架冻结之前。 + +--- + +::: + +## Freeze Behavior{lang="en"} + +::: en + +The relevant registries freeze after early initialization: + +- content registration freeze +- timeline registration freeze +- unlock registration freeze + +Once frozen, later registration attempts throw. + +This is intentional because the framework wants: + +- stable identity +- stable model lists +- deterministic unlock/filter behavior + +If a mod registers content late, the safest outcome is to fail early rather than let the game build partial caches. + +--- + +::: + +## Freeze 行为{lang="zh-CN"} + +::: zh-CN + +几个关键注册器都会在早期初始化后冻结: + +- 内容注册冻结 +- 时间线注册冻结 +- 解锁注册冻结 + +冻结之后再注册会直接抛异常。 + +这是有意为之,因为框架追求的是: + +- 身份稳定 +- 模型列表稳定 +- 解锁/过滤行为稳定 + +如果某个 Mod 在太晚的时候才注册内容,最安全的结果就是尽早失败,而不是让游戏带着半成品缓存继续跑下去。 + +--- + +::: + +## Manifests And Entry Objects{lang="en"} + +::: en + +If you want registration to be declared as data, you can package it into entry objects: + +```csharp +var contentEntries = new IContentRegistrationEntry[] +{ + new CharacterRegistrationEntry(), + new CardRegistrationEntry(), +}; + +var keywordEntries = new[] +{ + KeywordRegistrationEntry.OwnedCardByLocNamespace("MyMod", "brew"), +}; + +RitsuLibFramework.CreateContentPack("MyMod") + .Manifest(contentEntries, keywordEntries) + .Apply(); +``` + +This is useful when you want a declarative registration list or want to share registration bundles across modules. + +You can mix entry types freely—for example: + +```csharp +var contentEntries = new IContentRegistrationEntry[] +{ + new CharacterRegistrationEntry(), + new CardRegistrationEntry(), + new EnchantmentRegistrationEntry(), + new PowerRegistrationEntry(), + new SharedRelicPoolRegistrationEntry(), +}; +``` + +--- + +::: + +## Manifest 与 Entry 对象{lang="zh-CN"} + +::: zh-CN + +如果你希望把注册描述成数据,可以使用注册条目对象: + +```csharp +var contentEntries = new IContentRegistrationEntry[] +{ + new CharacterRegistrationEntry(), + new CardRegistrationEntry(), +}; + +var keywordEntries = new[] +{ + KeywordRegistrationEntry.OwnedCardByLocNamespace("MyMod", "brew"), +}; + +RitsuLibFramework.CreateContentPack("MyMod") + .Manifest(contentEntries, keywordEntries) + .Apply(); +``` + +这对“声明式注册列表”或“跨模块复用注册清单”的场景会很方便。 + +也可以混用多种 `IContentRegistrationEntry`,例如: + +```csharp +var contentEntries = new IContentRegistrationEntry[] +{ + new CharacterRegistrationEntry(), + new CardRegistrationEntry(), + new EnchantmentRegistrationEntry(), + new PowerRegistrationEntry(), + new SharedRelicPoolRegistrationEntry(), +}; +``` + +--- + +::: + +## Attribute-based registration (optional){lang="en"} + +::: en + +CLR attributes in `STS2RitsuLib.Interop.AutoRegistration` (for example `[RegisterSharedCardPool]`, `[RegisterCard(typeof(MyPool))]`) ultimately call the **same registry APIs** as the fluent builder, direct registries, and manifest entries. + +RitsuLib runs them during the early **mod type discovery** pass (`ModTypeDiscoveryPatch`). The built-in `AttributeAutoRegistrationTypeDiscoveryContributor` scans **concrete** CLR types in assemblies you register with **`ModTypeDiscoveryHub.RegisterModAssembly(modId, Assembly.GetExecutingAssembly())`** from your mod initializer **before** `PatchAll`. A type must resolve to a mod id (usually via the manifest-mapped assembly); if not, annotate the type with **`[RitsuLibOwnedBy("modId")]`**. + +This does **not** replace `CreateContentPack(...)`; it is an alternative authoring style. Mixing approaches is acceptable when ordering and freeze rules remain valid. + +### `Inherit` on `AutoRegistrationAttribute` + +Attributes apply to the type they annotate. **`Inherit`** defaults to **`false`**. When **`Inherit = true`** on an attribute declared on a **base class**, **concrete derived types** are handled as if the same attribute were declared on each subclass (the registry still receives the **subclass** `Type`). If a subclass already has a **direct** attribute that would produce the **same registration signature**, the inherited duplicate is skipped. Abstract base types are skipped by the scan; only concrete types are registered. + +--- + +::: + +## CLR 特性注册(可选){lang="zh-CN"} + +::: zh-CN + +`STS2RitsuLib.Interop.AutoRegistration` 下的特性(例如 `[RegisterSharedCardPool]`、`[RegisterCard(typeof(MyPool))]`)最终会调用与链式构建器、清单和**直接注册器**相同的底层 API。 + +它们在 RitsuLib 的早期 **Mod 类型发现** 阶段执行(`ModTypeDiscoveryPatch`):内置的 `AttributeAutoRegistrationTypeDiscoveryContributor` 会扫描你已用 **`ModTypeDiscoveryHub.RegisterModAssembly(modId, Assembly.GetExecutingAssembly())`** 登记的程序集中的**具体** CLR 类型(在 `PatchAll` **之前**于 Mod 初始化器里调用)。类型必须能解析到某个 mod 身份(通常由 manifest 映射到程序集);否则可在类型上使用 **`[RitsuLibOwnedBy("modId")]`**。 + +这**不代替** `CreateContentPack(...)`,只是另一种编写方式。只要注册顺序与冻结时机仍合法,可以与链式/清单混用。 + +### `AutoRegistrationAttribute.Inherit` + +特性默认只作用于其标注的类型。**`Inherit`** 默认为 **`false`**。在**基类**上将某特性设为 **`Inherit = true`** 时,**具体子类**会按「若子类自身也写了同一条特性」的方式处理(即仍以**子类的** `Type` 调用同一套注册 API)。若子类已有**直接**声明、且会产生**相同注册签名**的特性,则不再重复应用继承来的同签名项。扫描会跳过抽象基类,仅具体类型会进入注册流程。 + +--- + +::: + +## Content model registration matrix{lang="en"} + +::: en + +Every row below is **one conceptual kind of content**. You can register it in **three** primary equivalent ways below, plus the optional attribute path in the previous section (unless noted): + +1. **Fluent** — `ModContentPackBuilder` method on `CreateContentPack(...)` +2. **Registry** — `ModContentRegistry` method from `RitsuLibFramework.GetContentRegistry(modId)` or `ctx.Content` in `Custom(...)` +3. **Manifest entry** — a type implementing `IContentRegistrationEntry` in `STS2RitsuLib.Scaffolding.Content` (use `.Entry(...)`, `.Entries(...)`, or `.Manifest(...)`) + +| Content | Fluent | Registry | Manifest entry | +|---|---|---|---| +| Character | `.Character()` | `RegisterCharacter()` | `CharacterRegistrationEntry` | +| Act | `.Act()` | `RegisterAct()` | `ActRegistrationEntry` | +| Card in pool | `.Card(...)` | `RegisterCard(...)` | `CardRegistrationEntry` | +| Relic in pool | `.Relic(...)` | `RegisterRelic(...)` | `RelicRegistrationEntry` | +| Potion in pool | `.Potion(...)` | `RegisterPotion(...)` | `PotionRegistrationEntry` | +| Power | `.Power()` | `RegisterPower()` | `PowerRegistrationEntry` | +| Orb | `.Orb()` | `RegisterOrb()` | `OrbRegistrationEntry` | +| Enchantment | `.Enchantment()` | `RegisterEnchantment()` | `EnchantmentRegistrationEntry` | +| Affliction | `.Affliction()` | `RegisterAffliction()` | `AfflictionRegistrationEntry` | +| Achievement | `.Achievement()` | `RegisterAchievement()` | `AchievementRegistrationEntry` | +| Singleton | `.Singleton()` | `RegisterSingleton()` | `SingletonRegistrationEntry` | +| Daily modifier (good) | `.GoodModifier()` | `RegisterGoodModifier()` | `GoodModifierRegistrationEntry` | +| Daily modifier (bad) | `.BadModifier()` | `RegisterBadModifier()` | `BadModifierRegistrationEntry` | +| Shared card pool | `.SharedCardPool()` | `RegisterSharedCardPool()` | `SharedCardPoolRegistrationEntry` | +| Shared relic pool | `.SharedRelicPool()` | `RegisterSharedRelicPool()` | `SharedRelicPoolRegistrationEntry` | +| Shared potion pool | `.SharedPotionPool()` | `RegisterSharedPotionPool()` | `SharedPotionPoolRegistrationEntry` | +| Shared event | `.SharedEvent()` | `RegisterSharedEvent()` | `SharedEventRegistrationEntry` | +| Act encounter | `.ActEncounter()` | `RegisterActEncounter()` | `ActEncounterRegistrationEntry` | +| Act event | `.ActEvent()` | `RegisterActEvent()` | `ActEventRegistrationEntry` | +| Shared ancient | `.SharedAncient()` | `RegisterSharedAncient()` | `SharedAncientRegistrationEntry` | +| Act ancient | `.ActAncient()` | `RegisterActAncient()` | `ActAncientRegistrationEntry` | +| Monster | *(no fluent helper)* | `RegisterMonster()` | `MonsterRegistrationEntry` | +| Placeholder card / relic / potion | `.PlaceholderCard<...>(...)` etc. | `RegisterPlaceholderCard<...>(...)` etc. | `PlaceholderCardRegistrationEntry<...>` etc. | +| Archaic Tooth mapping | `.ArchaicToothTranscendence<...>()` or `.ArchaicToothTranscendence(id, type)` | `RitsuLibFramework.RegisterArchaicToothTranscendenceMapping(...)` | `ArchaicToothTranscendenceRegistrationEntry<...>` / `ArchaicToothTranscendenceByIdRegistrationEntry` | +| Touch of Orobas mapping | `.TouchOfOrobasRefinement<...>()` or `.TouchOfOrobasRefinement(id, type)` | `RitsuLibFramework.RegisterTouchOfOrobasRefinementMapping(...)` | `TouchOfOrobasRefinementRegistrationEntry<...>` / `TouchOfOrobasRefinementByIdRegistrationEntry` | + +**Enchantments:** optional authoring baseline `ModEnchantmentTemplate` plus `IModEnchantmentAssetOverrides` / `EnchantmentIntendedIconPathPatch` (see scaffolding content patches) for custom icon paths; registration in this table is still required for ownership, fixed `ModelId.Entry`, and dynamic-assembly injection like other model kinds. + +**Singletons:** there is no global `ModelDb` list to patch; registration still records ownership and injects dynamic types so `ModelDb.Singleton()` resolves correctly. + +--- + +::: + +## 内容模型注册速查表{lang="zh-CN"} + +::: zh-CN + +下表中每一行是一种**内容类别**。可主要用下面**三种**等价方式登记,另可加前一节所述的**可选特性路径**(另有注明的除外): + +1. **链式**:`CreateContentPack(...)` 上的 `ModContentPackBuilder` 方法 +2. **注册器**:`RitsuLibFramework.GetContentRegistry(modId)` 或 `Custom(ctx => ctx.Content...)` 上的 `ModContentRegistry` 方法 +3. **Manifest 条目**:`STS2RitsuLib.Scaffolding.Content` 中实现 `IContentRegistrationEntry` 的类型,经 `.Entry(...)`、`.Entries(...)` 或 `.Manifest(...)` 应用 + +| 内容 | 链式 | 注册器 | Manifest 条目 | +|---|---|---|---| +| 角色 | `.Character()` | `RegisterCharacter()` | `CharacterRegistrationEntry` | +| Act | `.Act()` | `RegisterAct()` | `ActRegistrationEntry` | +| 池内卡牌 | `.Card(...)` | `RegisterCard(...)` | `CardRegistrationEntry` | +| 池内遗物 | `.Relic(...)` | `RegisterRelic(...)` | `RelicRegistrationEntry` | +| 池内药水 | `.Potion(...)` | `RegisterPotion(...)` | `PotionRegistrationEntry` | +| 能力 | `.Power()` | `RegisterPower()` | `PowerRegistrationEntry` | +| 球体 | `.Orb()` | `RegisterOrb()` | `OrbRegistrationEntry` | +| 附魔 | `.Enchantment()` | `RegisterEnchantment()` | `EnchantmentRegistrationEntry` | +| 苦难 | `.Affliction()` | `RegisterAffliction()` | `AfflictionRegistrationEntry` | +| 成就 | `.Achievement()` | `RegisterAchievement()` | `AchievementRegistrationEntry` | +| 单例 | `.Singleton()` | `RegisterSingleton()` | `SingletonRegistrationEntry` | +| 每日修正(好) | `.GoodModifier()` | `RegisterGoodModifier()` | `GoodModifierRegistrationEntry` | +| 每日修正(坏) | `.BadModifier()` | `RegisterBadModifier()` | `BadModifierRegistrationEntry` | +| 共享卡池 | `.SharedCardPool()` | `RegisterSharedCardPool()` | `SharedCardPoolRegistrationEntry` | +| 共享遗物池 | `.SharedRelicPool()` | `RegisterSharedRelicPool()` | `SharedRelicPoolRegistrationEntry` | +| 共享药水池 | `.SharedPotionPool()` | `RegisterSharedPotionPool()` | `SharedPotionPoolRegistrationEntry` | +| 共享事件 | `.SharedEvent()` | `RegisterSharedEvent()` | `SharedEventRegistrationEntry` | +| Act 遭遇 | `.ActEncounter()` | `RegisterActEncounter()` | `ActEncounterRegistrationEntry` | +| Act 事件 | `.ActEvent()` | `RegisterActEvent()` | `ActEventRegistrationEntry` | +| 共享 Ancient | `.SharedAncient()` | `RegisterSharedAncient()` | `SharedAncientRegistrationEntry` | +| Act Ancient | `.ActAncient()` | `RegisterActAncient()` | `ActAncientRegistrationEntry` | +| 怪物 | *(无链式封装)* | `RegisterMonster()` | `MonsterRegistrationEntry` | +| 占位卡牌/遗物/药水 | `.PlaceholderCard<...>(...)` 等 | `RegisterPlaceholderCard<...>(...)` 等 | `PlaceholderCardRegistrationEntry<...>` 等 | +| Archaic Tooth 映射 | `.ArchaicToothTranscendence<...>()` 或 `.ArchaicToothTranscendence(id, type)` | `RitsuLibFramework.RegisterArchaicToothTranscendenceMapping(...)` | `ArchaicToothTranscendenceRegistrationEntry<...>` / `ArchaicToothTranscendenceByIdRegistrationEntry` | +| Touch of Orobas 映射 | `.TouchOfOrobasRefinement<...>()` 或 `.TouchOfOrobasRefinement(id, type)` | `RitsuLibFramework.RegisterTouchOfOrobasRefinementMapping(...)` | `TouchOfOrobasRefinementRegistrationEntry<...>` / `TouchOfOrobasRefinementByIdRegistrationEntry` | + +**附魔:**可选用脚手架里的 `ModEnchantmentTemplate`、`IModEnchantmentAssetOverrides` 与 `EnchantmentIntendedIconPathPatch` 自定义图标路径;上表中的注册仍负责归属、固定 `ModelId.Entry` 以及与别类模型一致的动态程序集注入。 + +**单例:**本体没有可补丁的「全局单例列表」;注册仍用于归属与动态类型注入,以便 `ModelDb.Singleton()` 能正确解析。 + +--- + +::: + +## Generated placeholder content{lang="en"} + +::: en + +Use this when you want pool entries and a **stable public `ModelId.Entry`** (via `ModelPublicEntryOptions.FromStem` / `FromFullPublicEntry`) **without authoring one CLR type per card/relic/potion**—for example so reward tables, unlocks, or saves can reference IDs while content is still WIP. RitsuLib generates sealed subclasses at runtime with **Reflection.Emit**; gameplay is intentionally **no-op** (empty `OnPlay` / `OnUse`, etc.). + +### API summary + +| Use case | Entry point | +|---|---| +| Fluent pack | `PlaceholderCard(stableEntryStem, PlaceholderCardDescriptor)`, `PlaceholderRelic(...)`, `PlaceholderPotion(...)` | +| Registry | `ModContentRegistry.RegisterPlaceholderCard(...)` (overloads accept `ModelPublicEntryOptions`, e.g. `FromFullPublicEntry`) | +| Shape | `PlaceholderCardDescriptor`, `PlaceholderRelicDescriptor`, `PlaceholderPotionDescriptor` (structs with defaults) | +| You already have a type | Two-type overload `PlaceholderCard(stem)` only pins the entry for an existing class | + +`ModPlaceholderCardTemplate` / `ModPlaceholderRelicTemplate` / `ModPlaceholderPotionTemplate` are bases for emitted types; **mods normally should not subclass them** unless you have an advanced reason. + +### Example + +```csharp +using MegaCrit.Sts2.Core.Entities.Cards; +using STS2RitsuLib.Content; + +RitsuLibFramework.CreateContentPack("MyMod") + .Manifest(contentEntries, keywordEntries) + .Custom(ctx => + { + ctx.Content.RegisterPlaceholderCard("wip_reward_attack", + new PlaceholderCardDescriptor( + BaseCost: 1, + Type: CardType.Attack, + Rarity: CardRarity.Common, + Target: TargetType.AnyEnemy)); + }) + .Apply(); +``` + +For relics, `PlaceholderRelicDescriptor.MerchantCostOverride`: **`< 0` (default `-1`)** keeps rarity-based shop pricing; **`≥ 0`** overrides `MerchantCost`. + +### Ordering + +If you combine `Manifest(...)` with placeholders, register placeholders **after** prerequisites exist (typical pattern: `.Manifest(...)` then `.Custom(ctx => ...)` calling `RegisterPlaceholder*`), so pools and other types are already registered. + +--- + +### Warnings (read carefully) + +> **Saves and entry stability** +> Once a placeholder id appears in saves or unlock data, its `ModelId.Entry` (from the stem or `FromFullPublicEntry`) is a long-lived contract. **Renaming stems or full-entry strings** can break old saves or unlock references. When shipping real content, keep the same entry or plan a migration. + +> **No gameplay effects** +> Placeholders do not implement damage, draw, relic triggers, etc. They prevent missing-model failures in some paths; **balance and UX can still be wrong** until you replace them with real types. + +> **Localization and assets** +> Placeholders still follow default loc-key and asset conventions from the entry. Missing translations or art may show raw keys or blanks—that is expected and does not mean registration failed. + +> **Multiplayer and `ModelIdSerializationCache.Hash`** +> Emitted types are **not** returned by the game’s vanilla `AllAbstractModelSubtypes` scan. RitsuLib injects dynamic-assembly models before `ModelDb.Init` and, after `ModelIdSerializationCache.Init`, **merges every model present in `ModelDb` into the net-ID tables and recomputes the hash** (same algorithm shape as vanilla). +> **Consequence**: different loaded mod sets → different hashes → clients **may not match** for multiplayer or replays. This is inherent to dynamic placeholders, not only a single-player concern. + +> **RitsuLib version coupling** +> Placeholder generation, `InjectDynamicRegisteredModels`, and serialization-cache integration follow the framework version you ship. Pin a compatible `STS2-RitsuLib` dependency and retest after upgrading the library. + +--- + +::: + +## 生成式占位内容{lang="zh-CN"} + +::: zh-CN + +用于在**尚未为每张牌 / 每个遗物 / 每个药水编写独立 CLR 类型**时,仍能注册进池子并获得**稳定、可预测的公开 `ModelId.Entry`**(与 `ModelPublicEntryOptions.FromStem` / `FromFullPublicEntry` 一致),以便奖励表、解锁、存档引用等流程先跑通。占位模型由 RitsuLib 在运行时通过 **Reflection.Emit** 生成密封子类,逻辑上为**无效果**(卡牌 `OnPlay`、药水 `OnUse` 等为空操作)。 + +### API 概要 + +| 场景 | 推荐入口 | +|---|---| +| 链式内容包 | `PlaceholderCard(stableEntryStem, PlaceholderCardDescriptor)`、`PlaceholderRelic(...)`、`PlaceholderPotion(...)` | +| 直接注册器 | `ModContentRegistry.RegisterPlaceholderCard(...)` 等;重载可传入 `ModelPublicEntryOptions`(例如 `FromFullPublicEntry`) | +| 形状参数 | `PlaceholderCardDescriptor`、`PlaceholderRelicDescriptor`、`PlaceholderPotionDescriptor`(结构体,带默认值,按需覆盖费用、类型、稀有度、目标等) | +| 仍自带 CLR 类型时 | 保留 `PlaceholderCard(stem)` 双泛型重载:仅为已有类型固定 entry,不生成新类型 | + +框架内部的 `ModPlaceholderCardTemplate` / `ModPlaceholderRelicTemplate` / `ModPlaceholderPotionTemplate` 供生成类型继承;**一般不必在 Mod 里再继承它们**,除非你有特殊手写需求。 + +### 示例 + +```csharp +using MegaCrit.Sts2.Core.Entities.Cards; +using STS2RitsuLib.Content; + +RitsuLibFramework.CreateContentPack("MyMod") + .Manifest(contentEntries, keywordEntries) + .Custom(ctx => + { + ctx.Content.RegisterPlaceholderCard("wip_reward_attack", + new PlaceholderCardDescriptor( + BaseCost: 1, + Type: CardType.Attack, + Rarity: CardRarity.Common, + Target: TargetType.AnyEnemy)); + }) + .Apply(); +``` + +遗物描述体中的 `MerchantCostOverride`:为 **`< 0`(默认 `-1`)** 时表示沿用稀有度默认商人价;**`≥ 0`** 时覆盖 `MerchantCost`。 + +### 与初始化顺序 + +若同时使用 `Manifest(...)` 与占位注册,请把占位步骤放在**已具备池类型等前置注册之后**(常见写法是在链上 `.Manifest(...)` 之后接 `.Custom(ctx => ...)` 调用 `RegisterPlaceholder*`),避免依赖尚未注册的池或角色。 + +--- + +### 警告(请务必阅读) + +> **存档与 Entry 稳定性** +> 占位一旦进入存档或解锁数据,其 `ModelId.Entry`(由 stem 或 `FromFullPublicEntry` 决定)即成为长期契约。**改名 / 改 stem / 改 `FromFullPublicEntry` 字符串**可能导致旧档、旧解锁引用失效。正式内容落地时,要么长期保留同一 entry,要么做迁移/兼容策略。 + +> **无玩法效果** +> 占位不会替你实现伤害、抽牌、遗物触发等。仅保证模型存在、池子能展开、部分 UI/流程不因缺模型而崩溃;**平衡与体验仍可能异常**,需尽快替换为实作类型。 + +> **本地化与资源** +> 占位仍使用基于 entry 的默认本地化键与资源路径约定;若未提供对应翻译或贴图,界面可能出现键名或缺图,这属于预期现象,不等于框架未注册成功。 + +> **联机与 `ModelIdSerializationCache.Hash`** +> 生成类型**不会**出现在游戏原生的 `AllAbstractModelSubtypes` 扫描结果中。RitsuLib 会在 `ModelDb.Init` 前注入动态程序集中的已注册模型,并在 `ModelIdSerializationCache.Init` 之后**把 `ModelDb` 中实际存在的模型一并并入联机序列化表并重算 Hash**。 +> **后果**:加载的 Mod 组合不同 → Hash 不同 → 与未使用占位/未使用相同 Mod 列表的客户端**可能无法联机或回放一致**。这是使用动态占位时的固有风险,而非单机独有。 + +> **依赖 RitsuLib 版本** +> 占位、`InjectDynamicRegisteredModels`、序列化缓存补丁等行为随 RitsuLib 演进;请为 Mod 声明合适的 `STS2-RitsuLib` 依赖版本,并在升级前置库后回归测试。 + +--- + +::: + +## Recommended Registration Pattern{lang="en"} + +::: en + +For most mods: + +1. create one content pack in the mod initializer +2. register all content, keywords, timeline nodes, and unlock rules there +3. keep `Custom(...)` steps small and explicit +4. avoid late registration from gameplay hooks +5. with `TypeListCardPoolModel`, register pool cards via `.Card()` or `CardRegistrationEntry`; **do not** override the obsolete `CardTypes` hook (the base already defaults to empty—see [Getting Started](/guide/getting-started)) + +If the mod grows large, keep the builder at the top level and feed it entry objects or helper methods from submodules. + +--- + +::: + +## 推荐注册模式{lang="zh-CN"} + +::: zh-CN + +对大多数 Mod,建议这样组织: + +1. 在初始化入口中创建一个内容包 +2. 在其中注册所有内容、关键词、时间线节点与解锁规则 +3. `Custom(...)` 保持小而显式 +4. 不要把注册拖到运行期 hook 再做 +5. 使用 `TypeListCardPoolModel` 时,用 `.Card<池, 牌>()` 或 `CardRegistrationEntry` 登记池内牌;**不要**覆写已过时的 `CardTypes`(基类已默认空序列,详见 [快速入门](/guide/getting-started)) + +如果 Mod 很大,可以保留一个顶层构建器,再由子模块提供注册条目对象或辅助方法。 + +--- + +::: + +## Related Documents{lang="en"} + +::: en + +- [Content Authoring Toolkit](/guide/content-authoring-toolkit) +- [Timeline & Unlocks](/guide/timeline-and-unlocks) +- [Framework Design](/guide/framework-design) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [内容注册规则](/guide/content-authoring-toolkit) +- [时间线与解锁](/guide/timeline-and-unlocks) +- [框架设计](/guide/framework-design) + +::: diff --git a/docs/pages/guide/creature-visuals-and-animation.md b/docs/pages/guide/creature-visuals-and-animation.md new file mode 100644 index 0000000..ec0edd1 --- /dev/null +++ b/docs/pages/guide/creature-visuals-and-animation.md @@ -0,0 +1,736 @@ +--- +title: + en: Creature Visuals & Animation + zh-CN: 生物体视觉与动画 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This document covers the runtime-Godot factory interfaces that let mod creatures +replace vanilla `CreateVisuals` / `GenerateAnimator`, and the backend-agnostic +animation state machine (`ModAnimStateMachine`) that drives non-Spine combat +visuals (`AnimatedSprite2D`, Godot `AnimationPlayer`, or cue frame sequences) +through the same trigger protocol Spine creatures use. + +For content pack registration, see [Content Packs & Registries](content-packs-and-registries.md). +For character assembly, see [Character & Unlock Templates](character-and-unlock-scaffolding.md). +For Harmony patch wiring in general, see [Patching Guide](patching-guide.md). + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文介绍一组 mod 可以接入的运行时 Godot 工厂接口(替换原版 +`CreateVisuals` / `GenerateAnimator`),以及后端无关的动画状态机 +`ModAnimStateMachine`。后者让非 Spine 的战斗视觉(`AnimatedSprite2D`、Godot +`AnimationPlayer`、cue 帧序列)通过和 Spine 生物**相同的**触发协议驱动动画。 + +内容包注册见 [内容包与注册器](content-packs-and-registries.md)。 +角色装配见 [角色与解锁模板](character-and-unlock-scaffolding.md)。 +Harmony 补丁机制见 [补丁系统](patching-guide.md)。 + +--- + +::: + +## Overview{lang="en"} + +::: en + +Vanilla binds a `MonsterModel` or `CharacterModel` to combat visuals through: + +- `Model.CreateVisuals()` — returns an `NCreatureVisuals` (the scene root under + the combat creature node). +- `Model.GenerateAnimator(MegaSprite controller)` — returns a `CreatureAnimator` + wrapping a Spine skeleton with an idle / hit / attack / cast / die / relaxed + state graph. +- `NCreature.SetAnimationTrigger(trigger)` — dispatches triggers + (`Idle`, `Attack`, `Cast`, `Hit`, `Dead`, `Revive`, ...) into that animator at + runtime. + +Mods commonly need one or more of: + +- supplying `NCreatureVisuals` from code (not a path); +- replacing the Spine state graph with a mod-authored one; +- animating creatures **without** a Spine skeleton (sprite sheets, frame + sequences, Godot `AnimationPlayer`). + +RitsuLib exposes three orthogonal factory interfaces for those hooks and one +state machine abstraction for the non-Spine case. All four interfaces are +creature-agnostic (players **and** monsters) and do not require subclassing any +template. + +| Interface | Purpose | Vanilla entry point | +|---|---|---| +| `IModCreatureVisualsFactory` | Build `NCreatureVisuals` from code | `CharacterModel.CreateVisuals`, `MonsterModel.CreateVisuals` | +| `IModCreatureAnimatorFactory` | Build Spine `CreatureAnimator` from code | `CharacterModel.GenerateAnimator`, `MonsterModel.GenerateAnimator` | +| `IModNonSpineAnimationStateMachineFactory` | Build `ModAnimStateMachine` for non-Spine visuals | `NCreature.SetAnimationTrigger` (routing patch) | +| `IModCharacterMerchantAnimationStateMachineFactory` | Build `ModAnimStateMachine` for merchant / rest-site character visuals | Merchant scene setup | + +The merchant factory is character-specific because monsters never appear in +merchant / rest-site scenes; the other three apply to any +`MegaCrit.Sts2.Core.Models.AbstractModel`. + +--- + +::: + +## 概览{lang="zh-CN"} + +::: zh-CN + +原版将 `MonsterModel` / `CharacterModel` 绑定到战斗视觉的三个入口: + +- `Model.CreateVisuals()` — 返回一个 `NCreatureVisuals`(战斗生物节点下的视觉 + 根场景)。 +- `Model.GenerateAnimator(MegaSprite controller)` — 返回一个 `CreatureAnimator`, + 内部封装 Spine 骨骼及 idle / hit / attack / cast / die / relaxed 状态图。 +- `NCreature.SetAnimationTrigger(trigger)` — 在运行时把触发器(`Idle`、 + `Attack`、`Cast`、`Hit`、`Dead`、`Revive` 等)派发给这个 animator。 + +Mod 常见的需求至少包括以下一种: + +- 用代码供给 `NCreatureVisuals`(而不是只用路径); +- 用 mod 自己写的状态图替换 Spine 状态图; +- 给 **没有** Spine 骨骼的生物做动画(精灵表、帧序列、Godot `AnimationPlayer`)。 + +RitsuLib 为这三种需求暴露了三个**彼此正交**的工厂接口,以及一个针对非 Spine +场景的状态机抽象。四个接口都对生物类型无感(玩家角色与怪物通用),也不要求 +继承任何模板。 + +| 接口 | 用途 | 对应原版入口 | +|---|---|---| +| `IModCreatureVisualsFactory` | 从代码构造 `NCreatureVisuals` | `CharacterModel.CreateVisuals`、`MonsterModel.CreateVisuals` | +| `IModCreatureAnimatorFactory` | 从代码构造 Spine `CreatureAnimator` | `CharacterModel.GenerateAnimator`、`MonsterModel.GenerateAnimator` | +| `IModNonSpineAnimationStateMachineFactory` | 为非 Spine 视觉构造 `ModAnimStateMachine` | `NCreature.SetAnimationTrigger`(路由补丁) | +| `IModCharacterMerchantAnimationStateMachineFactory` | 为商人 / 休息站中的角色视觉构造 `ModAnimStateMachine` | 商人场景初始化流程 | + +商人工厂专属玩家角色,因为怪物从不会出现在商人 / 休息站场景中;其余三个接口对 +任意 `MegaCrit.Sts2.Core.Models.AbstractModel` 都适用。 + +--- + +::: + +## Creature Visuals Factory{lang="en"} + +::: en + +`IModCreatureVisualsFactory` replaces the path-based +`(Character|Monster)Model.CreateVisuals` when it returns a non-null +`NCreatureVisuals`. `null` defers to `CustomVisualsPath` / vanilla resolution. + +```csharp +public class MyCharacter : ModCharacterTemplate<...> +{ + // IModCreatureVisualsFactory is already implemented by the template, + // forwarding to this protected virtual: + protected override NCreatureVisuals? TryCreateCreatureVisuals() + { + var scene = GD.Load( + "res://MyMod/scenes/my_character/my_character_visuals.tscn"); + return scene.Instantiate(); + } +} +``` + +For mods that do not use `ModCharacterTemplate` / `ModMonsterTemplate`, implement +the interface directly on your `CharacterModel` / `MonsterModel`: + +```csharp +public class MyRawCharacter : CharacterModel, IModCreatureVisualsFactory +{ + public NCreatureVisuals? TryCreateCreatureVisuals() => ...; +} +``` + +The routing patches (`CharacterCreatureVisualsRuntimeFactoryPatch`, +`MonsterCreatureVisualsRuntimeFactoryPatch`) run at Harmony `Priority.First`, so +they take effect before the vanilla path-based loader. + +--- + +::: + +## 生物视觉工厂{lang="zh-CN"} + +::: zh-CN + +`IModCreatureVisualsFactory` 在返回非 null 的 `NCreatureVisuals` 时,会替换 +`(Character|Monster)Model.CreateVisuals` 的原有行为;返回 `null` 则退回到 +`CustomVisualsPath` 等原版解析链路。 + +```csharp +public class MyCharacter : ModCharacterTemplate<...> +{ + // 模板已经实现了 IModCreatureVisualsFactory,并把调用转发到这个 + // protected virtual;重写它即可: + protected override NCreatureVisuals? TryCreateCreatureVisuals() + { + var scene = GD.Load( + "res://MyMod/scenes/my_character/my_character_visuals.tscn"); + return scene.Instantiate(); + } +} +``` + +如果不使用 `ModCharacterTemplate` / `ModMonsterTemplate`,直接在自己的 +`CharacterModel` / `MonsterModel` 上实现接口即可: + +```csharp +public class MyRawCharacter : CharacterModel, IModCreatureVisualsFactory +{ + public NCreatureVisuals? TryCreateCreatureVisuals() => ...; +} +``` + +路由补丁(`CharacterCreatureVisualsRuntimeFactoryPatch`、 +`MonsterCreatureVisualsRuntimeFactoryPatch`)以 Harmony `Priority.First` 运行, +在原版基于路径的加载逻辑之前生效。 + +--- + +::: + +## Creature Animator Factory (Spine){lang="en"} + +::: en + +`IModCreatureAnimatorFactory` replaces `GenerateAnimator` for Spine visuals. +Prefer `ModAnimStateMachines.Standard` to match the vanilla state shape: + +```csharp +public class MySpineCharacter : ModCharacterTemplate<...> +{ + protected override CreatureAnimator? SetupCustomCreatureAnimator(MegaSprite controller) => + ModAnimStateMachines.Standard( + controller, + idleName: "idle_loop", + deadName: "die", + hitName: "hit", + attackName: "attack", + castName: "cast", + relaxedName: "relaxed"); +} +``` + +`ModAnimStateMachines.Standard` returns a `CreatureAnimator` wired with any-state +triggers for `Idle`, `Dead`, `Hit`, `Attack`, `Cast`, `Relaxed`. Terminal states +(`Dead`) leave `NextState` unset so playback does not loop back to idle. + +The routing patches (`CharacterCreatureAnimatorRuntimeFactoryPatch`, +`MonsterCreatureAnimatorRuntimeFactoryPatch`) honour non-null factory output; +`null` defers to vanilla `GenerateAnimator`. + +--- + +::: + +## Spine Animator 工厂{lang="zh-CN"} + +::: zh-CN + +`IModCreatureAnimatorFactory` 用来替换 `GenerateAnimator`,适用于 Spine 视觉。 +推荐使用 `ModAnimStateMachines.Standard` 以复用原版状态图的形状: + +```csharp +public class MySpineCharacter : ModCharacterTemplate<...> +{ + protected override CreatureAnimator? SetupCustomCreatureAnimator(MegaSprite controller) => + ModAnimStateMachines.Standard( + controller, + idleName: "idle_loop", + deadName: "die", + hitName: "hit", + attackName: "attack", + castName: "cast", + relaxedName: "relaxed"); +} +``` + +`ModAnimStateMachines.Standard` 返回一个已经布好 `Idle` / `Dead` / `Hit` / +`Attack` / `Cast` / `Relaxed` any-state 触发器的 `CreatureAnimator`。终态 +(`Dead`)不设置 `NextState`,所以播放完不会自动回到 idle。 + +路由补丁(`CharacterCreatureAnimatorRuntimeFactoryPatch`、 +`MonsterCreatureAnimatorRuntimeFactoryPatch`)接受非 null 的工厂返回值;返回 +`null` 则退回到原版 `GenerateAnimator`。 + +--- + +::: + +## Non-Spine State Machine{lang="en"} + +::: en + +For creatures whose combat visuals are **not** Spine (no `MegaSprite` controller), +implement `IModNonSpineAnimationStateMachineFactory` and return a +`ModAnimStateMachine` bound to the visuals root. The +`ModCreatureNonSpineAnimationPlaybackPatch` routes +`NCreature.SetAnimationTrigger(trigger)` into `ModAnimStateMachine.SetTrigger`, +so the non-Spine path receives the **same trigger stream** as Spine creatures. + +### Opting in + +```csharp +public class MyWolf : ModMonsterTemplate +{ + // IModNonSpineAnimationStateMachineFactory is already implemented by the + // template, forwarding to this protected virtual: + protected override ModAnimStateMachine? SetupCustomNonSpineAnimationStateMachine( + Node visualsRoot, MonsterModel monster) + { + if (visualsRoot is not MyWolfVisuals wolfVisuals) + return null; + + var backend = new AnimatedSprite2DBackend(wolfVisuals.GetAnimatedSprite()); + + return ModAnimStateMachineBuilder.Create() + .AddState("idle", loop: true).AsInitial().Done() + .AddState("attack").WithNext("idle").Done() + .AddState("hurt").WithNext("idle").Done() + .AddState("die").Done() // terminal: no NextState + .AddAnyState("Idle", "idle") + .AddAnyState("Attack", "attack") + .AddAnyState("Hit", "hurt") + .AddAnyState("Dead", "die") + .Build(backend); + } +} +``` + +Equivalent if you do not use a template: + +```csharp +public class MyRawMonster : MonsterModel, IModNonSpineAnimationStateMachineFactory +{ + public ModAnimStateMachine? TryCreateNonSpineAnimationStateMachine(Node visualsRoot) + => /* same builder code */; +} +``` + +### Routing behaviour + +`ModCreatureNonSpineAnimationPlaybackPatch` is a prefix on +`NCreature.SetAnimationTrigger`: + +1. If the creature has a Spine animator, skip (vanilla path runs). +2. Look up the creature's model (`Entity.Player?.Character` or `Entity.Monster`). +3. If either implements `IModNonSpineAnimationStateMachineFactory` and + returns a non-null state machine, dispatch the trigger via + `ModAnimStateMachine.SetTrigger` and return. +4. Otherwise, fall back to single-shot cue playback + (`ModCreatureVisualPlayback.TryPlayFromCreatureAnimatorTrigger`). + +State machines are **cached per visuals root** with a +`ConditionalWeakTable`, so the factory runs at most once +per combat lifetime and is automatically released when the visuals node is +freed. + +### Shorthand: `ModAnimStateMachines.StandardCue` + +For visuals that follow the vanilla idle / dead / hit / attack / cast / relaxed +shape, `ModAnimStateMachines.StandardCue` builds the state graph for you. It +uses `CompositeBackendFactory` to pick the best backend per state (cue frame +sequences first, Godot `AnimationPlayer` or `AnimatedSprite2D` if they resolve +the animation id) and returns a ready-to-use `ModAnimStateMachine`. + +--- + +::: + +## 非 Spine 状态机{lang="zh-CN"} + +::: zh-CN + +如果生物的战斗视觉**不是** Spine(没有 `MegaSprite` 控制器),实现 +`IModNonSpineAnimationStateMachineFactory` 并返回绑定到视觉根节点的 +`ModAnimStateMachine`。`ModCreatureNonSpineAnimationPlaybackPatch` 把 +`NCreature.SetAnimationTrigger(trigger)` 路由到 `ModAnimStateMachine.SetTrigger`, +因此非 Spine 生物会接收到与 Spine 生物**完全相同**的触发流。 + +### 接入方式 + +```csharp +public class MyWolf : ModMonsterTemplate +{ + // 模板已经实现了 IModNonSpineAnimationStateMachineFactory,并把调用转发 + // 到这个 protected virtual;重写它即可: + protected override ModAnimStateMachine? SetupCustomNonSpineAnimationStateMachine( + Node visualsRoot, MonsterModel monster) + { + if (visualsRoot is not MyWolfVisuals wolfVisuals) + return null; + + var backend = new AnimatedSprite2DBackend(wolfVisuals.GetAnimatedSprite()); + + return ModAnimStateMachineBuilder.Create() + .AddState("idle", loop: true).AsInitial().Done() + .AddState("attack").WithNext("idle").Done() + .AddState("hurt").WithNext("idle").Done() + .AddState("die").Done() // 终态:不设置 NextState + .AddAnyState("Idle", "idle") + .AddAnyState("Attack", "attack") + .AddAnyState("Hit", "hurt") + .AddAnyState("Dead", "die") + .Build(backend); + } +} +``` + +不使用模板时同理: + +```csharp +public class MyRawMonster : MonsterModel, IModNonSpineAnimationStateMachineFactory +{ + public ModAnimStateMachine? TryCreateNonSpineAnimationStateMachine(Node visualsRoot) + => /* 同上的 builder 代码 */; +} +``` + +### 路由行为 + +`ModCreatureNonSpineAnimationPlaybackPatch` 是 `NCreature.SetAnimationTrigger` +上的 Prefix 补丁,流程如下: + +1. 如果生物已有 Spine animator,直接跳过(原版链路继续执行)。 +2. 定位生物对应的模型(`Entity.Player?.Character` 或 `Entity.Monster`)。 +3. 如果任一侧实现了 `IModNonSpineAnimationStateMachineFactory` 且返回非 null 的 + 状态机,调用 `ModAnimStateMachine.SetTrigger` 派发触发器,然后返回。 +4. 否则退回到单次 cue 播放 + (`ModCreatureVisualPlayback.TryPlayFromCreatureAnimatorTrigger`)。 + +状态机按视觉根节点缓存在 `ConditionalWeakTable` 中, +因此工厂在一次战斗生命周期内最多执行一次,节点释放时会自动回收。 + +### 快捷方式:`ModAnimStateMachines.StandardCue` + +如果视觉遵循 vanilla 的 idle / dead / hit / attack / cast / relaxed 结构, +直接使用 `ModAnimStateMachines.StandardCue` 即可由库内构建状态图。它会通过 +`CompositeBackendFactory` 为每个状态挑选最合适的后端(优先 cue 帧序列,其次 +Godot `AnimationPlayer` 或 `AnimatedSprite2D`),返回一个可直接使用的 +`ModAnimStateMachine`。 + +--- + +::: + +## Animation Backends{lang="en"} + +::: en + +`IAnimationBackend` is the uniform driver surface consumed by +`ModAnimStateMachine`. Each backend wraps a Godot animation subsystem and +reports `Started` / `Completed` / `Interrupted` events. + +| Backend | Drives | Used for | +|---|---|---| +| `AnimatedSprite2DBackend` | `AnimatedSprite2D` | Frame-based sprite animation | +| `GodotAnimationPlayerBackend` | `AnimationPlayer` | Godot `.tres` animation library | +| `CueAnimationBackend` | `VisualCueSet` (cue frame sequences, cue textures) | Per-cue static textures / sequence playback | +| `SpineAnimationBackend` | `MegaSprite` | Spine skeletal animation | +| `CompositeAnimationBackend` | Any mix | Multi-backend dispatch (one state plays via sprite, another via animation player, etc.) | + +### Event contract + +| Event | When it fires | +|---|---| +| `Started(id)` | Playback for `id` has started | +| `Completed(id)` | One-shot finished, or a loop cycle ended | +| `Interrupted(id)` | Playback was replaced before completion | + +`ModAnimState.NextState` advances on `Completed`, so backends must emit it +accurately for non-looping states (`attack -> idle` etc.). + +### Queue semantics + +`Queue(id, loop)` is semantically "play this after the currently active +animation finishes". Backends implement it differently: + +| Backend | `Queue` behaviour | +|---|---| +| `SpineAnimationBackend` | True native Spine queue (`AddAnimation` on the track) | +| `AnimatedSprite2DBackend` | Stores pending id, plays on next `animation_finished` signal | +| `GodotAnimationPlayerBackend` | Uses `AnimationPlayer.Queue` | +| `CueAnimationBackend` | Stores pending id, plays on sequence completion | + +In all cases, calling `Play` clears any pending queued animation. + +### `Stop()` and cross-backend transitions + +`IAnimationBackend.Stop()` (default interface method) halts the backend +**silently** — it neither fires `Completed` nor `Interrupted`, and clears any +queued animation. The primary consumer is `CompositeAnimationBackend` when +transitioning from one child backend to another: + +1. The new state's backend differs from the active one. +2. `Interrupted` is fired for the outgoing animation. +3. The outgoing backend's `Stop()` is called to clear its internal state. +4. The incoming backend's `Play` runs. + +Without `Stop()`, the outgoing backend could keep emitting `Completed` / +`Interrupted` events bound to its old state id and confuse the state machine. + +--- + +::: + +## 动画后端{lang="zh-CN"} + +::: zh-CN + +`IAnimationBackend` 是 `ModAnimStateMachine` 消费的统一驱动层。每个后端包装 +Godot 的一个动画子系统,并在对应时机发出 `Started` / `Completed` / +`Interrupted` 事件。 + +| 后端 | 驱动对象 | 适用场景 | +|---|---|---| +| `AnimatedSprite2DBackend` | `AnimatedSprite2D` | 基于帧的 sprite 动画 | +| `GodotAnimationPlayerBackend` | `AnimationPlayer` | Godot `.tres` 动画库 | +| `CueAnimationBackend` | `VisualCueSet`(cue 帧序列 / cue 贴图) | 单帧贴图或帧序列播放 | +| `SpineAnimationBackend` | `MegaSprite` | Spine 骨骼动画 | +| `CompositeAnimationBackend` | 任意组合 | 多后端派发(同一状态机内部分状态走 sprite,另一部分走 animation player 等) | + +### 事件契约 + +| 事件 | 触发时机 | +|---|---| +| `Started(id)` | `id` 对应的播放已开始 | +| `Completed(id)` | 单次播放结束,或一个循环周期结束 | +| `Interrupted(id)` | 播放被新动画抢占,尚未自然结束 | + +`ModAnimState.NextState` 在 `Completed` 时推进,因此对非循环状态(`attack -> +idle` 等),后端**必须**准确发出 `Completed`。 + +### 队列语义 + +`Queue(id, loop)` 的语义是「当前动画播完后再播这一个」。各后端实现略有差异: + +| 后端 | `Queue` 行为 | +|---|---| +| `SpineAnimationBackend` | Spine 原生队列(在 track 上 `AddAnimation`) | +| `AnimatedSprite2DBackend` | 记录待播 id,在下一次 `animation_finished` 信号时播放 | +| `GodotAnimationPlayerBackend` | 使用 `AnimationPlayer.Queue` | +| `CueAnimationBackend` | 记录待播 id,在当前序列结束时播放 | + +任何后端上调用 `Play` 都会清空已排队的动画。 + +### `Stop()` 与跨后端切换 + +`IAnimationBackend.Stop()`(默认接口方法)会**静默**停止后端——既不发 +`Completed` 也不发 `Interrupted`,并清掉排队动画。它的主要使用方是 +`CompositeAnimationBackend`,在不同子后端之间切换时: + +1. 新状态使用的后端与当前活动的后端不同。 +2. 为即将离开的动画发出 `Interrupted`。 +3. 调用离开后端的 `Stop()` 清理其内部状态。 +4. 调用新进入后端的 `Play`。 + +如果不调用 `Stop()`,离开的后端可能继续发出已失效状态 id 的 `Completed` / +`Interrupted` 事件,干扰上层状态机。 + +--- + +::: + +## Lifecycle Trigger Patches{lang="en"} + +::: en + +Vanilla `NCreature.StartDeathAnim` and `NCreature.StartReviveAnim` dispatch the +`Dead` / `Revive` triggers only when `_spineAnimator != null`. Non-Spine +creatures therefore never receive those triggers, so a custom state machine +never sees the death animation play when the run is abandoned or the player +dies. + +RitsuLib fixes this with two Postfix patches: + +- `NCreatureNonSpineDeathAnimationTriggerPatch` — dispatches `Dead` after + `StartDeathAnim`. +- `NCreatureNonSpineReviveAnimationTriggerPatch` — dispatches `Revive` after + `StartReviveAnim`. + +### Scope gate + +The patches are **opt-in**: they only fire when the creature has no Spine +animator and the model opts into the RitsuLib visuals pipeline. Specifically, +`NonSpineAnimationTriggerScope.AppliesTo(NCreature)` returns `true` only when +**one** of the following holds for the creature's model: + +| Model slot | Interface | Notes | +|---|---|---| +| `Entity.Player?.Character` | `IModNonSpineAnimationStateMachineFactory` | State machine path | +| `Entity.Monster` | `IModNonSpineAnimationStateMachineFactory` | State machine path | +| `Entity.Player?.Character` | `IModCharacterAssetOverrides` | Cue-playback fallback (player-only) | + +Vanilla creatures and mods that do not opt into RitsuLib visuals are never +affected. The gate is identical for the `Dead` and `Revive` patches. + +--- + +::: + +## 生命周期触发补丁{lang="zh-CN"} + +::: zh-CN + +原版 `NCreature.StartDeathAnim` 和 `NCreature.StartReviveAnim` 只在 +`_spineAnimator != null` 时派发 `Dead` / `Revive` 触发器。非 Spine 生物因此 +收不到这两个触发器,自定义状态机在「弃置当前游戏」或玩家死亡时永远看不到 +死亡动画。 + +RitsuLib 通过两个 Postfix 补丁修正这一缺陷: + +- `NCreatureNonSpineDeathAnimationTriggerPatch` — 在 `StartDeathAnim` 之后派发 + `Dead`。 +- `NCreatureNonSpineReviveAnimationTriggerPatch` — 在 `StartReviveAnim` 之后 + 派发 `Revive`。 + +### 作用域收敛 + +这两个补丁是**opt-in**:只有当生物没有 Spine animator 且模型**显式**接入了 +RitsuLib 视觉链路时才会触发。具体而言,`NonSpineAnimationTriggerScope.AppliesTo(NCreature)` +在以下任一条件成立时返回 `true`: + +| 模型槽位 | 接口 | 备注 | +|---|---|---| +| `Entity.Player?.Character` | `IModNonSpineAnimationStateMachineFactory` | 状态机路径 | +| `Entity.Monster` | `IModNonSpineAnimationStateMachineFactory` | 状态机路径 | +| `Entity.Player?.Character` | `IModCharacterAssetOverrides` | cue 播放回退(仅玩家) | + +原版生物、以及未接入 RitsuLib 视觉的其他 mod 都不会被影响。`Dead` 与 `Revive` +两个补丁使用完全相同的 gate。 + +--- + +::: + +## Migration & Deprecation{lang="en"} + +::: en + +Two factory interfaces were originally named after the creature kind. They are +now unified and the old names marked `[Obsolete]`: + +| New (preferred) | Obsolete aliases | +|---|---| +| `IModCreatureVisualsFactory` | `IModMonsterCreatureVisualsFactory`, `IModCharacterCreatureVisualsFactory` | +| `IModCreatureAnimatorFactory` | `IModCharacterCreatureAnimatorFactory` | + +### Compatibility guarantees + +- The routing patches check both the new and the obsolete interfaces on each + call, so mods that implement only the old interface continue to work without + any code change. +- `ModCharacterTemplate` / `ModMonsterTemplate` implement **both** the new and + the obsolete aliases and forward to the same protected virtual hooks, so + external `is IModCharacterCreatureVisualsFactory` checks against a template + subclass still succeed. +- Implementing an obsolete interface emits compiler warning **CS0618** to guide + migration. No runtime warning or behavioural change. + +### Migration steps + +1. Replace the old interface name in the `: Interfaces` list and in explicit + interface implementations: + - `IModMonsterCreatureVisualsFactory` → `IModCreatureVisualsFactory` + - `IModCharacterCreatureVisualsFactory` → `IModCreatureVisualsFactory` + - `IModCharacterCreatureAnimatorFactory` → `IModCreatureAnimatorFactory` +2. The method signatures (`TryCreateCreatureVisuals()`, + `TryCreateCreatureAnimator(MegaSprite)`) are unchanged; only the declaring + interface name differs. +3. Rebuild. CS0618 warnings disappear. + +No migration is required if you only subclass the templates and override the +protected virtual hooks (`TryCreateCreatureVisuals`, +`SetupCustomCreatureAnimator`); those hooks are unchanged. + +--- + +::: + +## 迁移与废弃{lang="zh-CN"} + +::: zh-CN + +两个工厂接口最初按生物种类分别命名,现在已统一,旧名称被标记为 `[Obsolete]`: + +| 新名称(推荐) | 已废弃的别名 | +|---|---| +| `IModCreatureVisualsFactory` | `IModMonsterCreatureVisualsFactory`、`IModCharacterCreatureVisualsFactory` | +| `IModCreatureAnimatorFactory` | `IModCharacterCreatureAnimatorFactory` | + +### 兼容性保证 + +- 路由补丁在每次调用时**同时**检查新接口和废弃接口,所以只实现旧接口的 mod + 无需任何代码改动即可继续工作。 +- `ModCharacterTemplate` / `ModMonsterTemplate` **同时**实现新接口和废弃别名, + 并把调用转发到同一批 protected virtual 钩子;外部对模板子类做 + `is IModCharacterCreatureVisualsFactory` 之类的类型检查仍然成立。 +- 实现废弃接口会触发编译警告 **CS0618** 引导迁移。运行时行为不变、没有运行时 + 警告。 + +### 迁移步骤 + +1. 在 `: Interfaces` 列表和显式接口实现中把旧名替换为新名: + - `IModMonsterCreatureVisualsFactory` → `IModCreatureVisualsFactory` + - `IModCharacterCreatureVisualsFactory` → `IModCreatureVisualsFactory` + - `IModCharacterCreatureAnimatorFactory` → `IModCreatureAnimatorFactory` +2. 方法签名(`TryCreateCreatureVisuals()`、 + `TryCreateCreatureAnimator(MegaSprite)`)保持不变,变化只在声明接口的名字上。 +3. 重新编译,CS0618 警告消失。 + +如果只是继承模板并重写 protected virtual 钩子 +(`TryCreateCreatureVisuals`、`SetupCustomCreatureAnimator`),无需任何迁移; +这些钩子未变。 + +--- + +::: + +## Summary Cheat-sheet{lang="en"} + +::: en + +```text +Goal Interface to implement +--------------------------------------------------------------------------- +Replace CreateVisuals (players or monsters) IModCreatureVisualsFactory +Replace Spine GenerateAnimator IModCreatureAnimatorFactory +Drive a non-Spine state machine IModNonSpineAnimationStateMachineFactory +Drive merchant / rest-site state machine IModCharacterMerchantAnimationStateMachineFactory +``` + +All four interfaces are honoured whether you inherit `ModCharacterTemplate` / +`ModMonsterTemplate` or implement them directly on your `CharacterModel` / +`MonsterModel`. The routing patches always run at Harmony `Priority.First` and +defer to vanilla when the factory returns `null`. + +::: + +## 速查表{lang="zh-CN"} + +::: zh-CN + +```text +目标 实现的接口 +--------------------------------------------------------------------------- +替换 CreateVisuals(玩家或怪物) IModCreatureVisualsFactory +替换 Spine GenerateAnimator IModCreatureAnimatorFactory +驱动非 Spine 状态机 IModNonSpineAnimationStateMachineFactory +驱动商人 / 休息站状态机 IModCharacterMerchantAnimationStateMachineFactory +``` + +无论你是继承 `ModCharacterTemplate` / `ModMonsterTemplate`,还是直接在 +`CharacterModel` / `MonsterModel` 上实现这些接口,路由补丁都会生效:它们以 +Harmony `Priority.First` 运行,且在工厂返回 `null` 时退回到原版行为。 + +::: diff --git a/docs/pages/guide/custom-events.md b/docs/pages/guide/custom-events.md new file mode 100644 index 0000000..4ba6658 --- /dev/null +++ b/docs/pages/guide/custom-events.md @@ -0,0 +1,632 @@ +--- +title: + en: Custom Events + zh-CN: 自定义事件 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This document explains how to plug custom events into the game's event pipeline using RitsuLib. + +It covers three registration shapes: + +- shared events: `SharedEvent()` +- act-specific events: `ActEvent()` +- ancients: `SharedAncient()` / `ActAncient()` + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文说明如何通过 RitsuLib 将自定义事件接入游戏的事件管线。 + +它覆盖三类注册: + +- 共享事件:`SharedEvent()` +- Act 专属事件:`ActEvent()` +- Ancient:`SharedAncient()` / `ActAncient()` + +--- + +::: + +## Base-game event pipeline{lang="en"} + +::: en + +> The following is the game's own event runtime flow, to help you see where RitsuLib registration ultimately takes effect. + +Event generation and execution in the game involve these stages: + +| Stage | Game type | Role | +|---|---|---| +| Candidate generation | `ActModel.GenerateRooms(...)` | Builds the candidate list from the act-local event pool and the `ModelDb.AllSharedEvents` shared pool | +| Filtering | `RoomSet.EnsureNextEventIsValid(...)` | Filters using `IsAllowed(runState)` and visited-state records | +| Entry | `EventRoom.Enter(...)` | Preloads assets, creates the mutable instance, and builds the event UI | +| Assets | `EventModel.GetAssetPaths(...)` | Supplies asset paths that must be ready before the event opens | + +--- + +::: + +## 游戏原版事件管线{lang="zh-CN"} + +::: zh-CN + +> 以下是游戏引擎自身的事件运行时流程,帮助理解 RitsuLib 的注册内容最终在哪里生效。 + +游戏中事件的生成与执行涉及以下环节: + +| 阶段 | 游戏类型 | 职责 | +|---|---|---| +| 候选生成 | `ActModel.GenerateRooms(...)` | 从 Act 本地事件池和 `ModelDb.AllSharedEvents` 共享池构建候选列表 | +| 过滤 | `RoomSet.EnsureNextEventIsValid(...)` | 按 `IsAllowed(runState)` 与已访问记录过滤 | +| 进入 | `EventRoom.Enter(...)` | 预加载资源、创建可变实例、搭建事件界面 | +| 资源 | `EventModel.GetAssetPaths(...)` | 提供进入事件前需要准备的资源路径 | + +--- + +::: + +## RitsuLib registration{lang="en"} + +::: en + +RitsuLib does not replace the flow above. At registration time it adds mod events into the same entry points the base game already uses: + +- shared events are appended to `ModelDb.AllSharedEvents` +- act events are appended to the selected act's event list +- ancients are appended to the corresponding shared or act-local ancient lists + +For authors, the work boils down to two steps: + +1. define a valid `EventModel` or `AncientEventModel` subtype +2. register it before content registration freezes + +--- + +::: + +## RitsuLib 的注册机制{lang="zh-CN"} + +::: zh-CN + +RitsuLib 不替换上述流程,而是在注册阶段把 Mod 事件补充进原版已有的事件入口: + +- 共享事件追加到 `ModelDb.AllSharedEvents` +- Act 事件追加到对应 Act 的事件列表 +- Ancient 追加到对应的共享或 Act 本地 Ancient 列表 + +对 Mod 作者来说,实际工作可以概括为两步: + +1. 定义一个合法的 `EventModel` 或 `AncientEventModel` 子类 +2. 在内容注册冻结之前将其注册 + +--- + +::: + +## Minimal normal event{lang="en"} + +::: en + +Prefer inheriting `ModEventTemplate` rather than subclassing the base `EventModel` directly (see below). + +```csharp +using MegaCrit.Sts2.Core.Events; +using STS2RitsuLib.Scaffolding.Content; + +public sealed class MyFirstEvent : ModEventTemplate +{ + protected override IReadOnlyList 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; + } +} +``` + +A minimal usable event should: + +- implement `GenerateInitialOptions()` +- advance or finish the event inside option callbacks +- keep localization keys aligned with the final `ModelId.Entry` + +--- + +::: + +## 最小普通事件{lang="zh-CN"} + +::: zh-CN + +推荐继承 `ModEventTemplate`,而不是直接继承原版 `EventModel`(原因见下文)。 + +```csharp +using MegaCrit.Sts2.Core.Events; +using STS2RitsuLib.Scaffolding.Content; + +public sealed class MyFirstEvent : ModEventTemplate +{ + protected override IReadOnlyList 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; + } +} +``` + +最小可用事件至少应满足: + +- 实现 `GenerateInitialOptions()` +- 在选项回调里推进或结束事件 +- 本地化键与最终 `ModelId.Entry` 保持一致 + +--- + +::: + +## Registration{lang="en"} + +::: en + +### Shared event + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + .SharedEvent() + .Apply(); +``` + +### Act-specific event + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + .ActEvent() + .Apply(); +``` + +### Ancient + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + .SharedAncient() + .Apply(); +``` + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + .ActAncient() + .Apply(); +``` + +--- + +::: + +## 注册方式{lang="zh-CN"} + +::: zh-CN + +### 共享事件 + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + .SharedEvent() + .Apply(); +``` + +### Act 专属事件 + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + .ActEvent() + .Apply(); +``` + +### Ancient + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + .SharedAncient() + .Apply(); +``` + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + .ActAncient() + .Apply(); +``` + +--- + +::: + +## Localization keys{lang="en"} + +::: en + +After registration through RitsuLib, the event's `ModelId.Entry` follows a fixed format: + +```text +_EVENT_ +``` + +For `MyMod` and `MyFirstEvent`: + +```text +MY_MOD_EVENT_MY_FIRST_EVENT +``` + +Example localization block for a minimal normal event: + +```json +{ + "MY_MOD_EVENT_MY_FIRST_EVENT.title": "A Strange Spring", + "MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.description": "A glowing spring waits by the roadside.", + "MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.options.ACCEPT.title": "Drink", + "MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.options.ACCEPT.description": "This might go well.", + "MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.options.LEAVE.title": "Leave", + "MY_MOD_EVENT_MY_FIRST_EVENT.pages.INITIAL.options.LEAVE.description": "Do not risk it.", + "MY_MOD_EVENT_MY_FIRST_EVENT.pages.ACCEPT.description": "You feel renewed.", + "MY_MOD_EVENT_MY_FIRST_EVENT.pages.LEAVE.description": "You walk away." +} +``` + +The key requirement is consistency: titles, page text, and option keys should all be derived from the same final `Id.Entry`. + +--- + +::: + +## 本地化键{lang="zh-CN"} + +::: zh-CN + +通过 RitsuLib 注册后,事件的 `ModelId.Entry` 采用固定格式: + +```text +_EVENT_ +``` + +例如 `MyMod` 与 `MyFirstEvent`: + +```text +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": "你转身离开。" +} +``` + +关键要求是一致性:事件标题、页面文本和选项键都应基于同一个最终的 `Id.Entry` 生成。 + +--- + +::: + +## Why use `ModEventTemplate`{lang="en"} + +::: en + +> The following explains a behavioral detail of the base game's `EventModel`. + +In the base game, `EventModel.InitialOptionKey(...)` and internal option-key helpers build key prefixes from `GetType().Name` (via `Slugify`), while titles, page text, and related lookups use `Id.Entry`. + +For vanilla events those two usually match. For events registered through RitsuLib, `GetType().Name` and `Id.Entry` differ, so some text lookups use a different key prefix than the rest. + +`ModEventTemplate` and `ModAncientEventTemplate` use `protected new` to hide the base `InitialOptionKey` helpers and generate option keys from the final registered `Id.Entry`, removing that mismatch. + +--- + +::: + +## 为什么要用 `ModEventTemplate`{lang="zh-CN"} + +::: zh-CN + +> 以下解释涉及游戏原版 `EventModel` 的一个行为特征。 + +原版 `EventModel.InitialOptionKey(...)` 及内部 option-key 辅助方法使用 `GetType().Name`(经 `Slugify` 处理)拼接键前缀,而事件标题、页面描述等使用 `Id.Entry`。 + +对原版事件,这两者通常一致。但对通过 RitsuLib 注册的事件,`GetType().Name` 和 `Id.Entry` 不同,会导致部分文本查找落在不同的键前缀上。 + +`ModEventTemplate` 和 `ModAncientEventTemplate` 通过 `protected new` 隐藏了基类的 `InitialOptionKey`,统一基于最终注册后的 `Id.Entry` 生成选项键,从而消除这种不一致。 + +--- + +::: + +## `IsAllowed`{lang="en"} + +::: en + +> The following describes the base game's event filtering mechanism. + +Override `IsAllowed(RunState runState)` when the event should only appear in some runs: + +```csharp +public override bool IsAllowed(RunState runState) +{ + return !runState.VisitedEventIds.Contains(Id); +} +``` + +At runtime, the game walks the candidate pool until it finds an event that satisfies both: + +- `IsAllowed(...)` returns `true` +- the event has not been visited in the current run yet + +`IsAllowed` expresses whether the event may appear in the current run, not registration-time setup. + +--- + +::: + +## `IsAllowed`{lang="zh-CN"} + +::: zh-CN + +> 以下描述游戏原版的事件过滤机制。 + +如果事件只应在部分跑局中出现,可以覆写 `IsAllowed(RunState runState)`: + +```csharp +public override bool IsAllowed(RunState runState) +{ + return !runState.VisitedEventIds.Contains(Id); +} +``` + +运行时,游戏会在候选事件池中轮询,直到找到同时满足以下条件的事件: + +- `IsAllowed(...)` 返回 `true` +- 当前跑局尚未访问过该事件 + +`IsAllowed` 表达的是"当前跑局是否允许出现",不是注册阶段的准备逻辑。 + +--- + +::: + +## Custom event scene{lang="en"} + +::: en + +> The following describes the base game's custom event layout mechanism. + +Return a custom layout type: + +```csharp +public override EventLayoutType LayoutType => EventLayoutType.Custom; +``` + +The game then loads: + +```text +res://scenes/events/custom/.tscn +``` + +The scene root must implement `ICustomEventNode` and provide at least `Initialize(EventModel)` and `CurrentScreenContext`. + +--- + +::: + +## 自定义事件场景{lang="zh-CN"} + +::: zh-CN + +> 以下描述游戏原版的自定义事件布局机制。 + +返回自定义布局类型: + +```csharp +public override EventLayoutType LayoutType => EventLayoutType.Custom; +``` + +此时游戏会加载: + +```text +res://scenes/events/custom/.tscn +``` + +该场景根节点必须实现 `ICustomEventNode`,至少提供 `Initialize(EventModel)` 和 `CurrentScreenContext`。 + +--- + +::: + +## Asset preloading{lang="en"} + +::: en + +> The following describes the base game's rules for event asset preloading. + +Normal events preload by default: + +- the layout scene +- `res://images/events/.png` +- optional `res://scenes/vfx/events/_vfx.tscn` + +Ancients preload by default: + +- the layout scene +- `res://scenes/events/background_scenes/.tscn` + +Override `GetAssetPaths(IRunState runState)` to append paths when you need extra assets. + +--- + +::: + +## 资源预加载{lang="zh-CN"} + +::: zh-CN + +> 以下描述游戏原版的事件资源预加载规则。 + +普通事件默认预加载: + +- 布局场景 +- `res://images/events/.png` +- 可选的 `res://scenes/vfx/events/_vfx.tscn` + +Ancient 默认预加载: + +- 布局场景 +- `res://scenes/events/background_scenes/.tscn` + +如需额外资源,可覆写 `GetAssetPaths(IRunState runState)` 追加路径。 + +--- + +::: + +## Minimal ancient example{lang="en"} + +::: en + +```csharp +using MegaCrit.Sts2.Core.Entities.Ancients; +using MegaCrit.Sts2.Core.Events; +using STS2RitsuLib.Scaffolding.Content; + +public sealed class MyAncient : ModAncientEventTemplate +{ + protected override AncientDialogueSet DefineDialogues() + { + return new AncientDialogueSet(); + } + + public override IEnumerable AllPossibleOptions => + [ + new EventOption(this, Accept, InitialOptionKey("ACCEPT")), + ]; + + protected override IReadOnlyList GenerateInitialOptions() + { + return AllPossibleOptions.ToArray(); + } + + private Task Accept() + { + SetEventFinished(L10NLookup($"{Id.Entry}.pages.ACCEPT.description")); + return Task.CompletedTask; + } +} +``` + +The same principle applies: keep option keys, page keys, and the final registered `Id.Entry` aligned. + +--- + +::: + +## Ancient 最小示例{lang="zh-CN"} + +::: zh-CN + +```csharp +using MegaCrit.Sts2.Core.Entities.Ancients; +using MegaCrit.Sts2.Core.Events; +using STS2RitsuLib.Scaffolding.Content; + +public sealed class MyAncient : ModAncientEventTemplate +{ + protected override AncientDialogueSet DefineDialogues() + { + return new AncientDialogueSet(); + } + + public override IEnumerable AllPossibleOptions => + [ + new EventOption(this, Accept, InitialOptionKey("ACCEPT")), + ]; + + protected override IReadOnlyList GenerateInitialOptions() + { + return AllPossibleOptions.ToArray(); + } + + private Task Accept() + { + SetEventFinished(L10NLookup($"{Id.Entry}.pages.ACCEPT.description")); + return Task.CompletedTask; + } +} +``` + +选项键、页面键与最终注册后的 `Id.Entry` 保持一致的原则同样适用。 + +--- + +::: + +## Related docs{lang="en"} + +::: en + +- [Content Authoring Toolkit](/guide/content-authoring-toolkit) +- [Content Packs & Registries](/guide/content-packs-and-registries) +- [Localization & Keywords](/guide/localization-and-keywords) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [内容注册规则](/guide/content-authoring-toolkit) +- [内容包与注册器](/guide/content-packs-and-registries) +- [本地化与关键词](/guide/localization-and-keywords) + +::: diff --git a/docs/pages/guide/diagnostics-and-compatibility.md b/docs/pages/guide/diagnostics-and-compatibility.md new file mode 100644 index 0000000..b1d0755 --- /dev/null +++ b/docs/pages/guide/diagnostics-and-compatibility.md @@ -0,0 +1,392 @@ +--- +title: + en: Diagnostics & Compatibility + zh-CN: 诊断与兼容层 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This document describes the diagnostic policy and compatibility layers that RitsuLib adds on top of the base game. + +It focuses on: + +- one-time warnings for recurring authoring errors +- debug-oriented fallbacks for missing localization and invalid unlock data +- narrow bridge patches where vanilla systems do not process mod content + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文说明 RitsuLib 在游戏原版之上提供的诊断策略与兼容层。 + +重点包括: + +- 用于定位重复性数据错误的一次性警告 +- 面向调试的缺失本地化与无效解锁数据回退 +- 原版系统不处理 Mod 内容时使用的窄桥接补丁 + +--- + +::: + +## Design Intent{lang="en"} + +::: en + +RitsuLib does not try to hide every engine limitation. It follows these rules: + +- Surface real errors as early as possible +- where vanilla offers no safe extension point, the framework may add a bridge +- if a fallback would conceal too much behavior, keep the system explicit + +This layer is deliberately narrow and only handles edge cases. + +--- + +::: + +## 设计意图{lang="zh-CN"} + +::: zh-CN + +RitsuLib 不会试图隐藏所有引擎限制。它遵循以下规则: + +- 能尽早暴露真实错误,就尽早暴露 +- 原版没有安全扩展点时,框架可以补桥 +- 某个回退会掩盖过多行为时,保持系统显式 + +这层能力是刻意收敛的,只处理边缘问题。 + +--- + +::: + +## One-Time Warning Policy{lang="en"} + +::: en + +Some RitsuLib diagnostics warn only once per issue (or once per stable key), including: + +- Missing resource paths (`AssetPathDiagnostics`) +- Missing `LocTable` keys when the master toggle and the **LocTable missing keys** toggle are enabled (`[Localization][DebugCompat]`) +- `THE_ARCHITECT` empty-`Lines` fallback when the debug compatibility master toggle and the **THE_ARCHITECT missing dialogue** toggle are enabled (`[Ancient]`) +- Other unlock-related one-shots (for example `ModUnlockMissingRuleWarnings`) + +Each stable key or issue class logs at most once so traces stay readable. + +--- + +::: + +## 一次性警告策略{lang="zh-CN"} + +::: zh-CN + +RitsuLib 的部分诊断只会对同一个问题(或同一稳定键)警告一次,包括: + +- 缺失资源路径(`AssetPathDiagnostics`) +- **总开关 + LocTable 子项**开启时缺失的 `LocTable` 键(`[Localization][DebugCompat]`) +- **调试总开关 + 建筑师子项**开启时,`THE_ARCHITECT` 无对话注入占位值(`[Ancient]`) +- 其他解锁相关的一次性提示(例如 `ModUnlockMissingRuleWarnings`) + +同一稳定键或同一类问题至多记录一次,在可读的日志量下保留定位信息。 + +--- + +::: + +## Asset Path Diagnostics{lang="en"} + +::: en + +Explicit asset override paths are validated by `AssetPathDiagnostics`. + +When a path is missing: + +- A one-time warning is logged (host type, model id, member name, missing path) +- Behavior falls back to the original asset path or original behavior + +This matters especially for character assets, where vanilla has almost no safe fallback. + +See [Asset Profiles & Fallbacks](/guide/asset-profiles-and-fallbacks). + +--- + +::: + +## 资源路径诊断{lang="zh-CN"} + +::: zh-CN + +显式资源覆写路径由 `AssetPathDiagnostics` 校验。 + +当资源路径不存在时: + +- 输出一次警告(包含宿主类型、模型标识、配置成员名和缺失路径) +- 回退到原始资源路径或原始行为 + +这对角色资源尤其重要,因为游戏原版对缺失角色资源几乎没有安全兜底。 + +详见 [资源配置与回退规则](/guide/asset-profiles-and-fallbacks)。 + +--- + +::: + +## Debug Compatibility Mode{lang="en"} + +::: en + +Optional compatibility fallbacks are grouped under `debug_compatibility_mode` and per-area toggles in mod settings. + +**Default (master toggle off):** vanilla behavior for the patched systems described here. + +**Master toggle on:** the settings UI shows a **Compatibility fallbacks** section. Per-feature toggles default to **on**. Turning a toggle **off** removes only that fallback. + +| Toggle | Effect when enabled | +|---|---| +| **LocTable missing keys** | Placeholder resolution + one-time `[Localization][DebugCompat]` warnings | +| **Invalid unlock epochs** | Skip the grant + one-time `[Unlocks][DebugCompat]` warnings | +| **THE_ARCHITECT missing dialogue** | Inject empty `Lines` entries for `ModContentRegistry` characters + one-time `[Ancient]` warning | + +Except for LocTable missing-key handling, each toggle typically applies only to content registered through RitsuLib. + +**`ModUnlockMissingRuleWarnings`** (e.g. missing boss-win rule registration): separate diagnostic path from the debug compatibility toggles. + +**Released content:** ship complete localization, timeline data, and dialogue. Treat the table above as an iteration aid. + +Windows settings path: + +```text +%appdata%\SlayTheSpire2\steam\\mod_data\com.ritsukage.sts2-RitsuLib\settings.json +``` + +--- + +::: + +## Debug 兼容模式{lang="zh-CN"} + +::: zh-CN + +可选兼容回退由 `debug_compatibility_mode`(总开关)与设置页中的分项子开关控制。 + +**默认(总开关关):** 走原版逻辑。 + +**总开关开:** 游戏内展开 **兼容回退项**;子项默认**开启**。关闭某一子项时,仅移除对应回退。 + +| 子项 | 开启时 | +|---|---| +| **LocTable 缺键** | 占位解析 + 一次性 `[Localization][DebugCompat]` 警告 | +| **无效解锁纪元(Epoch)** | 跳过该次授予 + 一次性 `[Unlocks][DebugCompat]` 警告 | +| **建筑师缺对话** | 对 `ModContentRegistry` 角色注入空 `Lines` 条目 + 一次性 `[Ancient]` 警告 | + +除 LocTable 缺键处理外,各子项通常只作用于通过 RitsuLib 注册的内容。 + +**`ModUnlockMissingRuleWarnings`**(例如未注册 Boss 胜场规则):独立于调试兼容子开关的诊断路径。 + +**发布内容:** 应提供完整本地化、时间线与对话数据;上表仅用于迭代阶段排障。 + +Windows 下设置文件路径: + +```text +%appdata%\SlayTheSpire2\steam\\mod_data\com.ritsukage.sts2-RitsuLib\settings.json +``` + +--- + +::: + +## Registration Conflict Diagnostics{lang="en"} + +::: en + +RitsuLib checks these conflicts explicitly: + +| Conflict | Typical cause | +|---|---| +| Model id collision | Two registered models in the same mod/category share the same CLR type name | +| Epoch id collision | Two epochs resolve to the same `Id` | +| Story id collision | Two stories resolve to the same story identity | + +When detected, the framework throws or logs errors — it does not accept ambiguous identity silently. + +--- + +::: + +## 注册冲突诊断{lang="zh-CN"} + +::: zh-CN + +RitsuLib 会显式检查以下冲突: + +| 冲突类型 | 常见触发场景 | +|---|---| +| 模型 ID 冲突 | 同 Mod / 同类别下两个已注册模型的 CLR 类型名相同 | +| 纪元 ID 冲突 | 两个纪元解析出同一个 `Id` | +| 故事 ID 冲突 | 两个故事解析出同一个故事标识 | + +检测到冲突时抛异常或输出错误日志,不会静默接受模糊身份。 + +--- + +::: + +## Ancient Dialogue Compatibility Layer{lang="en"} + +::: en + +Before `AncientDialogueSet.PopulateLocKeys`, the framework appends localization-defined ancient dialogue rows for registered mod characters. Authors own the keys; the framework discovers and injects them so mod characters use the same ancient-dialogue pipeline as vanilla. + +### `THE_ARCHITECT` dialogue fallback + +Gated on the debug compatibility master toggle and the **THE_ARCHITECT missing dialogue** toggle. If vanilla `TheArchitect.LoadDialogue` yields no dialogue, RitsuLib injects empty `Lines` entries for `ModContentRegistry` characters and logs **`[Ancient]`** once. + +For key format, see [Localization & Keywords](/guide/localization-and-keywords). + +--- + +::: + +## Ancient 对话兼容层{lang="zh-CN"} + +::: zh-CN + +框架在 `AncientDialogueSet.PopulateLocKeys` 之前为已注册 Mod 角色追加基于本地化键的 Ancient 对话条目;作者编写键,框架负责发现与注入,使 Mod 角色复用与原版相同的 Ancient 对话管线。 + +### `THE_ARCHITECT` 对话兜底 + +受调试兼容 **总开关 + 建筑师子项** 控制。若原版 `TheArchitect.LoadDialogue` 无结果,RitsuLib 对 `ModContentRegistry` 角色注入空 `Lines` 占位值并记录一次 **`[Ancient]`** 警告。 + +具体键结构见 [本地化与关键词](/guide/localization-and-keywords)。 + +--- + +::: + +## Unlock Compatibility Bridges{lang="en"} + +::: en + +Several vanilla progression checks only iterate vanilla characters. RitsuLib applies narrow patches so registered unlock rules participate at the same checkpoints for mod characters: + +| Bridge | Description | +|---|---| +| Elite wins | Elite kill count → epoch checks | +| Boss wins | Boss kill count → epoch checks | +| Ascension 1 | Ascension 1 → epoch checks | +| Post-run character unlock | Post-run character-unlock epochs | +| Ascension reveal | Ascension reveal unlock checks | + +Bridge patches forward RitsuLib-registered rules into vanilla progression checkpoints that otherwise skip mod characters. They do not introduce a separate progression store. + +See [Timeline & Unlocks](/guide/timeline-and-unlocks). + +--- + +::: + +## 解锁兼容桥{lang="zh-CN"} + +::: zh-CN + +若干原版进度检查仅针对 vanilla 角色遍历。RitsuLib 以窄补丁将已注册解锁规则挂到相同检查点,使 Mod 角色在同一节点上参与判定: + +| 桥接类型 | 说明 | +|---|---| +| 精英胜场 | 精英击杀计数的纪元判定桥接 | +| Boss 胜场 | Boss 击杀计数的纪元判定桥接 | +| 进阶 1 | 进阶 1 的纪元判定桥接 | +| 局后角色解锁 | 局后角色解锁纪元桥接 | +| 进阶显示 | 进阶显示解锁判定桥接 | + +桥接补丁会把 RitsuLib 已注册规则转发到原版会跳过 Mod 角色的进度检查点;不引入独立的进度存储。 + +详见 [时间线与解锁](/guide/timeline-and-unlocks)。 + +--- + +::: + +## Freeze Errors{lang="en"} + +::: en + +If content, timeline, or unlock registration runs after freeze, RitsuLib throws. + +That is intentional: late registration often means ModelDb caches are already built, fixed identity rules are in use, and unlock filters are active. Failing fast is the safe choice. + +--- + +::: + +## Freeze 异常{lang="zh-CN"} + +::: zh-CN + +当内容、时间线或解锁在冻结之后还被注册时,RitsuLib 会直接抛异常。 + +这是诊断机制:一旦晚注册,往往意味着 ModelDb 缓存已建立、固定身份规则已被使用、解锁过滤已在运行。此时最安全的做法是尽早失败。 + +--- + +::: + +## Troubleshooting notes{lang="en"} + +::: en + +1. Warnings usually point to mod data or configuration (paths, keys, rules), not random engine failure. +2. Fix missing assets and localization in source data rather than relying on placeholders long term. +3. Debug compatibility fallbacks are for iteration; release builds should ship with the master toggle off, or with per-feature toggles disabled and complete data. +4. Prefer explicit registration APIs; compatibility fallbacks are not a long-term architecture substitute. + +--- + +::: + +## 排查要点{lang="zh-CN"} + +::: zh-CN + +1. 警告多表示 Mod 数据或配置问题(路径、键、规则),而非随机引擎故障。 +2. 资源与本地化应在数据源补全,而不是长期依赖占位值或兼容回退。 +3. 调试兼容回退用于迭代排障;发布构建宜关闭总开关或关闭子项并交付完整数据。 +4. 优先使用显式注册 API;兼容回退不宜作为长期架构依赖。 + +--- + +::: + +## Related Documents{lang="en"} + +::: en + +- [Asset Profiles & Fallbacks](/guide/asset-profiles-and-fallbacks) +- [Localization & Keywords](/guide/localization-and-keywords) +- [Timeline & Unlocks](/guide/timeline-and-unlocks) +- [Godot Scene Authoring](/guide/godot-scene-authoring) +- [Framework Design](/guide/framework-design) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [资源配置与回退规则](/guide/asset-profiles-and-fallbacks) +- [本地化与关键词](/guide/localization-and-keywords) +- [时间线与解锁](/guide/timeline-and-unlocks) +- [Godot 场景编写说明](/guide/godot-scene-authoring) +- [框架设计](/guide/framework-design) + +::: diff --git a/docs/pages/guide/fmod-and-audio.md b/docs/pages/guide/fmod-and-audio.md new file mode 100644 index 0000000..5738f38 --- /dev/null +++ b/docs/pages/guide/fmod-and-audio.md @@ -0,0 +1,510 @@ +--- +title: + en: FMOD & Audio + zh-CN: FMOD 与音频 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This document describes the game's audio architecture and the layered API that RitsuLib provides on top of it. + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文档说明游戏的音频架构,以及 RitsuLib 在此基础上提供的分层 API。 + +--- + +::: + +## Game-native audio architecture{lang="en"} + +::: en + +> The following describes Slay the Spire 2 engine's own audio pipeline, to help explain the design background of RitsuLib's audio API. + +Slay the Spire 2 plays audio through **Godot's FMOD Studio GDExtension** (`FmodServer` singleton). On the C# side this is wrapped by **`NAudioManager`**, which indirectly calls `FmodServer` via the GDScript proxy **`AudioManagerProxy`**. + +This means: + +- All vanilla audio playback ultimately goes through **`NAudioManager` → `AudioManagerProxy` → `FmodServer`** +- **`NAudioManager`** applies **`TestMode`** muting, SFX volume scaling, and related behaviour +- If a mod wants audio to **sound like the base game**, it should use the same pipeline + +--- + +::: + +## 游戏原版音频架构{lang="zh-CN"} + +::: zh-CN + +> 以下描述杀戮尖塔 2 引擎自身的音频管线,帮助理解 RitsuLib 音频 API 的设计背景。 + +杀戮尖塔 2 通过 **Godot 的 FMOD Studio GDExtension**(`FmodServer` 单例)播放音频。C# 侧由 `NAudioManager` 封装,它通过 GDScript 代理 `AudioManagerProxy` 间接调用 `FmodServer`。 + +这意味着: + +- 原版的音频播放最终都经过 `NAudioManager` → `AudioManagerProxy` → `FmodServer` 这条路径 +- `NAudioManager` 包含 `TestMode` 静音、SFX 音量施加等行为 +- 如果 Mod 希望音频行为"听起来和原版一样",应该走同一条管线 + +--- + +::: + +## RitsuLib audio API{lang="en"} + +::: en + +RitsuLib layers the audio API so you can use the vanilla-aligned pipeline or talk to FMOD Studio directly when needed. + +### Entry selection + +| Need | Use | +|------|-----| +| Easier high-level playback, typed handles, lifecycle cleanup | **`GameFmod.Playback`** | +| Same routing / `TestMode` behaviour as vanilla | **`GameFmod.Studio`** → `NAudioManager` | +| Same guards as `SfxCmd` (non-interactive, combat ending, etc.) | **`Sts2SfxAlignedFmod`** | +| Load/unload Studio banks, check paths | **`FmodStudioServer`** | +| Fire-and-forget one-shots on `FmodServer` **without** going through `NAudioManager` | **`FmodStudioDirectOneShots`** | +| Bus volume/mute/pause, global parameters, DSP, performance data | **`FmodStudioBusAccess`**, **`FmodStudioMixerGlobals`** | +| Snapshots (`snapshot:/…`) | **`FmodStudioSnapshots`** | +| Long-lived `create_event_instance` handles | **`FmodStudioEventInstances`** | +| WAV/OGG/MP3 via plugin loaders | **`FmodStudioStreamingFiles`** | +| Cooldown / random pool helpers (no audio by themselves) | **`FmodPlaybackThrottle`**, **`FmodPathRoundRobinPool`** | + +### Direct FMOD vs vanilla pipeline + +- **`GameFmod.Studio`** and **`Sts2SfxAlignedFmod`** go through **`NAudioManager`** and share the game's GDScript proxy (including **`TestMode`**, SFX volume, etc.) +- **`FmodStudioDirectOneShots`** and most **`FmodStudio*`** helpers call **`FmodServer`** directly—good for custom banks, loose files, and bus debugging; one-shots are not guaranteed to match every subtlety of the in-game SFX bus path +- For **“sounds like vanilla”**, prefer **`GameFmod`** or **`Sts2SfxAlignedFmod`** + +--- + +::: + +## RitsuLib 音频 API{lang="zh-CN"} + +::: zh-CN + +RitsuLib 将音频 API 分层,既能走与原版一致的管线,也能在需要时直连 FMOD Studio。 + +### 入口选择 + +| 需求 | 使用 | +|---|---| +| 更易用的高层播放、返回 handle、自动生命周期清理 | **`GameFmod.Playback`** | +| 与原版相同的路由 / `TestMode` 行为 | **`GameFmod.Studio`** → `NAudioManager` | +| 与 `SfxCmd` 相同的防护(非交互、战斗结束等) | **`Sts2SfxAlignedFmod`** | +| 加载/卸载 Studio Bank、检查路径 | **`FmodStudioServer`** | +| 在 `FmodServer` 上直接 one-shot(不经过 `NAudioManager`) | **`FmodStudioDirectOneShots`** | +| Bus 音量/静音/暂停、全局参数、DSP、性能数据 | **`FmodStudioBusAccess`**、**`FmodStudioMixerGlobals`** | +| Snapshot(`snapshot:/…`) | **`FmodStudioSnapshots`** | +| 长期持有的 `create_event_instance` | **`FmodStudioEventInstances`** | +| 通过插件加载 WAV/OGG/MP3 | **`FmodStudioStreamingFiles`** | +| 冷却、随机池(本身不发声) | **`FmodPlaybackThrottle`**、**`FmodPathRoundRobinPool`** | + +### 直连 FMOD 与原版管线的区别 + +- `GameFmod.Studio` 和 `Sts2SfxAlignedFmod` 走 `NAudioManager`,与原版游戏共享 GDScript 代理(含 `TestMode`、SFX 音量等) +- `FmodStudioDirectOneShots` 及多数 `FmodStudio*` 直接调用 `FmodServer`,适合自定义 Bank、散文件、Bus 调试;但 one-shot 不保证与游戏 SFX Bus 处理完全一致 +- 如果要"听起来和原版一样",优先使用 `GameFmod` 或 `Sts2SfxAlignedFmod` + +--- + +::: + +## Quick examples{lang="en"} + +::: en + +**Vanilla-aligned one-shot** + +```csharp +using STS2RitsuLib.Audio; + +Sts2SfxAlignedFmod.PlayOneShot("event:/sfx/heal"); +GameFmod.Studio.PlayMusic("event:/music/menu_update"); +``` + +**Mod content bank + `guids.txt` (must match the game's FMOD Studio major version line)** + +```csharp +FmodStudioServer.TryLoadBank("res://mods/MyMod/banks/MyMod.bank"); +FmodStudioServer.TryWaitForAllLoads(); +if (!FmodStudioServer.TryLoadStudioGuidMappings("res://mods/MyMod/banks/MyMod.guids.txt")) + return; +if (FmodStudioServer.TryCheckEventPath("event:/mods/mymod/hit") is true) + GameFmod.Studio.PlayOneShot("event:/mods/mymod/hit"); +``` + +**Loose file (short SFX — loaded as sound)** + +```csharp +var sfxPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "ping.wav"); +FmodStudioStreamingFiles.TryPlaySoundFile(sfxPath, volume: 0.9f); +``` + +**Streaming music file (recommended: Playback/Handle API)** + +```csharp +var musicPath = ProjectSettings.GlobalizePath("user://mymod/loop.ogg"); +var handle = GameFmod.Playback.PlayMusic( + AudioSource.StreamingMusic(musicPath), + new AudioPlaybackOptions { Volume = 0.7f, Scope = AudioLifecycleScope.Room } +); +``` + +**Common adaptive music flow (room / combat / victory)** + +```csharp +var adaptive = GameFmod.Playback.FollowAdaptiveMusic( + AudioAdaptivePlans.FullRunOverride( + roomSource: AudioSource.StreamingMusic(roomLoopPath), + combatSource: AudioSource.StreamingMusic(combatLoopPath), + victorySource: AudioSource.StreamingMusic(victoryStingerPath) + ) +); +``` + +**Throttle rapid triggers** + +```csharp +if (FmodPlaybackThrottle.TryEnter("my_power_proc", cooldownMs: 120)) + Sts2SfxAlignedFmod.PlayOneShot("event:/sfx/buff"); +``` + +**Singleton channel: replace the current playback** + +```csharp +GameFmod.Playback.PlayMusic( + AudioSource.StreamingMusic(nextMusicPath), + new AudioPlaybackOptions + { + Volume = 0.8f, + Routing = new AudioRoutingOptions + { + Channel = "my-mod/music", + ChannelMode = AudioChannelMode.ReplaceExisting, + AllowFadeOutOnReplace = true, + }, + } +); +``` + +**Tagged group: replace an entire UI cue group** + +```csharp +GameFmod.Playback.Play( + AudioSource.File(uiCuePath), + new AudioPlaybackOptions + { + Routing = new AudioRoutingOptions + { + Tag = "my-mod/ui-tooltips", + ReplaceTaggedGroup = true, + }, + } +); +``` + +--- + +::: + +## 简短示例{lang="zh-CN"} + +::: zh-CN + +**与原版一致的 one-shot** + +```csharp +using STS2RitsuLib.Audio; + +Sts2SfxAlignedFmod.PlayOneShot("event:/sfx/heal"); +GameFmod.Studio.PlayMusic("event:/music/menu_update"); +``` + +**模组内容 Bank + `guids.txt`(须与游戏 FMOD 主版本线兼容)** + +```csharp +FmodStudioServer.TryLoadBank("res://mods/MyMod/banks/MyMod.bank"); +FmodStudioServer.TryWaitForAllLoads(); +if (!FmodStudioServer.TryLoadStudioGuidMappings("res://mods/MyMod/banks/MyMod.guids.txt")) + return; +if (FmodStudioServer.TryCheckEventPath("event:/mods/mymod/hit") is true) + GameFmod.Studio.PlayOneShot("event:/mods/mymod/hit"); +``` + +**短音效文件(按 sound 加载)** + +```csharp +var sfxPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "ping.wav"); +FmodStudioStreamingFiles.TryPlaySoundFile(sfxPath, volume: 0.9f); +``` + +**流式音乐(推荐:新 Playback/Handle API)** + +```csharp +var musicPath = ProjectSettings.GlobalizePath("user://mymod/loop.ogg"); +var handle = GameFmod.Playback.PlayMusic( + AudioSource.StreamingMusic(musicPath), + new AudioPlaybackOptions { Volume = 0.7f, Scope = AudioLifecycleScope.Room } +); +``` + +**跟随游戏自动切换的常见三段式音乐(房间 / 战斗 / 胜利)** + +```csharp +var adaptive = GameFmod.Playback.FollowAdaptiveMusic( + AudioAdaptivePlans.FullRunOverride( + roomSource: AudioSource.StreamingMusic(roomLoopPath), + combatSource: AudioSource.StreamingMusic(combatLoopPath), + victorySource: AudioSource.StreamingMusic(victoryStingerPath) + ) +); +``` + +**触发过快时节流** + +```csharp +if (FmodPlaybackThrottle.TryEnter("my_power_proc", cooldownMs: 120)) + Sts2SfxAlignedFmod.PlayOneShot("event:/sfx/buff"); +``` + +**单例频道:替换当前播放** + +```csharp +GameFmod.Playback.PlayMusic( + AudioSource.StreamingMusic(nextMusicPath), + new AudioPlaybackOptions + { + Volume = 0.8f, + Routing = new AudioRoutingOptions + { + Channel = "my-mod/music", + ChannelMode = AudioChannelMode.ReplaceExisting, + AllowFadeOutOnReplace = true, + }, + } +); +``` + +**标签分组:替换整组 UI 提示音** + +```csharp +GameFmod.Playback.Play( + AudioSource.File(uiCuePath), + new AudioPlaybackOptions + { + Routing = new AudioRoutingOptions + { + Tag = "my-mod/ui-tooltips", + ReplaceTaggedGroup = true, + }, + } +); +``` + +--- + +::: + +## Auxiliary types (`STS2RitsuLib.Audio`){lang="en"} + +::: en + +| Type | Description | +|------|-------------| +| `FmodEventPath` | Lightweight wrapper for `event:/…` paths | +| `FmodStudioRouting` | Common bus path constants | +| `FmodParameterMap` | Builds parameter dictionaries for **`GameFmod.Studio`** | + +**`STS2RitsuLib.Audio.Internal`** is internal implementation and is not a stable public API. + +--- + +::: + +## 辅助类型(`STS2RitsuLib.Audio`){lang="zh-CN"} + +::: zh-CN + +| 类型 | 说明 | +|---|---| +| `FmodEventPath` | `event:/…` 路径轻量封装 | +| `FmodStudioRouting` | 常用 Bus 路径常量 | +| `FmodParameterMap` | 为 `GameFmod.Studio` 构造参数字典 | + +`STS2RitsuLib.Audio.Internal` 为内部实现,不作为稳定公共 API。 + +--- + +::: + +## Recommended external toolchain{lang="en"} + +::: en + +RitsuLib does not include the following; they are common external workflows: + +| Tool | Role | +|------|------| +| [FMOD Studio](https://www.fmod.com/) | Edit banks and events. **Match the game's FMOD Studio major version line** (see the game's `addons/fmod` directory) | +| Built-in Godot FMOD plugin in the game | Same class of integration as `utopia-rise/fmod-gdextension`; provides the **`FmodServer`** singleton at runtime | +| [sts2-fmod-tools](https://github.com/elliotttate/sts2-fmod-tools) (community) | Optional: align Studio projects/events from the game-data side | +| DAW export | Export WAV/OGG, etc.; if mixing with vanilla SFX, watch loudness and dynamic range | + +> RitsuLib wires **guids.txt-style mappings** into **`NAudioManager`** for path-based Studio calls (one-shots, loops, music, stops, parameters, **`UpdateMusicParameter`**, etc.). After your mod loads its **`.bank`** and calls **`TryLoadStudioGuidMappings`**, **`event:/…`** paths keep using the same **`NAudioManager` → AudioManagerProxy** pipeline as vanilla. Custom Harmony that replaces or bypasses that chain must coordinate with other mods. + +--- + +::: + +## 推荐外部工具链{lang="zh-CN"} + +::: zh-CN + +RitsuLib 不包含下列工具,它们是常见的外部工作流: + +| 工具 | 作用 | +|---|---| +| [FMOD Studio](https://www.fmod.com/) | 编辑 Bank / Event。务必与游戏所用 FMOD 主版本线一致(可参考游戏目录 `addons/fmod`) | +| 游戏内置 Godot FMOD 插件 | 与 `utopia-rise/fmod-gdextension` 同类集成,运行时提供 `FmodServer` 单例 | +| [sts2-fmod-tools](https://github.com/elliotttate/sts2-fmod-tools)(社区) | 可选:从游戏数据侧辅助对齐 Studio 工程/事件 | +| DAW 导出 | 导出 WAV/OGG 等;若与原版 SFX 混播,注意响度与动态范围 | + +> RitsuLib 已对 **`NAudioManager`** 中与路径相关的 Studio 调用(OneShot / Loop / Music / Stop / SetParam / `UpdateMusicParameter` 等)接入 **guids.txt 映射**:模组在加载 **`.bank`** 后调用 **`TryLoadStudioGuidMappings`**,即可继续用 **`event:/…`** 字符串走与原版相同的 **`NAudioManager` → AudioManagerProxy** 管线。自定义替换或绕过该链路的 Harmony 补丁需自行与其它 Mod 协调。 + +--- + +::: + +## Authoring an extra mod bank (recommended workflow){lang="en"} + +::: en + +Use this workflow when you ship **only your own `.bank`** plus a **`*.guids.txt`** from the **same Studio build**. + +### 1. Bank type and naming + +- **Do not replace or overwrite** the shipped **`Master.bank`**. +- Ship a **separately named content bank** (sometimes called a sidecar / child bank). Its file name and the **Bank** name inside FMOD Studio should be **globally unique** among mods and future official banks to avoid **naming collisions**. +- That bank holds **your** events and media; the **mixer / Master routing** still comes from the game's already-loaded vanilla banks. + +### 2. Bus / Master alignment (match vanilla mixing) + +- At runtime, **`AudioManagerProxy`** expects buses such as **`bus:/master`**, **`bus:/master/sfx`**, **`bus:/master/music`**, **`bus:/master/ambience`** (consistent with the desktop bank load order). +- **For vanilla-like loudness slider and bus behaviour**, route your events to those **`bus:/…`** paths—the same hierarchy **defined by the game's Master-side data**—instead of publishing a competing top-level Master bank that replaces the official one. +- **When you must verify identifiers**: compare **Bus / VCA** paths and GUIDs against the game's **GUIDs.txt** or tools like **`sts2-fmod-tools`**. Export **GUIDs.txt** from **the same FMOD Studio build** as your **`.bank`** so text and binary never drift apart. + +### 3. Export GUIDs and ship them with the mod + +1. **Build** your bank in FMOD Studio. +2. Take **`GUIDs.txt`** from the build output (or export a GUID list). +3. Ship it as a text resource (e.g. **`YourMod.guids.txt`**): keep every **`event:/…`** line (`{guid} event:/…`, one record per line); you may keep other lines for debugging. +4. After **`TryLoadBank`** + **`TryWaitForAllLoads`**, call **`FmodStudioServer.TryLoadStudioGuidMappings("res://…/YourMod.guids.txt")`**. That fills the path → GUID table and logs success/failure; together with RitsuLib's **`NAudioManager`** Harmony prefixes, **`event:/…`** paths keep resolving through **`NAudioManager`**. + +### 4. Runtime order and stability + +- Load your mod bank **after** the game's FMOD bootstrap and **`NAudioManager`** are ready (for example from a deferred-init callback); loading too early can leave the Studio cache in a bad state for probes. +- Use **`FmodStudioServer.TryLoadBank`**: the implementation **pins** the returned **`FmodBank`** reference so it is not finalized immediately (the GDExtension **`FmodBank`** destructor calls **`unload_bank`**). + +### 5. Toolchain version and artefact pairing + +- Match the **FMOD Studio major line** to the game's **`addons/fmod`** / runtime. +- Always ship **`.bank`** and **`GUIDs.txt` slice** from the **same build**. Mixing an old bank with a new GUID file (or vice versa) breaks **`check_event_guid`** / path resolution at runtime. + +--- + +::: + +## 模组附加 Bank 制作(推荐流程){lang="zh-CN"} + +::: zh-CN + +模组仅发布 **自建 `.bank`** 与同次构建导出的 **`*.guids.txt`** 时,建议按下述方式制作与接入。 + +### 1. Bank 类型与命名 + +- **不要替换或改名覆盖**游戏自带的 **`Master.bank`**。 +- 模组应使用 **独立命名的内容 Bank**(又称「子 Bank」、Sidecar Bank),文件名与 Studio 内 **Bank 名称**在游戏内全局 **唯一**,避免与其它模组或未来的官方 Bank **重名冲突**。 +- 该 Bank 仅承载你的 Event / 采样;混音树上的 **Master / Routing** 仍依赖游戏已加载的原版 Master 管线。 + +### 2. Bus / Master 对齐(与原版混音一致) + +- 游戏里 **`AudioManagerProxy`** 使用的典型路径包括 **`bus:/master`**、**`bus:/master/sfx`**、**`bus:/master/music`**、**`bus:/master/ambience`**(与原版 `banks/desktop` 加载顺序下的 Studio 缓存一致)。 +- **若希望模组音效/音乐的分轨、衰减、音量滑条行为与原版一致**:在 FMOD Studio 中为 Event 指定的 **Routing / Output**,应落到上述 **已与原版一致的 `bus:/…`** 路径上(即路由到游戏里已经存在、GUID 由原版 Master 侧定义的 Bus),而不要自造一套与原版无关的顶层 Master Bank 去顶替官方。 +- **需要逐项对齐时**:可从官方/解包工程的 **GUIDs.txt**、`sts2-fmod-tools` 等对照 **Bus / VCA** 的路径与 GUID;同一 **FMOD Studio 主版本线** 前提下,与你的模组 Bank **同一次构建**导出 **GUIDs.txt**,避免 txt 与 `.bank` 二进制不一致。 + +### 3. 导出 GUID 并导入模组资源 + +1. 在 FMOD Studio 中 **Build** 你的模组 Bank。 +2. 在生成目录中取 **`GUIDs.txt`**(或由 Studio 导出 **GUID List**)。 +3. 拷贝为模组中的文本资源(例如 **`Evil.guids.txt`**):至少保留全部 **`event:/…`** 行(格式为 **`{xxxxxxxx-…} event:/…`**,一行一条);可按需保留与其它对象相关的行便于自查。 +4. 游戏初始化时在 **`TryLoadBank`**(或你的封装)加载 **`.bank`**、`TryWaitForAllLoads` 之后调用 **`FmodStudioServer.TryLoadStudioGuidMappings("res://…/YourMod.guids.txt")`**:框架会写入路径 → GUID 表并打日志;与 **RitsuLib** 内对 **`NAudioManager`** 的 Harmony 前缀配合后,即可用 **`event:/…`** 字符串走 **`NAudioManager`** 原版入口。 + +### 4. 运行时顺序与稳定性 + +- **在游戏的 FMOD 启动流程与 `NAudioManager` 已就绪之后**再 `TryLoadBank` 你的模组 Bank(例如在延迟初始化回调中);过早加载时 Studio 侧缓存可能尚未稳定,探测易失败。 +- 使用 **`FmodStudioServer.TryLoadBank`** 加载模组 Bank:实现会 **保留返回的 `FmodBank` 引用**,避免仅校验返回值后引用被回收导致 **引擎侧自动 unload**(见 GDExtension `FmodBank` 析构行为)。 + +### 5. 版本与产物一致性 + +- **FMOD Studio 主版本**须与游戏内 **`addons/fmod`** 所用库一致(或官方文档允许的兼容范围)。 +- **同一次 Build** 产出的 **`.bank`** 与 **`GUIDs.txt`** 必须成对发布;任意一侧来自旧构建都会导致 **`check_event_guid` / 路径解析失败**。 + +--- + +::: + +## Troubleshooting{lang="en"} + +::: en + +- **`FmodStudioServer.TryGet()` is null** — `FmodServer` not ready (scene, headless test, or extension failed to load); check the game log +- **`TryCheckEventPath` is false** — the **`.bank`** is missing or unloaded, the path is wrong, **`TryLoadStudioGuidMappings`** did not succeed, or the bank was unloaded (use **`FmodStudioServer.TryLoadBank`**, which **pins** the returned **`FmodBank`** reference) +- **No sound and no exception** — **`TestMode`** / **`NonInteractiveMode`** may suppress **`NAudioManager`**; direct **`FmodServer`** calls are not subject to those flags + +--- + +::: + +## 排错{lang="zh-CN"} + +::: zh-CN + +- **`FmodStudioServer.TryGet()` 为 null** — `FmodServer` 未就绪(场景、无头测试或扩展加载失败),查游戏日志 +- **`TryCheckEventPath` 为 false** — 对应 **`.bank` 未加载**、路径写错、**`TryLoadStudioGuidMappings` 未成功**,或 **Bank 已被卸载**(须使用会 **pin `FmodBank` 引用** 的 `TryLoadBank` 封装) +- **无声且无异常** — `TestMode` / `NonInteractiveMode` 可能抑制 `NAudioManager`;直连 `FmodServer` 不受这些标志约束 + +--- + +::: + +## Related documentation{lang="en"} + +::: en + +- [Diagnostics & Compatibility](/guide/diagnostics-and-compatibility) +- [Patching Guide](/guide/patching-guide) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [诊断与兼容层](/guide/diagnostics-and-compatibility) +- [补丁系统](/guide/patching-guide) + +::: diff --git a/docs/pages/guide/framework-design.md b/docs/pages/guide/framework-design.md new file mode 100644 index 0000000..00a8e48 --- /dev/null +++ b/docs/pages/guide/framework-design.md @@ -0,0 +1,392 @@ +--- +title: + en: Framework Design + zh-CN: 框架设计 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This document explains the architectural decisions behind RitsuLib and the constraints those decisions impose on mod code. + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文说明 RitsuLib 的核心架构决策,以及这些决策对 Mod 实现方式的影响。 + +--- + +::: + +## Core Goals{lang="en"} + +::: en + +RitsuLib is built around a small set of explicit design priorities: + +- explicit registration instead of opaque “magic” discovery +- fixed model identity instead of runtime name inference +- composable asset records instead of large inheritance hierarchies +- scene replacement instead of in-place mutation of vanilla assets +- compatibility fallbacks only where the base game has no safe extension point + +The framework reduces repetitive authoring work, but it does not convert the mod into an implicit runtime graph. + +Optional **attribute-based** registration is still explicit: only types in assemblies registered with `ModTypeDiscoveryHub.RegisterModAssembly` are considered, each attribute maps to ordinary registry calls, and `AutoRegistrationAttribute.Inherit` defaults to **off** so derived types do not pick up base annotations unless you opt in. Details: [Content Packs & Registries](ContentPacksAndRegistries.md#attribute-based-registration-optional). + +--- + +::: + +## 核心目标{lang="zh-CN"} + +::: zh-CN + +RitsuLib 以少量明确的设计原则为核心: + +- 使用显式注册,而非不透明、无约束的「魔法」式发现 +- 使用固定模型身份,而非运行时推断名称 +- 使用可组合的资源记录,而非大型继承层级 +- 使用场景替换,而非原版资源原地修改 +- 仅在原版缺少安全扩展点时引入兼容回退 + +框架会减少重复性工作,但不会把 Mod 运行时结构隐藏为不可见行为。 + +可选的 **CLR 特性**注册仍然是显式的:只有通过 `ModTypeDiscoveryHub.RegisterModAssembly` 登记的程序集才会参与扫描;每条特性最终仍对应与普通代码相同的注册器调用;`AutoRegistrationAttribute.Inherit` 默认为 **关闭**,避免子类在未声明的情况下继承基类上的特性。说明见 [内容包与注册器](ContentPacksAndRegistries.md#clr-特性注册可选)。 + +--- + +::: + +## Fixed Identity{lang="en"} + +::: en + +For models registered through the RitsuLib content registry, `ModelId.Entry` is deterministic: + +```text +__ +``` + +Why this matters: + +- localization keys stay stable and predictable +- refactors are easier to reason about +- content registration conflicts are easier to detect +- migration between project structures does not depend on reflection order or class discovery behavior + +The tradeoff is deliberate: renaming a published CLR type becomes a compatibility change. + +--- + +::: + +## 固定模型身份{lang="zh-CN"} + +::: zh-CN + +对通过 RitsuLib 内容注册器注册的模型,`ModelId.Entry` 是确定性的: + +```text +__ +``` + +这样做的好处: + +- 本地化 Key 稳定且可预测 +- 重构时更容易判断影响面 +- 内容冲突更容易定位 +- 不依赖反射顺序、自动扫描细节或类发现时机 + +这一取舍是明确的:已发布的 CLR 类型一旦改名,就属于兼容性变更。 + +--- + +::: + +## Registration Before Use{lang="en"} + +::: en + +RitsuLib relies on explicit registration during early boot. + +`CreateContentPack(modId)` is the convenience entry point, but the underlying registries remain first-class. + +Registration is frozen during early boot to preserve: + +- stable model identity +- stable model lists +- deterministic lookup and unlock behavior + +The framework therefore fails fast instead of mutating the model graph after runtime systems have started consuming it. + +See [Content Packs & Registries](/guide/content-packs-and-registries) for the concrete registration model. + +--- + +::: + +## 先注册,再使用{lang="zh-CN"} + +::: zh-CN + +RitsuLib 要求在早期引导阶段完成显式注册。 + +`CreateContentPack(modId)` 是便捷入口,但底层注册器仍然是第一层概念。 + +框架在早期引导阶段冻结注册,以保证: + +- 稳定的模型身份 +- 稳定的模型列表 +- 可预测的查找与解锁行为 + +因此,框架选择尽早失败,而不是在运行时系统已开始消费模型后继续修改模型图。 + +具体注册模型可见 [内容包与注册器](/guide/content-packs-and-registries)。 + +--- + +::: + +## Asset Profiles Instead Of Large Character Bases{lang="en"} + +::: en + +Character authoring is organized around asset profiles. + +Instead of requiring a monolithic custom-character base type with unrelated virtual members, RitsuLib groups assets into records such as: + +- `CharacterSceneAssetSet` +- `CharacterUiAssetSet` +- `CharacterVfxAssetSet` +- `CharacterAudioAssetSet` + +This keeps responsibility boundaries explicit: + +- scenes live together +- UI assets live together +- VFX tuning lives together +- audio overrides live together + +This is more verbose than a single placeholder property, but it scales better because each asset category can evolve independently. + +--- + +::: + +## 资源配置,而不是大型角色基类{lang="zh-CN"} + +::: zh-CN + +角色内容编写围绕结构化资源配置展开。 + +RitsuLib 不要求把所有角色资源塞进一个单体基类,而是按职责分组: + +- `CharacterSceneAssetSet` +- `CharacterUiAssetSet` +- `CharacterVfxAssetSet` +- `CharacterAudioAssetSet` + +这样可以保持职责边界清晰: + +- 场景资源放一起 +- UI 放一起 +- VFX 调整放一起 +- 音效放一起 + +这种方式确实比单一占位属性更冗长,但更利于独立扩展各类资源能力。 + +--- + +::: + +## Asset Safety Mechanisms{lang="en"} + +::: en + +The asset-profile system is paired with a small set of safety mechanisms: + +- character placeholder fallback for missing character resources +- separate APIs for full energy-counter scenes versus pool-linked icons +- one-time warnings when explicit resource paths are missing + +These behaviors are part of the same design: a structured asset API must remain usable during migration and partial-content development. + +See [Asset Profiles & Fallbacks](/guide/asset-profiles-and-fallbacks) for the detailed behavior and API surface. + +--- + +::: + +## 资源安全机制{lang="zh-CN"} + +::: zh-CN + +资源配置体系配套了一组小范围的安全机制: + +- 角色缺失资源时的占位角色回退 +- 完整能量球场景与池级图标的分层 API +- 显式资源路径不存在时的一次性警告 + +这些行为属于同一设计目标的一部分,用于保证结构化资源 API 在迁移和未完成内容阶段仍然可用。 + +具体行为与 API 细节见 [资源配置与回退规则](/guide/asset-profiles-and-fallbacks)。 + +--- + +::: + +## Compatibility Layers Stay Narrow{lang="en"} + +::: en + +RitsuLib includes compatibility-oriented patches, but they are intentionally narrow. + +The framework does not hide every engine limitation behind automation. It adds fallbacks only where the game or modding surface would otherwise be unsafe or excessively repetitive. + +Examples include `LocTable` and `THE_ARCHITECT` fallbacks under `debug_compatibility_mode`, ancient dialogue key injection, and unlock bridge patches for vanilla progression checks that skip mod characters. + +See [Diagnostics & Compatibility](/guide/diagnostics-and-compatibility) for the concrete compatibility layers. + +--- + +::: + +## 兼容层保持收敛{lang="zh-CN"} + +::: zh-CN + +RitsuLib 提供兼容型补丁,但范围刻意保持收敛。 + +框架不会用自动化去覆盖所有引擎限制。只有在原版扩展点不安全,或重复劳动明显过高时,才会加入兼容回退。 + +典型例子包括:`debug_compatibility_mode` 下的 `LocTable` 与 `THE_ARCHITECT` 回退、Ancient 对话键注入,以及原版进度检查跳过 Mod 角色时使用的解锁桥接补丁。 + +具体兼容层可见 [诊断与兼容层](/guide/diagnostics-and-compatibility)。 + +--- + +::: + +## Why The Patching Layer Exists{lang="en"} + +::: en + +Harmony is still the underlying patch engine, but RitsuLib wraps it with: + +- typed patch declarations via `IPatchMethod` +- critical vs optional patch semantics +- ignore-if-missing targets +- grouped registration helpers +- dynamic patch application support + +The goal is not to abstract Harmony away. The goal is to standardize patch declaration and failure handling so large mods remain maintainable. + +See [Patching Guide](/guide/patching-guide) for the patching workflow. + +--- + +::: + +## 为什么要有自己的补丁层{lang="zh-CN"} + +::: zh-CN + +底层仍然是 Harmony,但 RitsuLib 在其上增加了一层统一约定: + +- 用 `IPatchMethod` 声明补丁 +- 区分 critical / optional +- 支持忽略缺失目标 +- 支持分组注册 +- 支持动态补丁 + +目的不是隐藏 Harmony,而是统一补丁声明、失败处理与日志行为,降低大型 Mod 的维护成本。 + +具体流程见 [补丁系统](/guide/patching-guide)。 + +--- + +::: + +## Why Persistence Is Class-Based{lang="en"} + +::: en + +Persistent entries are registered as class types rather than loose primitives. + +That choice enables: + +- schema version fields +- structured migrations +- future expansion without breaking call sites +- safer serialization boundaries + +This adds some upfront structure, but avoids primitive save keys that later need to carry schema growth. + +See [Persistence Guide](/guide/persistence-guide) for the full data model. + +--- + +::: + +## 为什么持久化按类组织{lang="zh-CN"} + +::: zh-CN + +RitsuLib 的持久化条目是按类注册的,而不是随手塞原始值。 + +这样做可以自然支持: + +- 数据版本字段 +- 数据迁移 +- 后续扩展字段 +- 更清晰的序列化边界 + +前期会增加少量样板,但可以避免原始值存档在后期演化为复杂结构时的维护问题。 + +完整数据设计见 [持久化设计](/guide/persistence-guide)。 + +--- + +::: + +## Recommended Reading Order{lang="en"} + +::: en + +- [Getting Started](/guide/getting-started) +- [Content Authoring Toolkit](/guide/content-authoring-toolkit) +- [Content Packs & Registries](/guide/content-packs-and-registries) +- [Character & Unlock Templates](/guide/character-and-unlock-scaffolding) +- [Timeline & Unlocks](/guide/timeline-and-unlocks) +- [Asset Profiles & Fallbacks](/guide/asset-profiles-and-fallbacks) +- [Patching Guide](/guide/patching-guide) +- [Persistence Guide](/guide/persistence-guide) +- [Localization & Keywords](/guide/localization-and-keywords) +- [Diagnostics & Compatibility](/guide/diagnostics-and-compatibility) + +::: + +## 推荐阅读顺序{lang="zh-CN"} + +::: zh-CN + +- [快速入门](/guide/getting-started) +- [内容注册规则](/guide/content-authoring-toolkit) +- [内容包与注册器](/guide/content-packs-and-registries) +- [角色与解锁模板](/guide/character-and-unlock-scaffolding) +- [时间线与解锁](/guide/timeline-and-unlocks) +- [资源配置与回退规则](/guide/asset-profiles-and-fallbacks) +- [补丁系统](/guide/patching-guide) +- [持久化设计](/guide/persistence-guide) +- [本地化与关键词](/guide/localization-and-keywords) +- [诊断与兼容层](/guide/diagnostics-and-compatibility) + +::: diff --git a/docs/pages/guide/getting-started.md b/docs/pages/guide/getting-started.md new file mode 100644 index 0000000..bb57e38 --- /dev/null +++ b/docs/pages/guide/getting-started.md @@ -0,0 +1,480 @@ +--- +title: + en: Getting Started + zh-CN: 快速入门 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This guide walks through the full setup — from declaring the dependency to registering your first content. + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本指南覆盖从声明依赖到注册第一个内容的完整流程。 + +--- + +::: + +## 1. Declare the Dependency{lang="en"} + +::: en + +Add `STS2-RitsuLib` to your `mod_manifest.json`: + +```json +{ + "id": "MyMod", + "name": "My Mod", + "dependencies": ["STS2-RitsuLib"] +} +``` + +--- + +::: + +## 1. 声明依赖{lang="zh-CN"} + +::: zh-CN + +在 `mod_manifest.json` 中添加: + +```json +{ + "id": "MyMod", + "name": "My Mod", + "dependencies": ["STS2-RitsuLib"] +} +``` + +--- + +::: + +## 2. Initialize Your Mod{lang="en"} + +::: en + +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(); + patcher.PatchAll(); + + RitsuLibFramework.CreateContentPack("MyMod") + .Character() + .Card() + .Card() + .Relic() + .Apply(); + } +} +``` + +For the full mapping of fluent methods, `ModContentRegistry` calls, and `IContentRegistrationEntry` types (enchantments, achievements, shared pools, manifests, etc.), see [Content Packs & Registries](/guide/content-packs-and-registries). + +`CreatePatcher` takes a `patcherName` used for log identification. A mod may create multiple patchers. See [Patching Guide](/guide/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](/guide/godot-scene-authoring). + +--- + +::: + +## 2. 初始化 Mod{lang="zh-CN"} + +::: zh-CN + +使用 `[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(); + patcher.PatchAll(); + + RitsuLibFramework.CreateContentPack("MyMod") + .Character() + .Card() + .Card() + .Relic() + .Apply(); + } +} +``` + +链式方法、`ModContentRegistry` 与 `IContentRegistrationEntry`(附魔、成就、共享池、Manifest 等)的完整对照见 [内容包与注册器](/guide/content-packs-and-registries)。 + +`CreatePatcher` 的 `patcherName` 参数用于日志标识。同一个 Mod 可以创建多个 Patcher。完整补丁写法见 [补丁系统](/guide/patching-guide)。 + +如果你的 Mod 使用了自定义 Godot C# 场景脚本,请把 `EnsureGodotScriptsRegistered(...)` 保留在初始化入口里。详见 [Godot 场景编写说明](/guide/godot-scene-authoring)。 + +--- + +::: + +## 3. Define a Card Pool{lang="en"} + +::: en + +Use `TypeListCardPoolModel` for pool visuals and metadata (frame, energy color, etc.). **Each card that belongs in the pool** must be registered via `.Card()`, `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(...)` 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](/guide/content-packs-and-registries). + +--- + +::: + +## 3. 定义卡池{lang="zh-CN"} + +::: zh-CN + +使用 `TypeListCardPoolModel` 承载池的视觉与元数据(边框、能量色等)。**属于该池的每张牌**必须在内容包里通过 `.Card()`、`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(...)` 及遗物/药水对应 API。完整说明、示例与**必读警告**(存档 entry、联机 `ModelIdSerializationCache` Hash、无玩法效果等)见 [内容包与注册器](/guide/content-packs-and-registries) 中的「生成式占位内容」一节。 + +--- + +::: + +## 4. Define a Card{lang="en"} + +::: en + +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 => $"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. 定义卡牌{lang="zh-CN"} + +::: zh-CN + +继承 `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 => $"造成 {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{lang="en"} + +::: en + +The `ModelId.Entry` for any RitsuLib-registered model is derived as: + +``` +__ +``` + +All segments are normalized to UPPER_SNAKE_CASE. + +| Mod Id | C# Type | Category | Entry | +|---|---|---|---| +| `MyMod` | `MyCard` | card | `MY_MOD_CARD_MY_CARD` | +| `MyMod` | `MyRelic` | relic | `MY_MOD_RELIC_MY_RELIC` | +| `MyMod` | `MyCharacter` | character | `MY_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{lang="zh-CN"} + +::: zh-CN + +RitsuLib 注册的所有模型,其 `ModelId.Entry` 由以下规则推导(各字段规范化为全大写下划线格式): + +``` +__ +``` + +| Mod Id | C# 类型 | 类别 | Entry | +|---|---|---|---| +| `MyMod` | `MyCard` | card | `MY_MOD_CARD_MY_CARD` | +| `MyMod` | `MyRelic` | relic | `MY_MOD_RELIC_MY_RELIC` | +| `MyMod` | `MyCharacter` | character | `MY_MOD_CHARACTER_MY_CHARACTER` | + +本地化文件示例: + +```json +{ + "MY_MOD_CARD_MY_CARD.title": "打击", + "MY_MOD_CARD_MY_CARD.description": "造成 {damage} 点伤害。" +} +``` + +--- + +::: + +## 6. Subscribe to Lifecycle Events{lang="en"} + +::: en + +```csharp +// Runs once after game is ready +RitsuLibFramework.SubscribeLifecycle(evt => +{ + Logger.Info("Game ready."); +}); + +// On every combat start +RitsuLibFramework.SubscribeLifecycle(evt => +{ + // evt.RunState, evt.CombatState +}); +``` + +Replayable events (`IReplayableFrameworkLifecycleEvent`) fire immediately upon late subscription if the event has already occurred. + +--- + +::: + +## 6. 订阅生命周期事件{lang="zh-CN"} + +::: zh-CN + +```csharp +// 游戏就绪后执行一次 +RitsuLibFramework.SubscribeLifecycle(evt => +{ + Logger.Info("游戏已就绪。"); +}); + +// 每次战斗开始时 +RitsuLibFramework.SubscribeLifecycle(evt => +{ + // evt.RunState, evt.CombatState +}); +``` + +可重放事件(`IReplayableFrameworkLifecycleEvent`)即使在事件已发生后订阅也会立即回调,无需关心订阅时机。 + +--- + +::: + +## 7. Persistent Data{lang="en"} + +::: en + +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( + key: "my_counter", + fileName: "counter.json", + scope: SaveScope.Profile, + defaultFactory: () => new CounterData()); +} +``` + +See [Persistence Guide](/guide/persistence-guide) for scopes, reload timing, and migrations. + +--- + +::: + +## 7. 数据持久化{lang="zh-CN"} + +::: zh-CN + +使用 `BeginModDataRegistration` 批量注册存档数据键。持久化条目以类为单位注册,同时需要注册键和文件名: + +```csharp +public sealed class CounterData +{ + public int Value { get; set; } +} + +using (RitsuLibFramework.BeginModDataRegistration("MyMod")) +{ + var store = RitsuLibFramework.GetDataStore("MyMod"); + store.Register( + key: "my_counter", + fileName: "counter.json", + scope: SaveScope.Profile, + defaultFactory: () => new CounterData()); +} +``` + +关于作用域、重载时机和迁移机制,可继续阅读 [持久化设计](/guide/persistence-guide)。 + +--- + +::: + +## Next Steps{lang="en"} + +::: en + +- [Content Authoring Toolkit](/guide/content-authoring-toolkit) +- [Character & Unlock Templates](/guide/character-and-unlock-scaffolding) +- [Card Dynamic Variables](/guide/card-dynamic-var-toolkit) +- [Lifecycle Events](/guide/lifecycle-events) +- [Patching Guide](/guide/patching-guide) +- [Persistence Guide](/guide/persistence-guide) +- [Localization & Keywords](/guide/localization-and-keywords) +- [Framework Design](/guide/framework-design) +- [Content Packs & Registries](/guide/content-packs-and-registries) +- [Godot Scene Authoring](/guide/godot-scene-authoring) +- [Timeline & Unlocks](/guide/timeline-and-unlocks) +- [Asset Profiles & Fallbacks](/guide/asset-profiles-and-fallbacks) +- [Diagnostics & Compatibility](/guide/diagnostics-and-compatibility) + +::: + +## 继续阅读{lang="zh-CN"} + +::: zh-CN + +- [内容注册规则](/guide/content-authoring-toolkit) +- [角色与解锁模板](/guide/character-and-unlock-scaffolding) +- [卡牌动态变量](/guide/card-dynamic-var-toolkit) +- [生命周期事件](/guide/lifecycle-events) +- [补丁系统](/guide/patching-guide) +- [持久化设计](/guide/persistence-guide) +- [本地化与关键词](/guide/localization-and-keywords) +- [框架设计](/guide/framework-design) +- [内容包与注册器](/guide/content-packs-and-registries) +- [Godot 场景编写说明](/guide/godot-scene-authoring) +- [时间线与解锁](/guide/timeline-and-unlocks) +- [资源配置与回退规则](/guide/asset-profiles-and-fallbacks) +- [诊断与兼容层](/guide/diagnostics-and-compatibility) + +::: diff --git a/docs/pages/guide/godot-scene-authoring.md b/docs/pages/guide/godot-scene-authoring.md new file mode 100644 index 0000000..df947d4 --- /dev/null +++ b/docs/pages/guide/godot-scene-authoring.md @@ -0,0 +1,356 @@ +--- +title: + en: Godot Scene Authoring + zh-CN: Godot 场景编写说明 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This document covers two practical concerns for STS2 mods authoring Godot scenes: + +- Scene-facing game types should be subclassed in the mod first, then bound in the editor +- Godot C# scripts in the mod assembly must be registered during initialization + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文说明 STS2 Mod 在 Godot 场景编写时的两个实践性问题: + +- 面向场景的游戏类型应先在 Mod 里继承一层本地子类,再让编辑器绑定 +- Mod 程序集里的 Godot C# 脚本需要在初始化时注册 + +--- + +::: + +## Why Mod-Local Subclasses{lang="en"} + +::: en + +> The following describes an engine behavior in the Godot Mono workflow. + +In the Godot Mono workflow used for STS2 modding, binding C# types from the game assembly directly to `.tscn` scenes is unreliable in the editor. + +Experience shows that opening, serializing, and rebinding works more reliably when the `.tscn` binds to a script type from your own mod assembly. + +Practical rule: + +- Whenever a scene node needs to behave as an in-game Godot type, add a thin mod-local subclass first, then bind the scene to that subclass + +--- + +::: + +## 为什么需要本地子类{lang="zh-CN"} + +::: zh-CN + +> 以下涉及 Godot Mono 工作流的一个引擎行为特征。 + +在当前 STS2 modding 使用的 Godot Mono 工作流里,来自游戏程序集的 C# 类型直接绑定到 `.tscn` 场景上时,编辑器行为并不稳定。 + +实际经验表明,只有当 `.tscn` 里绑定的是 Mod 自己程序集里的脚本类型时,编辑器的打开、序列化、重新绑定才更可靠。 + +稳定的经验法则: + +- 只要场景节点需要表现为游戏里的 Godot 类型,就先在 Mod 本地继承一个子类,再让场景绑定这个子类 + +--- + +::: + +## Wrapper Pattern{lang="en"} + +::: en + +Do not bind scenes directly to game types such as `NEnergyCounter`. Write a mod-local script: + +```csharp +using MegaCrit.Sts2.Core.Nodes.Combat; + +namespace MyMod.Scripts +{ + public partial class MyEnergyCounter : NEnergyCounter + { + } +} +``` + +Bind the `.tscn` to `MyEnergyCounter`, not to `NEnergyCounter` directly. + +The wrapper can be empty. Its purpose is to give the editor a local script type owned by your mod. + +--- + +::: + +## 包装子类模式{lang="zh-CN"} + +::: zh-CN + +不要让场景直接绑定 `NEnergyCounter` 这种游戏类型,而是先写一个本地脚本: + +```csharp +using MegaCrit.Sts2.Core.Nodes.Combat; + +namespace MyMod.Scripts +{ + public partial class MyEnergyCounter : NEnergyCounter + { + } +} +``` + +然后在 `.tscn` 里绑定 `MyEnergyCounter`,而不是直接绑定 `NEnergyCounter`。 + +这个包装子类完全可以是空的。它存在的意义是给编辑器一个属于 Mod 自身的本地脚本类型。 + +--- + +::: + +## Common Types That Need Wrapping{lang="en"} + +::: en + +| Game type | Typical use | +|---|---| +| `NEnergyCounter` | Energy orb scene root | +| `NRestSiteCharacter` | Rest site character scene | +| `NCreatureVisuals` | Character visuals scene | +| `NSelectionReticle` | Selection reticle | +| `MegaLabel` | Label child control | + +--- + +::: + +## 常见需要包装的类型{lang="zh-CN"} + +::: zh-CN + +| 游戏类型 | 典型用途 | +|---|---| +| `NEnergyCounter` | 能量球场景根节点 | +| `NRestSiteCharacter` | 休息点角色场景 | +| `NCreatureVisuals` | 角色视觉场景 | +| `NSelectionReticle` | 选择准星 | +| `MegaLabel` | 标签子控件 | + +--- + +::: + +## Generic Binding Examples{lang="en"} + +::: en + +Custom energy orb scene: + +- Root script → `MyEnergyCounter : NEnergyCounter` +- Label child → `MyCounterLabel : MegaLabel` + +Character visuals scene: + +- Root script → `MyCreatureVisuals : NCreatureVisuals` + +Rest site scene: + +- Root script → `MyRestSiteCharacter : NRestSiteCharacter` + +The point is not the class names — it is that bound scripts live in your mod assembly. + +--- + +::: + +## 通用绑定示例{lang="zh-CN"} + +::: zh-CN + +自定义能量球场景: + +- 根脚本 → `MyEnergyCounter : NEnergyCounter` +- 标签子节点 → `MyCounterLabel : MegaLabel` + +角色视觉场景: + +- 根脚本 → `MyCreatureVisuals : NCreatureVisuals` + +休息点场景: + +- 根脚本 → `MyRestSiteCharacter : NRestSiteCharacter` + +重点不在于类名,而在于场景里绑定的脚本应属于 Mod 自己的程序集。 + +--- + +::: + +## Editor Rule{lang="en"} + +::: en + +Whenever the Godot editor must open, serialize, or rebind a script in your mod scene, prefer a mod-local subclass. + +Even when: + +- You have no extra logic yet +- Inheritance is a single line +- The runtime type already exists in the game assembly + +The wrapper is the compatibility layer between your scene and the editor. + +--- + +::: + +## 编辑器侧规则{lang="zh-CN"} + +::: zh-CN + +只要 Godot 编辑器需要打开、序列化或重新绑定某个 Mod 场景里的脚本,就优先使用 Mod 本地子类。 + +即使: + +- 暂时没有额外逻辑 +- 只是简单继承一行 +- 运行时目标类型在游戏程序集里已存在 + +这个包装本质上是场景与编辑器之间的兼容层。 + +--- + +::: + +## Runtime Script Registration{lang="en"} + +::: en + +If your mod uses Godot C# scene scripts, call this during initialization: + +```csharp +using System.Reflection; + +RitsuLibFramework.EnsureGodotScriptsRegistered( + Assembly.GetExecutingAssembly(), + Logger); +``` + +This lets Godot’s script bridge discover and register C# scripts from your mod assembly. + +Do this before content registration so scene scripts resolve reliably at runtime. + +--- + +::: + +## 运行时脚本注册{lang="zh-CN"} + +::: zh-CN + +如果 Mod 使用了 Godot C# 场景脚本,需在初始化阶段调用: + +```csharp +using System.Reflection; + +RitsuLibFramework.EnsureGodotScriptsRegistered( + Assembly.GetExecutingAssembly(), + Logger); +``` + +这让 Godot 的脚本桥接层发现并注册 Mod 程序集里的 C# 脚本。 + +应在内容注册之前完成此步骤,确保运行时能稳定发现场景脚本。 + +--- + +::: + +## Recommended Workflow{lang="en"} + +::: en + +1. Pick the game-side base type you need +2. Add a thin mod-local `partial class` that inherits it +3. Bind the `.tscn` to that local script +4. In your entry point, call `EnsureGodotScriptsRegistered(Assembly.GetExecutingAssembly(), Logger)` + +--- + +::: + +## 推荐工作流{lang="zh-CN"} + +::: zh-CN + +1. 确定需要的游戏侧基类 +2. 在 Mod 本地创建继承它的薄包装 `partial class` +3. 让 `.tscn` 绑定这个本地脚本 +4. 在初始化入口调用 `EnsureGodotScriptsRegistered(Assembly.GetExecutingAssembly(), Logger)` + +--- + +::: + +## When You Do Not Need Wrapping{lang="en"} + +::: en + +You usually do not need extra wrapper subclasses for: + +- Plain content model classes (card / relic / power / character) +- Pure C# helpers not used as Godot scripts +- Logic classes never bound to `.tscn` resources + +This document is only about Godot scenes and script binding. + +--- + +::: + +## 不需要包装的情况{lang="zh-CN"} + +::: zh-CN + +以下内容通常不需要额外包装子类: + +- 普通内容模型类(card / relic / power / character) +- 不作为 Godot 脚本使用的纯 C# 辅助类 +- 从不绑定到 `.tscn` 场景资源的逻辑类 + +本文只针对 Godot 场景编写与脚本绑定问题。 + +--- + +::: + +## Related Documents{lang="en"} + +::: en + +- [Getting Started](/guide/getting-started) +- [Character & Unlock Templates](/guide/character-and-unlock-scaffolding) +- [Asset Profiles & Fallbacks](/guide/asset-profiles-and-fallbacks) +- [Diagnostics & Compatibility](/guide/diagnostics-and-compatibility) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [快速入门](/guide/getting-started) +- [角色与解锁模板](/guide/character-and-unlock-scaffolding) +- [资源配置与回退规则](/guide/asset-profiles-and-fallbacks) +- [诊断与兼容层](/guide/diagnostics-and-compatibility) + +::: diff --git a/docs/pages/guide/index.md b/docs/pages/guide/index.md new file mode 100644 index 0000000..d8ae23a --- /dev/null +++ b/docs/pages/guide/index.md @@ -0,0 +1,132 @@ +--- +title: + en: Documentation index + zh-CN: 文档目录 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Overview{lang="en"} + +## 概述{lang="zh-CN"} + +::: en + +Use the sidebar or the tables below. Guide pages are Markdown in this repo under [`docs/pages/guide/`](https://github.com/WRXinYue/STS2-RitsuLib/tree/main/docs/pages/guide). + +::: + +::: zh-CN + +可通过侧栏或下方表格浏览。正文为仓库内 [`docs/pages/guide/`](https://github.com/WRXinYue/STS2-RitsuLib/tree/main/docs/pages/guide) 下的 Markdown,可直接编辑。 + +::: + +## Start here{lang="en"} + +## 从这里开始{lang="zh-CN"} + +::: en + +| Page | Description | +| --- | --- | +| [Getting started](/guide/getting-started) | Dependency, bootstrap, first registered content | +| [Framework design](/guide/framework-design) | Architecture and recommended reading order | +| [Terminology](/guide/terminology) | Canonical terms used across these docs | + +::: + +::: zh-CN + +| 文档 | 说明 | +| --- | --- | +| [快速入门](/guide/getting-started) | 依赖声明、引导流程与第一个已注册内容 | +| [框架设计](/guide/framework-design) | 核心架构与推荐阅读顺序 | +| [术语表](/guide/terminology) | 文档中统一使用的术语 | + +::: + +## Content authoring{lang="en"} + +## 内容编写{lang="zh-CN"} + +::: en + +| Page | Description | +| --- | --- | +| [Content authoring toolkit](/guide/content-authoring-toolkit) | Identity rules, localization contracts, asset overrides | +| [Content packs & registries](/guide/content-packs-and-registries) | Registry model, fixed identity, registration flow | +| [Character & unlock scaffolding](/guide/character-and-unlock-scaffolding) | Character assembly, registration, unlock integration | +| [Card dynamic variables](/guide/card-dynamic-var-toolkit) | Custom card vars with tooltip support | +| [Custom events](/guide/custom-events) | Event registration, localization, custom scenes | +| [Timeline & unlocks](/guide/timeline-and-unlocks) | Story / epoch registration, progression, compatibility | +| [Asset profiles & fallbacks](/guide/asset-profiles-and-fallbacks) | Placeholder fallback, profiles, path diagnostics | +| [Godot scene authoring](/guide/godot-scene-authoring) | Scene-script wrappers, editor notes, runtime registration | +| [Mod settings](/guide/mod-settings) | Settings UI architecture, bindings, controls, pages | + +::: + +::: zh-CN + +| 文档 | 说明 | +| --- | --- | +| [内容注册规则](/guide/content-authoring-toolkit) | 身份规则、本地化约束与资源覆写基础 | +| [内容包与注册器](/guide/content-packs-and-registries) | 注册器模型、固定身份与注册流程 | +| [角色与解锁模板](/guide/character-and-unlock-scaffolding) | 角色装配、注册与解锁集成 | +| [卡牌动态变量](/guide/card-dynamic-var-toolkit) | 带 Tooltip 支持的自定义卡牌变量 | +| [自定义事件](/guide/custom-events) | 事件注册、本地化对齐与自定义场景 | +| [时间线与解锁](/guide/timeline-and-unlocks) | Story / Epoch 注册、进度规则与兼容桥接 | +| [资源配置与回退](/guide/asset-profiles-and-fallbacks) | Placeholder、Profile 与路径诊断 | +| [Godot 场景编写](/guide/godot-scene-authoring) | 场景脚本包装、编辑器注意事项与运行时注册 | +| [Mod 设置界面](/guide/mod-settings) | 设置 UI、绑定、控件与页面组合 | + +::: + +## Localization{lang="en"} + +## 本地化{lang="zh-CN"} + +::: en + +| Page | Description | +| --- | --- | +| [Localization & keywords](/guide/localization-and-keywords) | `I18N`, keyword registration, ancient dialogue | +| [LocString placeholder resolution](/guide/loc-string-placeholder-resolution) | Placeholder syntax, formatters, extension points | + +::: + +::: zh-CN + +| 文档 | 说明 | +| --- | --- | +| [本地化与关键词](/guide/localization-and-keywords) | `I18N`、关键词注册与 Ancient 对话本地化 | +| [LocString 占位符解析](/guide/loc-string-placeholder-resolution) | 占位符语法、格式化器与扩展点 | + +::: + +## Runtime & infrastructure{lang="en"} + +## 运行时与基础设施{lang="zh-CN"} + +::: en + +| Page | Description | +| --- | --- | +| [Lifecycle events](/guide/lifecycle-events) | Lifecycle reference and timing | +| [Patching guide](/guide/patching-guide) | `ModPatcher`, `IPatchMethod`, dynamic patches | +| [Persistence guide](/guide/persistence-guide) | Store scopes, save lifecycle, migrations | +| [FMOD & audio](/guide/fmod-and-audio) | `GameFmod`, banks, buses, streaming files | +| [Diagnostics & compatibility](/guide/diagnostics-and-compatibility) | Diagnostics policy, fallbacks, bridge patches | + +::: + +::: zh-CN + +| 文档 | 说明 | +| --- | --- | +| [生命周期事件](/guide/lifecycle-events) | 事件参考与触发时机 | +| [补丁系统](/guide/patching-guide) | `ModPatcher`、`IPatchMethod` 与动态补丁 | +| [持久化设计](/guide/persistence-guide) | 存储作用域、存档生命周期与迁移 | +| [FMOD 与音频](/guide/fmod-and-audio) | `GameFmod`、Bank、Bus、流式文件等 | +| [诊断与兼容层](/guide/diagnostics-and-compatibility) | 诊断策略、兼容回退与桥接补丁 | + +::: diff --git a/docs/pages/guide/lifecycle-events.md b/docs/pages/guide/lifecycle-events.md new file mode 100644 index 0000000..c721eb9 --- /dev/null +++ b/docs/pages/guide/lifecycle-events.md @@ -0,0 +1,526 @@ +--- +title: + en: Lifecycle Events + zh-CN: 生命周期事件参考 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This document lists all lifecycle events provided by RitsuLib, explains subscription patterns, and details replayable event behavior. + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文列出 RitsuLib 提供的全部生命周期事件,介绍订阅方式及可重放事件的行为。 + +--- + +::: + +## Subscription Patterns{lang="en"} + +::: en + +### Subscribe by Event Type (Recommended) + +```csharp +var sub = RitsuLibFramework.SubscribeLifecycle(evt => +{ + Logger.Info($"Game ready: {evt.Game}"); +}); + +// Unsubscribe +sub.Dispose(); +``` + +### Subscribe via `ILifecycleObserver` + +```csharp +public class MyObserver : ILifecycleObserver +{ + public void OnEvent(IFrameworkLifecycleEvent evt) + { + if (evt is CombatStartingEvent combat) + HandleCombatStart(combat); + else if (evt is RunEndedEvent run) + HandleRunEnd(run); + } +} + +RitsuLibFramework.SubscribeLifecycle(new MyObserver()); +``` + +> **Replayable events** (`IReplayableFrameworkLifecycleEvent`): if you subscribe after the event has already fired, the framework immediately calls your handler with the stored event instance — no timing concerns. + +--- + +::: + +## 订阅方式{lang="zh-CN"} + +::: zh-CN + +### 按类型订阅(推荐) + +```csharp +var sub = RitsuLibFramework.SubscribeLifecycle(evt => +{ + Logger.Info($"游戏已就绪:{evt.Game}"); +}); + +// 取消订阅 +sub.Dispose(); +``` + +### 通过 `ILifecycleObserver` 订阅多种事件 + +```csharp +public class MyObserver : ILifecycleObserver +{ + public void OnEvent(IFrameworkLifecycleEvent evt) + { + if (evt is CombatStartingEvent combat) + HandleCombatStart(combat); + else if (evt is RunEndedEvent run) + HandleRunEnd(run); + } +} + +RitsuLibFramework.SubscribeLifecycle(new MyObserver()); +``` + +> **可重放事件(`IReplayableFrameworkLifecycleEvent`):** 若在事件已发生后才订阅,框架会立即以已存储的事件实例回调,无需关心订阅时机。 + +--- + +::: + +## Framework Events{lang="en"} + +::: en + +Fired during framework initialization and profile service setup. + +| Event | Replayable | Payload | +|---|---|---| +| `FrameworkInitializingEvent` | — | `FrameworkModId`, `FrameworkVersion` | +| `FrameworkInitializedEvent` | ✓ | `FrameworkModId`, `IsActive` | +| `ProfileServicesInitializingEvent` | — | — | +| `ProfileServicesInitializedEvent` | ✓ | `ProfileId` | + +--- + +::: + +## 框架事件{lang="zh-CN"} + +::: zh-CN + +框架初始化与 Profile 服务初始化阶段触发。 + +| 事件 | 可重放 | 携带数据 | +|---|---|---| +| `FrameworkInitializingEvent` | — | `FrameworkModId`、`FrameworkVersion` | +| `FrameworkInitializedEvent` | ✓ | `FrameworkModId`、`IsActive` | +| `ProfileServicesInitializingEvent` | — | — | +| `ProfileServicesInitializedEvent` | ✓ | `ProfileId` | + +--- + +::: + +## Game Bootstrap Events{lang="en"} + +::: en + +Fired in sequence during game startup, from model registration through to game ready. + +| Event | Replayable | Payload | +|---|---|---| +| `EssentialInitializationStartingEvent` | — | — | +| `EssentialInitializationCompletedEvent` | ✓ | — | +| `DeferredInitializationStartingEvent` | — | — | +| `DeferredInitializationCompletedEvent` | ✓ | — | +| `ContentRegistrationClosedEvent` | ✓ | `Reason` | +| `ModelRegistryInitializingEvent` | — | — | +| `ModelRegistryInitializedEvent` | ✓ | `RegisteredModelTypeCount` | +| `ModelIdsInitializingEvent` | — | — | +| `ModelIdsInitializedEvent` | ✓ | — | +| `ModelPreloadingStartingEvent` | — | — | +| `ModelPreloadingCompletedEvent` | ✓ | — | +| `GameTreeEnteredEvent` | ✓ | `Game` | +| `GameReadyEvent` | ✓ | `Game` | + +```csharp +RitsuLibFramework.SubscribeLifecycle(_ => +{ + var id = ModelDb.GetId(); +}); +``` + +--- + +::: + +## 游戏引导事件{lang="zh-CN"} + +::: zh-CN + +游戏启动流程中依次触发,覆盖 Model 注册到游戏就绪全程。 + +| 事件 | 可重放 | 携带数据 | +|---|---|---| +| `EssentialInitializationStartingEvent` | — | — | +| `EssentialInitializationCompletedEvent` | ✓ | — | +| `DeferredInitializationStartingEvent` | — | — | +| `DeferredInitializationCompletedEvent` | ✓ | — | +| `ContentRegistrationClosedEvent` | ✓ | `Reason` | +| `ModelRegistryInitializingEvent` | — | — | +| `ModelRegistryInitializedEvent` | ✓ | `RegisteredModelTypeCount` | +| `ModelIdsInitializingEvent` | — | — | +| `ModelIdsInitializedEvent` | ✓ | — | +| `ModelPreloadingStartingEvent` | — | — | +| `ModelPreloadingCompletedEvent` | ✓ | — | +| `GameTreeEnteredEvent` | ✓ | `Game` | +| `GameReadyEvent` | ✓ | `Game` | + +```csharp +RitsuLibFramework.SubscribeLifecycle(_ => +{ + var id = ModelDb.GetId(); +}); +``` + +--- + +::: + +## Run Events{lang="en"} + +::: en + +| Event | Replayable | Payload | +|---|---|---| +| `RunStartedEvent` | — | `RunState`, `IsMultiplayer`, `IsDaily` | +| `RunLoadedEvent` | — | `RunState`, `IsMultiplayer`, `IsDaily` | +| `RunEndedEvent` | — | `Run`, `IsVictory`, `IsAbandoned` | + +--- + +::: + +## 跑局事件{lang="zh-CN"} + +::: zh-CN + +| 事件 | 可重放 | 携带数据 | +|---|---|---| +| `RunStartedEvent` | — | `RunState`、`IsMultiplayer`、`IsDaily` | +| `RunLoadedEvent` | — | `RunState`、`IsMultiplayer`、`IsDaily` | +| `RunEndedEvent` | — | `Run`、`IsVictory`、`IsAbandoned` | + +--- + +::: + +## Room & Act Events{lang="en"} + +::: en + +| Event | Payload | +|---|---| +| `RoomEnteringEvent` | `RunState`, `Room` | +| `RoomEnteredEvent` | `RunState`, `Room` | +| `RoomExitedEvent` | `RunManager`, `Room` | +| `ActEnteringEvent` | `RunManager`, `TargetActIndex`, `DoTransition` | +| `ActEnteredEvent` | `RunState`, `CurrentActIndex` | +| `RewardsScreenContinuingEvent` | `RunManager` | + +--- + +::: + +## 房间与章节事件{lang="zh-CN"} + +::: zh-CN + +| 事件 | 携带数据 | +|---|---| +| `RoomEnteringEvent` | `RunState`、`Room` | +| `RoomEnteredEvent` | `RunState`、`Room` | +| `RoomExitedEvent` | `RunManager`、`Room` | +| `ActEnteringEvent` | `RunManager`、`TargetActIndex`、`DoTransition` | +| `ActEnteredEvent` | `RunState`、`CurrentActIndex` | +| `RewardsScreenContinuingEvent` | `RunManager` | + +--- + +::: + +## Combat Events{lang="en"} + +::: en + +| Event | Payload | +|---|---| +| `CombatStartingEvent` | `RunState`, `CombatState?` | +| `CombatEndedEvent` | `RunState`, `CombatState?`, `Room` | +| `CombatVictoryEvent` | `RunState`, `CombatState?`, `Room` | +| `SideTurnStartingEvent` | `CombatState`, `Side` | +| `SideTurnStartedEvent` | `CombatState`, `Side` | +| `CardPlayingEvent` | `CombatState`, `CardPlay` | +| `CardPlayedEvent` | `CombatState`, `CardPlay` | +| `CardDrawnEvent` | `CombatState`, `Card`, `FromHandDraw` | +| `CardDiscardedEvent` | `CombatState`, `Card` | +| `CardExhaustedEvent` | `CombatState`, `Card`, `CausedByEthereal` | +| `CardRetainedEvent` | `CombatState`, `Card` | +| `CardMovedBetweenPilesEvent` | `RunState`, `CombatState?`, `Card`, `PreviousPile`, `Source` | + +### Creature Events + +| Event | Payload | +|---|---| +| `CreatureDyingEvent` | `CombatState`, `Creature` | +| `CreatureDiedEvent` | `CombatState`, `Creature` | + +```csharp +RitsuLibFramework.SubscribeLifecycle(evt => +{ + if (evt.Card is MyCard myCard) + myCard.OnDrawn(evt.CombatState); +}); +``` + +--- + +::: + +## 战斗事件{lang="zh-CN"} + +::: zh-CN + +| 事件 | 携带数据 | +|---|---| +| `CombatStartingEvent` | `RunState`、`CombatState?` | +| `CombatEndedEvent` | `RunState`、`CombatState?`、`Room` | +| `CombatVictoryEvent` | `RunState`、`CombatState?`、`Room` | +| `SideTurnStartingEvent` | `CombatState`、`Side` | +| `SideTurnStartedEvent` | `CombatState`、`Side` | +| `CardPlayingEvent` | `CombatState`、`CardPlay` | +| `CardPlayedEvent` | `CombatState`、`CardPlay` | +| `CardDrawnEvent` | `CombatState`、`Card`、`FromHandDraw` | +| `CardDiscardedEvent` | `CombatState`、`Card` | +| `CardExhaustedEvent` | `CombatState`、`Card`、`CausedByEthereal` | +| `CardRetainedEvent` | `CombatState`、`Card` | +| `CardMovedBetweenPilesEvent` | `RunState`、`CombatState?`、`Card`、`PreviousPile`、`Source` | + +### 生物事件 + +| 事件 | 携带数据 | +|---|---| +| `CreatureDyingEvent` | `CombatState`、`Creature` | +| `CreatureDiedEvent` | `CombatState`、`Creature` | + +```csharp +RitsuLibFramework.SubscribeLifecycle(evt => +{ + if (evt.Card is MyCard myCard) + myCard.OnDrawn(evt.CombatState); +}); +``` + +--- + +::: + +## Reward Events{lang="en"} + +::: en + +| Event | Payload | +|---|---| +| `GoldGainedEvent` | `Amount` | +| `GoldLostEvent` | `Amount` | +| `PotionProcuredEvent` | `Potion` | +| `PotionDiscardedEvent` | `Potion` | +| `RelicObtainedEvent` | `Relic` | +| `RelicRemovedEvent` | `Relic` | +| `RewardTakenEvent` | `Reward` | + +--- + +::: + +## 奖励事件{lang="zh-CN"} + +::: zh-CN + +| 事件 | 携带数据 | +|---|---| +| `GoldGainedEvent` | `Amount` | +| `GoldLostEvent` | `Amount` | +| `PotionProcuredEvent` | `Potion` | +| `PotionDiscardedEvent` | `Potion` | +| `RelicObtainedEvent` | `Relic` | +| `RelicRemovedEvent` | `Relic` | +| `RewardTakenEvent` | `Reward` | + +--- + +::: + +## Unlock Events{lang="en"} + +::: en + +| Event | Payload | +|---|---| +| `EpochObtainedEvent` | `Epoch` | +| `EpochRevealedEvent` | `Epoch` | +| `UnlockIncrementedEvent` | `UnlockState` | + +--- + +::: + +## 解锁事件{lang="zh-CN"} + +::: zh-CN + +| 事件 | 携带数据 | +|---|---| +| `EpochObtainedEvent` | `Epoch` | +| `EpochRevealedEvent` | `Epoch` | +| `UnlockIncrementedEvent` | `UnlockState` | + +--- + +::: + +## Save & Persistence Events{lang="en"} + +::: en + +### Profile Lifecycle + +| Event | Payload | +|---|---| +| `ProfileIdInitializedEvent` | `ProfileId` | +| `ProfileSwitchingEvent` | `OldProfileId`, `NewProfileId` | +| `ProfileSwitchedEvent` | `ProfileId` | +| `ProfileDeletingEvent` | `ProfileId` | +| `ProfileDeletedEvent` | `ProfileId` | + +### Save Writing + +| Event | Payload | +|---|---| +| `RunSavingEvent` | `RunState` | +| `RunSavedEvent` | `RunState` | +| `ProgressSavingEvent` | — | +| `ProgressSavedEvent` | — | + +### ModDataStore Data Events + +Used internally by `ModDataStore`, also available for mods to react to save state changes. + +| Event | Description | +|---|---| +| `ProfileDataReadyEvent` | Save data loaded — safe to read/write | +| `ProfileDataChangedEvent` | Save data changed | +| `ProfileDataInvalidatedEvent` | Save data invalidated (e.g. profile switch) | + +--- + +::: + +## 存档与持久化事件{lang="zh-CN"} + +::: zh-CN + +### Profile 生命周期 + +| 事件 | 携带数据 | +|---|---| +| `ProfileIdInitializedEvent` | `ProfileId` | +| `ProfileSwitchingEvent` | `OldProfileId`、`NewProfileId` | +| `ProfileSwitchedEvent` | `ProfileId` | +| `ProfileDeletingEvent` | `ProfileId` | +| `ProfileDeletedEvent` | `ProfileId` | + +### 存档写入 + +| 事件 | 携带数据 | +|---|---| +| `RunSavingEvent` | `RunState` | +| `RunSavedEvent` | `RunState` | +| `ProgressSavingEvent` | — | +| `ProgressSavedEvent` | — | + +### ModDataStore 数据事件 + +由 `ModDataStore` 内部使用,也可供 Mod 监听存档状态变化。 + +| 事件 | 说明 | +|---|---| +| `ProfileDataReadyEvent` | 存档数据加载完毕,可安全读写 | +| `ProfileDataChangedEvent` | 存档数据发生变更 | +| `ProfileDataInvalidatedEvent` | 存档数据失效(如切换档案) | + +--- + +::: + +## Game Over Events{lang="en"} + +::: en + +| Event | Payload | +|---|---| +| `GameOverScreenCreatedEvent` | `Screen` | + +--- + +::: + +## 游戏结算事件{lang="zh-CN"} + +::: zh-CN + +| 事件 | 携带数据 | +|---|---| +| `GameOverScreenCreatedEvent` | `Screen` | + +--- + +::: + +## Related Documents{lang="en"} + +::: en + +- [Getting Started](/guide/getting-started) +- [Content Authoring Toolkit](/guide/content-authoring-toolkit) +- [Persistence Guide](/guide/persistence-guide) +- [Timeline & Unlocks](/guide/timeline-and-unlocks) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [快速入门](/guide/getting-started) +- [内容注册规则](/guide/content-authoring-toolkit) +- [持久化设计](/guide/persistence-guide) +- [时间线与解锁](/guide/timeline-and-unlocks) + +::: diff --git a/docs/pages/guide/loc-string-placeholder-resolution.md b/docs/pages/guide/loc-string-placeholder-resolution.md new file mode 100644 index 0000000..231bb61 --- /dev/null +++ b/docs/pages/guide/loc-string-placeholder-resolution.md @@ -0,0 +1,478 @@ +--- +title: + en: LocString Placeholder Resolution + zh-CN: LocString 占位符解析 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This document covers two topics: the **game-native** localization system (`LocString`, SmartFormat configuration, built-in formatters) and the **extension guide** for registering custom `IFormatter` implementations from mods. + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文档分为两部分:**游戏原版机制**(`LocString`、SmartFormat 配置、内置格式化器)和**扩展指南**(Mod 如何注册自定义 `IFormatter`)。 + +--- + +::: + +## Part 1: Game-native system{lang="en"} + +::: en + +> The following describes the Slay the Spire 2 engine's own localization mechanism, not RitsuLib functionality. + +### Core components + +- **`LocString`**: holds a localization table id, entry key, and variable dictionary; `GetFormattedText()` triggers formatting. +- **`LocManager.SmartFormat`**: retrieves the raw template from `LocTable`, selects `CultureInfo` based on whether the key is localized, then calls `SmartFormatter.Format(...)`. +- **`LocManager.LoadLocFormatters`**: constructs `SmartFormatter`, registers data sources and formatter extensions. + +### Variable binding + +Variables are written to `LocString` via `Add`. **Spaces in variable names are replaced with hyphens.** + +```csharp +var locString = new LocString("cards", "strike"); +locString.Add("damage", 6); +string result = locString.GetFormattedText(); +``` + +### Placeholder syntax + +Game localization JSON uses SmartFormat placeholders. + +**Variable only** — outputs the formatted value of the variable: + +``` +{VariableName} +``` + +**With formatter** — the formatter is specified after a colon using function-call syntax. The content inside `( )` is passed to the formatter as `IFormattingInfo.FormatterOptions`: + +``` +{VariableName:formatterName()} +{VariableName:formatterName(options)} +``` + +Formatters are matched by `IFormatter.Name`. The parentheses are a required part of the invocation syntax. + +**Formatters with format segments** (e.g. `show`, `choose`, `cond`) receive additional text after a second colon, split by `|`. See individual formatter notes and the advanced examples below. + +**Example:** + +```json +{ + "damage_text": "Deal {Damage:diff()} damage to all enemies.", + "energy_text": "Gain {Energy:energyIcons()} this turn." +} +``` + +### SmartFormat built-in extensions + +Standard SmartFormat extensions registered by the game (non-exhaustive): + +| Type | Role | +|------|------| +| `ListFormatter` | List formatting | +| `DictionarySource` | Keyed variable lookup | +| `ValueTupleSource` | Value tuples | +| `ReflectionSource` | Reflection-based property access | +| `DefaultSource` | Fallback source | +| `PluralLocalizationFormatter` | Locale-sensitive pluralization | +| `ConditionalFormatter` | Conditional formatting | +| `ChooseFormatter` | `choose(...)` | +| `SubStringFormatter` | Substrings | +| `IsMatchFormatter` | Regex matching | +| `LocaleNumberFormatter` | Locale number formatting | +| `DefaultFormatter` | Fallback when no formatter matches | + +### Game-specific formatters + +The game registers the following `IFormatter` types in `MegaCrit.Sts2.Core.Localization.Formatters`: + +| `IFormatter.Name` | Placeholder | `FormatterOptions` | Notes | +|-------------------|-----------|--------------------|-------| +| `abs` | `{v:abs()}` | unused | Outputs the absolute value of a number | +| `energyIcons` | `{Energy:energyIcons()}` or `{energyPrefix:energyIcons(n)}` | Required as integer icon count when `CurrentValue` is `string` | Renders a value as energy icon glyphs; see details below | +| `starIcons` | `{v:starIcons()}` | unused | Renders a value as star icon glyphs | +| `diff` | `{v:diff()}` | unused | Highlights value changes (green for upgrades); requires `DynamicVar` | +| `inverseDiff` | `{v:inverseDiff()}` | unused | Same as `diff` with inverted color direction; requires `DynamicVar` | +| `percentMore` | `{v:percentMore()}` | unused | Converts a multiplier to a percent increase, e.g. `1.25` → `25` | +| `percentLess` | `{v:percentLess()}` | unused | Converts a multiplier to a percent decrease, e.g. `0.75` → `25` | +| `show` | `{v:show:upgrade text\|normal text}` | unused (options come from the format segment split on `|`) | Conditionally shows text based on upgrade state; requires `IfUpgradedVar` | + +**`energyIcons` details** + +The source of the icon count depends on `CurrentValue`: + +- `EnergyVar`: uses `PreviewValue` and an optional color prefix. Use `{Energy:energyIcons()}`. +- `CalculatedVar` or numeric type: uses the numeric value directly. Use `{Energy:energyIcons()}`. +- `string` (e.g. the `energyPrefix` variable used in fixed-cost text): count is read from `FormatterOptions` and must be an integer literal, e.g. `{energyPrefix:energyIcons(1)}`. + +Rendering rule: counts 1–3 repeat the icon glyph; counts ≤0 or ≥4 output the digit followed by one icon. + +**`show` details** + +The format segment after `show:` is split on `|` into one or two child formats: + +- `Upgraded`: renders the first segment. +- `Normal`: renders the second segment; if only one segment is provided, nothing is rendered. +- `UpgradePreview`: renders the first segment wrapped in `[green]...[/green]`. + +### DynamicVar types + +`DynamicVar` subclasses carry metadata consumed by formatters such as `diff` and `inverseDiff`: + +| Type | Description | +|------|-------------| +| `DamageVar` | Damage value with highlight metadata | +| `BlockVar` | Block value | +| `EnergyVar` | Energy value with color information | +| `CalculatedVar` | Base class for calculated values | +| `CalculatedDamageVar` / `CalculatedBlockVar` | Calculated damage / block | +| `ExtraDamageVar` | Extra damage | +| `BoolVar` / `IntVar` / `StringVar` | Primitive types | +| `GoldVar` / `HealVar` / `HpLossVar` / `MaxHpVar` | Resource types | +| `PowerVar` | Power value (generic) | +| `StarsVar` / `CardsVar` | Stars / card references | +| `IfUpgradedVar` | Upgrade UI display state | +| `ForgeVar` / `RepeatVar` / `SummonVar` | Other card variables | + +### Formatting pipeline + +1. `LocString.GetFormattedText()` is called +2. `LocManager.SmartFormat` retrieves the raw template from `LocTable` +3. `CultureInfo` is selected based on whether the key is localized +4. `SmartFormatter.Format` evaluates placeholders and dispatches to matching formatters +5. On failure (`FormattingException` or `ParsingErrors`): error is logged and the raw template is returned + +### Advanced examples + +**Conditional** (`ConditionalFormatter`) + +```json +{ "text": "{HasRider:This card has a rider effect|This card has no rider}" } +``` + +**Choose** (`ChooseFormatter`) + +```json +{ "text": "{CardType:choose(Attack|Skill|Power):Attack text|Skill text|Power text}" } +``` + +**Nested formatters** + +```json +{ + "text": "{Violence:Deal {Damage:diff()} damage {ViolenceHits:diff()} times|Deal {Damage:diff()} damage}" +} +``` + +**BBCode color tags** + +```json +{ "text": "Gain [gold]{Gold}[/gold] gold. Current HP: [green]{Hp}[/green]." } +``` + +Common tags: `[gold]`, `[green]`, `[red]`, `[blue]`. + +--- + +::: + +## 第一部分:游戏原版机制{lang="zh-CN"} + +::: zh-CN + +> 以下内容描述的是杀戮尖塔 2 引擎自身的本地化解析机制,不是 RitsuLib 提供的功能。 + +### 核心组件 + +- **`LocString`**:持有本地化表 id、条目键与变量字典,调用 `GetFormattedText()` 执行格式化。 +- **`LocManager.SmartFormat`**:从 `LocTable` 取原始模板,根据键是否已本地化选择 `CultureInfo`,再由 `SmartFormatter.Format(...)` 解析。 +- **`LocManager.LoadLocFormatters`**:初始化 `SmartFormatter`,注册数据源与格式化器扩展。 + +### 变量绑定 + +变量通过 `LocString.Add` 写入字典,**名称中的空格会被替换为连字符**。 + +```csharp +var locString = new LocString("cards", "strike"); +locString.Add("damage", 6); +string result = locString.GetFormattedText(); +``` + +### 占位符语法 + +游戏本地化 JSON 中使用 SmartFormat 占位符。 + +**仅变量名** — 直接输出变量值: + +``` +{VariableName} +``` + +**指定格式化器** — 格式化器以函数调用形式写在冒号后,括号内内容(`FormatterOptions`)由格式化器自行解读: + +``` +{VariableName:formatterName()} +{VariableName:formatterName(options)} +``` + +格式化器由 `IFormatter.Name` 匹配。`(` `)` 是调用语法的必要组成部分,不可省略。 + +**带额外格式段的格式化器**(如 `show`、`choose`、`cond`)在调用后通过第二个冒号传递格式文本,详见后续各格式化器说明及高级示例。 + +**示例:** + +```json +{ + "damage_text": "对所有敌人造成 {Damage:diff()} 点伤害。", + "energy_text": "本回合获得 {Energy:energyIcons()}。" +} +``` + +### SmartFormat 内置扩展 + +游戏注册的标准 SmartFormat 扩展(节选): + +| 类型 | 作用 | +|------|------| +| `ListFormatter` | 列表格式化 | +| `DictionarySource` | 按键读取变量 | +| `ValueTupleSource` | 值元组 | +| `ReflectionSource` | 反射访问属性 | +| `DefaultSource` | 默认数据源 | +| `PluralLocalizationFormatter` | 语言环境复数 | +| `ConditionalFormatter` | 条件格式化 | +| `ChooseFormatter` | `choose(...)` | +| `SubStringFormatter` | 子字符串 | +| `IsMatchFormatter` | 正则匹配 | +| `LocaleNumberFormatter` | 区域数字格式 | +| `DefaultFormatter` | 无匹配时的回退 | + +### 游戏自定义格式化器 + +游戏在 `MegaCrit.Sts2.Core.Localization.Formatters` 中注册了以下 `IFormatter`: + +| `IFormatter.Name` | 占位符写法 | `FormatterOptions` | 说明 | +|-------------------|-----------|--------------------|------| +| `abs` | `{v:abs()}` | 不使用 | 输出数值的绝对值 | +| `energyIcons` | `{Energy:energyIcons()}` 或 `{energyPrefix:energyIcons(n)}` | `CurrentValue` 为 `string` 时,必须提供整数参数作为图标个数 | 将数值渲染为能量图标,详见下方说明 | +| `starIcons` | `{v:starIcons()}` | 不使用 | 将数值渲染为星星图标 | +| `diff` | `{v:diff()}` | 不使用 | 以绿色(升级)高亮显示数值变化,需传入 `DynamicVar` | +| `inverseDiff` | `{v:inverseDiff()}` | 不使用 | 与 `diff` 相同但颜色方向相反,需传入 `DynamicVar` | +| `percentMore` | `{v:percentMore()}` | 不使用 | 将乘数转换为增加百分比,例如 `1.25` 输出 `25` | +| `percentLess` | `{v:percentLess()}` | 不使用 | 将乘数转换为减少百分比,例如 `0.75` 输出 `25` | +| `show` | `{v:show:升级文案\|普通文案}` | 不使用(选项由格式段 `|` 分隔提供) | 根据升级状态条件显示文案,需传入 `IfUpgradedVar` | + +**`energyIcons` 用法补充** + +`CurrentValue` 决定图标个数的来源: + +- `EnergyVar`:使用 `PreviewValue` 与可选颜色前缀,使用 `{Energy:energyIcons()}`。 +- `CalculatedVar` 或数值类型:直接使用数值,使用 `{Energy:energyIcons()}`。 +- `string`(如固定文本中的 `energyPrefix` 变量):个数由 `FormatterOptions` 提供,必须写 `energyIcons(n)`,例如 `{energyPrefix:energyIcons(1)}`。 + +图标渲染规则:个数 1–3 重复单独图标;个数 ≤0 或 ≥4 输出数字加单个图标。 + +**`show` 用法补充** + +`show:` 后的格式文本按 `|` 拆分为一至两段: + +- 升级状态(`Upgraded`):渲染第一段。 +- 普通状态(`Normal`):渲染第二段;若只有一段则输出空白。 +- 升级预览(`UpgradePreview`):以绿色渲染第一段。 + +### DynamicVar 类型 + +`DynamicVar` 子类携带格式化元数据,是 `diff`、`inverseDiff` 等格式化器的必要输入: + +| 类型 | 说明 | +|------|------| +| `DamageVar` | 伤害值,携带高亮元数据 | +| `BlockVar` | 格挡值 | +| `EnergyVar` | 能量值,携带颜色信息 | +| `CalculatedVar` | 计算值基类 | +| `CalculatedDamageVar` / `CalculatedBlockVar` | 计算后的伤害/格挡 | +| `ExtraDamageVar` | 额外伤害 | +| `BoolVar` / `IntVar` / `StringVar` | 基础类型 | +| `GoldVar` / `HealVar` / `HpLossVar` / `MaxHpVar` | 资源类型 | +| `PowerVar` | 能力值(泛型) | +| `StarsVar` / `CardsVar` | 星/牌引用 | +| `IfUpgradedVar` | 升级显示状态 | +| `ForgeVar` / `RepeatVar` / `SummonVar` | 其它卡牌变量 | + +### 格式化流程 + +1. 调用 `LocString.GetFormattedText()` +2. `LocManager.SmartFormat` 从 `LocTable` 取原始模板 +3. 根据键是否已本地化选择 `CultureInfo` +4. `SmartFormatter.Format` 解析占位符并调用匹配的格式化器 +5. 若格式化失败(`FormattingException` 或 `ParsingErrors`),记录错误并返回原始模板 + +### 高级示例 + +**条件格式**(`ConditionalFormatter`) + +```json +{ "text": "{HasRider:此卡有附加效果|此卡无附加效果}" } +``` + +**选择格式**(`ChooseFormatter`) + +```json +{ "text": "{CardType:choose(Attack|Skill|Power):攻击文本|技能文本|能力文本}" } +``` + +**嵌套格式化器** + +```json +{ + "text": "{Violence:造成 {Damage:diff()} 点伤害 {ViolenceHits:diff()} 次|造成 {Damage:diff()} 点伤害}" +} +``` + +**BBCode 颜色标签** + +```json +{ "text": "获得 [gold]{Gold}[/gold] 金币,当前生命 [green]{Hp}[/green]。" } +``` + +常用标签:`[gold]`、`[green]`、`[red]`、`[blue]`。 + +--- + +::: + +## Part 2: Custom formatters (mods){lang="en"} + +::: en + +> The following describes how to register additional formatters via the RitsuLib patching system. + +A `Postfix` patch on `LocManager.LoadLocFormatters` provides access to the `SmartFormatter` instance, which accepts additional `IFormatter` implementations. + +**Implementing `IFormatter`:** + +```csharp +public class MyCustomFormatter : IFormatter +{ + public string Name { get => "myCustom"; set { } } + public bool CanAutoDetect { get; set; } + + public bool TryEvaluateFormat(IFormattingInfo formattingInfo) + { + formattingInfo.Write($"Custom output: {formattingInfo.CurrentValue}"); + return true; + } +} +``` + +- `Name` is the formatter identifier matched in placeholder strings (the `myCustom` in `{Var:myCustom()}`). +- Access `formattingInfo.FormatterOptions` to read any text supplied inside the parentheses. + +**Registration patch:** + +```csharp +public class RegisterMyFormatterPatch : IPatchMethod +{ + public static string PatchId => "register_my_formatter"; + public static string Description => "Register custom SmartFormat formatter"; + public static bool IsCritical => true; + + public static ModPatchTarget[] GetTargets() + => [new(typeof(LocManager), "LoadLocFormatters")]; + + public static void Postfix(SmartFormatter ____smartFormatter) + => ____smartFormatter.AddExtensions(new MyCustomFormatter()); +} +``` + +Once registered, invoke the formatter in JSON as `{SomeVar:myCustom()}` or `{SomeVar:myCustom(args)}`. + +--- + +::: + +## 第二部分:自定义格式化器(Mod){lang="zh-CN"} + +::: zh-CN + +> 以下内容描述如何通过 RitsuLib 补丁系统为游戏注册自定义格式化器。 + +通过对 `LocManager.LoadLocFormatters` 打 `Postfix` 补丁,可在 `SmartFormatter` 中注册额外的 `IFormatter` 实现。 + +**实现 `IFormatter`:** + +```csharp +public class MyCustomFormatter : IFormatter +{ + public string Name { get => "myCustom"; set { } } + public bool CanAutoDetect { get; set; } + + public bool TryEvaluateFormat(IFormattingInfo formattingInfo) + { + formattingInfo.Write($"自定义输出: {formattingInfo.CurrentValue}"); + return true; + } +} +``` + +- `Name` 是格式化器标识符,对应 JSON 中 `{Var:myCustom()}` 的 `myCustom` 部分。 +- 若需要参数,通过 `formattingInfo.FormatterOptions` 读取括号内的字符串。 + +**注册补丁:** + +```csharp +public class RegisterMyFormatterPatch : IPatchMethod +{ + public static string PatchId => "register_my_formatter"; + public static string Description => "Register custom SmartFormat formatter"; + public static bool IsCritical => true; + + public static ModPatchTarget[] GetTargets() + => [new(typeof(LocManager), "LoadLocFormatters")]; + + public static void Postfix(SmartFormatter ____smartFormatter) + => ____smartFormatter.AddExtensions(new MyCustomFormatter()); +} +``` + +注册后,在 JSON 中通过 `{SomeVar:myCustom()}` 或 `{SomeVar:myCustom(args)}` 调用。 + +--- + +::: + +## Related documents{lang="en"} + +::: en + +- [Localization & Keywords](/guide/localization-and-keywords) +- [Card Dynamic Variables](/guide/card-dynamic-var-toolkit) +- [Patching Guide](/guide/patching-guide) +- [Content Authoring Toolkit](/guide/content-authoring-toolkit) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [本地化与关键词](/guide/localization-and-keywords) +- [卡牌动态变量](/guide/card-dynamic-var-toolkit) +- [补丁系统](/guide/patching-guide) +- [内容注册规则](/guide/content-authoring-toolkit) + +::: diff --git a/docs/pages/guide/localization-and-keywords.md b/docs/pages/guide/localization-and-keywords.md new file mode 100644 index 0000000..09c1336 --- /dev/null +++ b/docs/pages/guide/localization-and-keywords.md @@ -0,0 +1,521 @@ +--- +title: + en: Localization & Keywords + zh-CN: 本地化与关键词 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +RitsuLib separates localization into two distinct layers: + +- **The base game's `LocString` model-key pipeline** — in-game text such as model titles and descriptions +- **Framework-provided `I18N` helper localization** — auxiliary text for the mod itself + +It also provides a lightweight keyword registry to unify hover tips and keyword text. + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +RitsuLib 将本地化明确分为两层: + +- **游戏原版的 `LocString` 模型键管线** — 模型标题、描述等游戏内文本 +- **框架自带的 `I18N` 辅助本地化** — Mod 自身的辅助文本 + +同时提供轻量关键词注册器,用来统一悬浮提示和关键词文本。 + +--- + +::: + +## Game Model Localization{lang="en"} + +::: en + +> The following describes the game engine's own localization mechanism; RitsuLib does not replace this system. + +The game reads model text through `LocString` and various localization tables, commonly including: + +- `cards` +- `relics` +- `powers` +- `characters` +- `card_keywords` + +Those keys are built on `ModelId.Entry`. + +RitsuLib's role is limited to making model identity more stable and predictable so keys are easier to author. For concrete model ID rules, see [Content Authoring Toolkit](/guide/content-authoring-toolkit). + +--- + +::: + +## 游戏原版模型本地化{lang="zh-CN"} + +::: zh-CN + +> 以下描述游戏引擎自身的本地化机制,RitsuLib 不替换此系统。 + +游戏通过 `LocString` 和各本地化表来读取模型文本,常见表包括: + +- `cards`、`relics`、`powers`、`characters`、`card_keywords` + +这些键建立在 `ModelId.Entry` 之上。 + +RitsuLib 的作用仅限于让模型身份更稳定、更可预测,从而使键更容易编写。具体的模型 ID 规则见 [内容注册规则](/guide/content-authoring-toolkit)。 + +--- + +::: + +## `CreateLocalization` And `CreateModLocalization`{lang="en"} + +::: en + +`I18N` is RitsuLib's helper-text localization system, independent of the game's `LocString`: + +```csharp +var i18n = RitsuLibFramework.CreateModLocalization( + modId: "MyMod", + instanceName: "MyMod-I18N", + resourceFolders: ["MyMod.localization"], + pckFolders: ["res://MyMod/localization"]); +``` + +`CreateModLocalization` is a convenience wrapper over `CreateLocalization`. +If you do not provide file-system folders, it defaults to: + +```text +user://mod-configs//localization +``` + +--- + +::: + +## `CreateLocalization` 与 `CreateModLocalization`{lang="zh-CN"} + +::: zh-CN + +`I18N` 是 RitsuLib 提供的辅助文本本地化系统,独立于游戏的 `LocString`: + +```csharp +var i18n = RitsuLibFramework.CreateModLocalization( + modId: "MyMod", + instanceName: "MyMod-I18N", + resourceFolders: ["MyMod.localization"], + pckFolders: ["res://MyMod/localization"]); +``` + +`CreateModLocalization` 是 `CreateLocalization` 的便捷包装。如果不传文件系统目录,默认使用: + +```text +user://mod-configs//localization +``` + +--- + +::: + +## Source Merge Order{lang="en"} + +::: en + +`I18N` can merge translations from three source kinds: + +1. file system folders +2. embedded resources +3. PCK folders + +Merge behavior is first-wins: + +- file-system entries are loaded first +- embedded entries only fill missing keys +- PCK entries only fill keys still missing after that + +This lets local overrides take priority over packaged defaults. + +--- + +::: + +## 资源合并顺序{lang="zh-CN"} + +::: zh-CN + +`I18N` 支持三类来源: + +1. 文件系统目录 +2. 嵌入资源 +3. PCK 目录 + +合并策略是"先到先得": + +- 先加载文件系统目录 +- 嵌入资源只补缺失键 +- PCK 再补剩余缺失键 + +这样本地覆写可以自然优先于打包默认值。 + +--- + +::: + +## Language Normalization{lang="en"} + +::: en + +`I18N` normalizes locale names before loading JSON files: + +| Input | Normalized | +|---|---| +| `en`, `en_us`, `eng` | `eng` | +| `zh`, `zh_cn`, `zh_hans` | `zhs` | +| `ja`, `ja_jp` | `jpn` | + +If no language can be resolved, it falls back to `eng`. + +--- + +::: + +## 语言代码归一化{lang="zh-CN"} + +::: zh-CN + +`I18N` 在加载 JSON 之前会规范化语言代码: + +| 输入 | 归一化结果 | +|---|---| +| `en`、`en_us`、`eng` | `eng` | +| `zh`、`zh_cn`、`zh_hans` | `zhs` | +| `ja`、`ja_jp` | `jpn` | + +无法解析的语言默认回退到 `eng`。 + +--- + +::: + +## Runtime Reload Behavior{lang="en"} + +::: en + +`I18N` subscribes to locale changes when possible: + +- when the game language changes, helper localization reloads automatically +- `Changed` is raised after reload completes +- if the game localization manager is unavailable at that moment, `I18N` falls back to lazy detection + +This behavior is independent of base-game `LocString` resolution. + +--- + +::: + +## 运行时重载行为{lang="zh-CN"} + +::: zh-CN + +`I18N` 会在可能的情况下订阅语言切换事件: + +- 游戏语言改变时,辅助本地化自动重载 +- 重载完成后触发 `Changed` 事件 +- 如果当前阶段拿不到游戏本地化管理器,则退回懒检测模式 + +此行为与游戏原版 `LocString` 的解析相互独立。 + +--- + +::: + +## Debug Compatibility Mode{lang="en"} + +::: en + +`LocTable` placeholder resolution is part of RitsuLib’s debug compatibility fallbacks. See [Diagnostics & Compatibility](/guide/diagnostics-and-compatibility) for the master toggle, the **LocTable missing keys** toggle, and one-time `[Localization][DebugCompat]` warnings. + +Use this for troubleshooting, not as a substitute for authoring real keys. + +--- + +::: + +## 调试兼容模式{lang="zh-CN"} + +::: zh-CN + +`LocTable` 占位值解析属于 RitsuLib 调试兼容回退之一:总开关、**LocTable** 子项与一次性 `[Localization][DebugCompat]` 警告见 [诊断与兼容层](/guide/diagnostics-and-compatibility)。 + +用于排障,不能代替补全真实键。 + +--- + +::: + +## Keyword Registry{lang="en"} + +::: en + +Use `ModKeywordRegistry` when you want reusable keyword definitions and hover tips: + +```csharp +var keywords = RitsuLibFramework.GetKeywordRegistry("MyMod"); + +keywords.RegisterCardKeywordOwnedByLocNamespace( + localKeywordStem: "brew", + iconPath: "res://MyMod/ui/keywords/brew.png"); +``` + +This creates a normalized keyword id and binds it to title / description localization keys. + +--- + +::: + +## 关键词注册器{lang="zh-CN"} + +::: zh-CN + +`ModKeywordRegistry` 用于统一定义关键词及其悬浮提示: + +```csharp +var keywords = RitsuLibFramework.GetKeywordRegistry("MyMod"); + +keywords.RegisterCardKeywordOwnedByLocNamespace( + localKeywordStem: "brew", + iconPath: "res://MyMod/ui/keywords/brew.png"); +``` + +注册后会生成规范化标识,并绑定标题/描述的本地化键。 + +--- + +::: + +## Automatic keyword registration (optional: CLR attributes){lang="en"} + +::: en + +If you already use `ModTypeDiscoveryHub.RegisterModAssembly(...)` to let RitsuLib scan your assemblies, you can declare keyword registration with CLR attributes: + +```csharp +using STS2RitsuLib.Interop.AutoRegistration; + +[RegisterOwnedCardKeyword("brew", LocNamespace = "my_mod", IconPath = "res://MyMod/ui/keywords/brew.png")] +public sealed class BrewKeywordMarker; +``` + +`LocNamespace` only affects the localization namespace (the `modid` portion). The keyword stem (`brew`) participates in the default rule `_`, producing: + +- `_.title` +- `_.description` + +> Compatibility note: the legacy `LocKeyPrefix` / `locKeyPrefix` historically represents the **full stem** and is easy to misread as a prefix + keyword composition, so it is now obsolete. Use `LocNamespace` for new code. + +--- + +::: + +## 自动注册关键词(可选:CLR 特性){lang="zh-CN"} + +::: zh-CN + +如果你已经使用 `ModTypeDiscoveryHub.RegisterModAssembly(...)` 让 RitsuLib 扫描你的程序集,也可以用特性声明关键词注册: + +```csharp +using STS2RitsuLib.Interop.AutoRegistration; + +[RegisterOwnedCardKeyword("brew", LocNamespace = "my_mod", IconPath = "res://MyMod/ui/keywords/brew.png")] +public sealed class BrewKeywordMarker; +``` + +这里 `LocNamespace` 只影响本地化键的 namespace(即 `modid` 部分)。关键词 stem(`brew`)会自动参与默认生成规则:`_`,并形成: + +- `_.title` +- `_.description` + +> 兼容性说明:旧字段 `LocKeyPrefix`/`locKeyPrefix` 历史上实际代表“完整 stem”,容易误解为 prefix + keyword,已标记为过时;新代码请使用 `LocNamespace`。 + +--- + +::: + +## Using Keywords In Code{lang="en"} + +::: en + +Common helpers: + +| Method | Description | +|---|---| +| `ModKeywordRegistry.CreateHoverTip(id)` | Create hover tip | +| `ModKeywordRegistry.GetTitle(id)` | Get title | +| `ModKeywordRegistry.GetDescription(id)` | Get description | +| `keywordId.GetModKeywordCardText()` | Get card text | +| `enumerable.ToHoverTips()` | Batch-convert to hover tips | + +You can also attach runtime keywords to arbitrary objects via `ModKeywordExtensions`: + +```csharp +card.AddModKeyword("brew"); + +if (card.HasModKeyword("brew")) +{ + // ... +} +``` + +This is useful when keyword presence is driven by runtime state rather than static card text. + +--- + +::: + +## 在代码里使用关键词{lang="zh-CN"} + +::: zh-CN + +常用辅助方法: + +| 方法 | 说明 | +|---|---| +| `ModKeywordRegistry.CreateHoverTip(id)` | 创建悬浮提示 | +| `ModKeywordRegistry.GetTitle(id)` | 获取标题 | +| `ModKeywordRegistry.GetDescription(id)` | 获取描述 | +| `keywordId.GetModKeywordCardText()` | 获取卡牌文本 | +| `enumerable.ToHoverTips()` | 批量转换为悬浮提示 | + +也可以通过 `ModKeywordExtensions` 把运行时关键词挂在任意对象上: + +```csharp +card.AddModKeyword("brew"); + +if (card.HasModKeyword("brew")) +{ + // ... +} +``` + +适合"关键词是否存在由运行时状态决定"的场景。 + +--- + +::: + +## Ancient Dialogue Localization{lang="en"} + +::: en + +RitsuLib includes `AncientDialogueLocalization`. It serves two roles: + +- helper API for scanning dialogue from localization keys +- automatic append of localization-defined mod-character ancient dialogues before `AncientDialogueSet.PopulateLocKeys` runs + +The key format matches the base game: + +| Key component | Description | +|---|---| +| `.talk..-.ancient` | Ancient line | +| `.talk..-.char` | Character line | +| Optional suffix `r` | Repeated dialogue | +| Optional suffix `.sfx` | Sound effect | +| Optional suffix `-visit` | Visit override | +| Optional suffix `-attack` | Architect-only attacker override | + +Authors only need to write localization entries to add ancient dialogue for custom characters, without manually patching each `AncientDialogueSet`. + +If **no** keys exist for an ancient, vanilla may still show `PROCEED` for `THE_ARCHITECT` while `WinRun` assumes `Dialogue` is non-null. RitsuLib adds a narrow compatibility fallback (empty `Lines`, safe attackers) for `ModContentRegistry` characters **only** when the debug compatibility master toggle and the **THE_ARCHITECT missing dialogue** toggle are enabled, with a one-time `[Ancient]` warning. + +--- + +::: + +## Ancient 对话本地化{lang="zh-CN"} + +::: zh-CN + +RitsuLib 内置了 `AncientDialogueLocalization`,它有两个作用: + +- 提供从本地化键扫描对话的辅助 API +- 在游戏原版 `AncientDialogueSet.PopulateLocKeys` 之前,自动为已注册的 Mod 角色追加基于本地化定义的 Ancient 对话 + +键格式与原版保持一致: + +| 键组件 | 说明 | +|---|---| +| `.talk..-.ancient` | Ancient 台词 | +| `.talk..-.char` | 角色台词 | +| 可选后缀 `r` | 重复对话 | +| 可选后缀 `.sfx` | 音效 | +| 可选后缀 `-visit` | 访问覆盖 | +| 可选后缀 `-attack` | Architect 专用攻击者覆盖 | + +作者只需编写本地化条目,即可为自定义角色补充 Ancient 对话,无需手动为每个 `AncientDialogueSet` 添加补丁。 + +若某个 Ancient **完全没有**对应键,原版仍可能在 `THE_ARCHITECT` 显示 `PROCEED`,但 `WinRun` 会假定 `Dialogue` 非空。RitsuLib 仅在调试**总开关 + 建筑师子项**开启时,对 `ModContentRegistry` 角色注入窄范围兼容回退(空 `Lines`、安全的攻击方枚举),并记录一次 `[Ancient]` 警告。 + +--- + +::: + +## Recommended Split{lang="en"} + +::: en + +| Use case | Tool | +|---|---| +| Game model text (titles, descriptions) | Base game `LocString` tables | +| Mod-owned auxiliary text (settings, explanations) | `I18N` | +| Reusable keyword definitions | `ModKeywordRegistry` | +| Ancient dialogue | Localization keys + `AncientDialogueLocalization` | + +--- + +::: + +## 推荐分工{lang="zh-CN"} + +::: zh-CN + +| 用途 | 工具 | +|---|---| +| 游戏模型的文本(标题、描述) | 游戏原版 `LocString` 表 | +| Mod 自有辅助文本(设置页、说明) | `I18N` | +| 可复用关键词定义 | `ModKeywordRegistry` | +| Ancient 对话 | 本地化键 + `AncientDialogueLocalization` | + +--- + +::: + +## Related Documents{lang="en"} + +::: en + +- [Content Authoring Toolkit](/guide/content-authoring-toolkit) +- [Character & Unlock Templates](/guide/character-and-unlock-scaffolding) +- [Diagnostics & Compatibility](/guide/diagnostics-and-compatibility) +- [LocString Placeholder Resolution](/guide/loc-string-placeholder-resolution) +- [Mod Settings UI](/guide/mod-settings) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [内容注册规则](/guide/content-authoring-toolkit) +- [角色与解锁模板](/guide/character-and-unlock-scaffolding) +- [诊断与兼容层](/guide/diagnostics-and-compatibility) +- [LocString 占位符解析](/guide/loc-string-placeholder-resolution) +- [Mod 设置界面](/guide/mod-settings) + +::: diff --git a/docs/pages/guide/mod-settings.md b/docs/pages/guide/mod-settings.md new file mode 100644 index 0000000..44f1721 --- /dev/null +++ b/docs/pages/guide/mod-settings.md @@ -0,0 +1,911 @@ +--- +title: + en: Mod Settings + zh-CN: Mod 设置界面 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +RitsuLib provides a settings UI layer for player-editable values. +It is built on top of `ModDataStore`, but it does not replace the persistence model. + +Use this system when you need to expose a selected subset of persisted values, organize them into pages and sections, and localize the visible text. Settings pages are registered explicitly by design. + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +RitsuLib 提供一套用于玩家可编辑值的设置 UI。它构建在 `ModDataStore` 之上,但不替代底层持久化模型。 + +这套系统适合用于暴露一部分持久化字段、按页面和分区组织设置项,并统一管理界面文案。所有设置项都需要显式注册,这一限制是有意设计。 + +--- + +::: + +## Architecture{lang="en"} + +::: en + +Keep these responsibilities separate: + +- `ModDataStore`: persistence, scopes, defaults, migrations +- `IModSettingsValueBinding`: read/write bridge between UI and stored data +- page and section builders: UI structure and ordering +- `ModSettingsText`: text source abstraction for labels and descriptions + +This separation prevents runtime state, internal metadata, and user-editable configuration from collapsing into one model. + +--- + +::: + +## 架构分层{lang="zh-CN"} + +::: zh-CN + +建议保持以下职责分离: + +- `ModDataStore`:持久化、作用域、默认值、迁移 +- `IModSettingsValueBinding`:UI 与存储值之间的读写桥接 +- 页面 / 分区构建器:页面结构、层级与排序 +- `ModSettingsText`:标签与描述的文本来源抽象 + +这样可以避免把运行时状态、内部元数据与玩家配置混入同一个模型。 + +--- + +::: + +## Core APIs{lang="en"} + +::: en + +| API | Purpose | +|---|---| +| `RitsuLibFramework.RegisterModSettings(modId, configure, pageId?)` | Register a settings page; when `pageId` is omitted it defaults to `modId` | +| `RitsuLibFramework.GetRegisteredModSettings()` | Return all registered pages | +| `ModSettingsBindings.Global(...)` / `Profile(...)` | Bind a control to persisted data | +| `ModSettingsBindings.InMemory(...)` | Bind a control to preview-only state | +| `ModSettingsText.Literal(...)` | Plain text | +| `ModSettingsText.I18N(...)` | `I18N`-backed settings text | +| `ModSettingsText.LocString(...)` | Game-native localization text | +| `ModSettingsText.Dynamic(...)` | Re-evaluate text on UI refresh | +| `WithModDisplayName(...)` | Override the mod label shown in the sidebar | +| `WithSortOrder(...)` | Sort sibling pages within one mod | +| `AsChildOf(parentPageId)` | Register a page as a child page | +| `section.Collapsible(startCollapsed?)` | Make a section collapsible | +| `page.WithVisibleWhen(...)` / `section.WithVisibleWhen(...)` | Conditional page or section visibility | +| `AddToggle(...)`, `AddSlider(...)`, `AddIntSlider(...)`, `AddChoice(...)`, `AddEnumChoice(...)` | Standard value editors | +| `AddColor(...)`, `AddKeyBinding(...)`, `AddImage(...)` | Specialized editors and previews | +| `AddButton(...)`, `AddHeader(...)`, `AddParagraph(...)` | Structural and action entries | +| `AddSubpage(...)` | Navigate to a child page | +| `AddList(...)` | Structured list editor | +| `ModSettingsUiActionRegistry.Register*ActionAppender(...)` | Extend the actions menu for rows, list items, pages, or sections | + +--- + +::: + +## 核心 API{lang="zh-CN"} + +::: zh-CN + +| API | 作用 | +|---|---| +| `RitsuLibFramework.RegisterModSettings(modId, configure, pageId?)` | 注册设置页;省略 `pageId` 时默认为 `modId` | +| `RitsuLibFramework.GetRegisteredModSettings()` | 返回当前所有已注册设置页 | +| `ModSettingsBindings.Global(...)` / `Profile(...)` | 将控件绑定到持久化数据 | +| `ModSettingsBindings.InMemory(...)` | 绑定到仅预览状态 | +| `ModSettingsText.Literal(...)` | 纯文本 | +| `ModSettingsText.I18N(...)` | 基于 `I18N` 的设置界面文本 | +| `ModSettingsText.LocString(...)` | 游戏原生本地化文本 | +| `ModSettingsText.Dynamic(...)` | 在 UI 刷新时重新求值 | +| `WithModDisplayName(...)` | 覆盖侧栏中的 Mod 名称 | +| `WithSortOrder(...)` | 控制同级页面排序 | +| `AsChildOf(parentPageId)` | 将页面注册为子页 | +| `section.Collapsible(startCollapsed?)` | 声明可折叠分区 | +| `page.WithVisibleWhen(...)` / `section.WithVisibleWhen(...)` | 按条件显示或隐藏页面、分区 | +| `AddToggle(...)`、`AddSlider(...)`、`AddIntSlider(...)`、`AddChoice(...)`、`AddEnumChoice(...)` | 标准值编辑控件 | +| `AddColor(...)`、`AddKeyBinding(...)`、`AddImage(...)` | 专用编辑控件与预览 | +| `AddButton(...)`、`AddHeader(...)`、`AddParagraph(...)` | 结构项与动作项 | +| `AddSubpage(...)` | 导航到子页 | +| `AddList(...)` | 结构化列表编辑器 | +| `ModSettingsUiActionRegistry.Register*ActionAppender(...)` | 扩展行、列表项、页面或分区的 Actions 菜单 | + +--- + +::: + +## Recommended Flow{lang="en"} + +::: en + +1. Register the complete persisted model in `ModDataStore`. +2. Create bindings only for fields that players should edit. +3. Register pages and sections around those bindings. +4. Localize all visible labels, descriptions, and option names. + +The result is an explicit contract between stored data and the settings UI. + +--- + +::: + +## 推荐流程{lang="zh-CN"} + +::: zh-CN + +1. 在 `ModDataStore` 中注册完整持久化模型。 +2. 仅为需要暴露给玩家的字段创建绑定。 +3. 围绕这些绑定注册页面和分区。 +4. 补齐所有可见标签、描述与选项名称的本地化。 + +这样可以把存储结构与设置 UI 的公开范围明确分开。 + +--- + +::: + +## UI Behavior{lang="en"} + +::: en + +- Entry point: Main menu -> `Settings` -> `General`. When at least one page is registered, RitsuLib injects a `Mod Settings (RitsuLib)` row that opens `RitsuModSettingsSubmenu`. +- Sidebar: grouped by mod. One mod group is expanded at a time. The selected page also exposes section shortcuts. +- Content pane: page header, optional back navigation for child pages, and a scrollable section body. +- Save timing: dirty bindings are flushed on a debounce of about `0.35s`. Closing or hiding the submenu, leaving the tree, or changing the game locale forces an immediate flush. + +`WithVisibleWhen(...)` and row-level `visibleWhen` predicates are re-evaluated on debounced refresh. Predicates should stay cheap and should not throw. If evaluation fails, the control remains visible. + +--- + +::: + +## 界面行为{lang="zh-CN"} + +::: zh-CN + +- **入口**:主菜单 -> `设置` -> `General`。当至少存在一个已注册页面时,RitsuLib 会注入 `Mod Settings (RitsuLib)` 入口并打开 `RitsuModSettingsSubmenu`。 +- **侧栏**:按 Mod 分组,同一时间只展开一个分组。当前页下方会显示对应分区快捷入口。 +- **内容区**:顶部显示页面标题;子页提供返回导航;正文按分区滚动显示。 +- **保存时机**:绑定被标记为脏后,约 `0.35s` 防抖保存;关闭或隐藏子菜单、退出场景树、切换游戏语言时会立即刷写。 + +`WithVisibleWhen(...)` 与行级 `visibleWhen` 谓词会在防抖刷新时重新计算。谓词应保持轻量且避免抛异常;如果求值失败,控件保持显示。 + +--- + +::: + +## Auto-Mirror Policy (BaseLib / ModConfig){lang="en"} + +::: en + +`RitsuModSettingsSubmenu` automatically tries to mirror settings from both `BaseLib` and `ModConfig`. +When your mod intentionally supports multiple settings stacks, you can control mirror behavior with assembly-level `AssemblyMetadata` directives (requires only `System.Reflection`, no `STS2RitsuLib` reference). + +Supported keys (case-insensitive): + +- `RitsuLib.ModSettingsMirror.Global.DisableSources` +- `RitsuLib.ModSettingsMirror.Global.PreferredSource` +- `RitsuLib.ModSettingsMirror.Mod..DisableSources` +- `RitsuLib.ModSettingsMirror.Mod..PreferredSource` +- `RitsuLib.ModSettingsMirror.Type..DisableSources` +- `RitsuLib.ModSettingsMirror.Type..PreferredSource` + +Value rules: + +- `DisableSources`: `baselib`, `modconfig`, `all` (multiple values can be separated by `,` / `;` / `|`) +- `PreferredSource`: `baselib` or `modconfig` + +Priority (high -> low): `Type` -> `Mod` -> `Global`. +`PreferredSource` suppresses non-preferred mirror sources, and `DisableSources` blocks specific sources directly. + +Example: + +```csharp +using System.Reflection; + +[assembly: AssemblyMetadata("RitsuLib.ModSettingsMirror.Mod.MyMod.DisableSources", "modconfig")] +[assembly: AssemblyMetadata("RitsuLib.ModSettingsMirror.Mod.MyMod.PreferredSource", "baselib")] +[assembly: AssemblyMetadata( + "RitsuLib.ModSettingsMirror.Type.MyMod.Config.AdvancedSettings.DisableSources", + "baselib")] +``` + +You can also place the same directives directly in `csproj`: + +```xml + + + + + +``` + +--- + +::: + +## 自动镜像策略(BaseLib / ModConfig){lang="zh-CN"} + +::: zh-CN + +`RitsuModSettingsSubmenu` 会自动尝试镜像 `BaseLib` 与 `ModConfig` 的设置页。 +当你的模组同时接入多套设置源时,可以通过程序集级 `AssemblyMetadata` 指令(仅依赖 `System.Reflection`)控制镜像行为,无需引用 `STS2RitsuLib`。 + +支持的键(不区分大小写): + +- `RitsuLib.ModSettingsMirror.Global.DisableSources` +- `RitsuLib.ModSettingsMirror.Global.PreferredSource` +- `RitsuLib.ModSettingsMirror.Mod..DisableSources` +- `RitsuLib.ModSettingsMirror.Mod..PreferredSource` +- `RitsuLib.ModSettingsMirror.Type..DisableSources` +- `RitsuLib.ModSettingsMirror.Type..PreferredSource` + +值约定: + +- `DisableSources`:`baselib`、`modconfig`、`all`(可用 `,` / `;` / `|` 分隔多个值) +- `PreferredSource`:`baselib` 或 `modconfig` + +优先级(高 -> 低):`Type` -> `Mod` -> `Global`。 +`PreferredSource` 会让非首选来源不参与镜像;`DisableSources` 会直接禁用对应来源镜像。 + +示例: + +```csharp +using System.Reflection; + +[assembly: AssemblyMetadata("RitsuLib.ModSettingsMirror.Mod.MyMod.DisableSources", "modconfig")] +[assembly: AssemblyMetadata("RitsuLib.ModSettingsMirror.Mod.MyMod.PreferredSource", "baselib")] +[assembly: AssemblyMetadata( + "RitsuLib.ModSettingsMirror.Type.MyMod.Config.AdvancedSettings.DisableSources", + "baselib")] +``` + +也可以直接写在 `csproj` 中: + +```xml + + + + + +``` + +--- + +::: + +## Runtime Reflection Protocol (No Library Reference){lang="en"} + +::: en + +Besides BaseLib / ModConfig mirrors, RitsuLib also supports a pure reflection protocol for settings pages. +Your mod does not need to reference `STS2RitsuLib`; you only need to explicitly declare provider types in assembly metadata: + +```xml + + + +``` + +Runtime-initiated explicit registration is also supported (for reflection-driven init flows): + +- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderType(string providerTypeFullName, string? assemblyName = null)` +- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderType(Type providerType)` +- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderTypeAndTryRegister(string providerTypeFullName, string? assemblyName = null)` +- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderTypeAndTryRegister(Type providerType)` + +Provider contract (all methods are `static`): + +- `object CreateRitsuLibSettingsSchema()` +- `object? GetRitsuLibSettingValue(string key)` +- `void SetRitsuLibSettingValue(string key, object value)` +- Optional: `void SaveRitsuLibSettings()` +- Optional: `void InvokeRitsuLibSettingAction(string key)` (for button actions) +- Optional typed overrides (preferred over object resolver): + - `bool GetRitsuLibSettingBool(string key)` / `void SetRitsuLibSettingBool(string key, bool value)` + - `int GetRitsuLibSettingInt(string key)` / `void SetRitsuLibSettingInt(string key, int value)` + - `double GetRitsuLibSettingDouble(string key)` / `void SetRitsuLibSettingDouble(string key, double value)` + - `string GetRitsuLibSettingString(string key)` / `void SetRitsuLibSettingString(string key, string value)` + +`CreateRitsuLibSettingsSchema()` can return: + +- `Dictionary` (or equivalent object) +- a JSON string (root must be an object) +- a JSON file path (file root must be an object) + +Godot paths (`res://`, `user://`) are recommended, and regular file paths are also supported. + +Structure: + +- page: `modId`, `pageId`, `title`, `description`, `sortOrder`, `sections` +- section: `id`, `title`, `description`, `entries` +- entry: + - common fields: `id`, `type`, `key`, `label`, `description`, `scope` + - `type=toggle|string|button|choice|slider|int-slider` + - `choice`: `options` (`[{ value, label }]`) + - `slider/int-slider`: `min`, `max`, `step` + - `string`: `maxLength` + - `button`: `buttonText`, `tone` + +--- + +::: + +## 运行时反射协议(无库引用){lang="zh-CN"} + +::: zh-CN + +除了 BaseLib / ModConfig 镜像外,RitsuLib 还支持“纯反射协议”注册设置页。 +模组无需引用 `STS2RitsuLib`,只需在程序集元数据中显式声明 provider 类型: + +```xml + + + +``` + +也支持在运行时主动注册 provider(适合你在初始化流程中按需反射调用): + +- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderType(string providerTypeFullName, string? assemblyName = null)` +- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderType(Type providerType)` +- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderTypeAndTryRegister(string providerTypeFullName, string? assemblyName = null)` +- `ModSettingsRuntimeReflectionInteropMirror.RegisterProviderTypeAndTryRegister(Type providerType)` + +Provider 约定(全部为 `static` 方法): + +- `object CreateRitsuLibSettingsSchema()` +- `object? GetRitsuLibSettingValue(string key)` +- `void SetRitsuLibSettingValue(string key, object value)` +- 可选:`void SaveRitsuLibSettings()` +- 可选:`void InvokeRitsuLibSettingAction(string key)`(用于 button) +- 可选强类型覆盖(优先于 object resolver): + - `bool GetRitsuLibSettingBool(string key)` / `void SetRitsuLibSettingBool(string key, bool value)` + - `int GetRitsuLibSettingInt(string key)` / `void SetRitsuLibSettingInt(string key, int value)` + - `double GetRitsuLibSettingDouble(string key)` / `void SetRitsuLibSettingDouble(string key, double value)` + - `string GetRitsuLibSettingString(string key)` / `void SetRitsuLibSettingString(string key, string value)` + +`CreateRitsuLibSettingsSchema()` 可以返回: + +- `Dictionary`(或等价对象) +- JSON 字符串(根节点必须是对象) +- JSON 文件路径(内容根节点必须是对象) + +推荐使用 Godot 路径(`res://`、`user://`),也支持普通文件路径。 + +字段结构: + +- page: `modId`, `pageId`, `title`, `description`, `sortOrder`, `sections` +- section: `id`, `title`, `description`, `entries` +- entry: + - 公共字段:`id`, `type`, `key`, `label`, `description`, `scope` + - `type=toggle|string|button|choice|slider|int-slider` + - `choice`:`options`(`[{ value, label }]`) + - `slider/int-slider`:`min`, `max`, `step` + - `string`:`maxLength` + - `button`:`buttonText`, `tone` + +--- + +::: + +## Minimal Example{lang="en"} + +::: en + +First register persisted data: + +```csharp +using STS2RitsuLib.Data; +using STS2RitsuLib.Utils.Persistence; + +public sealed class MyModSettings +{ + public bool EnableFancyVfx { get; set; } = true; + public double ScreenShakeScale { get; set; } = 1.0; + public MyDifficultyMode DifficultyMode { get; set; } = MyDifficultyMode.Normal; +} + +using (RitsuLibFramework.BeginModDataRegistration("MyMod")) +{ + var store = RitsuLibFramework.GetDataStore("MyMod"); + + store.Register( + key: "settings", + fileName: "settings.json", + scope: SaveScope.Global, + defaultFactory: () => new MyModSettings(), + autoCreateIfMissing: true); +} +``` + +Then create bindings and register the page: + +```csharp +using STS2RitsuLib.Settings; + +var settingsLoc = RitsuLibFramework.CreateModLocalization( + modId: "MyMod", + instanceName: "MyMod-Settings", + resourceFolders: ["MyMod.Localization.Settings"]); + +var fancyVfx = ModSettingsBindings.Global( + "MyMod", + "settings", + model => model.EnableFancyVfx, + (model, value) => model.EnableFancyVfx = value); + +var shakeScale = ModSettingsBindings.Global( + "MyMod", + "settings", + model => model.ScreenShakeScale, + (model, value) => model.ScreenShakeScale = value); + +var difficulty = ModSettingsBindings.Global( + "MyMod", + "settings", + model => model.DifficultyMode, + (model, value) => model.DifficultyMode = value); + +RitsuLibFramework.RegisterModSettings("MyMod", page => page + .WithModDisplayName(ModSettingsText.I18N(settingsLoc, "mod.display_name", "My Fancy Mod")) + .WithTitle(ModSettingsText.I18N(settingsLoc, "page.title", "Settings")) + .WithDescription(ModSettingsText.I18N(settingsLoc, "page.description", "Player-facing options for this mod.")) + .AddSection("general", section => section + .WithTitle(ModSettingsText.I18N(settingsLoc, "general.title", "General")) + .AddToggle( + "fancy_vfx", + ModSettingsText.I18N(settingsLoc, "fancy_vfx.label", "Fancy VFX"), + fancyVfx, + ModSettingsText.I18N(settingsLoc, "fancy_vfx.desc", "Enable additional visual polish.")) + .AddSlider( + "screen_shake_scale", + ModSettingsText.I18N(settingsLoc, "screen_shake.label", "Screen Shake Scale"), + shakeScale, + minValue: 0.0, + maxValue: 2.0, + step: 0.05, + valueFormatter: value => $"{value:0.00}x") + .AddEnumChoice( + "difficulty_mode", + ModSettingsText.I18N(settingsLoc, "difficulty.label", "Difficulty"), + difficulty, + value => ModSettingsText.I18N(settingsLoc, $"difficulty.{value}", value.ToString())))); +``` + +`WithModDisplayName(...)` controls the label used in the left navigation. If it is omitted, RitsuLib falls back to the manifest name and then the mod id. + +--- + +::: + +## 最小示例{lang="zh-CN"} + +::: zh-CN + +先注册持久化数据: + +```csharp +using STS2RitsuLib.Data; +using STS2RitsuLib.Utils.Persistence; + +public sealed class MyModSettings +{ + public bool EnableFancyVfx { get; set; } = true; + public double ScreenShakeScale { get; set; } = 1.0; + public MyDifficultyMode DifficultyMode { get; set; } = MyDifficultyMode.Normal; +} + +using (RitsuLibFramework.BeginModDataRegistration("MyMod")) +{ + var store = RitsuLibFramework.GetDataStore("MyMod"); + + store.Register( + key: "settings", + fileName: "settings.json", + scope: SaveScope.Global, + defaultFactory: () => new MyModSettings(), + autoCreateIfMissing: true); +} +``` + +然后创建绑定并注册设置页: + +```csharp +using STS2RitsuLib.Settings; + +var settingsLoc = RitsuLibFramework.CreateModLocalization( + modId: "MyMod", + instanceName: "MyMod-Settings", + resourceFolders: ["MyMod.Localization.Settings"]); + +var fancyVfx = ModSettingsBindings.Global( + "MyMod", + "settings", + model => model.EnableFancyVfx, + (model, value) => model.EnableFancyVfx = value); + +var shakeScale = ModSettingsBindings.Global( + "MyMod", + "settings", + model => model.ScreenShakeScale, + (model, value) => model.ScreenShakeScale = value); + +var difficulty = ModSettingsBindings.Global( + "MyMod", + "settings", + model => model.DifficultyMode, + (model, value) => model.DifficultyMode = value); + +RitsuLibFramework.RegisterModSettings("MyMod", page => page + .WithModDisplayName(ModSettingsText.I18N(settingsLoc, "mod.display_name", "My Fancy Mod")) + .WithTitle(ModSettingsText.I18N(settingsLoc, "page.title", "Settings")) + .WithDescription(ModSettingsText.I18N(settingsLoc, "page.description", "Player-facing options for this mod.")) + .AddSection("general", section => section + .WithTitle(ModSettingsText.I18N(settingsLoc, "general.title", "General")) + .AddToggle( + "fancy_vfx", + ModSettingsText.I18N(settingsLoc, "fancy_vfx.label", "Fancy VFX"), + fancyVfx, + ModSettingsText.I18N(settingsLoc, "fancy_vfx.desc", "Enable additional visual polish.")) + .AddSlider( + "screen_shake_scale", + ModSettingsText.I18N(settingsLoc, "screen_shake.label", "Screen Shake Scale"), + shakeScale, + minValue: 0.0, + maxValue: 2.0, + step: 0.05, + valueFormatter: value => $"{value:0.00}x") + .AddEnumChoice( + "difficulty_mode", + ModSettingsText.I18N(settingsLoc, "difficulty.label", "Difficulty"), + difficulty, + value => ModSettingsText.I18N(settingsLoc, $"difficulty.{value}", value.ToString())))); +``` + +`WithModDisplayName(...)` 控制左侧导航中的 Mod 标签。若未设置,RitsuLib 会回退到 manifest 名称,再回退到 mod id。 + +--- + +::: + +## Ordering And Navigation{lang="en"} + +::: en + +- **Mod groups**: call `WithModSidebarOrder(int)` on the page builder, or `ModSettingsRegistry.RegisterModSidebarOrder` / `RitsuLibFramework.RegisterModSettingsSidebarOrder`. Lower values appear earlier. +- **Pages within one mod**: use `WithSortOrder(int)` for sibling pages that share the same `ParentPageId`. +- **Child pages**: register the child separately with `AsChildOf(parentPageId)`, then link to it from the parent with `AddSubpage(...)`. + +### Multiple Pages And Subpages + +- **Default page id**: `RegisterModSettings("MyMod", configure)` uses `PageId == "MyMod"`. +- **Extra root pages**: call `RegisterModSettings("MyMod", configure, pageId: "audio")` and use `WithSortOrder(...)` to order multiple root pages. +- **Child page registration**: register the child in its own call and chain `AsChildOf("parentPageId")`. +- **Child UI**: child pages show a back control in the header; the sidebar tree still reflects the hierarchy. + +--- + +::: + +## 排序与导航{lang="zh-CN"} + +::: zh-CN + +- **Mod 分组**:在页面构建器上调用 `WithModSidebarOrder(int)`,或使用 `ModSettingsRegistry.RegisterModSidebarOrder` / `RitsuLibFramework.RegisterModSettingsSidebarOrder`。数值越小越靠前。 +- **同一 Mod 内的页面**:对共享 `ParentPageId` 的兄弟页使用 `WithSortOrder(int)`。 +- **子页**:子页需单独注册,并通过 `AsChildOf(parentPageId)` 绑定父页,再在父页中使用 `AddSubpage(...)` 跳转。 + +### 多页面与子页面 + +- **默认页面 id**:`RegisterModSettings("MyMod", configure)` 的 `PageId` 默认为 `"MyMod"`。 +- **额外根页**:调用 `RegisterModSettings("MyMod", configure, pageId: "audio")`,并通过 `WithSortOrder(...)` 控制多个根页的顺序。 +- **子页注册**:子页必须单独注册,并链式调用 `AsChildOf("parentPageId")`。 +- **子页 UI**:子页标题栏提供返回控件,侧栏树仍保留完整层级。 + +--- + +::: + +## Text Sources{lang="en"} + +::: en + +Use `ModSettingsText` so the page definition stays independent from how text is loaded. + +- `Literal(...)`: simple hardcoded text or quick prototypes +- `I18N(...)`: mod-owned settings text +- `LocString(...)`: text already managed by the game localization pipeline +- `Dynamic(...)`: delegate resolved on each UI rebuild + +Recommended split: + +- gameplay and content-facing names -> `LocString` +- settings-only labels and descriptions -> `I18N` + +--- + +::: + +## 文本来源{lang="zh-CN"} + +::: zh-CN + +使用 `ModSettingsText`,可以让页面定义不依赖具体文本加载方式。 + +- `Literal(...)`:简单硬编码文本或快速原型 +- `I18N(...)`:Mod 自有的设置界面文本 +- `LocString(...)`:已纳入游戏本地化管线的文本 +- `Dynamic(...)`:在每次 UI 刷新时通过委托重新生成文本 + +推荐分工: + +- 游戏内容和内容名称 -> `LocString` +- 设置页专用标签与描述 -> `I18N` + +--- + +::: + +## Supported Controls{lang="en"} + +::: en + +- `AddToggle(...)` for `bool` +- `AddSlider(...)` for `double` +- `AddIntSlider(...)` for `int` +- `AddChoice(...)` / `AddEnumChoice(...)` for option lists; optional `ModSettingsChoicePresentation`: `Stepper` or `Dropdown` +- `AddColor(...)` for color strings +- `AddKeyBinding(...)` for binding strings +- `AddImage(...)` for a `Func` preview with height +- `AddButton(...)` for custom actions +- `AddSubpage(...)` to navigate to a registered child page +- `AddList(...)` for reorderable structured collections +- `AddHeader(...)` / `AddParagraph(...)` for explanatory structure +- collapsible sections via `.Collapsible(startCollapsed: false)` on the section builder + +--- + +::: + +## 支持的控件类型{lang="zh-CN"} + +::: zh-CN + +- `AddToggle(...)`:`bool` +- `AddSlider(...)`:`double` +- `AddIntSlider(...)`:`int` +- `AddChoice(...)` / `AddEnumChoice(...)`:候选列表;可选 `ModSettingsChoicePresentation`:`Stepper` 或 `Dropdown` +- `AddColor(...)`:颜色字符串 +- `AddKeyBinding(...)`:按键绑定字符串 +- `AddImage(...)`:通过 `Func` 提供图像预览 +- `AddButton(...)`:自定义动作按钮 +- `AddSubpage(...)`:跳转到已注册子页 +- `AddList(...)`:可排序结构化集合 +- `AddHeader(...)` / `AddParagraph(...)`:说明与结构辅助项 +- 可折叠分区:在分区构建器上调用 `.Collapsible(startCollapsed: false)` + +--- + +::: + +## Structured Lists{lang="en"} + +::: en + +`AddList(...)` is the entry point for structured list editing. + +It supports: + +- add / remove / reorder +- nested list editors +- item-level structured copy / paste / duplicate +- custom item editors via `ModSettingsListItemContext` + +If the item type is structured, provide an item adapter so copy/paste and duplication can clone and serialize reliably. + +--- + +::: + +## 结构化列表{lang="zh-CN"} + +::: zh-CN + +`AddList(...)` 是结构化列表编辑入口。 + +它支持: + +- 新增 / 删除 / 排序 +- 嵌套列表编辑 +- 列表项级复制 / 粘贴 / 创建副本 +- 通过 `ModSettingsListItemContext` 自定义列表项编辑器 + +如果列表项类型是结构化数据,建议提供 item adapter,以保证复制、粘贴和副本操作可以正确克隆与序列化。 + +--- + +::: + +## Page Structure{lang="en"} + +::: en + +The UI hierarchy is: + +- mod group +- page +- section +- entry + +For most mods, one root page with several sections is sufficient. Introduce additional pages only when the content represents a distinct feature area. + +Use: + +- multiple pages for large feature areas +- `AddSubpage(...)` for drill-down flows +- collapsible sections for low-frequency settings +- lists when players edit collections rather than single values + +--- + +::: + +## 页面结构{lang="zh-CN"} + +::: zh-CN + +当前 UI 层级为: + +- mod 分组 +- page +- section +- entry + +对于大多数 Mod,一个根页面配多个分区就足够。只有在功能区域明确分离时,才建议拆出额外页面。 + +适合使用的场景: + +- 多页面:大型功能区分离 +- `AddSubpage(...)`:钻取式设置流 +- 可折叠 section:收纳低频选项 +- 列表:编辑集合而非单个值 + +--- + +::: + +## Scope Guidance{lang="en"} + +::: en + +Bindings preserve the scope of the underlying persisted value. + +- `SaveScope.Global`: shared across all profiles +- `SaveScope.Profile`: varies by player profile + +Typical usage: + +- `Global`: graphics, accessibility, debug toggles, machine-level defaults +- `Profile`: profile-specific gameplay preferences or campaign-adjacent options + +--- + +::: + +## 作用域建议{lang="zh-CN"} + +::: zh-CN + +绑定会保留底层持久化值的作用域。 + +- `SaveScope.Global`:所有档位共享 +- `SaveScope.Profile`:按玩家档位区分 + +常见用途: + +- `Global`:画面、辅助功能、调试开关、机器级默认项 +- `Profile`:按档位变化的玩法偏好或流程相关设置 + +--- + +::: + +## What To Expose{lang="en"} + +::: en + +Good candidates for the settings UI: + +- feature toggles +- cosmetic preferences +- accessibility adjustments +- gameplay options players are expected to tune + +Poor candidates for the settings UI: + +- caches +- migration bookkeeping +- runtime mirrors +- purely internal implementation state + +The intended pattern is to persist a complete model, then expose only the user-editable subset. + +--- + +::: + +## 适合暴露到设置页的内容{lang="zh-CN"} + +::: zh-CN + +适合放入设置界面的内容: + +- 功能开关 +- 外观偏好 +- 辅助功能调整项 +- 玩家预期可调的玩法参数 + +不适合放入设置界面的内容: + +- 缓存 +- 迁移元数据 +- 运行时镜像状态 +- 纯内部实现字段 + +推荐模式是先持久化完整模型,再选择性暴露玩家真正需要调整的那部分。 + +--- + +::: + +## Built-In Reference Page{lang="en"} + +::: en + +RitsuLib registers its own page as a reference implementation. It demonstrates persisted settings, preview-only bindings, collapsible sections, nested list editing, and item copy/paste workflows. + +--- + +::: + +## 内置参考页{lang="zh-CN"} + +::: zh-CN + +RitsuLib 自身注册了一页参考设置,用于展示已持久化设置、仅预览绑定、可折叠分区、嵌套列表编辑以及列表项复制粘贴工作流。 + +--- + +::: + +## Related Docs{lang="en"} + +::: en + +- [Persistence Guide](/guide/persistence-guide) +- [Localization & Keywords](/guide/localization-and-keywords) +- [Lifecycle Events](/guide/lifecycle-events) +- [Patching Guide](/guide/patching-guide) (`Settings/Patches/ModSettingsUiPatches.cs` contains the menu entry and submenu injection) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [持久化设计](/guide/persistence-guide) +- [本地化与关键词](/guide/localization-and-keywords) +- [生命周期事件](/guide/lifecycle-events) +- [补丁系统](/guide/patching-guide)(`Settings/Patches/ModSettingsUiPatches.cs` 包含菜单入口与子菜单注入逻辑) + +::: diff --git a/docs/pages/guide/patching-guide.md b/docs/pages/guide/patching-guide.md new file mode 100644 index 0000000..7f19817 --- /dev/null +++ b/docs/pages/guide/patching-guide.md @@ -0,0 +1,545 @@ +--- +title: + en: Patching Guide + zh-CN: 补丁系统 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +RitsuLib uses Harmony underneath, but wraps it in a patching layer that standardizes declaration shape, registration, and failure handling. + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +RitsuLib 底层仍然使用 Harmony,但在上面包了一层补丁系统,用来统一补丁声明形状、注册方式和失败处理。 + +--- + +::: + +## Main Types{lang="en"} + +::: en + +| Type | Purpose | +|---|---| +| `RitsuLibFramework.CreatePatcher(...)` | Create a `ModPatcher` instance | +| `ModPatcher` | Register and apply patches | +| `IPatchMethod` | Static patch declaration contract | +| `IModPatches` | Group multiple patch registrations together | +| `DynamicPatchBuilder` | Build patches from runtime-discovered methods | + +--- + +::: + +## 主要类型{lang="zh-CN"} + +::: zh-CN + +| 类型 | 作用 | +|---|---| +| `RitsuLibFramework.CreatePatcher(...)` | 创建 `ModPatcher` | +| `ModPatcher` | 注册并应用补丁 | +| `IPatchMethod` | 单个补丁的静态声明接口 | +| `IModPatches` | 用于分组注册多个补丁 | +| `DynamicPatchBuilder` | 处理运行时发现目标的方法补丁 | + +--- + +::: + +## The Normal Workflow{lang="en"} + +::: en + +```csharp +var patcher = RitsuLibFramework.CreatePatcher("MyMod", "core-patches"); +patcher.RegisterPatch(); +patcher.RegisterPatches(); + +if (!patcher.PatchAll()) + throw new InvalidOperationException("Required patches failed."); +``` + +Recommended pattern: + +- create one patcher per logical patch area +- register all patches first +- call `PatchAll()` once +- treat a `false` return as a startup failure for that patcher + +--- + +::: + +## 常规流程{lang="zh-CN"} + +::: zh-CN + +```csharp +var patcher = RitsuLibFramework.CreatePatcher("MyMod", "core-patches"); +patcher.RegisterPatch(); +patcher.RegisterPatches(); + +if (!patcher.PatchAll()) + throw new InvalidOperationException("Required patches failed."); +``` + +推荐做法: + +- 每个逻辑区域使用一个 patcher +- 先注册完所有补丁 +- 最后统一调用一次 `PatchAll()` +- 如果返回 `false`,就把它视为该 patcher 的启动失败 + +--- + +::: + +## Writing A Single Patch With `IPatchMethod`{lang="en"} + +::: en + +`IPatchMethod` is the most common patch shape. + +```csharp +using STS2RitsuLib.Patching.Models; + +public class ExamplePatch : IPatchMethod +{ + public static string PatchId => "example_patch"; + public static string Description => "Log when the method runs"; + public static bool IsCritical => false; + + public static ModPatchTarget[] GetTargets() + { + return [new(typeof(SomeType), nameof(SomeType.SomeMethod))]; + } + + public static void Prefix() + { + // Harmony prefix + } +} +``` + +Important points: + +- `PatchId` must be unique within the patcher +- `GetTargets()` can return one or many targets +- `Prefix`, `Postfix`, `Transpiler`, and `Finalizer` are discovered by name +- if none of those methods exist, patch application fails + +--- + +::: + +## 用 `IPatchMethod` 编写单个补丁{lang="zh-CN"} + +::: zh-CN + +`IPatchMethod` 是最常见的补丁形式。 + +```csharp +using STS2RitsuLib.Patching.Models; + +public class ExamplePatch : IPatchMethod +{ + public static string PatchId => "example_patch"; + public static string Description => "Log when the method runs"; + public static bool IsCritical => false; + + public static ModPatchTarget[] GetTargets() + { + return [new(typeof(SomeType), nameof(SomeType.SomeMethod))]; + } + + public static void Prefix() + { + // Harmony prefix + } +} +``` + +需要注意: + +- `PatchId` 在同一个 patcher 里必须唯一 +- `GetTargets()` 可以返回一个或多个目标 +- `Prefix`、`Postfix`、`Transpiler`、`Finalizer` 通过命名约定发现 +- 如果这些方法一个都没有,补丁会被视为失败 + +--- + +::: + +## Grouping Patches With `IModPatches`{lang="en"} + +::: en + +When you want one type to register several patches, implement `IModPatches`: + +```csharp +using STS2RitsuLib.Patching.Core; +using STS2RitsuLib.Patching.Models; + +public class MyPatchSet : IModPatches +{ + public static void AddTo(ModPatcher patcher) + { + patcher.RegisterPatch(); + patcher.RegisterPatch(); + } +} +``` + +Then register the group with: + +```csharp +patcher.RegisterPatches(); +``` + +This is the preferred replacement for older "apply this patch bundle object" examples. + +--- + +::: + +## 用 `IModPatches` 分组注册{lang="zh-CN"} + +::: zh-CN + +如果你希望一个类型统一注册多个补丁,可以实现 `IModPatches`: + +```csharp +using STS2RitsuLib.Patching.Core; +using STS2RitsuLib.Patching.Models; + +public class MyPatchSet : IModPatches +{ + public static void AddTo(ModPatcher patcher) + { + patcher.RegisterPatch(); + patcher.RegisterPatch(); + } +} +``` + +然后这样注册: + +```csharp +patcher.RegisterPatches(); +``` + +这就是旧文档里那种“直接 apply 一个补丁集合对象”的现代替代写法。 + +--- + +::: + +## Critical vs Optional Patches{lang="en"} + +::: en + +Each `IPatchMethod` can declare `IsCritical`. + +- `true`: failure causes `PatchAll()` to fail and the patcher rolls back +- `false`: failure is logged, but the patcher may still succeed overall + +Use `IsCritical = true` when the mod cannot safely run without the patch. +Use `false` for cosmetic features, optional compatibility hooks, or best-effort enhancements. + +--- + +::: + +## Critical 与 Optional 补丁{lang="zh-CN"} + +::: zh-CN + +每个 `IPatchMethod` 都可以声明 `IsCritical`。 + +- `true`:失败后 `PatchAll()` 会失败,patcher 会回滚 +- `false`:失败会记录日志,但 patcher 仍可能整体成功 + +什么时候该设成 `true`: + +- 缺了这个补丁 Mod 根本无法安全运行 + +什么时候适合 `false`: + +- 纯 UI / 表现增强 +- 兼容性补丁 +- 最佳努力型功能 + +--- + +::: + +## Ignore Missing Targets{lang="en"} + +::: en + +`ModPatchTarget` supports an `ignoreIfMissing` flag: + +```csharp +public static ModPatchTarget[] GetTargets() +{ + return [new(typeof(SomeType), "SomeOptionalMethod", ignoreIfMissing: true)]; +} +``` + +Use this when: + +- a target only exists on some game versions +- a compatibility target may not be present +- the patch is optional by design + +This differs from `IsCritical = false`: + +- `ignoreIfMissing` means "missing target is expected and not an error" +- `IsCritical = false` means "target exists, but patch failure should not abort the patcher" + +--- + +::: + +## Ignore Missing Target{lang="zh-CN"} + +::: zh-CN + +`ModPatchTarget` 支持 `ignoreIfMissing`: + +```csharp +public static ModPatchTarget[] GetTargets() +{ + return [new(typeof(SomeType), "SomeOptionalMethod", ignoreIfMissing: true)]; +} +``` + +适用场景: + +- 某个目标只在部分游戏版本存在 +- 某个兼容目标可能不存在 +- 缺失目标本来就是预期情况 + +它和 `IsCritical = false` 不是一回事: + +- `ignoreIfMissing` 表示“目标不存在也不算错误” +- `IsCritical = false` 表示“目标存在,但补丁失败不应终止整个 patcher” + +--- + +::: + +## Multiple Targets In One Patch{lang="en"} + +::: en + +One `IPatchMethod` can patch several methods that share the same Harmony logic. + +RitsuLib automatically expands `GetTargets()` into multiple `ModPatchInfo` entries. +If there is more than one target, the framework appends the target name to the generated patch id. + +That lets you keep related logic together without manually duplicating patch classes. + +--- + +::: + +## 一个补丁作用多个目标{lang="zh-CN"} + +::: zh-CN + +一个 `IPatchMethod` 可以同时补多个方法,只要它们共享同一套 Harmony 逻辑。 + +RitsuLib 会把 `GetTargets()` 自动展开成多个 `ModPatchInfo`。 +当目标不止一个时,框架会自动把目标名附加到补丁标识上,避免冲突。 + +这样你就能把相关逻辑放在一起,而不需要手动复制多个补丁类。 + +--- + +::: + +## Dynamic Patches{lang="en"} + +::: en + +Use `DynamicPatchBuilder` when targets are discovered at runtime. + +```csharp +using HarmonyLib; +using STS2RitsuLib.Patching.Builders; + +var builder = new DynamicPatchBuilder("my_dynamic") + .AddMethod( + targetType: typeof(SomeType), + methodName: "SomeMethod", + postfix: DynamicPatchBuilder.FromMethod(typeof(MyRuntimePatch), nameof(MyRuntimePatch.Postfix)), + isCritical: false, + description: "Runtime-discovered patch"); + +patcher.ApplyDynamic(builder, rollbackOnCriticalFailure: false); +``` + +Use dynamic patches when static `GetTargets()` is not practical, for example: + +- patching generated runtime types +- patching property getters selected from reflection scans +- patching a variable set of discovered methods + +--- + +::: + +## 动态补丁{lang="zh-CN"} + +::: zh-CN + +当补丁目标需要运行时发现时,可以使用 `DynamicPatchBuilder`。 + +```csharp +using HarmonyLib; +using STS2RitsuLib.Patching.Builders; + +var builder = new DynamicPatchBuilder("my_dynamic") + .AddMethod( + targetType: typeof(SomeType), + methodName: "SomeMethod", + postfix: DynamicPatchBuilder.FromMethod(typeof(MyRuntimePatch), nameof(MyRuntimePatch.Postfix)), + isCritical: false, + description: "Runtime-discovered patch"); + +patcher.ApplyDynamic(builder, rollbackOnCriticalFailure: false); +``` + +常见用途: + +- 给运行时生成的类型打补丁 +- 通过反射扫描后决定要给哪些属性读取器打补丁 +- 给一组动态发现的方法打补丁 + +--- + +::: + +## Logging And Patch Boundaries{lang="en"} + +::: en + +`CreatePatcher(ownerModId, patcherName, patcherLabel)` gives each patcher: + +- a stable Harmony id: `.` +- its own logger prefix +- independent registration and application lifecycle + +Splitting patchers by feature area is usually worth it because logs stay easier to read. + +--- + +::: + +## 日志与补丁边界{lang="zh-CN"} + +::: zh-CN + +`CreatePatcher(ownerModId, patcherName, patcherLabel)` 会为每个补丁器生成: + +- 稳定的 Harmony id:`.` +- 独立的日志前缀 +- 独立的注册和应用生命周期 + +实际开发里,把补丁器按功能拆开通常非常值得,因为日志会清晰很多。 + +--- + +::: + +## Suggested Structure{lang="en"} + +::: en + +For medium or large mods, this layout works well: + +- one patch namespace per feature area +- one `IModPatches` type per feature area +- small `IPatchMethod` classes with one clear purpose each +- optional compatibility patches marked `IsCritical = false` + +This matches how RitsuLib itself organizes its internal framework patchers. + +--- + +::: + +## 推荐结构{lang="zh-CN"} + +::: zh-CN + +对中大型 Mod,比较建议这样组织: + +- 每个功能区一个补丁命名空间 +- 每个功能区一个 `IModPatches` 分组类型 +- 每个 `IPatchMethod` 只做一件明确的事 +- 兼容补丁默认设为 `IsCritical = false` + +这也是 RitsuLib 自己组织内部框架补丁时采用的方式。 + +--- + +::: + +## Common Mistakes{lang="en"} + +::: en + +- calling `PatchAll()` before registering all patches +- marking compatibility patches as critical without a real need +- using `IsCritical = false` when `ignoreIfMissing` is the real intent +- writing an `IPatchMethod` with no `Prefix` / `Postfix` / `Transpiler` / `Finalizer` +- keeping all unrelated patches in one giant patcher with unreadable logs + +--- + +::: + +## 常见错误{lang="zh-CN"} + +::: zh-CN + +- 还没注册完补丁就调用 `PatchAll()` +- 没必要的兼容补丁却标成 critical +- 真正意图是“目标可能不存在”,却只写了 `IsCritical = false` +- `IPatchMethod` 里没有 `Prefix` / `Postfix` / `Transpiler` / `Finalizer` +- 把所有不相关补丁都塞进一个巨大 patcher,导致日志难读 + +--- + +::: + +## Related Documents{lang="en"} + +::: en + +- [Getting Started](/guide/getting-started) +- [Framework Design](/guide/framework-design) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [快速入门](/guide/getting-started) +- [框架设计](/guide/framework-design) + +::: diff --git a/docs/pages/guide/persistence-guide.md b/docs/pages/guide/persistence-guide.md new file mode 100644 index 0000000..9dbe734 --- /dev/null +++ b/docs/pages/guide/persistence-guide.md @@ -0,0 +1,636 @@ +--- +title: + en: Persistence Guide + zh-CN: 持久化设计 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +RitsuLib provides a structured persistence layer for mod data, with scoped storage, profile switching support, backup fallback, and schema migrations. + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +RitsuLib 提供了一套结构化的 Mod 数据持久化层,支持作用域存储、档位切换、备份回退以及 schema 迁移。 + +--- + +::: + +## Main APIs{lang="en"} + +::: en + +| API | Purpose | +|---|---| +| `RitsuLibFramework.BeginModDataRegistration(modId)` | Batch registration scope | +| `RitsuLibFramework.GetDataStore(modId)` | Access the mod's `ModDataStore` | +| `ModDataStore.Register(...)` | Register one persistent entry | +| `ModDataStore.Get(key)` | Read data | +| `ModDataStore.Modify(key, ...)` | Mutate data | +| `ModDataStore.Save(key)` / `SaveAll()` | Persist changes | + +--- + +::: + +## 主要 API{lang="zh-CN"} + +::: zh-CN + +| API | 作用 | +|---|---| +| `RitsuLibFramework.BeginModDataRegistration(modId)` | 批量注册作用域 | +| `RitsuLibFramework.GetDataStore(modId)` | 获取该 Mod 的 `ModDataStore` | +| `ModDataStore.Register(...)` | 注册一个持久化条目 | +| `ModDataStore.Get(key)` | 读取数据 | +| `ModDataStore.Modify(key, ...)` | 修改数据 | +| `ModDataStore.Save(key)` / `SaveAll()` | 持久化写盘 | + +--- + +::: + +## Why Data Is Registered As Classes{lang="en"} + +::: en + +Persistent entries are registered as `class` types with a parameterless constructor. + +This allows the framework to support: + +- structured JSON payloads +- future schema expansion +- versioned migration +- safer defaults and cloning + +So instead of registering a raw integer, define a small data object: + +```csharp +public sealed class CounterData +{ + public int Value { get; set; } +} +``` + +--- + +::: + +## 为什么数据以 class 形式注册{lang="zh-CN"} + +::: zh-CN + +RitsuLib 的持久化条目要求是带无参构造的类。 + +这么做是为了自然支持: + +- 结构化 JSON +- 后续字段扩展 +- schema 迁移 +- 更安全的默认值克隆 + +所以不要注册一个裸 `int`,而是定义一个小数据对象: + +```csharp +public sealed class CounterData +{ + public int Value { get; set; } +} +``` + +--- + +::: + +## Registering Data{lang="en"} + +::: en + +```csharp +using STS2RitsuLib.Data; +using STS2RitsuLib.Utils.Persistence; + +using (RitsuLibFramework.BeginModDataRegistration("MyMod")) +{ + var store = RitsuLibFramework.GetDataStore("MyMod"); + + store.Register( + key: "counter", + fileName: "counter.json", + scope: SaveScope.Profile, + defaultFactory: () => new CounterData(), + autoCreateIfMissing: true); +} +``` + +Parameters worth understanding: + +- `key`: lookup key inside the store +- `fileName`: file name written under the resolved mod-data path +- `scope`: `Global` or `Profile` +- `defaultFactory`: default value when no file exists or recovery is needed +- `autoCreateIfMissing`: immediately write the default file when missing + +--- + +::: + +## 注册数据{lang="zh-CN"} + +::: zh-CN + +```csharp +using STS2RitsuLib.Data; +using STS2RitsuLib.Utils.Persistence; + +using (RitsuLibFramework.BeginModDataRegistration("MyMod")) +{ + var store = RitsuLibFramework.GetDataStore("MyMod"); + + store.Register( + key: "counter", + fileName: "counter.json", + scope: SaveScope.Profile, + defaultFactory: () => new CounterData(), + autoCreateIfMissing: true); +} +``` + +这些参数的含义需要特别注意: + +- `key`:在 store 内部查找该条目的键 +- `fileName`:写入磁盘时使用的文件名 +- `scope`:`Global` 或 `Profile` +- `defaultFactory`:没有文件或需要恢复时使用的默认值 +- `autoCreateIfMissing`:文件不存在时是否立即写出默认文件 + +--- + +::: + +## Global vs Profile Scope{lang="en"} + +::: en + +`SaveScope` has two values: + +- `Global`: shared across all profiles +- `Profile`: isolated per game profile + +Design intent: + +- use `Global` for mod settings or machine-wide caches +- use `Profile` for unlocks, progression, and run-adjacent player data + +Profile-scoped entries are initialized only after profile services are ready. + +--- + +::: + +## Global 与 Profile 作用域{lang="zh-CN"} + +::: zh-CN + +`SaveScope` 只有两个值: + +- `Global`:所有档位共享 +- `Profile`:按游戏档位隔离 + +设计建议: + +- Mod 设置、机器级缓存适合 `Global` +- 解锁、进度、玩家档位相关数据适合 `Profile` + +`Profile` 作用域的数据只会在档位服务准备好之后初始化。 + +--- + +::: + +## Reading And Writing{lang="en"} + +::: en + +```csharp +var store = RitsuLibFramework.GetDataStore("MyMod"); + +var counter = store.Get("counter"); + +store.Modify("counter", data => +{ + data.Value += 1; +}); + +store.Save("counter"); +``` + +Notes: + +- `Get` returns the live registered object +- `Modify` is just a convenience wrapper around that live object +- saving is explicit unless you choose to save immediately after mutation + +--- + +::: + +## 读取与写入{lang="zh-CN"} + +::: zh-CN + +```csharp +var store = RitsuLibFramework.GetDataStore("MyMod"); + +var counter = store.Get("counter"); + +store.Modify("counter", data => +{ + data.Value += 1; +}); + +store.Save("counter"); +``` + +几点说明: + +- `Get` 返回的是当前注册条目的活动对象 +- `Modify` 本质上只是对这个活动对象做一次包装 +- 保存默认是显式的,是否每次改完立刻写盘由作者自己决定 + +--- + +::: + +## Registration Timing{lang="en"} + +::: en + +`BeginModDataRegistration` is the recommended registration pattern because it lets the store defer initialization until the batch is complete. + +That helps avoid partial setup states when a mod registers several entries in one place. + +At the end of the registration scope: + +- global entries initialize immediately +- profile entries initialize when profile services are available + +--- + +::: + +## 注册时机{lang="zh-CN"} + +::: zh-CN + +推荐始终通过 `BeginModDataRegistration` 批量注册。 + +这样做的好处是,数据存储器可以在整个批次结束后再统一初始化,避免半注册状态。 + +作用域结束时: + +- 全局条目会立即初始化 +- 档位条目会在档位服务可用时初始化 + +--- + +::: + +## Profile Changes{lang="en"} + +::: en + +Profile-scoped entries are aware of profile switching. + +When the active profile changes, RitsuLib: + +- saves the old profile-scoped data to the old profile path +- reloads the data from the new profile path + +This is handled by the framework; mods do not need to manually rebind their profile-scoped stores. + +--- + +::: + +## 档位切换{lang="zh-CN"} + +::: zh-CN + +档位作用域的数据会自动感知档位切换。 + +当当前档位改变时,RitsuLib 会: + +- 先把旧档位数据保存回旧档位路径 +- 再从新档位路径重新加载 + +这部分由框架接管,Mod 不需要手写档位切换时的重绑定逻辑。 + +--- + +::: + +## Existing Data Checks{lang="en"} + +::: en + +```csharp +if (store.HasExistingData("counter")) +{ + // There was already persisted data on disk +} +``` + +This is useful when you want different startup behavior for first-time initialization vs loading an existing save. + +--- + +::: + +## 判断是否已有存档数据{lang="zh-CN"} + +::: zh-CN + +```csharp +if (store.HasExistingData("counter")) +{ + // 磁盘上已经存在旧数据 +} +``` + +这个判断常用于区分“首次初始化”和“读取旧存档”两种启动路径。 + +--- + +::: + +## Recovery And Backup Behavior{lang="en"} + +::: en + +The persistence layer tries to be defensive: + +- if the main file cannot be read, it attempts backup fallback +- if migrated backup data loads successfully, it can be written back +- if migration or parsing fails badly enough, corrupt data can be renamed with a `.corrupt` suffix +- when recovery fails, the entry falls back to default values + +This is meant to keep the mod usable even when local data is damaged. + +--- + +::: + +## 备份与恢复行为{lang="zh-CN"} + +::: zh-CN + +持久化层会尽量采用保守策略: + +- 主文件读取失败时尝试备份回退 +- 如果从备份成功恢复并完成迁移,可以写回主文件 +- 当迁移或解析严重失败时,损坏文件可能被重命名为 `.corrupt` +- 若恢复失败,则回退为默认值 + +目标是:即使本地数据损坏,Mod 仍尽量保持可用。 + +--- + +::: + +## Migrations{lang="en"} + +::: en + +`Register` accepts both migration config and migration steps: + +```csharp +store.Register( + key: "settings", + fileName: "settings.json", + scope: SaveScope.Global, + defaultFactory: () => new MyData(), + migrationConfig: new ModDataMigrationConfig(currentDataVersion: 2, minimumSupportedDataVersion: 1), + migrations: + [ + new SettingsV1ToV2Migration(), + ]); +``` + +Migration rules: + +- if no config is registered, data is deserialized directly +- if config exists, the framework reads the schema version field +- migrations run in version order +- data below the minimum supported version is rejected for recovery +- successfully migrated data is saved back in the new format + +Use migrations when a file format is published and later evolves. + +--- + +::: + +## 数据迁移{lang="zh-CN"} + +::: zh-CN + +`Register` 支持同时传入迁移配置与迁移步骤: + +```csharp +store.Register( + key: "settings", + fileName: "settings.json", + scope: SaveScope.Global, + defaultFactory: () => new MyData(), + migrationConfig: new ModDataMigrationConfig(currentDataVersion: 2, minimumSupportedDataVersion: 1), + migrations: + [ + new SettingsV1ToV2Migration(), + ]); +``` + +迁移规则: + +- 没有 migration config 时,直接反序列化 +- 有 config 时,框架会先读取 schema version 字段 +- migration 会按版本顺序执行 +- 低于最小支持版本的数据会被拒绝并进入恢复路径 +- 成功迁移后的数据会回写成新格式 + +只要文件格式已经发布并且后续会演进,就建议尽早引入迁移版本号。 + +--- + +::: + +## AttachedState vs SavedAttachedState{lang="en"} + +::: en + +`AttachedState` is for runtime-only sidecar state on reference objects. + +Use it when: + +- the value only matters during the current process +- the key object already defines the lifetime you want +- you do not want to subclass or mutate the target type + +`SavedAttachedState` is the persisted counterpart for objects that already flow through `SavedProperties.FromInternal(...)` and `SavedProperties.FillInternal(...)`. + +Use it when: + +- the key is a model object that participates in vanilla save serialization +- the attached value should survive save/load round-trips +- the value type is already supported by `SavedProperties` + +Supported value types are: + +- `int` +- `bool` +- `string` +- `ModelId` +- enums +- `int[]` +- enum arrays +- `SerializableCard` +- `SerializableCard[]` +- `List` + +Example: + +```csharp +using STS2RitsuLib.Utils; + +private static readonly SavedAttachedState BonusDamage = + new("bonus_damage", () => 0); + +BonusDamage[model] = 4; + +var bonus = BonusDamage.GetOrCreate(model); +``` + +Notes: + +- persisted names must be globally unique after the `"{typeof(TKey).Name}_{name}"` prefix is applied +- `SavedAttachedState` is not a generic JSON sideband channel; it is intentionally limited to `SavedProperties`-compatible value types +- reward-specific `EncounterState` sideband serialization remains a special-case implementation, not the default persistence pattern + +--- + +::: + +## AttachedState 与 SavedAttachedState{lang="zh-CN"} + +::: zh-CN + +`AttachedState` 用于给引用类型对象挂运行时 sidecar 状态。 + +适合场景: + +- 值只在当前进程内有效 +- 希望状态生命周期跟随 key 对象 +- 不想为目标类型做继承或直接改模型字段 + +`SavedAttachedState` 是它的可持久化版本,面向已经会经过 `SavedProperties.FromInternal(...)` 和 `SavedProperties.FillInternal(...)` 的对象。 + +适合场景: + +- key 是会参与原生存档序列化的模型对象 +- 附加值需要跨 save/load 保留 +- 值类型本身受 `SavedProperties` 支持 + +当前支持的值类型: + +- `int` +- `bool` +- `string` +- `ModelId` +- enum +- `int[]` +- enum 数组 +- `SerializableCard` +- `SerializableCard[]` +- `List` + +示例: + +```csharp +using STS2RitsuLib.Utils; + +private static readonly SavedAttachedState BonusDamage = + new("bonus_damage", () => 0); + +BonusDamage[model] = 4; + +var bonus = BonusDamage.GetOrCreate(model); +``` + +说明: + +- 持久化字段名在套用 `"{typeof(TKey).Name}_{name}"` 前缀后必须全局唯一 +- `SavedAttachedState` 不是任意 JSON sideband 通道,而是刻意限制在 `SavedProperties` 可表示的值类型范围内 +- reward 专用的 `EncounterState` sideband 序列化依然只是特例,不是默认推荐模式 + +--- + +::: + +## Recommended Usage Pattern{lang="en"} + +::: en + +- define one data class per persisted concept +- use `AttachedState` for ephemeral runtime-only object state +- use `SavedAttachedState` only for model objects that already participate in `SavedProperties` +- keep file names stable after release +- use `Profile` scope by default for progression-like data +- batch registration inside `BeginModDataRegistration` +- add schema versions before you need them, not after a breaking change has already shipped + +--- + +::: + +## 推荐实践{lang="zh-CN"} + +::: zh-CN + +- 每个持久化概念定义一个独立 class +- 纯运行时对象状态优先使用 `AttachedState` +- 只有模型对象本来就参与 `SavedProperties` 时才使用 `SavedAttachedState` +- 发布后尽量保持 `fileName` 稳定 +- 进度类数据默认优先考虑 `Profile` +- 始终在 `BeginModDataRegistration` 中批量注册 +- schema version 最好在真正需要迁移前就准备好 + +--- + +::: + +## Related Documents{lang="en"} + +::: en + +- [Getting Started](/guide/getting-started) +- [Framework Design](/guide/framework-design) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [快速入门](/guide/getting-started) +- [框架设计](/guide/framework-design) + +::: diff --git a/docs/pages/guide/terminology.md b/docs/pages/guide/terminology.md new file mode 100644 index 0000000..bbd6cf1 --- /dev/null +++ b/docs/pages/guide/terminology.md @@ -0,0 +1,114 @@ +--- +title: + en: Terminology + zh-CN: 术语表 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This document defines the canonical terms used across the RitsuLib documentation. + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文定义 RitsuLib 文档中统一使用的核心术语及推荐译法。 + +--- + +::: + +## Core Terms{lang="en"} + +::: en + +| Term | Preferred usage | Notes | +|---|---|---| +| settings UI | settings UI | Use for the mod configuration interface as a whole. | +| settings page | page | A single registered page in the settings UI. | +| section | section | A structured group within a page. | +| entry | entry | One visible row or control within a section. | +| binding | binding | The read/write link between UI and stored or in-memory state. | +| persistence | persistence | The storage layer and save lifecycle. | +| persisted | persisted | Use for values written through the persistence layer. | +| preview-only | preview-only | Use for controls or bindings that never persist data. | +| fallback | fallback | Preferred over `shim` for compatibility behavior. | +| compatibility fallback | compatibility fallback | A narrowly scoped behavior used when vanilla data or APIs are incomplete. | +| bridge patch | bridge patch | A patch that forwards mod content into vanilla logic that would otherwise skip it. | +| registry | registry | The runtime registration container for a content type. | +| content pack | content pack | The convenience entry point that writes into multiple registries. | +| builder | builder | Use for fluent page, section, or content construction APIs. | +| override | override | Use for replacing an asset path, behavior, or value source. | +| placeholder | placeholder | A temporary fallback value used when data is missing. | +| scope | scope | The storage scope of a persisted value. | +| profile | profile | Per-profile save scope. | +| global | global | Cross-profile save scope. | +| epoch | epoch | Keep the game term `epoch` in English. | +| story | story | Keep the game term `story` in English. | +| Ancient dialogue | Ancient dialogue | Use this spelling for the game system and related keys. | + +--- + +::: + +## 核心术语{lang="zh-CN"} + +::: zh-CN + +| 英文术语 | 推荐中文 | 说明 | +|---|---|---| +| settings UI | 设置界面 | 指整体玩家配置界面。 | +| page | 页面 | 设置界面中的单个已注册页面。 | +| section | 分区 | 页面中的结构化分组。 | +| entry | 条目 | 分区中的单行可见控件或文本项。 | +| binding | 绑定 | UI 与存储值或内存状态之间的读写连接。 | +| persistence | 持久化 | 存储层与保存生命周期。 | +| persisted | 已持久化 / 会持久化 | 用于描述会写入持久化层的值。 | +| preview-only | 仅预览 | 指不会写入持久化层的控件或绑定。 | +| fallback | 回退 | 兼容或缺失数据场景下的回退行为。 | +| compatibility fallback | 兼容回退 | 优先使用该术语,避免使用“垫片”。 | +| bridge patch | 桥接补丁 | 将 Mod 内容转发到原版逻辑检查点的补丁。 | +| registry | 注册器 | 某类内容的运行时注册容器。 | +| content pack | 内容包 | 向多个注册器写入内容的便捷入口。 | +| builder | 构建器 | 用于链式构造页面、分区或内容的 API。 | +| override | 覆写 | 对资源路径、行为或值来源进行替换。 | +| placeholder | 占位值 | 数据缺失时使用的临时值。 | +| scope | 作用域 | 持久化值的存储范围。 | +| profile | 档位 | 按玩家档位区分的保存范围。 | +| global | 全局 | 跨档位共享的保存范围。 | +| epoch | 纪元(Epoch) | 中文文档首次出现可带英文,后续可简称“纪元”。 | +| story | 故事(Story) | 中文文档首次出现可带英文,后续可简称“故事”。 | +| Ancient dialogue | Ancient 对话 | 与游戏系统保持一致,不改写为其他称呼。 | + +--- + +::: + +## Related Documents{lang="en"} + +::: en + +- [Framework Design](/guide/framework-design) +- [Mod Settings](/guide/mod-settings) +- [Diagnostics & Compatibility](/guide/diagnostics-and-compatibility) +- [Localization & Keywords](/guide/localization-and-keywords) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [框架设计](/guide/framework-design) +- [Mod 设置界面](/guide/mod-settings) +- [诊断与兼容层](/guide/diagnostics-and-compatibility) +- [本地化与关键词](/guide/localization-and-keywords) + +::: diff --git a/docs/pages/guide/timeline-and-unlocks.md b/docs/pages/guide/timeline-and-unlocks.md new file mode 100644 index 0000000..3e5d3b4 --- /dev/null +++ b/docs/pages/guide/timeline-and-unlocks.md @@ -0,0 +1,626 @@ +--- +title: + en: Timeline & Unlocks + zh-CN: 时间线与解锁 +cover: https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp +--- + +## Introduction{lang="en"} + +::: en + +This is the reference for timeline registration and unlock semantics. + +RitsuLib splits timeline registration and unlock rules into two systems that are meant to work together. This document covers: + +- How `Story` and `Epoch` are registered +- What the template types are responsible for +- How unlock rules are evaluated +- Limitations of vanilla progression for mod characters and RitsuLib’s compatibility bridges + +--- + +::: + +## 简介{lang="zh-CN"} + +::: zh-CN + +本文是时间线注册与解锁语义的参考文档。 + +RitsuLib 将时间线注册和解锁规则拆成两个系统,配合使用。本文说明: + +- `Story` / `Epoch` 的注册方式 +- 模板类型的职责 +- 解锁规则的判定机制 +- 原版进度逻辑对 Mod 角色的局限性与 RitsuLib 的兼容桥接 + +--- + +::: + +## The Two Registries{lang="en"} + +::: en + +| Registry | Role | +|---|---| +| `ModTimelineRegistry` | Registers `StoryModel` and `EpochModel` | +| `ModUnlockRegistry` | Defines unlock conditions for content or epochs | + +In the fluent builder, these correspond to: + +- `.Story()`, `.Epoch()` +- `.RequireEpoch()`, `.UnlockEpochAfter...()` + +Core distinction: + +- **Timeline registration** answers “does this thing exist?” +- **Unlock registration** answers “when does it become available?” + +--- + +::: + +## 两个注册器{lang="zh-CN"} + +::: zh-CN + +| 注册器 | 职责 | +|---|---| +| `ModTimelineRegistry` | 注册 `StoryModel` 和 `EpochModel` | +| `ModUnlockRegistry` | 定义内容或纪元的解锁条件 | + +在链式构建器里,对应: + +- `.Story()`、`.Epoch()` +- `.RequireEpoch()`、`.UnlockEpochAfter...()` + +核心区别: + +- **时间线注册**回答"这个东西是否存在" +- **解锁注册**回答"它什么时候可用" + +--- + +::: + +## Story Registration{lang="en"} + +::: en + +Use `ModStoryTemplate` for the story **type** (slug id from `StoryKey` only). Epoch **order** is not a hard-coded list on the story class; register each epoch against the story in manifest order: + +```csharp +public class MyStory : ModStoryTemplate +{ + protected override string StoryKey => "my-story"; +} + +// Fluent (order = column order): +// .StoryEpoch() +// .StoryEpoch() +// .Story() + +// Or IModContentPackEntry list (same idea as card manifest entries): +// new TimelineColumnPackEntry(c => c.Epoch()...), +// new StoryPackEntry(), +``` + +`ModStoryTemplate` is responsible for: + +- Deriving a normalized story identity from `StoryKey` +- Building `Epochs` from `ModStoryEpochBindings` (filled by `ModTimelineRegistry.RegisterStoryEpoch()`) + +`RegisterStoryEpoch` registers the epoch with vanilla discovery **and** appends it to that story’s column. Use `.Epoch()` only for epochs that are **not** part of a mod story column. + +--- + +::: + +## `Story` 注册{lang="zh-CN"} + +::: zh-CN + +故事类型仍用 `ModStoryTemplate`,只实现 `StoryKey`。栏内 **Epoch 顺序**不要在故事类里写死;按注册顺序把每个 Epoch 绑到该故事: + +```csharp +public class MyStory : ModStoryTemplate +{ + protected override string StoryKey => "my-story"; +} + +// 流式: .StoryEpoch() … .Story() +// 或 IModContentPackEntry: TimelineColumnPackEntry / StoryPackEntry +``` + +`ModStoryTemplate` 的职责: + +- 通过 `StoryKey` 自动生成规范化的故事标识 +- 通过 `ModStoryEpochBindings`(`RegisterStoryEpoch` 写入)组装 `Epochs` + +`RegisterStoryEpoch` 会注册 Epoch 并追加到该故事栏。不属于 mod 故事栏的 Epoch 可继续只用 `.Epoch()`。 + +--- + +::: + +## Epoch Registration{lang="en"} + +::: en + +You can write plain `EpochModel` subclasses, or use RitsuLib template types: + +| Template | Description | +|---|---| +| `CharacterUnlockEpochTemplate` | Epoch that unlocks the character | +| `CardUnlockEpochTemplate` | Epoch that unlocks extra cards | +| `RelicUnlockEpochTemplate` | Epoch that unlocks extra relics | +| `PotionUnlockEpochTemplate` | Epoch that unlocks extra potions | + +These templates mainly handle: + +- Enqueue logic for the timeline unlock UI +- Follow-up epochs via `ExpansionEpochTypes` + +### Character unlock epoch template + +Built-in behavior of `CharacterUnlockEpochTemplate`: + +- Queues a character unlock in `NTimelineScreen` +- Writes the pending character unlock to save progress +- If `ExpansionEpochTypes` is set, queues further epochs into the timeline expansion + +### Card / relic / potion epoch templates + +`CardUnlockEpochTemplate`, `RelicUnlockEpochTemplate`, and `PotionUnlockEpochTemplate` work similarly: + +- You declare the model types to unlock +- The template resolves types through `ModelDb` +- `UnlockText` is generated automatically +- `QueueUnlocks()` pushes into the timeline UI + +--- + +::: + +## `Epoch` 注册{lang="zh-CN"} + +::: zh-CN + +可以直接写原生 `EpochModel` 子类,也可以使用 RitsuLib 提供的模板类型: + +| 模板 | 说明 | +|---|---| +| `CharacterUnlockEpochTemplate` | 解锁角色本身的纪元 | +| `CardUnlockEpochTemplate` | 解锁额外卡牌的纪元 | +| `RelicUnlockEpochTemplate` | 解锁额外遗物的纪元 | +| `PotionUnlockEpochTemplate` | 解锁额外药水的纪元 | + +这些模板主要负责: + +- 生成时间线界面的解锁入队逻辑 +- 通过 `ExpansionEpochTypes` 支持后续纪元展开 + +### 角色解锁纪元模板 + +`CharacterUnlockEpochTemplate` 的内置行为: + +- 向 `NTimelineScreen` 队列一个角色解锁 +- 把待解锁角色写入进度存档 +- 若配置了 `ExpansionEpochTypes`,继续把后续纪元加入时间线展开 + +### 卡牌/遗物/药水纪元模板 + +`CardUnlockEpochTemplate`、`RelicUnlockEpochTemplate`、`PotionUnlockEpochTemplate` 的工作方式相似: + +- 声明要解锁的模型类型 +- 模板通过 `ModelDb` 解析类型 +- `UnlockText` 自动生成 +- `QueueUnlocks()` 自动推入时间线界面 + +--- + +::: + +## Expansion Epochs{lang="en"} + +::: en + +All unlock epoch templates support: + +```csharp +protected virtual IEnumerable ExpansionEpochTypes => []; +``` + +When the current epoch completes, these epochs are added automatically as timeline expansions, which helps chain unlocks: + +1. Unlock the character first +2. Then reveal card unlocks +3. Then reveal relic unlocks + +--- + +::: + +## Expansion Epochs{lang="zh-CN"} + +::: zh-CN + +所有解锁纪元模板都支持: + +```csharp +protected virtual IEnumerable ExpansionEpochTypes => []; +``` + +当前纪元完成时会自动把这些纪元作为时间线扩展加入,用于组织解锁链: + +1. 先解锁角色 +2. 再展开卡牌解锁 +3. 再展开遗物解锁 + +--- + +::: + +## Registration Timing and Freeze{lang="en"} + +::: en + +Both the timeline and unlock registries freeze after early initialization because: + +- Story and epoch identities must stay stable +- Unlock filtering and compatibility patches need a finalized rule set + +Register `Story`, `Epoch`, and unlock rules from your initializer — not later at runtime. + +--- + +::: + +## 注册时机与冻结{lang="zh-CN"} + +::: zh-CN + +时间线和解锁两个注册器都会在早期初始化后冻结。原因是: + +- 故事/纪元标识必须稳定 +- 解锁过滤与兼容补丁需要面对最终确定的规则表 + +`Story`、`Epoch` 和解锁规则都应在初始化入口中注册,不要拖到运行期。 + +--- + +::: + +## Requiring an Epoch for Content{lang="en"} + +::: en + +When a model is registered but should only appear after an epoch is obtained, use `RequireEpoch()`. + +Typical uses: + +- Late-game cards stay out of the pool until progress is met +- Relics open only after a specific story branch +- Shared ancients / events need a timeline milestone + +RitsuLib applies the gate across multiple entry points: + +- `UnlockState.Characters` +- Unlocked card / relic / potion pool queries +- Shared ancient lists +- Events generated for acts + +This is not UI-only filtering; it changes what the game can actually offer. + +### Epoch progress vs. timeline reveal + +Vanilla `UnlockState` built from save progress mainly reflects epochs that have reached **`EpochState.Revealed`** (visible on the timeline) in **`UnlockedEpochs`**. **`SaveManager.ObtainEpoch`** can set **`Obtained`** / **`ObtainedNoSlot`** *before* the timeline slot is revealed. + +**`ModUnlockRegistry.IsUnlocked`** (used when applying **`RequireEpoch`** gating) treats the requirement as satisfied if **either**: + +- the epoch id is in **`unlockState.UnlockedEpochs`**, or +- **`SaveManager.Instance.Progress.IsEpochObtained(epochId)`** is true. + +So pool / character / event gating lines up with mod rules that call **`ObtainEpoch`**, not only with vanilla timeline reveal timing. + +--- + +::: + +## 为内容设置 Epoch 门槛{lang="zh-CN"} + +::: zh-CN + +当模型已注册,但应在某个纪元解锁后才出现时,使用 `RequireEpoch()`。 + +常见用途: + +- 后期卡牌在进度达成前不进入牌池 +- 遗物只在特定故事分支后开放 +- 共享 Ancient / 事件需要时间线进度门槛 + +RitsuLib 将门槛应用到多个访问入口: + +- `UnlockState.Characters` +- 卡牌/遗物/药水的已解锁池查询 +- 共享 Ancient 列表 +- Act 生成出来的事件列表 + +这不是单纯 UI 过滤,而是真正影响游戏可提供内容的规则。 + +### 纪元进度与时间线「揭示」 + +从存档生成的原版 **`UnlockState`** 里,**`UnlockedEpochs`** 主要反映已进入 **`EpochState.Revealed`**(时间线栏位已显示)的纪元。而 **`SaveManager.ObtainEpoch`** 可能先把纪元标成 **`Obtained`** / **`ObtainedNoSlot`**,时间线槽位尚未揭示。 + +应用 **`RequireEpoch`** 门槛时,**`ModUnlockRegistry.IsUnlocked`** 在以下**任一**成立时即视为已满足: + +- 该纪元 id 出现在 **`unlockState.UnlockedEpochs`** 中,或 +- **`SaveManager.Instance.Progress.IsEpochObtained(epochId)`** 为真。 + +这样,牌池 / 角色 / 事件等门槛会与通过 Mod 规则调用 **`ObtainEpoch`** 的进度一致,而不必等到原版时间线 UI 完全跟上。 + +--- + +::: + +## Post-Run Epoch Rules{lang="en"} + +::: en + +Common convenience APIs on `ModUnlockRegistry`: + +| Method | Description | +|---|---| +| `UnlockEpochAfterRunAs()` | Unlock after completing a run with the given character | +| `UnlockEpochAfterWinAs()` | Unlock after a win with that character | +| `UnlockEpochAfterAscensionWin(level)` | Unlock after a win at the given ascension | +| `UnlockEpochAfterRunCount(requiredRuns, requireVictory)` | Unlock after enough runs | + +These all compile to `PostRunEpochUnlockRule`. + +You can also register a custom rule: + +```csharp +unlocks.RegisterPostRunRule( + PostRunEpochUnlockRule.Create( + epochId: new MyEpoch().Id, + description: "Unlock after any abandoned ascension-5 run", + shouldUnlock: ctx => ctx.IsAbandoned && ctx.AscensionLevel >= 5)); +``` + +--- + +::: + +## 局后 Epoch 规则{lang="zh-CN"} + +::: zh-CN + +`ModUnlockRegistry` 提供的常用便捷 API: + +| 方法 | 说明 | +|---|---| +| `UnlockEpochAfterRunAs()` | 使用指定角色完成一局后解锁 | +| `UnlockEpochAfterWinAs()` | 使用指定角色胜利后解锁 | +| `UnlockEpochAfterAscensionWin(level)` | 指定进阶等级胜利后解锁 | +| `UnlockEpochAfterRunCount(requiredRuns, requireVictory)` | 累计跑局次数后解锁 | + +这些最终都转成 `PostRunEpochUnlockRule`。 + +也可以直接注册自定义规则: + +```csharp +unlocks.RegisterPostRunRule( + PostRunEpochUnlockRule.Create( + epochId: new MyEpoch().Id, + description: "在任意一次被放弃的 5 层进阶局后解锁", + shouldUnlock: ctx => ctx.IsAbandoned && ctx.AscensionLevel >= 5)); +``` + +--- + +::: + +## Counted Progression Rules{lang="en"} + +::: en + +| Method | Description | +|---|---| +| `UnlockEpochAfterEliteVictories(count)` | Elite kill count | +| `UnlockEpochAfterBossVictories(count)` | Boss kill count | +| `UnlockEpochAfterAscensionOneWin()` | Ascension 1 win | +| `RevealAscensionAfterEpoch()` | Show ascension after the epoch | +| `UnlockCharacterAfterRunAs()` | Unlock character after using that character | + +--- + +::: + +## 累计进度型规则{lang="zh-CN"} + +::: zh-CN + +| 方法 | 说明 | +|---|---| +| `UnlockEpochAfterEliteVictories(count)` | 精英击杀数 | +| `UnlockEpochAfterBossVictories(count)` | Boss 击杀数 | +| `UnlockEpochAfterAscensionOneWin()` | 进阶 1 胜利 | +| `RevealAscensionAfterEpoch()` | 纪元后显示进阶 | +| `UnlockCharacterAfterRunAs()` | 使用角色后解锁角色 | + +--- + +::: + +## Compatibility Patches{lang="en"} + +::: en + +> This section explains how vanilla progression limits mod characters and how RitsuLib bridges those gaps. + +Several vanilla progression checks assume vanilla characters and do not naturally include mod characters. RitsuLib applies narrow bridge patches so registered unlock rules still apply at those checkpoints: + +- Elite kill count → epoch checks +- Boss kill count → epoch checks +- Ascension 1 → epoch checks +- Post-run character-unlock epochs +- Ascension reveal unlock checks + +These patches do not replace vanilla progression; they only add a bridge where vanilla would skip mod characters. That is why the unlock registry stores rules explicitly by `ModelId` instead of inferring all progression from the timeline graph alone. + +--- + +::: + +## 兼容补丁{lang="zh-CN"} + +::: zh-CN + +> 以下解释原版进度系统对 Mod 角色的局限性,以及 RitsuLib 的桥接策略。 + +原版的若干进度检查是按原版角色设计的,不会自然支持 Mod 角色。RitsuLib 通过以下桥接补丁,让注册的解锁规则在这些检查点上生效: + +- 精英击杀计数的纪元判定桥接 +- Boss 击杀计数的纪元判定桥接 +- 进阶 1 的纪元判定桥接 +- 局后角色解锁纪元桥接 +- 进阶显示解锁判定桥接 + +这些补丁并不重写原版进度系统,只是在原版会跳过 Mod 角色的节点上补一层桥。这也是为什么解锁注册器会显式按 `ModelId` 保存规则,而不是试图仅从时间线图推断全部进度逻辑。 + +--- + +::: + +## Recommended Pattern{lang="en"} + +::: en + +For a story-driven character mod: + +1. Register character, pools, epochs, and story in one content pack +2. Use `CharacterUnlockEpochTemplate` for the character unlock epoch +3. Use card / relic / potion epoch templates for follow-up content +4. Use `RequireEpoch()` for late-game gates +5. Prefer a small set of clear progression rules over many overlapping ones + +--- + +::: + +## 推荐模式{lang="zh-CN"} + +::: zh-CN + +对故事驱动型角色 Mod: + +1. 在一个内容包里注册角色、池、纪元和故事 +2. 用 `CharacterUnlockEpochTemplate` 作为角色解锁纪元 +3. 用卡牌/遗物/药水纪元模板做后续内容展开 +4. 用 `RequireEpoch()` 给后期内容加门槛 +5. 使用少量清晰的进度规则,而不是堆叠重叠规则 + +--- + +::: + +## Builder Example{lang="en"} + +::: en + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + .Character() + .Card() + .Relic() + .Epoch() + .Epoch() + .Story() + .RequireEpoch() + .RequireEpoch() + .UnlockEpochAfterWinAs() + .UnlockEpochAfterAscensionWin(10) + .Apply(); +``` + +--- + +::: + +## 构建器示例{lang="zh-CN"} + +::: zh-CN + +```csharp +RitsuLibFramework.CreateContentPack("MyMod") + .Character() + .Card() + .Relic() + .Epoch() + .Epoch() + .Story() + .RequireEpoch() + .RequireEpoch() + .UnlockEpochAfterWinAs() + .UnlockEpochAfterAscensionWin(10) + .Apply(); +``` + +--- + +::: + +## Common Mistakes{lang="en"} + +::: en + +- Registering epochs but forgetting the story that lists those epochs +- Registering story/epochs after the timeline has frozen +- Using `RequireEpoch` without any rule that can actually unlock that epoch +- Stacking many overlapping rules for the same epoch without a clear design +- Assuming vanilla counted progression works for mod characters without registering RitsuLib unlock rules +- Leaving **`UnlocksAfterRunAsType`** at the default on a mod character while **`unlockText`** uses **`{Prerequisite}`** — the character-select hover then shows the generic locked title (often **`???`**). Set **`UnlocksAfterRunAsType`** to the same prerequisite character type as in **`UnlockEpochAfterWinAs`** / **`UnlockEpochAfterRunAs<…>`** (see [Character & Unlock Templates](/guide/character-and-unlock-scaffolding)) + +--- + +::: + +## 常见错误{lang="zh-CN"} + +::: zh-CN + +- 注册了纪元,却忘了注册包含这些纪元的故事 +- 在时间线冻结之后才注册故事/纪元 +- 给内容设置了 `RequireEpoch`,却没有任何规则能真正解锁该纪元 +- 对同一个纪元叠很多重叠解锁规则,却没有明确设计理由 +- 误以为原版累计进度逻辑会自动兼容 Mod 角色,而没有注册 RitsuLib 解锁规则 +- Mod 角色的 **`unlockText`** 里用了 **`{Prerequisite}`**,却未覆盖 **`UnlocksAfterRunAsType`**(默认为 `null`)——选人界面悬停说明里的前置名会变成通用锁定标题(常显示为 **`???`**)。应将其设为与 **`UnlockEpochAfterWinAs`** / **`UnlockEpochAfterRunAs<…>`** 中 **`TCharacter`** 一致的前置角色类型(详见 [角色与解锁模板](/guide/character-and-unlock-scaffolding)) + +--- + +::: + +## Related Documents{lang="en"} + +::: en + +- [Character & Unlock Templates](/guide/character-and-unlock-scaffolding) +- [Content Packs & Registries](/guide/content-packs-and-registries) +- [Diagnostics & Compatibility](/guide/diagnostics-and-compatibility) +- [Framework Design](/guide/framework-design) + +::: + +## 相关文档{lang="zh-CN"} + +::: zh-CN + +- [角色与解锁模板](/guide/character-and-unlock-scaffolding) +- [内容包与注册器](/guide/content-packs-and-registries) +- [诊断与兼容层](/guide/diagnostics-and-compatibility) +- [框架设计](/guide/framework-design) + +::: diff --git a/docs/pages/index.md b/docs/pages/index.md new file mode 100644 index 0000000..52f4c9d --- /dev/null +++ b/docs/pages/index.md @@ -0,0 +1,71 @@ +--- +title: + en: RitsuLib + zh-CN: RitsuLib + +features: + title: + en: Overview + zh-CN: 概览 + subtitle: + en: Slay the Spire 2 + zh-CN: 杀戮尖塔 2 + text: + en: >- + A shared mod framework for Slay the Spire 2: registry-driven content, Harmony patching helpers, persistence stores, + lifecycle events, localization (`I18N` / keywords), Godot scene registration, FMOD helpers, and diagnostics that + align with vanilla progression checks. + zh-CN: >- + 面向《杀戮尖塔 2》的共享模组框架:注册器驱动的内容、Harmony 补丁封装、持久化存储与迁移、生命周期事件、 + 本地化(`I18N` / 关键词)、Godot 场景注册、FMOD 辅助接口以及与原版进度节点对齐的诊断与兼容层。 + + cards: + - title: + en: Content & registries + zh-CN: 内容与注册器 + details: + en: >- + Fixed identity, content packs, character/unlock scaffolding, custom events, timelines, and placeholder content rules. + zh-CN: >- + 固定身份、内容包、角色与解锁装配、自定义事件、时间线以及占位内容等注册与约束 + - title: + en: Patching & lifecycle + zh-CN: 补丁与生命周期 + details: + en: >- + `ModPatcher`, `IPatchMethod`, grouped targets, plus lifecycle subscriptions and replay semantics for engine events. + zh-CN: >- + `ModPatcher`、`IPatchMethod`、分组目标,以及针对引擎事件的订阅与可重放语义 + - title: + en: Persistence & settings + zh-CN: 持久化与设置 + details: + en: >- + Scoped mod data stores with migrations and profile switching; optional settings UI bound to `ModDataStore`. + zh-CN: >- + 带迁移与档位切换的作用域存储;可选的设置界面并与 `ModDataStore` 绑定 + - title: + en: Localization & audio + zh-CN: 本地化与音频 + details: + en: >- + `I18N`, keyword registry, LocString tooling, and FMOD Studio path → GUID mapping helpers on top of the vanilla audio pipeline. + zh-CN: >- + `I18N`、关键词注册、LocString 工具,以及在原版音频管线之上提供 FMOD Studio 路径映射等能力 + - title: + en: Godot & diagnostics + zh-CN: Godot 与诊断 + details: + en: >- + Scene script registration, asset profile fallbacks, narrow compatibility patches, and one-time diagnostic warnings. + zh-CN: >- + 场景脚本注册、资源配置回退、窄兼容补丁以及一次性诊断警告策略 + - title: + en: Documentation + zh-CN: 文档 + details: + en: >- + Guides are maintained as Markdown under `docs/pages/guide/` in this repository and built with Valaxy. + zh-CN: >- + 指南文档在仓库 `docs/pages/guide/` 中维护,并由 Valaxy 构建为站点 +--- diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 0000000..6ee60da --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,7833 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + valaxy: + specifier: ^0.28.4 + version: 0.28.5(@babel/parser@7.29.2)(@types/markdown-it@14.1.2)(@types/node@25.6.0)(@vue/compiler-dom@3.5.32)(@vue/compiler-sfc@3.5.32)(axios@1.15.1)(eslint@10.2.1(jiti@2.6.1))(postcss@8.5.10)(terser@5.46.1)(typescript@6.0.3)(yaml@2.8.3) + valaxy-theme-nova: + specifier: ^0.3.1 + version: 0.3.1(markdown-it@14.1.1)(valaxy@0.28.5(@babel/parser@7.29.2)(@types/markdown-it@14.1.2)(@types/node@25.6.0)(@vue/compiler-dom@3.5.32)(@vue/compiler-sfc@3.5.32)(axios@1.15.1)(eslint@10.2.1(jiti@2.6.1))(postcss@8.5.10)(terser@5.46.1)(typescript@6.0.3)(yaml@2.8.3)) + devDependencies: + typescript: + specifier: ^6.0.2 + version: 6.0.3 + +packages: + + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@antfu/utils@0.7.10': + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + + '@antfu/utils@9.3.0': + resolution: {integrity: sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==} + + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@8.0.0-rc.3': + resolution: {integrity: sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@8.0.0-rc.3': + resolution: {integrity: sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@8.0.0-rc.3': + resolution: {integrity: sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.7': + resolution: {integrity: sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@8.0.0-rc.3': + resolution: {integrity: sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + '@babel/plugin-proposal-decorators@7.29.0': + resolution: {integrity: sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.28.6': + resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.7': + resolution: {integrity: sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@babel/types@8.0.0-rc.3': + resolution: {integrity: sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@chevrotain/cst-dts-gen@12.0.0': + resolution: {integrity: sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==} + + '@chevrotain/gast@12.0.0': + resolution: {integrity: sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==} + + '@chevrotain/regexp-to-ast@12.0.0': + resolution: {integrity: sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==} + + '@chevrotain/types@12.0.0': + resolution: {integrity: sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==} + + '@chevrotain/utils@12.0.0': + resolution: {integrity: sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==} + + '@clack/core@1.2.0': + resolution: {integrity: sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==} + + '@clack/prompts@1.2.0': + resolution: {integrity: sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==} + + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.5.5': + resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.1': + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.1.1': + resolution: {integrity: sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@iconify-json/f7@1.2.2': + resolution: {integrity: sha512-7+RhvnncD+LRu0smuHOun4emOUu48XUGG6G0tYYbrXS7n4Io2xntx6nxa09/iI41t9hnH2oHHU3Dg7Ee60Ovow==} + + '@iconify-json/logos@1.2.11': + resolution: {integrity: sha512-fOo4pGEatuyuCFNL+cwquYMa2Im0oJHRHV7lt/Qqs5Ode/lPImHCQcfTtPzZj7qYMPb/h8YHN3TG54uEowrjNQ==} + + '@iconify-json/ri@1.2.10': + resolution: {integrity: sha512-WWMhoncVVM+Xmu9T5fgu2lhYRrKTEWhKk3Com0KiM111EeEsRLiASjpsFKnC/SrB6covhUp95r2mH8tGxhgd5Q==} + + '@iconify-json/tabler@1.2.33': + resolution: {integrity: sha512-q9nUQfE/cjIrGh5bAKHTphitAZpT0kX9SxDgZo3Sx8ofeDTsaHVdRwrn+CfKiJ5vQ1b1btqVwizXzIgz9KEPjA==} + + '@iconify-json/vscode-icons@1.2.45': + resolution: {integrity: sha512-ow+ueibMIq79ueM1kv6cOWgHx8jfh1XJQi2RrqMHb4HLbvIBlxpy5PCMvOJXlA68R6fBAHpWQeh6uWx7VKEVsA==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + + '@intlify/bundle-utils@11.0.7': + resolution: {integrity: sha512-fEO3CJGPymxieGh8BHox7d6stgajDQae7wgpH6YYw7WX+cdW6jTTXyljZqz7OV3JcwlS9M9UHSoO+YwiO56IhA==} + engines: {node: '>= 20'} + peerDependencies: + petite-vue-i18n: '*' + vue-i18n: '*' + peerDependenciesMeta: + petite-vue-i18n: + optional: true + vue-i18n: + optional: true + + '@intlify/core-base@11.3.2': + resolution: {integrity: sha512-cgsUaV/dyD6aS49UPgerIblrWeXAZHNaDWqm4LujOGC7IafSyhghGXEiSVvuDYaDPiQTP+tSFSTM1HIu7Yp1nA==} + engines: {node: '>= 16'} + + '@intlify/devtools-types@11.3.2': + resolution: {integrity: sha512-q96G2ZZw0FNoXzejbjIf9dbfgz1xyYBZu6ZT4b5TE/55j8d1O9X5jv0k+U+L3fVe7uebPcqRQFD0ffm30i5mJA==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@11.3.2': + resolution: {integrity: sha512-d/awyHUkNSaGPxBxT/qlUpfRizxHX9dt55CnW03xx5p1KmMyfYHKupCnvzINX+Na8JR8LAR7y32lPKjoeQGmzA==} + engines: {node: '>= 16'} + + '@intlify/shared@11.3.2': + resolution: {integrity: sha512-x66fjdH6i+lNYPae5URSQGTjBL68Av6hi09jvC5Ci96iTkwfqrPhCj46aylQZmgMaG89rOZCIKqS7ApC8ZDVjg==} + engines: {node: '>= 16'} + + '@intlify/unplugin-vue-i18n@11.0.7': + resolution: {integrity: sha512-wswKprS1D8VfnxxVhKxug5wa3MbDSOcCoXOBjnzhMK+6NfP6h6UI8pFqSBIvcW8nPDuzweTc0Sk3PeBCcubfoQ==} + engines: {node: '>= 20'} + peerDependencies: + petite-vue-i18n: '*' + vue: ^3.2.25 + vue-i18n: '*' + peerDependenciesMeta: + petite-vue-i18n: + optional: true + vue-i18n: + optional: true + + '@intlify/vue-i18n-extensions@8.0.0': + resolution: {integrity: sha512-w0+70CvTmuqbskWfzeYhn0IXxllr6mU+IeM2MU0M+j9OW64jkrvqY+pYFWrUnIIC9bEdij3NICruicwd5EgUuQ==} + engines: {node: '>= 18'} + peerDependencies: + '@intlify/shared': ^9.0.0 || ^10.0.0 || ^11.0.0 + '@vue/compiler-dom': ^3.0.0 + vue: ^3.0.0 + vue-i18n: ^9.0.0 || ^10.0.0 || ^11.0.0 + peerDependenciesMeta: + '@intlify/shared': + optional: true + '@vue/compiler-dom': + optional: true + vue: + optional: true + vue-i18n: + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + + '@kwsites/promise-deferred@1.1.1': + resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + + '@mdit-vue/plugin-component@3.0.2': + resolution: {integrity: sha512-Fu53MajrZMOAjOIPGMTdTXgHLgGU9KwTqKtYc6WNYtFZNKw04euSfJ/zFg8eBY/2MlciVngkF7Gyc2IL7e8Bsw==} + engines: {node: '>=20.0.0'} + + '@mdit-vue/plugin-frontmatter@3.0.2': + resolution: {integrity: sha512-QKKgIva31YtqHgSAz7S7hRcL7cHXiqdog4wxTfxeQCHo+9IP4Oi5/r1Y5E93nTPccpadDWzAwr3A0F+kAEnsVQ==} + engines: {node: '>=20.0.0'} + + '@mdit-vue/types@3.0.2': + resolution: {integrity: sha512-00aAZ0F0NLik6I6Yba2emGbHLxv+QYrPH00qQ5dFKXlAo1Ll2RHDXwY7nN2WAfrx2pP+WrvSRFTGFCNGdzBDHw==} + engines: {node: '>=20.0.0'} + + '@mermaid-js/parser@1.1.0': + resolution: {integrity: sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@octokit/auth-token@6.0.0': + resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} + engines: {node: '>= 20'} + + '@octokit/core@7.0.6': + resolution: {integrity: sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==} + engines: {node: '>= 20'} + + '@octokit/endpoint@11.0.3': + resolution: {integrity: sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==} + engines: {node: '>= 20'} + + '@octokit/graphql@9.0.3': + resolution: {integrity: sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==} + engines: {node: '>= 20'} + + '@octokit/openapi-types@27.0.0': + resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==} + + '@octokit/plugin-paginate-rest@14.0.0': + resolution: {integrity: sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@6.0.0': + resolution: {integrity: sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@17.0.0': + resolution: {integrity: sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@7.1.0': + resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==} + engines: {node: '>= 20'} + + '@octokit/request@10.0.8': + resolution: {integrity: sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==} + engines: {node: '>= 20'} + + '@octokit/rest@22.0.1': + resolution: {integrity: sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==} + engines: {node: '>= 20'} + + '@octokit/types@16.0.0': + resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + + '@oxc-project/types@0.126.0': + resolution: {integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==} + + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rolldown/binding-android-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.16': + resolution: {integrity: sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': + resolution: {integrity: sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': + resolution: {integrity: sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': + resolution: {integrity: sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': + resolution: {integrity: sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': + resolution: {integrity: sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': + resolution: {integrity: sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': + resolution: {integrity: sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.13': + resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==} + + '@rolldown/pluginutils@1.0.0-rc.16': + resolution: {integrity: sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==} + + '@rollup/plugin-virtual@3.0.2': + resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + + '@shikijs/core@3.23.0': + resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} + + '@shikijs/engine-javascript@3.23.0': + resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} + + '@shikijs/engine-oniguruma@3.23.0': + resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + + '@shikijs/langs@3.23.0': + resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + + '@shikijs/themes@3.23.0': + resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + + '@shikijs/transformers@3.23.0': + resolution: {integrity: sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==} + + '@shikijs/types@3.23.0': + resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@simple-git/args-pathspec@1.0.3': + resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==} + + '@simple-git/argv-parser@1.1.1': + resolution: {integrity: sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/http-proxy@1.17.17': + resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==} + + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/katex@0.16.8': + resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@typescript-eslint/project-service@8.59.0': + resolution: {integrity: sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.0': + resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.0': + resolution: {integrity: sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.0': + resolution: {integrity: sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.0': + resolution: {integrity: sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.0': + resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unhead/addons@2.1.13': + resolution: {integrity: sha512-xiM5ERU68FEuiBCCiPZ1EDkja+kH4hKKot/7dNJufneACtGoAFWnKUcmj/iB9BKjVwgBBF3sFYO3qXjkNFXWxA==} + peerDependencies: + unhead: ^2.1.13 + + '@unhead/dom@2.1.13': + resolution: {integrity: sha512-Owct50klBgE/YhttekuAIg/wDNVvHWHekpCv8l9JMqnSWc+VP0p3164qtrG2NbJElMLQJV582gdWPaU/mYokFQ==} + peerDependencies: + unhead: 2.1.13 + + '@unhead/schema-org@2.1.13': + resolution: {integrity: sha512-D9458jeB20lgLRgZxiTGi/iSbqP/pPsD2klGO5P2PlHOFxvejh9RUAhsz6LILac28Z6lFZPY4hs3mpW3batUtA==} + peerDependencies: + '@unhead/react': ^2.1.13 + '@unhead/solid-js': ^2.1.13 + '@unhead/svelte': ^2.1.13 + '@unhead/vue': ^2.1.13 + peerDependenciesMeta: + '@unhead/react': + optional: true + '@unhead/solid-js': + optional: true + '@unhead/svelte': + optional: true + '@unhead/vue': + optional: true + + '@unhead/vue@2.1.13': + resolution: {integrity: sha512-HYy0shaHRnLNW9r85gppO8IiGz0ONWVV3zGdlT8CQ0tbTwixznJCIiyqV4BSV1aIF1jJIye0pd1p/k6Eab8Z/A==} + peerDependencies: + vue: '>=3.5.18' + + '@unocss/astro@66.5.10': + resolution: {integrity: sha512-R1UU8lfIqcuorGpiuU+9pQEmK8uBBk1sf5re1db9kr23924Ia/aBCmfs4W2xyVCwJ0cGBv9C3ywDgOsgkHFCbQ==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + peerDependenciesMeta: + vite: + optional: true + + '@unocss/cli@66.5.10': + resolution: {integrity: sha512-3tGBTGLLTtwGEwXGWsL77K4bTvNG115VJvYPPit68Z7uXnA6S8xpkwaFFDJ3kbrsWtgXBpIgM06HhtT6/3MILg==} + engines: {node: '>=14'} + hasBin: true + + '@unocss/config@66.5.10': + resolution: {integrity: sha512-udBhfMe+2MU70ZdjnRLnwLQ+0EHYJ4f5JjjvHsfmQ0If4KeYmSStWBuX+/LHNQidhl487JiwW1lBDQ8pKHmbiw==} + engines: {node: '>=14'} + + '@unocss/core@66.5.10': + resolution: {integrity: sha512-SEmPE4pWNn9VcCvZqovPwFGuG/j69W3zh+x1Ky4z/I2pnyoB0Y0lBmq22KVu/dwExe+ZKKTQpxa0j5rbE27rDQ==} + + '@unocss/extractor-arbitrary-variants@66.5.10': + resolution: {integrity: sha512-9JsAY1a68WZaIbSiwQa7LLAO+t4T5nnhgmNxY3MGaK58k6Qa9ayZb4AG4fqOpw+Zn8tmKd7yXJ0s+27sx1n2BA==} + + '@unocss/inspector@66.5.10': + resolution: {integrity: sha512-L/Nvi4bkXFxbGNOi7TPNnIIDfY1zKghfJ+cF7To/WrXplP1Y4nEZa2kGwcVBcsaysACri0whU19Dh3yf+bG+Pg==} + + '@unocss/postcss@66.5.10': + resolution: {integrity: sha512-Hp9k+1AB0qxc6b7Sh7JPKwYgcklIvRhleYtQldFbdU5eAY5InOy9m7gSZxRsz2WQb6IzliqO7Or34PbhnMlcFQ==} + engines: {node: '>=14'} + peerDependencies: + postcss: ^8.4.21 + + '@unocss/preset-attributify@66.5.10': + resolution: {integrity: sha512-dEFs8kXC9xoqolQBFvtgXvdzWQqHoWqSj/eosX2oDmy8REk7UErpBvMmqR4pCP7mqdtG8yZ2l34Gtb42hDM3JA==} + + '@unocss/preset-icons@66.5.10': + resolution: {integrity: sha512-zf4Sev/F2QQgVjGjKBCw3BKc15HQAtvUrNX2zymXXbAjt83Lf27ofYzTAUVUO9mi/oQhXcP5sQrIGIe7iQX3hw==} + + '@unocss/preset-mini@66.5.10': + resolution: {integrity: sha512-jRmweaPhaTGBSDKFuhEGayGyuGr66rTRRqzv5EAdHH4x43TFlJ1RO5SVlzzJdo1zJy4vyGSINIVKeI49FYhEKQ==} + + '@unocss/preset-tagify@66.5.10': + resolution: {integrity: sha512-SLfMhNQCFEXspp/zREZv61dmuvRQ+CVI04zcpGpg4LnqvMKkLVyPPetlhgJwW1hd9D7OWkUGoQm9JA0O4+9XJA==} + + '@unocss/preset-typography@66.5.10': + resolution: {integrity: sha512-GMchTwywSA6vwiZ2w8svBY9U9br/OW7vIjwyYis0c9kp4h8apKCrLtAv2LjmlKyg12IDy9d8jp/hZ1zP9umung==} + + '@unocss/preset-uno@66.5.10': + resolution: {integrity: sha512-O3R99td+Jt3XAJh1pVbOSTu3z7jUosg80y90iu6JQIpvXI/pGanWJEhoEz95SgJmRV+vXNEn4f6tIvfUXkTd/w==} + + '@unocss/preset-web-fonts@66.5.10': + resolution: {integrity: sha512-rA9pjL+CuDpyEekawX54pkWHc4n+kfhoYsAFBWBtNHl4akDYsbnSA+2EF/XiEbRvz1YVFYDucZ9KpUiaq9+xtQ==} + + '@unocss/preset-wind3@66.5.10': + resolution: {integrity: sha512-N2Wgu+AnTSr4jIEAfajOfUtwESE/Zzr0GxwW88+MHIw6Tzj6tZeCEKNNKFzsgwfGkoNjvwIeIbkaIrIGJ7SveA==} + + '@unocss/preset-wind4@66.5.10': + resolution: {integrity: sha512-PXLxEcYJUsysQvK4xj3iA7plvq5RcAt9S1vLlOmBtl2X66dWU6XqiGEu7lLfqoypip1bPCOGlRB7HbfMuQpftQ==} + + '@unocss/preset-wind@66.5.10': + resolution: {integrity: sha512-tR8JaXHnL006qcIEbD4lalZoqvW78SE+OvD7Sv5yj6s5FjwLZTiaJP8/0RTlx8SvhM6bw+NDxKQq678ntiZdiA==} + + '@unocss/reset@66.5.10': + resolution: {integrity: sha512-xlydsCqbmVtA8QbVWv8+R66v4MJzeDXYsdoGDz7xsa2r65RD4UvJFZuyueY7+/bhzns9QhNOxltEiPi06j3Gvw==} + + '@unocss/rule-utils@66.5.10': + resolution: {integrity: sha512-497GPWZpArNG25cto0Yq3/Yw+i0x7/N/ySq1HHeE3lB43sdmCv6+m6QEv14I/9/e5WJhQOmrY5LmHZYXC7xxMw==} + engines: {node: '>=14'} + + '@unocss/transformer-attributify-jsx@66.5.10': + resolution: {integrity: sha512-WAAVWWx/BVQ9dk1W9FCP7UL9dLScmNDrRwBRah5WJMtKaV890RaL4wLItfQH0SN31C+quTwuaU0Hi6BiBsc9qw==} + + '@unocss/transformer-compile-class@66.5.10': + resolution: {integrity: sha512-NFXf5qTVJXZNnZTpnCSQmNwJhQrmCQv/tgmX69rwNDYKmYcBufpaKfwKzO+EkVQz4A6ySv09Q9PaNBCH5N0FTQ==} + + '@unocss/transformer-directives@66.5.10': + resolution: {integrity: sha512-EDak3DGW+rSYjoZNwU8xJIXbwif+q9e3cjhCZy48ll1nfyg2E1Znqtwv/X8vLRr8fJ0gWn75P2uGi4jfGLZzMg==} + + '@unocss/transformer-variant-group@66.5.10': + resolution: {integrity: sha512-9DWi9bLOGwdw6whCTdywVD9+lA5lkeqcgy9sMoizfUa4CfT1bSdMT27VoAbYhxeEznV92BCW2jCYt0I8M00phw==} + + '@unocss/vite@66.5.10': + resolution: {integrity: sha512-GegFDmcWe0V2CR/uN1f+iQuDh2R1vA6EAwSvl1nyL+6ue0/zLyF9yhdVnypIVlJnS6RK/xaLPOP6vWJnqRGhZg==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + + '@valaxyjs/devtools@0.28.5': + resolution: {integrity: sha512-8w9hpTQ3zXykSLhbLUvuIOt/0SEkf68Zuh9wcGBSjFkqNVn6OYiKUdOHCg+UMRNjWHLHAyKsIVkUOTrRRlFUCQ==} + + '@valaxyjs/utils@0.28.5': + resolution: {integrity: sha512-ortle+/bgY8AugR0NTLbgHtLkhUYtBAUw2CEJ0/RhF1Pc3UQo4ADqUoJPtQJAbhaZX5O8i1hYwZpNnP46+zj0g==} + + '@vitejs/plugin-vue@6.0.6': + resolution: {integrity: sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + + '@vue-macros/common@3.1.2': + resolution: {integrity: sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==} + engines: {node: '>=20.19.0'} + peerDependencies: + vue: ^2.7.0 || ^3.2.25 + peerDependenciesMeta: + vue: + optional: true + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.22': + resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==} + + '@vue/compiler-core@3.5.32': + resolution: {integrity: sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==} + + '@vue/compiler-dom@3.5.22': + resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==} + + '@vue/compiler-dom@3.5.32': + resolution: {integrity: sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==} + + '@vue/compiler-sfc@3.5.22': + resolution: {integrity: sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==} + + '@vue/compiler-sfc@3.5.32': + resolution: {integrity: sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==} + + '@vue/compiler-ssr@3.5.22': + resolution: {integrity: sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==} + + '@vue/compiler-ssr@3.5.32': + resolution: {integrity: sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.2': + resolution: {integrity: sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA==} + + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-api@8.1.1': + resolution: {integrity: sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==} + + '@vue/devtools-core@8.1.1': + resolution: {integrity: sha512-bCCsSABp1/ot4j8xJEycM6Mtt2wbuucfByr6hMgjbYhrtlscOJypZKvy8f1FyWLYrLTchB5Qz216Lm92wfbq0A==} + peerDependencies: + vue: ^3.0.0 + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-kit@8.1.1': + resolution: {integrity: sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/devtools-shared@8.1.1': + resolution: {integrity: sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==} + + '@vue/reactivity@3.5.22': + resolution: {integrity: sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==} + + '@vue/runtime-core@3.5.22': + resolution: {integrity: sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==} + + '@vue/runtime-dom@3.5.22': + resolution: {integrity: sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==} + + '@vue/server-renderer@3.5.22': + resolution: {integrity: sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==} + peerDependencies: + vue: 3.5.22 + + '@vue/shared@3.5.22': + resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} + + '@vue/shared@3.5.32': + resolution: {integrity: sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==} + + '@vueuse/core@14.2.1': + resolution: {integrity: sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/integrations@14.2.1': + resolution: {integrity: sha512-2LIUpBi/67PoXJGqSDQUF0pgQWpNHh7beiA+KG2AbybcNm+pTGWT6oPGlBgUoDWmYwfeQqM/uzOHqcILpKL7nA==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 || ^8 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 || ^8 + vue: ^3.5.0 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@14.2.1': + resolution: {integrity: sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==} + + '@vueuse/shared@14.2.1': + resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==} + peerDependencies: + vue: ^3.5.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} + + ast-kit@3.0.0-beta.1: + resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==} + engines: {node: '>=20.19.0'} + + ast-walker-scope@0.8.3: + resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} + engines: {node: '>=20.19.0'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.15.1: + resolution: {integrity: sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.20: + resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + beasties@0.4.2: + resolution: {integrity: sha512-NvcGjG/7AVUAfRbvrJmHunDQS9uHnE6Q/7AkaPr8oKE8HjOlpjRG5075z/th2Tmlezk3VlaaS8+X9I1RwHJMQw==} + engines: {node: '>=18.0.0'} + + before-after-hook@4.0.0: + resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + blueimp-md5@2.19.0: + resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001788: + resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + + chevrotain-allstar@0.4.1: + resolution: {integrity: sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==} + peerDependencies: + chevrotain: ^12.0.0 + + chevrotain@12.0.0: + resolution: {integrity: sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==} + engines: {node: '>=22.0.0'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@3.4.0: + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} + engines: {node: '>=18.20'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + + css-i18n@0.0.5: + resolution: {integrity: sha512-iIBukKoe2xPzecUUEeIsTp7jr78A55+BKMgM18esXni4i3J4//qadHlAyvkgO6/4VsAa+eLF8uOMqlhvGNSjfg==} + + css-select@6.0.0: + resolution: {integrity: sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@7.0.0: + resolution: {integrity: sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==} + engines: {node: '>= 6'} + + cssstyle@6.2.0: + resolution: {integrity: sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==} + engines: {node: '>=20'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.33.2: + resolution: {integrity: sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-config-ts@0.1.3: + resolution: {integrity: sha512-fPalhdQj/p7qPIieyP5wbCo9a+EQ0ggeL6a/EDdbLe0Px/NCPK8gt2eaHKq4y//EtTMlOQwcLVXEptdfZeVbGg==} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + dompurify@3.4.0: + resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@5.0.2: + resolution: {integrity: sha512-IpbUaI/CAW86l3f+T8zN0iggSc0LmMZLcIW5eRVStLVNCoTXkE0YlncbbH50fp8Cl6zHIky0sW2uUbhBqGw0Jw==} + engines: {node: '>=0.12.18'} + hasBin: true + + electron-to-chromium@1.5.340: + resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==} + + email-validator@2.0.4: + resolution: {integrity: sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==} + engines: {node: '>4.0'} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.2.1: + resolution: {integrity: sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + fast-content-type-parse@3.0.0: + resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-string-truncated-width@1.2.1: + resolution: {integrity: sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==} + + fast-string-width@1.1.0: + resolution: {integrity: sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fast-wrap-ansi@0.1.6: + resolution: {integrity: sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + feed@5.2.1: + resolution: {integrity: sha512-jTynzYPWs9ALjro0GW8j7sv9y7cJBeOdD4Y88kVqYy/eyusIX3g+499JiTDIlD9Ge/unebx57T4Uzo6vpYvMtA==} + engines: {node: '>=20', pnpm: '>=10'} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + floating-vue@5.2.2: + resolution: {integrity: sha512-afW+h2CFafo+7Y9Lvw/xsqjaQlKLdJV7h1fCHfcYQ1C4SVMlu7OAekqWgu5d4SgvkBVU0pVpLlVsrSTBURFRkg==} + peerDependencies: + '@nuxt/kit': ^3.2.0 + vue: ^3.2.0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + fuse.js@7.3.0: + resolution: {integrity: sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==} + engines: {node: '>=10'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gravatar@1.8.2: + resolution: {integrity: sha512-GdRwLM3oYpFQKy47MKuluw9hZ2gaCtiKPbDGdcDEuYDKlc8eNnW27KYL9LVbIDzEsx88WtDWQm2ClBcsgBnj6w==} + engines: {node: '>=10'} + hasBin: true + + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + gsap@3.15.0: + resolution: {integrity: sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==} + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + hookable@6.1.1: + resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + html-minifier-terser@7.2.0: + resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + html5parser@2.0.2: + resolution: {integrity: sha512-L0y+IdTVxHsovmye8MBtFgBvWZnq1C9WnI/SmJszxoQjmUH1psX2uzDk21O5k5et6udxdGjwxkbmT9eVRoG05w==} + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http-proxy-middleware@3.0.5: + resolution: {integrity: sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + immutable@5.1.5: + resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsdom@28.1.0: + resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-with-bigint@3.5.8: + resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-eslint-parser@2.4.2: + resolution: {integrity: sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + katex@0.16.45: + resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + langium@4.2.2: + resolution: {integrity: sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==} + engines: {node: '>=20.10.0', npm: '>=10.2.3'} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string-ast@1.0.3: + resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} + engines: {node: '>=20.19.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + + markdown-exit@1.0.0-beta.9: + resolution: {integrity: sha512-5tzrMKMF367amyBly131vm6eGuWRL2DjBqWaFmPzPbLyuxP0XOmyyyroOAIXuBAMF/3kZbbfqOxvW/SotqKqbQ==} + + markdown-it-anchor@9.2.0: + resolution: {integrity: sha512-sa2ErMQ6kKOA4l31gLGYliFQrMKkqSO0ZJgGhDHKijPf0pNFM9vghjAh3gn26pS4JDRs7Iwa9S36gxm3vgZTzg==} + peerDependencies: + '@types/markdown-it': '*' + markdown-it: '*' + + markdown-it-async@2.2.0: + resolution: {integrity: sha512-sITME+kf799vMeO/ww/CjH6q+c05f6TLpn6VOmmWCGNqPJzSh+uFgZoMB9s0plNtW6afy63qglNAC3MhrhP/gg==} + + markdown-it-attrs@4.3.1: + resolution: {integrity: sha512-/ko6cba+H6gdZ0DOw7BbNMZtfuJTRp9g/IrGIuz8lYc/EfnmWRpaR3CFPnNbVz0LDvF8Gf1hFGPqrQqq7De0rg==} + engines: {node: '>=6'} + peerDependencies: + markdown-it: '>= 9.0.0' + + markdown-it-container@4.0.0: + resolution: {integrity: sha512-HaNccxUH0l7BNGYbFbjmGpf5aLHAMTinqRZQAEQbMr2cdD3z91Q6kIo1oUn1CQndkT03jat6ckrdRYuwwqLlQw==} + + markdown-it-emoji@3.0.0: + resolution: {integrity: sha512-+rUD93bXHubA4arpEZO3q80so0qgoFJEKRkRbjKX8RTdca89v2kfyF+xR3i2sQTwql9tpPZPOQN5B+PunspXRg==} + + markdown-it-footnote@4.0.0: + resolution: {integrity: sha512-WYJ7urf+khJYl3DqofQpYfEYkZKbmXmwxQV8c8mO/hGIhgZ1wOe7R4HLFNwqx7TjILbnC98fuyeSsin19JdFcQ==} + + markdown-it-github-alerts@1.0.1: + resolution: {integrity: sha512-NNATF4QdoGI07hyCitoB2YqJ1YcNVCKT89ut2VtfFY9rkeFCXe/V2lOonKQLpJiq5DjiZZepf97BJx5xOjFIAw==} + peerDependencies: + markdown-it: '>= 13.0.0' + + markdown-it-image-figures@2.1.1: + resolution: {integrity: sha512-mwXSQ2nPeVUzCMIE3HlLvjRioopiqyJLNph0pyx38yf9mpqFDhNGnMpAXF9/A2Xv0oiF2cVyg9xwfF0HNAz05g==} + engines: {node: '>=12.0.0'} + peerDependencies: + markdown-it: '*' + + markdown-it-link-attributes@4.0.1: + resolution: {integrity: sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==} + + markdown-it-table-of-contents@1.2.0: + resolution: {integrity: sha512-8+8WYI9snTDMB/uGqVGze9YV6Pf4TtQmv79UUl/98ohxobxLayVdiE78zX2VLYKCtNm1ml2b+8dzI54UGesHlA==} + + markdown-it-task-lists@2.1.1: + resolution: {integrity: sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==} + + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + medium-zoom@1.1.0: + resolution: {integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + mermaid@11.14.0: + resolution: {integrity: sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minisearch@7.2.0: + resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + + open@10.1.0: + resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==} + engines: {node: '>=18'} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@9.3.0: + resolution: {integrity: sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==} + engines: {node: '>=20'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@7.0.4: + resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} + engines: {node: '>=18'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + pascalcase@2.0.0: + resolution: {integrity: sha512-DHpENy5Qm/FaX+x3iBLoMLG/XHNCTgL+yErm1TwuVaj6u4fiOSkYkf60vGtITk7hrKHOO4uCl9vRrD4hqjNKjg==} + engines: {node: '>=14'} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + + postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + querystring@0.2.0: + resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-global@2.0.0: + resolution: {integrity: sha512-gnAQ0Q/KkupGkuiMyX4L0GaBV8iFwlmoXsMtOz+DFTaKmHhOO/dSlP1RMKhpvHv/dh6K/IQkowGJBqUG0NfBUw==} + engines: {node: '>=18'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + rolldown@1.0.0-rc.16: + resolution: {integrity: sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass@1.99.0: + resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==} + engines: {node: '>=14.0.0'} + hasBin: true + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shiki@3.23.0: + resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-git@3.36.0: + resolution: {integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + star-markdown-css@0.5.3: + resolution: {integrity: sha512-ewlx4ge1i6B2jopgOJbhgTbshNFI8HEH5vD2itxDh3qKYz+a9+H2EJC1pyPapDeJEOm0DRpHiAvBOSB/H1RMvA==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stdin-discarder@0.3.2: + resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} + engines: {node: '>=18'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + engines: {node: '>=20'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + terser@5.46.1: + resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} + engines: {node: '>=10'} + hasBin: true + + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tldts-core@7.0.28: + resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==} + + tldts@7.0.28: + resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + unconfig@7.5.0: + resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} + + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + + unhead@2.1.13: + resolution: {integrity: sha512-jO9M1sI6b2h/1KpIu4Jeu+ptumLmUKboRRLxys5pYHFeT+lqTzfNHbYUX9bxVDhC1FBszAGuWcUVlmvIPsah8Q==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unocss@66.5.10: + resolution: {integrity: sha512-h3OjHVKsYFiet7ZSgxD6+odC1bpx+N0JYP2bWy/vcqjrApaZmYg4CKmvxCFNxw1+qVoxyfhhjcVZHGUpf9jaKA==} + engines: {node: '>=14'} + peerDependencies: + '@unocss/webpack': 66.5.10 + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + peerDependenciesMeta: + '@unocss/webpack': + optional: true + vite: + optional: true + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unplugin-ast@0.16.0: + resolution: {integrity: sha512-1ow2FlRznoSKE7Fjk2bSxqDsvHyj/O876RqsNlipsM6A+I91t7Mi+jG7tCNNcl3vZx14z4pGXBLSl8KOPrMuFQ==} + engines: {node: '>=20.19.0'} + + unplugin-utils@0.3.1: + resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} + engines: {node: '>=20.19.0'} + + unplugin-vue-components@28.0.0: + resolution: {integrity: sha512-vYe0wSyqTVhyNFIad1iiGyQGhG++tDOMgohqenMDOAooMJP9vvzCdXTqCVx20A0rCQXFNjgoRbSeDAioLPH36Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/parser': ^7.15.8 + '@nuxt/kit': ^3.2.2 + vue: 2 || 3 + peerDependenciesMeta: + '@babel/parser': + optional: true + '@nuxt/kit': + optional: true + + unplugin-vue-markdown@30.0.0: + resolution: {integrity: sha512-FVdKAb7jmZslfdkOCfm6jxHaUafltBpOXdoLvKY+0I0EeMmhxXTSzeDldwXFJeV0IH8LyIXIiU29E6gv02WJFQ==} + engines: {node: '>=20'} + peerDependencies: + vite: ^2.0.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0 + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + valaxy-addon-git-log@0.4.2: + resolution: {integrity: sha512-nFQ3dpettLqv1MpgYSuK5gEW00nRRWCDrFYht4RK1spW3ytXSkFITX9g8D42deY7fqBcQQqcGtjqDUKG73gh7w==} + peerDependencies: + valaxy: '>=0.22' + + valaxy-theme-nova@0.3.1: + resolution: {integrity: sha512-fG7S7+nYAIZjbieq7msYVFUOWyXQH/FGkGv5w4C0t9sIjUlN9nOusWSGER3Mkj5V8rsR76rekmpW0oHkBXHyyg==} + + valaxy@0.28.5: + resolution: {integrity: sha512-Mv5AntRQcLp/tXSdhUS9WaxUFDoW6Lt3ljBL9h6OGTsdAPGQetVURib+ruEQtn1aYP4yiioBvuf2auq6STHM+g==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vanilla-lazyload@19.1.3: + resolution: {integrity: sha512-bBMERPu2AFJc35krS+8BOhq++c6dRfL6q368lJPnkS5U92fRQagTR3FsNta69/GukfZzDwDEjD5M3U7VuSiCDw==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite-dev-rpc@1.1.0: + resolution: {integrity: sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 + + vite-hot-client@2.1.0: + resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite-plugin-inspect@11.3.3: + resolution: {integrity: sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + vite-plugin-vue-devtools@8.1.1: + resolution: {integrity: sha512-9qTpOmZ2vHpvlI9hdVXAQ1Ry4I8GcBArU7aPi0qfIaV7fQIXy0L1nb6X4mFY2Gw0dYshHuLbIl0Ulb572SCjsQ==} + engines: {node: '>=v14.21.3'} + peerDependencies: + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + + vite-plugin-vue-inspector@5.4.0: + resolution: {integrity: sha512-Iq/024CydcE46FZqWPU4t4lw4uYOdLnFSO1RNxJVt2qY9zxIjmnkBqhHnYaReWM82kmNnaXs7OkfgRrV2GEjyw==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + vite-plugin-vue-layouts-next@2.1.0: + resolution: {integrity: sha512-yTL9TWloj7fgnQgi1JASkp63s0qyQP/A3KnOY24wGhUf7Yecb/rxdNylhT+AIDsjf+jfcd1oXVEfWjismH4oBQ==} + peerDependencies: + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.4 + vue-router: ^4.0.11 || ^5.0.0 + + vite-ssg-sitemap@0.10.0: + resolution: {integrity: sha512-OIja4fqUMcvWl5+bxQARe3LgzWTd8U/dWHWgrqiC7vv3AmTn0YnhMNUAimQ0M/0Aa9myEIAGLV0yKlYbKP8BJQ==} + + vite-ssg@28.3.0: + resolution: {integrity: sha512-dIUjv+scfhJTfYGwf83R0KGAmr/duIo9oln5ZsQOIZZACm3voAON//7oKhtEwaOTtD4QTTab+nhvyn0QiIaFMA==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + beasties: ^0.3.5 + prettier: ^3.3.0 + vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0-0 || ^8.0.0-0 + vue: ^3.2.10 + vue-router: ^4.0.1 || ^5.0.0-0 + peerDependenciesMeta: + beasties: + optional: true + prettier: + optional: true + vue-router: + optional: true + + vite@8.0.9: + resolution: {integrity: sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitepress-plugin-group-icons@1.7.5: + resolution: {integrity: sha512-QzcroUuIiVKyXpmEiiHVbfRTQIy9Zbwxpk5JC/zavO8mavitwumz2RZWlwTchMCCHducYyPptkYvXvdnNUWkog==} + peerDependencies: + vite: '>=3' + peerDependenciesMeta: + vite: + optional: true + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-flow-layout@0.2.0: + resolution: {integrity: sha512-zKgsWWkXq0xrus7H4Mc+uFs1ESrmdTXlO0YNbR6wMdPaFvosL3fMB8N7uTV308UhGy9UvTrGhIY7mVz9eN+L0Q==} + + vue-i18n@11.3.2: + resolution: {integrity: sha512-gmFrvM+iuf2AH4ygligw/pC7PRJ63AdRNE68E0GPlQ83Mzfyck6g6cRQC3KzkYXr+ZidR91wq+5YBmAMpkgE1A==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + + vue-resize@2.0.0-alpha.1: + resolution: {integrity: sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==} + peerDependencies: + vue: ^3.0.0 + + vue-router@5.0.4: + resolution: {integrity: sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg==} + peerDependencies: + '@pinia/colada': '>=0.21.2' + '@vue/compiler-sfc': ^3.5.17 + pinia: ^3.0.4 + vue: ^3.5.0 + peerDependenciesMeta: + '@pinia/colada': + optional: true + '@vue/compiler-sfc': + optional: true + pinia: + optional: true + + vue@3.5.22: + resolution: {integrity: sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml-eslint-parser@1.3.2: + resolution: {integrity: sha512-odxVsHAkZYYglR30aPYRY4nUGJnoJ2y1ww2HDvZALo0BDETv9kWbi16J52eHs+PWRNmF4ub6nZqfVOeesOvntg==} + engines: {node: ^14.17.0 || >=16.0.0} + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@acemir/cssom@0.9.31': {} + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.1.1 + + '@antfu/utils@0.7.10': {} + + '@antfu/utils@9.3.0': {} + + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.3.5 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/generator@8.0.0-rc.3': + dependencies: + '@babel/parser': 8.0.0-rc.3 + '@babel/types': 8.0.0-rc.3 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-string-parser@8.0.0-rc.3': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-identifier@8.0.0-rc.3': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.27.7': + dependencies: + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/parser@8.0.0-rc.3': + dependencies: + '@babel/types': 8.0.0-rc.3 + + '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.27.7': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.27.7 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@babel/types@8.0.0-rc.3': + dependencies: + '@babel/helper-string-parser': 8.0.0-rc.3 + '@babel/helper-validator-identifier': 8.0.0-rc.3 + + '@braintree/sanitize-url@7.1.2': {} + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@chevrotain/cst-dts-gen@12.0.0': + dependencies: + '@chevrotain/gast': 12.0.0 + '@chevrotain/types': 12.0.0 + + '@chevrotain/gast@12.0.0': + dependencies: + '@chevrotain/types': 12.0.0 + + '@chevrotain/regexp-to-ast@12.0.0': {} + + '@chevrotain/types@12.0.0': {} + + '@chevrotain/utils@12.0.0': {} + + '@clack/core@1.2.0': + dependencies: + fast-wrap-ansi: 0.1.6 + sisteransi: 1.0.5 + + '@clack/prompts@1.2.0': + dependencies: + '@clack/core': 1.2.0 + fast-string-width: 1.1.0 + fast-wrap-ansi: 0.1.6 + sisteransi: 1.0.5 + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.1(jiti@2.6.1))': + dependencies: + eslint: 10.2.1(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.5.5': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.1': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@exodus/bytes@1.15.0': {} + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.1.1': + dependencies: + '@floating-ui/core': 1.7.5 + + '@floating-ui/utils@0.2.11': {} + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@iconify-json/f7@1.2.2': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/logos@1.2.11': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/ri@1.2.10': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/tabler@1.2.33': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/vscode-icons@1.2.45': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + mlly: 1.8.2 + + '@intlify/bundle-utils@11.0.7(vue-i18n@11.3.2(vue@3.5.22(typescript@6.0.3)))': + dependencies: + '@intlify/message-compiler': 11.3.2 + '@intlify/shared': 11.3.2 + acorn: 8.16.0 + esbuild: 0.25.12 + escodegen: 2.1.0 + estree-walker: 2.0.2 + jsonc-eslint-parser: 2.4.2 + source-map-js: 1.2.1 + yaml-eslint-parser: 1.3.2 + optionalDependencies: + vue-i18n: 11.3.2(vue@3.5.22(typescript@6.0.3)) + + '@intlify/core-base@11.3.2': + dependencies: + '@intlify/devtools-types': 11.3.2 + '@intlify/message-compiler': 11.3.2 + '@intlify/shared': 11.3.2 + + '@intlify/devtools-types@11.3.2': + dependencies: + '@intlify/core-base': 11.3.2 + '@intlify/shared': 11.3.2 + + '@intlify/message-compiler@11.3.2': + dependencies: + '@intlify/shared': 11.3.2 + source-map-js: 1.2.1 + + '@intlify/shared@11.3.2': {} + + '@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)(vue-i18n@11.3.2(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3))': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) + '@intlify/bundle-utils': 11.0.7(vue-i18n@11.3.2(vue@3.5.22(typescript@6.0.3))) + '@intlify/shared': 11.3.2 + '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.3.2)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3)) + '@rollup/pluginutils': 5.3.0 + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + debug: 4.4.3 + fast-glob: 3.3.3 + pathe: 2.0.3 + picocolors: 1.1.1 + unplugin: 2.3.11 + vue: 3.5.22(typescript@6.0.3) + optionalDependencies: + vue-i18n: 11.3.2(vue@3.5.22(typescript@6.0.3)) + transitivePeerDependencies: + - '@vue/compiler-dom' + - eslint + - rollup + - supports-color + - typescript + + '@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.3.2)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3))': + dependencies: + '@babel/parser': 7.29.2 + optionalDependencies: + '@intlify/shared': 11.3.2 + '@vue/compiler-dom': 3.5.32 + vue: 3.5.22(typescript@6.0.3) + vue-i18n: 11.3.2(vue@3.5.22(typescript@6.0.3)) + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@kwsites/promise-deferred@1.1.1': {} + + '@mdit-vue/plugin-component@3.0.2': + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.1 + + '@mdit-vue/plugin-frontmatter@3.0.2': + dependencies: + '@mdit-vue/types': 3.0.2 + '@types/markdown-it': 14.1.2 + gray-matter: 4.0.3 + markdown-it: 14.1.1 + + '@mdit-vue/types@3.0.2': {} + + '@mermaid-js/parser@1.1.0': + dependencies: + langium: 4.2.2 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@octokit/auth-token@6.0.0': {} + + '@octokit/core@7.0.6': + dependencies: + '@octokit/auth-token': 6.0.0 + '@octokit/graphql': 9.0.3 + '@octokit/request': 10.0.8 + '@octokit/request-error': 7.1.0 + '@octokit/types': 16.0.0 + before-after-hook: 4.0.0 + universal-user-agent: 7.0.3 + + '@octokit/endpoint@11.0.3': + dependencies: + '@octokit/types': 16.0.0 + universal-user-agent: 7.0.3 + + '@octokit/graphql@9.0.3': + dependencies: + '@octokit/request': 10.0.8 + '@octokit/types': 16.0.0 + universal-user-agent: 7.0.3 + + '@octokit/openapi-types@27.0.0': {} + + '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.6)': + dependencies: + '@octokit/core': 7.0.6 + '@octokit/types': 16.0.0 + + '@octokit/plugin-request-log@6.0.0(@octokit/core@7.0.6)': + dependencies: + '@octokit/core': 7.0.6 + + '@octokit/plugin-rest-endpoint-methods@17.0.0(@octokit/core@7.0.6)': + dependencies: + '@octokit/core': 7.0.6 + '@octokit/types': 16.0.0 + + '@octokit/request-error@7.1.0': + dependencies: + '@octokit/types': 16.0.0 + + '@octokit/request@10.0.8': + dependencies: + '@octokit/endpoint': 11.0.3 + '@octokit/request-error': 7.1.0 + '@octokit/types': 16.0.0 + fast-content-type-parse: 3.0.0 + json-with-bigint: 3.5.8 + universal-user-agent: 7.0.3 + + '@octokit/rest@22.0.1': + dependencies: + '@octokit/core': 7.0.6 + '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) + '@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.6) + '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.6) + + '@octokit/types@16.0.0': + dependencies: + '@octokit/openapi-types': 27.0.0 + + '@oxc-project/types@0.126.0': {} + + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.4 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + optional: true + + '@polka/url@1.0.0-next.29': {} + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.13': {} + + '@rolldown/pluginutils@1.0.0-rc.16': {} + + '@rollup/plugin-virtual@3.0.2': {} + + '@rollup/pluginutils@5.3.0': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.4 + + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + + '@shikijs/core@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/themes@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/transformers@3.23.0': + dependencies: + '@shikijs/core': 3.23.0 + '@shikijs/types': 3.23.0 + + '@shikijs/types@3.23.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@simple-git/args-pathspec@1.0.3': {} + + '@simple-git/argv-parser@1.1.1': + dependencies: + '@simple-git/args-pathspec': 1.0.3 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.8': {} + + '@types/geojson@7946.0.16': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/http-proxy@1.17.17': + dependencies: + '@types/node': 25.6.0 + + '@types/jsesc@2.5.1': {} + + '@types/json-schema@7.0.15': {} + + '@types/katex@0.16.8': {} + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + + '@types/trusted-types@2.0.7': + optional: true + + '@types/unist@3.0.3': {} + + '@types/web-bluetooth@0.0.21': {} + + '@typescript-eslint/project-service@8.59.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3) + '@typescript-eslint/types': 8.59.0 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + + '@typescript-eslint/tsconfig-utils@8.59.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/types@8.59.0': {} + + '@typescript-eslint/typescript-estree@8.59.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3) + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + eslint-visitor-keys: 5.0.1 + + '@ungap/structured-clone@1.3.0': {} + + '@unhead/addons@2.1.13(unhead@2.1.13)': + dependencies: + '@rollup/pluginutils': 5.3.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + mlly: 1.8.2 + ufo: 1.6.3 + unhead: 2.1.13 + unplugin: 3.0.0 + unplugin-ast: 0.16.0 + transitivePeerDependencies: + - rollup + + '@unhead/dom@2.1.13(unhead@2.1.13)': + dependencies: + unhead: 2.1.13 + + '@unhead/schema-org@2.1.13(@unhead/vue@2.1.13(vue@3.5.22(typescript@6.0.3)))': + dependencies: + ufo: 1.6.3 + unhead: 2.1.13 + optionalDependencies: + '@unhead/vue': 2.1.13(vue@3.5.22(typescript@6.0.3)) + + '@unhead/vue@2.1.13(vue@3.5.22(typescript@6.0.3))': + dependencies: + hookable: 6.1.1 + unhead: 2.1.13 + vue: 3.5.22(typescript@6.0.3) + + '@unocss/astro@66.5.10(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))': + dependencies: + '@unocss/core': 66.5.10 + '@unocss/reset': 66.5.10 + '@unocss/vite': 66.5.10(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + optionalDependencies: + vite: 8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + + '@unocss/cli@66.5.10': + dependencies: + '@jridgewell/remapping': 2.3.5 + '@unocss/config': 66.5.10 + '@unocss/core': 66.5.10 + '@unocss/preset-uno': 66.5.10 + cac: 6.7.14 + chokidar: 3.6.0 + colorette: 2.0.20 + consola: 3.4.2 + magic-string: 0.30.21 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + tinyglobby: 0.2.16 + unplugin-utils: 0.3.1 + + '@unocss/config@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + unconfig: 7.5.0 + + '@unocss/core@66.5.10': {} + + '@unocss/extractor-arbitrary-variants@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + + '@unocss/inspector@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + '@unocss/rule-utils': 66.5.10 + colorette: 2.0.20 + gzip-size: 6.0.0 + sirv: 3.0.2 + vue-flow-layout: 0.2.0 + + '@unocss/postcss@66.5.10(postcss@8.5.10)': + dependencies: + '@unocss/config': 66.5.10 + '@unocss/core': 66.5.10 + '@unocss/rule-utils': 66.5.10 + css-tree: 3.2.1 + postcss: 8.5.10 + tinyglobby: 0.2.16 + + '@unocss/preset-attributify@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + + '@unocss/preset-icons@66.5.10': + dependencies: + '@iconify/utils': 3.1.0 + '@unocss/core': 66.5.10 + ofetch: 1.5.1 + + '@unocss/preset-mini@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + '@unocss/extractor-arbitrary-variants': 66.5.10 + '@unocss/rule-utils': 66.5.10 + + '@unocss/preset-tagify@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + + '@unocss/preset-typography@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + '@unocss/rule-utils': 66.5.10 + + '@unocss/preset-uno@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + '@unocss/preset-wind3': 66.5.10 + + '@unocss/preset-web-fonts@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + ofetch: 1.5.1 + + '@unocss/preset-wind3@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + '@unocss/preset-mini': 66.5.10 + '@unocss/rule-utils': 66.5.10 + + '@unocss/preset-wind4@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + '@unocss/extractor-arbitrary-variants': 66.5.10 + '@unocss/rule-utils': 66.5.10 + + '@unocss/preset-wind@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + '@unocss/preset-wind3': 66.5.10 + + '@unocss/reset@66.5.10': {} + + '@unocss/rule-utils@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + magic-string: 0.30.21 + + '@unocss/transformer-attributify-jsx@66.5.10': + dependencies: + '@babel/parser': 7.27.7 + '@babel/traverse': 7.27.7 + '@unocss/core': 66.5.10 + transitivePeerDependencies: + - supports-color + + '@unocss/transformer-compile-class@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + + '@unocss/transformer-directives@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + '@unocss/rule-utils': 66.5.10 + css-tree: 3.2.1 + + '@unocss/transformer-variant-group@66.5.10': + dependencies: + '@unocss/core': 66.5.10 + + '@unocss/vite@66.5.10(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))': + dependencies: + '@jridgewell/remapping': 2.3.5 + '@unocss/config': 66.5.10 + '@unocss/core': 66.5.10 + '@unocss/inspector': 66.5.10 + chokidar: 3.6.0 + magic-string: 0.30.21 + pathe: 2.0.3 + tinyglobby: 0.2.16 + unplugin-utils: 0.3.1 + vite: 8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + '@valaxyjs/devtools@0.28.5(debug@4.4.3)': + dependencies: + '@rollup/pluginutils': 5.3.0 + axios: 1.15.1(debug@4.4.3) + body-parser: 2.2.2 + cors: 2.8.6 + fs-extra: 11.3.4 + http-proxy-middleware: 3.0.5 + js-yaml: 4.1.1 + magicast: 0.5.2 + pathe: 2.0.3 + sirv: 3.0.2 + transitivePeerDependencies: + - debug + - rollup + - supports-color + + '@valaxyjs/utils@0.28.5': {} + + '@vitejs/plugin-vue@6.0.6(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.22(typescript@6.0.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.13 + vite: 8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + vue: 3.5.22(typescript@6.0.3) + + '@vue-macros/common@3.1.2(vue@3.5.22(typescript@6.0.3))': + dependencies: + '@vue/compiler-sfc': 3.5.32 + ast-kit: 2.2.0 + local-pkg: 1.1.2 + magic-string-ast: 1.0.3 + unplugin-utils: 0.3.1 + optionalDependencies: + vue: 3.5.22(typescript@6.0.3) + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.29.0)': + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.29.0) + '@vue/shared': 3.5.32 + optionalDependencies: + '@babel/core': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.29.0)': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/parser': 7.29.2 + '@vue/compiler-sfc': 3.5.32 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.22': + dependencies: + '@babel/parser': 7.29.2 + '@vue/shared': 3.5.22 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-core@3.5.32': + dependencies: + '@babel/parser': 7.29.2 + '@vue/shared': 3.5.32 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.22': + dependencies: + '@vue/compiler-core': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/compiler-dom@3.5.32': + dependencies: + '@vue/compiler-core': 3.5.32 + '@vue/shared': 3.5.32 + + '@vue/compiler-sfc@3.5.22': + dependencies: + '@babel/parser': 7.29.2 + '@vue/compiler-core': 3.5.22 + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-ssr': 3.5.22 + '@vue/shared': 3.5.22 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.10 + source-map-js: 1.2.1 + + '@vue/compiler-sfc@3.5.32': + dependencies: + '@babel/parser': 7.29.2 + '@vue/compiler-core': 3.5.32 + '@vue/compiler-dom': 3.5.32 + '@vue/compiler-ssr': 3.5.32 + '@vue/shared': 3.5.32 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.10 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.22': + dependencies: + '@vue/compiler-dom': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/compiler-ssr@3.5.32': + dependencies: + '@vue/compiler-dom': 3.5.32 + '@vue/shared': 3.5.32 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.2': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-api@8.1.1': + dependencies: + '@vue/devtools-kit': 8.1.1 + + '@vue/devtools-core@8.1.1(vue@3.5.22(typescript@6.0.3))': + dependencies: + '@vue/devtools-kit': 8.1.1 + '@vue/devtools-shared': 8.1.1 + vue: 3.5.22(typescript@6.0.3) + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-kit@8.1.1': + dependencies: + '@vue/devtools-shared': 8.1.1 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + + '@vue/devtools-shared@8.1.1': {} + + '@vue/reactivity@3.5.22': + dependencies: + '@vue/shared': 3.5.22 + + '@vue/runtime-core@3.5.22': + dependencies: + '@vue/reactivity': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/runtime-dom@3.5.22': + dependencies: + '@vue/reactivity': 3.5.22 + '@vue/runtime-core': 3.5.22 + '@vue/shared': 3.5.22 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.22(vue@3.5.22(typescript@6.0.3))': + dependencies: + '@vue/compiler-ssr': 3.5.22 + '@vue/shared': 3.5.22 + vue: 3.5.22(typescript@6.0.3) + + '@vue/shared@3.5.22': {} + + '@vue/shared@3.5.32': {} + + '@vueuse/core@14.2.1(vue@3.5.22(typescript@6.0.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.2.1 + '@vueuse/shared': 14.2.1(vue@3.5.22(typescript@6.0.3)) + vue: 3.5.22(typescript@6.0.3) + + '@vueuse/integrations@14.2.1(axios@1.15.1)(fuse.js@7.3.0)(nprogress@0.2.0)(qrcode@1.5.4)(vue@3.5.22(typescript@6.0.3))': + dependencies: + '@vueuse/core': 14.2.1(vue@3.5.22(typescript@6.0.3)) + '@vueuse/shared': 14.2.1(vue@3.5.22(typescript@6.0.3)) + vue: 3.5.22(typescript@6.0.3) + optionalDependencies: + axios: 1.15.1(debug@4.4.3) + fuse.js: 7.3.0 + nprogress: 0.2.0 + qrcode: 1.5.4 + + '@vueuse/metadata@14.2.1': {} + + '@vueuse/shared@14.2.1(vue@3.5.22(typescript@6.0.3))': + dependencies: + vue: 3.5.22(typescript@6.0.3) + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + agent-base@7.1.4: {} + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + ansis@4.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + ast-kit@2.2.0: + dependencies: + '@babel/parser': 7.29.2 + pathe: 2.0.3 + + ast-kit@3.0.0-beta.1: + dependencies: + '@babel/parser': 8.0.0-rc.3 + estree-walker: 3.0.3 + pathe: 2.0.3 + + ast-walker-scope@0.8.3: + dependencies: + '@babel/parser': 7.29.2 + ast-kit: 2.2.0 + + astral-regex@2.0.0: {} + + asynckit@0.4.0: {} + + axios@1.15.1(debug@4.4.3): + dependencies: + follow-redirects: 1.16.0(debug@4.4.3) + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.20: {} + + beasties@0.4.2: + dependencies: + css-select: 6.0.0 + css-what: 7.0.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + htmlparser2: 10.1.0 + picocolors: 1.1.1 + postcss: 8.5.10 + postcss-media-query-parser: 0.2.3 + postcss-safe-parser: 7.0.1(postcss@8.5.10) + + before-after-hook@4.0.0: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + binary-extensions@2.3.0: {} + + birpc@2.9.0: {} + + blueimp-md5@2.19.0: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + boolbase@1.0.0: {} + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.20 + caniuse-lite: 1.0.30001788 + electron-to-chromium: 1.5.340 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + buffer-from@1.1.2: {} + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bytes@3.1.2: {} + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.8.1 + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001788: {} + + ccount@2.0.1: {} + + chalk@5.6.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + charenc@0.0.2: {} + + chevrotain-allstar@0.4.1(chevrotain@12.0.0): + dependencies: + chevrotain: 12.0.0 + lodash-es: 4.18.1 + + chevrotain@12.0.0: + dependencies: + '@chevrotain/cst-dts-gen': 12.0.0 + '@chevrotain/gast': 12.0.0 + '@chevrotain/regexp-to-ast': 12.0.0 + '@chevrotain/types': 12.0.0 + '@chevrotain/utils': 12.0.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@3.4.0: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@2.0.3: {} + + commander@10.0.1: {} + + commander@2.20.3: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + consola@3.4.2: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypt@0.0.2: {} + + css-i18n@0.0.5: {} + + css-select@6.0.0: + dependencies: + boolbase: 1.0.0 + css-what: 7.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css-what@7.0.0: {} + + cssstyle@6.2.0: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + css-tree: 3.2.1 + lru-cache: 11.3.5 + + csstype@3.2.3: {} + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.2): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.33.2 + + cytoscape-fcose@2.2.0(cytoscape@3.33.2): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.33.2 + + cytoscape@3.33.2: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.14: + dependencies: + d3: 7.9.0 + lodash-es: 4.18.1 + + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + dayjs@1.11.20: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-config-ts@0.1.3: + dependencies: + jiti: 2.6.1 + + define-lazy-prop@3.0.0: {} + + defu@6.1.7: {} + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dijkstrajs@1.0.3: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + dompurify@3.4.0: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer@0.1.2: {} + + ee-first@1.1.1: {} + + ejs@5.0.2: {} + + electron-to-chromium@1.5.340: {} + + email-validator@2.0.4: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + entities@4.5.0: {} + + entities@7.0.1: {} + + entities@8.0.0: {} + + error-stack-parser-es@1.0.5: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.2.1(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.5.5 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + espree@9.6.1: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 3.4.3 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + eventemitter3@4.0.7: {} + + exsolve@1.0.8: {} + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + fast-content-type-parse@3.0.0: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-string-truncated-width@1.2.1: {} + + fast-string-width@1.1.0: + dependencies: + fast-string-truncated-width: 1.2.1 + + fast-uri@3.1.0: {} + + fast-wrap-ansi@0.1.6: + dependencies: + fast-string-width: 1.1.0 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + feed@5.2.1: + dependencies: + xml-js: 1.6.11 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + floating-vue@5.2.2(vue@3.5.22(typescript@6.0.3)): + dependencies: + '@floating-ui/dom': 1.1.1 + vue: 3.5.22(typescript@6.0.3) + vue-resize: 2.0.0-alpha.1(vue@3.5.22(typescript@6.0.3)) + + follow-redirects@1.16.0(debug@4.4.3): + optionalDependencies: + debug: 4.4.3 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + fuse.js@7.3.0: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.5.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + globals@11.12.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + gravatar@1.8.2: + dependencies: + blueimp-md5: 2.19.0 + email-validator: 2.0.4 + querystring: 0.2.0 + yargs: 15.4.1 + + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.2 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + + gsap@3.15.0: {} + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + hachure-fill@0.5.2: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hookable@5.5.3: {} + + hookable@6.1.1: {} + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.0 + transitivePeerDependencies: + - '@noble/hashes' + + html-minifier-terser@7.2.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 10.0.1 + entities: 4.5.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.46.1 + + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + + html-void-elements@3.0.0: {} + + html5parser@2.0.2: + dependencies: + tslib: 2.8.1 + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http-proxy-middleware@3.0.5: + dependencies: + '@types/http-proxy': 1.17.17 + debug: 4.4.3 + http-proxy: 1.18.1(debug@4.4.3) + is-glob: 4.0.3 + is-plain-object: 5.0.0 + micromatch: 4.0.8 + transitivePeerDependencies: + - supports-color + + http-proxy@1.18.1(debug@4.4.3): + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.16.0(debug@4.4.3) + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + immutable@5.1.5: {} + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + ini@4.1.1: {} + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-buffer@1.1.6: {} + + is-docker@3.0.0: {} + + is-extendable@0.1.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@2.0.0: {} + + is-number@7.0.0: {} + + is-plain-object@5.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-unicode-supported@2.1.0: {} + + is-what@5.5.0: {} + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + jiti@2.6.1: {} + + js-base64@3.7.8: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsdom@28.1.0: + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@bramus/specificity': 2.4.2 + '@exodus/bytes': 1.15.0 + cssstyle: 6.2.0 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - supports-color + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-with-bigint@3.5.8: {} + + json5@2.2.3: {} + + jsonc-eslint-parser@2.4.2: + dependencies: + acorn: 8.16.0 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + semver: 7.7.4 + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + katex@0.16.45: + dependencies: + commander: 8.3.0 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + khroma@2.1.0: {} + + kind-of@6.0.3: {} + + kolorist@1.8.0: {} + + langium@4.2.2: + dependencies: + '@chevrotain/regexp-to-ast': 12.0.0 + chevrotain: 12.0.0 + chevrotain-allstar: 0.4.1(chevrotain@12.0.0) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + + leac@0.6.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + local-pkg@0.5.1: + dependencies: + mlly: 1.8.2 + pkg-types: 1.3.1 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.0 + quansync: 0.2.11 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.18.1: {} + + lodash.truncate@4.4.2: {} + + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.1.2 + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lru-cache@11.3.5: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string-ast@1.0.3: + dependencies: + magic-string: 0.30.21 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.2: + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + markdown-exit@1.0.0-beta.9: + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + entities: 7.0.1 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + markdown-it-anchor@9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.1): + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.1 + + markdown-it-async@2.2.0: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.1 + + markdown-it-attrs@4.3.1(markdown-it@14.1.1): + dependencies: + markdown-it: 14.1.1 + + markdown-it-container@4.0.0: {} + + markdown-it-emoji@3.0.0: {} + + markdown-it-footnote@4.0.0: {} + + markdown-it-github-alerts@1.0.1(markdown-it@14.1.1): + dependencies: + markdown-it: 14.1.1 + + markdown-it-image-figures@2.1.1(markdown-it@14.1.1): + dependencies: + markdown-it: 14.1.1 + + markdown-it-link-attributes@4.0.1: {} + + markdown-it-table-of-contents@1.2.0: {} + + markdown-it-task-lists@2.1.1: {} + + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + marked@16.4.2: {} + + math-intrinsics@1.1.0: {} + + md5@2.3.0: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdn-data@2.27.1: {} + + mdurl@2.0.0: {} + + media-typer@1.1.0: {} + + medium-zoom@1.1.0: {} + + merge2@1.4.1: {} + + mermaid@11.14.0: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.0 + '@mermaid-js/parser': 1.1.0 + '@types/d3': 7.4.3 + '@upsetjs/venn.js': 2.0.0 + cytoscape: 3.33.2 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.2) + cytoscape-fcose: 2.2.0(cytoscape@3.33.2) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.14 + dayjs: 1.11.20 + dompurify: 3.4.0 + katex: 0.16.45 + khroma: 2.1.0 + lodash-es: 4.18.1 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.4.0 + ts-dedent: 2.2.0 + uuid: 11.1.0 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-function@5.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.0 + + minisearch@7.2.0: {} + + mitt@3.0.1: {} + + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + node-addon-api@7.1.1: + optional: true + + node-fetch-native@1.6.7: {} + + node-releases@2.0.37: {} + + normalize-path@3.0.0: {} + + nprogress@0.2.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.3 + + ohash@2.0.11: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + + open@10.1.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + is-wsl: 3.1.1 + + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@9.3.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.4.0 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.3.2 + string-width: 8.2.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@7.0.4: {} + + p-try@2.2.0: {} + + package-manager-detector@1.6.0: {} + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + pascalcase@2.0.0: + dependencies: + camelcase: 6.3.0 + + path-data-parser@0.1.0: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + pathe@2.0.3: {} + + peberminta@0.9.0: {} + + perfect-debounce@1.0.0: {} + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pinia@3.0.4(typescript@6.0.3)(vue@3.5.22(typescript@6.0.3)): + dependencies: + '@vue/devtools-api': 7.7.9 + vue: 3.5.22(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + pngjs@5.0.0: {} + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + postcss-media-query-parser@0.2.3: {} + + postcss-safe-parser@7.0.1(postcss@8.5.10): + dependencies: + postcss: 8.5.10 + + postcss@8.5.10: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + property-information@7.1.0: {} + + proxy-from-env@2.1.0: {} + + punycode.js@2.3.1: {} + + punycode@2.3.1: {} + + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + + quansync@0.2.11: {} + + quansync@1.0.0: {} + + querystring@0.2.0: {} + + queue-microtask@1.2.3: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + readdirp@4.1.2: {} + + readdirp@5.0.0: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + relateurl@0.2.7: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-main-filename@2.0.0: {} + + requires-port@1.0.0: {} + + resolve-global@2.0.0: + dependencies: + global-directory: 4.0.1 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + robust-predicates@3.0.3: {} + + rolldown@1.0.0-rc.16: + dependencies: + '@oxc-project/types': 0.126.0 + '@rolldown/pluginutils': 1.0.0-rc.16 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-x64': 1.0.0-rc.16 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.16 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.16 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.16 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.16 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.16 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.16 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.16 + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rw@1.3.3: {} + + safer-buffer@2.1.2: {} + + sass@1.99.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.5 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.6 + + sax@1.6.0: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scule@1.3.0: {} + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + + semver@6.3.1: {} + + semver@7.7.4: {} + + set-blocking@2.0.0: {} + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shiki@3.23.0: + dependencies: + '@shikijs/core': 3.23.0 + '@shikijs/engine-javascript': 3.23.0 + '@shikijs/engine-oniguruma': 3.23.0 + '@shikijs/langs': 3.23.0 + '@shikijs/themes': 3.23.0 + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@4.1.0: {} + + simple-git@3.36.0: + dependencies: + '@kwsites/file-exists': 1.1.1 + '@kwsites/promise-deferred': 1.1.1 + '@simple-git/args-pathspec': 1.0.3 + '@simple-git/argv-parser': 1.1.1 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + sisteransi@1.0.5: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + space-separated-tokens@2.0.2: {} + + speakingurl@14.0.1: {} + + sprintf-js@1.0.3: {} + + star-markdown-css@0.5.3: {} + + statuses@2.0.2: {} + + stdin-discarder@0.3.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + string-width@8.2.0: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom-string@1.0.0: {} + + stylis@4.4.0: {} + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + symbol-tree@3.2.4: {} + + table@6.9.0: + dependencies: + ajv: 8.18.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + terser@5.46.1: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + tinyexec@1.1.1: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tldts-core@7.0.28: {} + + tldts@7.0.28: + dependencies: + tldts-core: 7.0.28 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + totalist@3.0.1: {} + + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.28 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + trim-lines@3.0.1: {} + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + ts-dedent@2.2.0: {} + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@6.0.3: {} + + uc.micro@2.1.0: {} + + ufo@1.6.3: {} + + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + unconfig@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + defu: 6.1.7 + jiti: 2.6.1 + quansync: 1.0.0 + unconfig-core: 7.5.0 + + undici-types@7.19.2: {} + + undici@7.25.0: {} + + unhead@2.1.13: + dependencies: + hookable: 6.1.1 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + universal-user-agent@7.0.3: {} + + universalify@2.0.1: {} + + unocss@66.5.10(postcss@8.5.10)(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): + dependencies: + '@unocss/astro': 66.5.10(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + '@unocss/cli': 66.5.10 + '@unocss/core': 66.5.10 + '@unocss/postcss': 66.5.10(postcss@8.5.10) + '@unocss/preset-attributify': 66.5.10 + '@unocss/preset-icons': 66.5.10 + '@unocss/preset-mini': 66.5.10 + '@unocss/preset-tagify': 66.5.10 + '@unocss/preset-typography': 66.5.10 + '@unocss/preset-uno': 66.5.10 + '@unocss/preset-web-fonts': 66.5.10 + '@unocss/preset-wind': 66.5.10 + '@unocss/preset-wind3': 66.5.10 + '@unocss/preset-wind4': 66.5.10 + '@unocss/transformer-attributify-jsx': 66.5.10 + '@unocss/transformer-compile-class': 66.5.10 + '@unocss/transformer-directives': 66.5.10 + '@unocss/transformer-variant-group': 66.5.10 + '@unocss/vite': 66.5.10(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + optionalDependencies: + vite: 8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + transitivePeerDependencies: + - postcss + - supports-color + + unpipe@1.0.0: {} + + unplugin-ast@0.16.0: + dependencies: + '@babel/generator': 8.0.0-rc.3 + '@babel/parser': 8.0.0-rc.3 + '@babel/types': 8.0.0-rc.3 + ast-kit: 3.0.0-beta.1 + magic-string-ast: 1.0.3 + unplugin: 3.0.0 + + unplugin-utils@0.3.1: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.4 + + unplugin-vue-components@28.0.0(@babel/parser@7.29.2)(vue@3.5.22(typescript@6.0.3)): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.3.0 + chokidar: 3.6.0 + debug: 4.4.3 + fast-glob: 3.3.3 + local-pkg: 0.5.1 + magic-string: 0.30.21 + minimatch: 9.0.9 + mlly: 1.8.2 + unplugin: 2.3.11 + vue: 3.5.22(typescript@6.0.3) + optionalDependencies: + '@babel/parser': 7.29.2 + transitivePeerDependencies: + - rollup + - supports-color + + unplugin-vue-markdown@30.0.0(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): + dependencies: + '@mdit-vue/plugin-component': 3.0.2 + '@mdit-vue/plugin-frontmatter': 3.0.2 + '@mdit-vue/types': 3.0.2 + markdown-exit: 1.0.0-beta.9 + unplugin: 2.3.11 + unplugin-utils: 0.3.1 + vite: 8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.16.0 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + uuid@11.1.0: {} + + valaxy-addon-git-log@0.4.2(valaxy@0.28.5(@babel/parser@7.29.2)(@types/markdown-it@14.1.2)(@types/node@25.6.0)(@vue/compiler-dom@3.5.32)(@vue/compiler-sfc@3.5.32)(axios@1.15.1)(eslint@10.2.1(jiti@2.6.1))(postcss@8.5.10)(terser@5.46.1)(typescript@6.0.3)(yaml@2.8.3)): + dependencies: + '@octokit/rest': 22.0.1 + fs-extra: 11.3.4 + gravatar: 1.8.2 + md5: 2.3.0 + simple-git: 3.36.0 + valaxy: 0.28.5(@babel/parser@7.29.2)(@types/markdown-it@14.1.2)(@types/node@25.6.0)(@vue/compiler-dom@3.5.32)(@vue/compiler-sfc@3.5.32)(axios@1.15.1)(eslint@10.2.1(jiti@2.6.1))(postcss@8.5.10)(terser@5.46.1)(typescript@6.0.3)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + + valaxy-theme-nova@0.3.1(markdown-it@14.1.1)(valaxy@0.28.5(@babel/parser@7.29.2)(@types/markdown-it@14.1.2)(@types/node@25.6.0)(@vue/compiler-dom@3.5.32)(@vue/compiler-sfc@3.5.32)(axios@1.15.1)(eslint@10.2.1(jiti@2.6.1))(postcss@8.5.10)(terser@5.46.1)(typescript@6.0.3)(yaml@2.8.3)): + dependencies: + '@iconify-json/f7': 1.2.2 + '@iconify-json/ri': 1.2.10 + '@iconify-json/tabler': 1.2.33 + '@rollup/plugin-virtual': 3.0.2 + d3: 7.9.0 + gsap: 3.15.0 + markdown-it-github-alerts: 1.0.1(markdown-it@14.1.1) + markdown-it-link-attributes: 4.0.1 + valaxy-addon-git-log: 0.4.2(valaxy@0.28.5(@babel/parser@7.29.2)(@types/markdown-it@14.1.2)(@types/node@25.6.0)(@vue/compiler-dom@3.5.32)(@vue/compiler-sfc@3.5.32)(axios@1.15.1)(eslint@10.2.1(jiti@2.6.1))(postcss@8.5.10)(terser@5.46.1)(typescript@6.0.3)(yaml@2.8.3)) + transitivePeerDependencies: + - markdown-it + - rollup + - supports-color + - valaxy + + valaxy@0.28.5(@babel/parser@7.29.2)(@types/markdown-it@14.1.2)(@types/node@25.6.0)(@vue/compiler-dom@3.5.32)(@vue/compiler-sfc@3.5.32)(axios@1.15.1)(eslint@10.2.1(jiti@2.6.1))(postcss@8.5.10)(terser@5.46.1)(typescript@6.0.3)(yaml@2.8.3): + dependencies: + '@antfu/install-pkg': 1.1.0 + '@antfu/utils': 9.3.0 + '@clack/prompts': 1.2.0 + '@iconify-json/ri': 1.2.10 + '@intlify/unplugin-vue-i18n': 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3)(vue-i18n@11.3.2(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3)) + '@shikijs/transformers': 3.23.0 + '@types/katex': 0.16.8 + '@unhead/addons': 2.1.13(unhead@2.1.13) + '@unhead/schema-org': 2.1.13(@unhead/vue@2.1.13(vue@3.5.22(typescript@6.0.3))) + '@unhead/vue': 2.1.13(vue@3.5.22(typescript@6.0.3)) + '@valaxyjs/devtools': 0.28.5(debug@4.4.3) + '@valaxyjs/utils': 0.28.5 + '@vitejs/plugin-vue': 6.0.6(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.22(typescript@6.0.3)) + '@vue/devtools-api': 7.7.2 + '@vueuse/core': 14.2.1(vue@3.5.22(typescript@6.0.3)) + '@vueuse/integrations': 14.2.1(axios@1.15.1)(fuse.js@7.3.0)(nprogress@0.2.0)(qrcode@1.5.4)(vue@3.5.22(typescript@6.0.3)) + beasties: 0.4.2 + consola: 3.4.2 + cross-spawn: 7.0.6 + css-i18n: 0.0.5 + dayjs: 1.11.20 + debug: 4.4.3 + define-config-ts: 0.1.3 + defu: 6.1.7 + ejs: 5.0.2 + escape-html: 1.0.3 + fast-glob: 3.3.3 + feed: 5.2.1 + floating-vue: 5.2.2(vue@3.5.22(typescript@6.0.3)) + fs-extra: 11.3.4 + fuse.js: 7.3.0 + gray-matter: 4.0.3 + hookable: 6.1.1 + html-to-text: 9.0.5 + jiti: 2.6.1 + js-base64: 3.7.8 + js-yaml: 4.1.1 + katex: 0.16.45 + lru-cache: 11.3.5 + markdown-it: 14.1.1 + markdown-it-anchor: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.1) + markdown-it-async: 2.2.0 + markdown-it-attrs: 4.3.1(markdown-it@14.1.1) + markdown-it-container: 4.0.0 + markdown-it-emoji: 3.0.0 + markdown-it-footnote: 4.0.0 + markdown-it-image-figures: 2.1.1(markdown-it@14.1.1) + markdown-it-table-of-contents: 1.2.0 + markdown-it-task-lists: 2.1.1 + medium-zoom: 1.1.0 + mermaid: 11.14.0 + minisearch: 7.2.0 + mlly: 1.8.2 + nprogress: 0.2.0 + open: 10.1.0 + ora: 9.3.0 + p-map: 7.0.4 + pascalcase: 2.0.0 + pathe: 2.0.3 + pinia: 3.0.4(typescript@6.0.3)(vue@3.5.22(typescript@6.0.3)) + qrcode: 1.5.4 + resolve-global: 2.0.0 + sass: 1.99.0 + shiki: 3.23.0 + star-markdown-css: 0.5.3 + table: 6.9.0 + unhead: 2.1.13 + unocss: 66.5.10(postcss@8.5.10)(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + unplugin-vue-components: 28.0.0(@babel/parser@7.29.2)(vue@3.5.22(typescript@6.0.3)) + unplugin-vue-markdown: 30.0.0(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + vanilla-lazyload: 19.1.3 + vite: 8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + vite-plugin-vue-devtools: 8.1.1(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.22(typescript@6.0.3)) + vite-plugin-vue-layouts-next: 2.1.0(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3)) + vite-ssg: 28.3.0(beasties@0.4.2)(unhead@2.1.13)(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3)) + vite-ssg-sitemap: 0.10.0 + vitepress-plugin-group-icons: 1.7.5(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + vue: 3.5.22(typescript@6.0.3) + vue-i18n: 11.3.2(vue@3.5.22(typescript@6.0.3)) + vue-router: 5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3)) + yargs: 18.0.0 + transitivePeerDependencies: + - '@babel/parser' + - '@noble/hashes' + - '@nuxt/kit' + - '@pinia/colada' + - '@types/markdown-it' + - '@types/node' + - '@unhead/react' + - '@unhead/solid-js' + - '@unhead/svelte' + - '@unocss/webpack' + - '@vitejs/devtools' + - '@vue/compiler-dom' + - '@vue/compiler-sfc' + - async-validator + - axios + - canvas + - change-case + - drauu + - esbuild + - eslint + - focus-trap + - idb-keyval + - jwt-decode + - less + - petite-vue-i18n + - postcss + - prettier + - rollup + - sass-embedded + - sortablejs + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - universal-cookie + - yaml + + vanilla-lazyload@19.1.3: {} + + vary@1.1.2: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite-dev-rpc@1.1.0(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): + dependencies: + birpc: 2.9.0 + vite: 8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + vite-hot-client: 2.1.0(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + + vite-hot-client@2.1.0(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): + dependencies: + vite: 8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + + vite-plugin-inspect@11.3.3(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): + dependencies: + ansis: 4.2.0 + debug: 4.4.3 + error-stack-parser-es: 1.0.5 + ohash: 2.0.11 + open: 10.2.0 + perfect-debounce: 2.1.0 + sirv: 3.0.2 + unplugin-utils: 0.3.1 + vite: 8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + vite-dev-rpc: 1.1.0(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + transitivePeerDependencies: + - supports-color + + vite-plugin-vue-devtools@8.1.1(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.22(typescript@6.0.3)): + dependencies: + '@vue/devtools-core': 8.1.1(vue@3.5.22(typescript@6.0.3)) + '@vue/devtools-kit': 8.1.1 + '@vue/devtools-shared': 8.1.1 + sirv: 3.0.2 + vite: 8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + vite-plugin-inspect: 11.3.3(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + vite-plugin-vue-inspector: 5.4.0(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + transitivePeerDependencies: + - '@nuxt/kit' + - supports-color + - vue + + vite-plugin-vue-inspector@5.4.0(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0) + '@vue/compiler-dom': 3.5.32 + kolorist: 1.8.0 + magic-string: 0.30.21 + vite: 8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + + vite-plugin-vue-layouts-next@2.1.0(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3)): + dependencies: + debug: 4.4.3 + fast-glob: 3.3.3 + vite: 8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + vue: 3.5.22(typescript@6.0.3) + vue-router: 5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3)) + transitivePeerDependencies: + - supports-color + + vite-ssg-sitemap@0.10.0: {} + + vite-ssg@28.3.0(beasties@0.4.2)(unhead@2.1.13)(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3)): + dependencies: + '@unhead/dom': 2.1.13(unhead@2.1.13) + '@unhead/vue': 2.1.13(vue@3.5.22(typescript@6.0.3)) + ansis: 4.2.0 + cac: 6.7.14 + html-minifier-terser: 7.2.0 + html5parser: 2.0.2 + jsdom: 28.1.0 + vite: 8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + vue: 3.5.22(typescript@6.0.3) + optionalDependencies: + beasties: 0.4.2 + vue-router: 5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3)) + transitivePeerDependencies: + - '@noble/hashes' + - canvas + - supports-color + - unhead + + vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.10 + rolldown: 1.0.0-rc.16 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.6.0 + fsevents: 2.3.3 + jiti: 2.6.1 + sass: 1.99.0 + terser: 5.46.1 + yaml: 2.8.3 + + vitepress-plugin-group-icons@1.7.5(vite@8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): + dependencies: + '@iconify-json/logos': 1.2.11 + '@iconify-json/vscode-icons': 1.2.45 + '@iconify/utils': 3.1.0 + optionalDependencies: + vite: 8.0.9(@types/node@25.6.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.1.0: {} + + vue-flow-layout@0.2.0: {} + + vue-i18n@11.3.2(vue@3.5.22(typescript@6.0.3)): + dependencies: + '@intlify/core-base': 11.3.2 + '@intlify/devtools-types': 11.3.2 + '@intlify/shared': 11.3.2 + '@vue/devtools-api': 6.6.4 + vue: 3.5.22(typescript@6.0.3) + + vue-resize@2.0.0-alpha.1(vue@3.5.22(typescript@6.0.3)): + dependencies: + vue: 3.5.22(typescript@6.0.3) + + vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.22(typescript@6.0.3)))(vue@3.5.22(typescript@6.0.3)): + dependencies: + '@babel/generator': 7.29.1 + '@vue-macros/common': 3.1.2(vue@3.5.22(typescript@6.0.3)) + '@vue/devtools-api': 8.1.1 + ast-walker-scope: 0.8.3 + chokidar: 5.0.0 + json5: 2.2.3 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.2 + muggle-string: 0.4.1 + pathe: 2.0.3 + picomatch: 4.0.4 + scule: 1.3.0 + tinyglobby: 0.2.16 + unplugin: 3.0.0 + unplugin-utils: 0.3.1 + vue: 3.5.22(typescript@6.0.3) + yaml: 2.8.3 + optionalDependencies: + '@vue/compiler-sfc': 3.5.32 + pinia: 3.0.4(typescript@6.0.3)(vue@3.5.22(typescript@6.0.3)) + + vue@3.5.22(typescript@6.0.3): + dependencies: + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-sfc': 3.5.22 + '@vue/runtime-dom': 3.5.22 + '@vue/server-renderer': 3.5.22(vue@3.5.22(typescript@6.0.3)) + '@vue/shared': 3.5.22 + optionalDependencies: + typescript: 6.0.3 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + which-module@2.0.1: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + + xml-js@1.6.11: + dependencies: + sax: 1.6.0 + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml-eslint-parser@1.3.2: + dependencies: + eslint-visitor-keys: 3.4.3 + yaml: 2.8.3 + + yaml@2.8.3: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@22.0.0: {} + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yocto-queue@0.1.0: {} + + yoctocolors@2.1.2: {} + + zwitch@2.0.4: {} diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 0000000..0e01953 --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "jsx": "preserve", + "lib": ["DOM", "ESNext"], + "baseUrl": ".", + "module": "ESNext", + "moduleResolution": "bundler", + "paths": { + "~/*": ["./*"] + }, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "exclude": ["dist", "node_modules"] +} diff --git a/docs/valaxy.config.ts b/docs/valaxy.config.ts new file mode 100644 index 0000000..837d71d --- /dev/null +++ b/docs/valaxy.config.ts @@ -0,0 +1,59 @@ +import type { ThemeConfig } from 'valaxy-theme-nova' +import { defineValaxyConfig } from 'valaxy' +import modManifest from '../mod_manifest.json' + +export default defineValaxyConfig({ + theme: 'nova', + + siteConfig: { + title: 'RitsuLib', + url: 'https://ritsulib-sts2.local', + description: + 'RitsuLib — Slay the Spire 2 mod framework: patching, persistence, lifecycle, localization, and authoring helpers', + lang: 'en', + languages: ['en', 'zh-CN'], + + author: { + name: 'OLC', + }, + + search: { + enable: false, + }, + }, + + themeConfig: { + colors: { + primary: '#3D5A80', + }, + + navTitle: { en: 'RitsuLib', 'zh-CN': 'RitsuLib' }, + + nav: [ + { locale: 'nav.home', link: '/' }, + { + locale: 'nav.guide', + link: '/guide/', + }, + { + text: `v${modManifest.version}`, + link: 'https://github.com/WRXinYue/STS2-RitsuLib/releases', + }, + ], + + navTools: [['toggleLocale', 'toggleTheme']], + + hero: { + title: { en: 'RITSULIB', 'zh-CN': 'RITSULIB' }, + motto: { + en: 'Slay the Spire 2 mod framework — registries, lifecycle, patches & tooling', + 'zh-CN': '《杀戮尖塔 2》模组框架:注册器、生命周期、补丁与工具链', + }, + img: 'https://wrxinyue.s3.bitiful.net/slay-the-spire-2-wallpaper.webp', + }, + + footer: { + since: 2026, + }, + }, +}) diff --git a/docs/vercel.json b/docs/vercel.json new file mode 100644 index 0000000..f02d582 --- /dev/null +++ b/docs/vercel.json @@ -0,0 +1,4 @@ +{ + "cleanUrls": true, + "rewrites": [{ "source": "/:path*", "destination": "/index.html" }] +}