Skip to content

Commit d02f53a

Browse files
committed
chore(release): merge dev into main for v0.3.10
2 parents c3e2a62 + c2e2d09 commit d02f53a

23 files changed

Lines changed: 1045 additions & 33 deletions

Const.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public static class Const
2222
/// Assembly / manifest version string.
2323
/// 程序集/清单版本字符串。
2424
/// </summary>
25-
public const string Version = "0.3.9";
25+
public const string Version = "0.3.10";
2626

2727
/// <summary>
2828
/// Root key for RitsuLib JSON settings under the mod’s user folder.

Content/DynamicActContentPatcher.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ internal static void EnsurePatched()
3535
DynamicPatchBuilder.FromMethod(typeof(DynamicActContentPatcher), nameof(AllAncientsPostfix));
3636
var encountersPostfix =
3737
DynamicPatchBuilder.FromMethod(typeof(DynamicActContentPatcher), nameof(AllEncountersPostfix));
38+
var bossDiscoveryOrderPostfix = DynamicPatchBuilder.FromMethod(
39+
typeof(DynamicActContentPatcher),
40+
nameof(BossDiscoveryOrderPostfix));
3841
var unlockedAncientsPostfix = DynamicPatchBuilder.FromMethod(
3942
typeof(DynamicActContentPatcher),
4043
nameof(GetUnlockedAncientsPostfix));
@@ -43,6 +46,12 @@ internal static void EnsurePatched()
4346
{
4447
TryAddPropertyGetterPatch(builder, actType, nameof(ActModel.AllEvents), eventsPostfix, logger);
4548
TryAddPropertyGetterPatch(builder, actType, nameof(ActModel.AllAncients), ancientsPostfix, logger);
49+
TryAddPropertyGetterPatch(
50+
builder,
51+
actType,
52+
nameof(ActModel.BossDiscoveryOrder),
53+
bossDiscoveryOrderPostfix,
54+
logger);
4655
TryAddMethodPatch(
4756
builder,
4857
actType,
@@ -133,8 +142,17 @@ private static void AllAncientsPostfix(ActModel __instance, ref IEnumerable<Anci
133142
private static void AllEncountersPostfix(ActModel __instance, ref IEnumerable<EncounterModel> __result)
134143
// ReSharper restore InconsistentNaming
135144
{
136-
__result = ModContentRegistry.AppendGlobalEncounters(
137-
ModContentRegistry.AppendActEncounters(__instance, __result));
145+
__result = ModEncounterActValidityFilter.FilterForAct(
146+
__instance,
147+
ModContentRegistry.AppendGlobalEncounters(
148+
ModContentRegistry.AppendActEncounters(__instance, __result)));
149+
}
150+
151+
// ReSharper disable InconsistentNaming
152+
private static void BossDiscoveryOrderPostfix(ActModel __instance, ref IEnumerable<EncounterModel> __result)
153+
// ReSharper restore InconsistentNaming
154+
{
155+
__result = ModEncounterActValidityFilter.FilterForAct(__instance, __result);
138156
}
139157

140158
// ReSharper disable InconsistentNaming

Content/Patches/ActContentPatches.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class DynamicActContentPatchBootstrap : IPatchMethod
1616

1717
/// <inheritdoc />
1818
public static string Description =>
19-
"Dynamically patch all loaded ActModel implementations for registered events and ancients";
19+
"Dynamically patch all loaded ActModel implementations for registered events, ancients, and encounters";
2020

2121
/// <inheritdoc />
2222
public static bool IsCritical => true;

Diagnostics/Commands/SelfCheckConsoleCommands.cs

Lines changed: 149 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using MegaCrit.Sts2.Core.DevConsole;
22
using MegaCrit.Sts2.Core.DevConsole.ConsoleCommands;
33
using MegaCrit.Sts2.Core.Entities.Players;
4+
using STS2RitsuLib.Settings;
45

56
namespace STS2RitsuLib.Diagnostics.Commands
67
{
@@ -10,17 +11,19 @@ namespace STS2RitsuLib.Diagnostics.Commands
1011
/// </summary>
1112
public sealed class RitsuLibConsoleCmd : AbstractConsoleCmd
1213
{
13-
private static readonly string[] RootCommands = ["selfcheck"];
14+
private static readonly string[] RootCommands = ["selfcheck", "settings"];
1415
private static readonly string[] SelfCheckActions = ["run", "open-output"];
16+
private static readonly string[] SettingsActions = ["open"];
1517

1618
/// <inheritdoc />
1719
public override string CmdName => "ritsulib";
1820

1921
/// <inheritdoc />
20-
public override string Args => "selfcheck run|open-output";
22+
public override string Args =>
23+
"selfcheck run|open-output OR settings open <modId> [pageId] [sectionId] [entryId]";
2124

2225
/// <inheritdoc />
23-
public override string Description => "RitsuLib tools: selfcheck run/open-output.";
26+
public override string Description => "RitsuLib tools: selfcheck run/open-output; settings open.";
2427

2528
/// <inheritdoc />
2629
public override bool IsNetworked => false;
@@ -34,6 +37,9 @@ public override CompletionResult GetArgumentCompletions(Player? player, string[]
3437
return CompleteArgument(RootCommands, [], partial, CompletionType.Subcommand);
3538
}
3639

40+
if (args[0].Equals("settings", StringComparison.OrdinalIgnoreCase))
41+
return CompleteSettingsArguments(args);
42+
3743
if (!args[0].Equals("selfcheck", StringComparison.OrdinalIgnoreCase))
3844
return base.GetArgumentCompletions(player, args);
3945
{
@@ -46,8 +52,11 @@ public override CompletionResult GetArgumentCompletions(Player? player, string[]
4652
/// <inheritdoc />
4753
public override CmdResult Process(Player? issuingPlayer, string[] args)
4854
{
55+
if (args.Length >= 1 && args[0].Equals("settings", StringComparison.OrdinalIgnoreCase))
56+
return ProcessSettings(args);
57+
4958
if (args.Length < 2 || !args[0].Equals("selfcheck", StringComparison.OrdinalIgnoreCase))
50-
return new(false, "Usage: ritsulib selfcheck run|open-output");
59+
return new(false, UsageText());
5160

5261
if (args[1].Equals("run", StringComparison.OrdinalIgnoreCase))
5362
{
@@ -56,9 +65,144 @@ public override CmdResult Process(Player? issuingPlayer, string[] args)
5665
}
5766

5867
if (!args[1].Equals("open-output", StringComparison.OrdinalIgnoreCase))
59-
return new(false, "Usage: ritsulib selfcheck run|open-output");
68+
return new(false, UsageText());
6069
SelfCheckBundleCoordinator.TryOpenOutputFolderFromSettings();
6170
return new(true, "Requested to open RitsuLib self-check output folder.");
6271
}
72+
73+
private static CmdResult ProcessSettings(string[] args)
74+
{
75+
if (args.Length < 3 || args.Length > 6 || !args[1].Equals("open", StringComparison.OrdinalIgnoreCase))
76+
return new(false, UsageText());
77+
78+
var result = ModSettingsNavigator.RequestOpenByIds(
79+
args[2],
80+
GetOptionalArg(args, 3),
81+
GetOptionalArg(args, 4),
82+
GetOptionalArg(args, 5));
83+
return new(result.Success, result.Message);
84+
}
85+
86+
private CompletionResult CompleteSettingsArguments(string[] args)
87+
{
88+
var partial = args[^1];
89+
var completed = args.Take(args.Length - 1).ToArray();
90+
if (args.Length <= 2)
91+
return CompleteArgument(SettingsActions, completed, partial, CompletionType.Subcommand);
92+
93+
if (!args[1].Equals("open", StringComparison.OrdinalIgnoreCase))
94+
return base.GetArgumentCompletions(null, args);
95+
96+
return args.Length switch
97+
{
98+
3 => CompleteArgument(GetModIdCandidates(), completed, partial),
99+
4 => CompleteArgument(GetPageIdCandidates(args[2]), completed, partial),
100+
5 => CompleteArgument(GetSectionIdCandidates(args[2], args[3]), completed, partial),
101+
6 => CompleteArgument(GetEntryIdCandidates(args[2], args[3], args[4]), completed, partial),
102+
_ => base.GetArgumentCompletions(null, args),
103+
};
104+
}
105+
106+
private static string? GetOptionalArg(string[] args, int index)
107+
{
108+
return args.Length <= index || string.IsNullOrWhiteSpace(args[index]) ? null : args[index];
109+
}
110+
111+
private static string UsageText()
112+
{
113+
return
114+
"Usage: ritsulib selfcheck run|open-output OR ritsulib settings open <modId> [pageId] [sectionId] [entryId]";
115+
}
116+
117+
private static string[] GetModIdCandidates()
118+
{
119+
RefreshSettingsPagesForCompletion();
120+
return ModSettingsRegistry.GetPages()
121+
.Where(IsPageVisible)
122+
.Select(static page => page.ModId)
123+
.Distinct(StringComparer.OrdinalIgnoreCase)
124+
.Order(StringComparer.OrdinalIgnoreCase)
125+
.ToArray();
126+
}
127+
128+
private static string[] GetPageIdCandidates(string modId)
129+
{
130+
RefreshSettingsPagesForCompletion();
131+
return ModSettingsRegistry.GetPages()
132+
.Where(page => string.Equals(page.ModId, modId, StringComparison.OrdinalIgnoreCase))
133+
.Where(IsPageVisible)
134+
.Select(static page => page.Id)
135+
.Order(StringComparer.OrdinalIgnoreCase)
136+
.ToArray();
137+
}
138+
139+
private static string[] GetSectionIdCandidates(string modId, string pageId)
140+
{
141+
RefreshSettingsPagesForCompletion();
142+
return ModSettingsRegistry.TryGetPage(modId, pageId, out var page) && page != null && IsPageVisible(page)
143+
? page.Sections.Where(IsSectionVisible).Select(static section => section.Id)
144+
.Order(StringComparer.OrdinalIgnoreCase).ToArray()
145+
: [];
146+
}
147+
148+
private static string[] GetEntryIdCandidates(string modId, string pageId, string sectionId)
149+
{
150+
RefreshSettingsPagesForCompletion();
151+
if (!ModSettingsRegistry.TryGetPage(modId, pageId, out var page) || page == null || !IsPageVisible(page))
152+
return [];
153+
154+
var section = page.Sections.FirstOrDefault(s => string.Equals(s.Id, sectionId,
155+
StringComparison.OrdinalIgnoreCase));
156+
return section == null || !IsSectionVisible(section)
157+
? []
158+
: section.Entries.Where(IsEntryVisible).Select(static entry => entry.Id)
159+
.Order(StringComparer.OrdinalIgnoreCase).ToArray();
160+
}
161+
162+
private static void RefreshSettingsPagesForCompletion()
163+
{
164+
try
165+
{
166+
RitsuLibModSettingsBootstrap.EnsureFrameworkPagesRegistered();
167+
ModSettingsMirrorRegistrarBootstrap.TryRegisterMirroredPages();
168+
RitsuLibModSettingsBootstrap.RefreshDynamicPages();
169+
}
170+
catch
171+
{
172+
// Completion is best-effort; command execution reports concrete failures.
173+
}
174+
}
175+
176+
private static bool IsPageVisible(ModSettingsPage page)
177+
{
178+
return ModSettingsHostSurfaceResolver.IsVisibleOnCurrentHost(page.VisibleOnHostSurfaces) &&
179+
SafePredicate(page.VisibleWhen);
180+
}
181+
182+
private static bool IsSectionVisible(ModSettingsSection section)
183+
{
184+
return ModSettingsHostSurfaceResolver.IsVisibleOnCurrentHost(section.VisibleOnHostSurfaces) &&
185+
SafePredicate(section.VisibleWhen);
186+
}
187+
188+
private static bool IsEntryVisible(ModSettingsEntryDefinition entry)
189+
{
190+
return SafePredicate(entry.VisibilityPredicate);
191+
}
192+
193+
private static bool SafePredicate(Func<bool>? predicate)
194+
{
195+
if (predicate == null)
196+
return true;
197+
198+
try
199+
{
200+
return predicate();
201+
}
202+
catch
203+
{
204+
return true;
205+
}
206+
}
63207
}
64208
}

STS2-RitsuLib.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
<PropertyGroup Label="NuGet package">
3535
<IsPackable>true</IsPackable>
36-
<Version>0.3.9</Version>
36+
<Version>0.3.10</Version>
3737
<Authors>OLC</Authors>
3838
<Description>Shared framework library for Slay the Spire 2 mods.</Description>
3939
<PackageReadmeFile>README.md</PackageReadmeFile>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using MegaCrit.Sts2.Core.Models;
2+
3+
namespace STS2RitsuLib.Scaffolding.Content
4+
{
5+
/// <summary>
6+
/// Optional gate for whether a mod <see cref="EncounterModel" /> may enter an act's encounter pool for a given
7+
/// <see cref="ActModel" /> during room generation. Implement on
8+
/// <see cref="ModEncounterTemplate" /> or your encounter type; the framework applies it in
9+
/// <see cref="ModEncounterActValidityFilter" />.
10+
/// </summary>
11+
public interface IModEncounterActValidity
12+
{
13+
/// <summary>
14+
/// When false, this encounter is excluded from <see cref="ActModel.GenerateAllEncounters" /> results for
15+
/// <paramref name="act" />. This covers monster, elite, and boss encounter pools.
16+
/// </summary>
17+
bool IsValidForAct(ActModel act);
18+
}
19+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using MegaCrit.Sts2.Core.Models;
2+
3+
namespace STS2RitsuLib.Scaffolding.Content
4+
{
5+
/// <summary>
6+
/// Applies <see cref="IModEncounterActValidity" /> when building per-act encounter candidate pools.
7+
/// </summary>
8+
public static class ModEncounterActValidityFilter
9+
{
10+
/// <summary>
11+
/// Returns whether <paramref name="encounter" /> may appear for <paramref name="act" />.
12+
/// Encounters that do not implement <see cref="IModEncounterActValidity" /> are always allowed.
13+
/// </summary>
14+
public static bool IsValidForAct(ActModel act, EncounterModel encounter)
15+
{
16+
ArgumentNullException.ThrowIfNull(act);
17+
ArgumentNullException.ThrowIfNull(encounter);
18+
19+
return encounter is not IModEncounterActValidity validity || validity.IsValidForAct(act);
20+
}
21+
22+
/// <summary>
23+
/// Keeps only encounters that pass <see cref="IsValidForAct" /> for <paramref name="act" />.
24+
/// </summary>
25+
public static IEnumerable<EncounterModel> FilterForAct(
26+
ActModel act,
27+
IEnumerable<EncounterModel> encounters)
28+
{
29+
ArgumentNullException.ThrowIfNull(act);
30+
ArgumentNullException.ThrowIfNull(encounters);
31+
32+
return encounters.Where(encounter => IsValidForAct(act, encounter));
33+
}
34+
}
35+
}

Scaffolding/Content/ModEncounterTemplate.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ namespace STS2RitsuLib.Scaffolding.Content
88
{
99
/// <summary>
1010
/// Base <see cref="EncounterModel" /> for mods: <see cref="IModEncounterAssetOverrides" /> (combat scene path,
11-
/// backgrounds, boss node, map-node preload, extra paths), optional <see cref="TryCreateEncounterCombatScene" />.
11+
/// backgrounds, boss node, map-node preload, extra paths), optional <see cref="TryCreateEncounterCombatScene" />,
12+
/// and <see cref="IModEncounterActValidity" /> for act-specific spawn gating.
1213
/// The background pipeline matches vanilla <c>EncounterModel.HasCustomBackground</c> semantics, with an explicit
1314
/// switch to keep using the act’s combat
1415
/// background when desired. For disk-free backgrounds, set <see cref="UseProgrammaticCombatBackground" /> and
@@ -37,7 +38,7 @@ namespace STS2RitsuLib.Scaffolding.Content
3738
/// <para />
3839
/// </summary>
3940
public abstract class ModEncounterTemplate : EncounterModel, IModEncounterAssetOverrides,
40-
IModEncounterCombatSceneFactory
41+
IModEncounterCombatSceneFactory, IModEncounterActValidity
4142
{
4243
private BackgroundAssets? _programmaticCombatBackgroundSlot;
4344

@@ -91,6 +92,12 @@ public abstract class ModEncounterTemplate : EncounterModel, IModEncounterAssetO
9192
/// </summary>
9293
protected virtual bool SuppliesEncounterCombatSceneFromFactory => false;
9394

95+
/// <inheritdoc />
96+
public virtual bool IsValidForAct(ActModel act)
97+
{
98+
return true;
99+
}
100+
94101
/// <inheritdoc />
95102
public virtual EncounterAssetProfile AssetProfile => EncounterAssetProfile.Empty;
96103

Settings/Localization/ModSettingsUi/eng.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,8 @@
430430
"button.actionsShort": "Actions",
431431
"button.actionsGlyph": "\u22ee",
432432
"button.resetDefault": "Reset to default",
433+
"button.resetPageDefaults": "Reset page to defaults",
434+
"button.resetSectionDefaults": "Reset section to defaults",
433435
"color.title": "Color Editor",
434436
"color.mode.rgb": "RGB",
435437
"color.mode.hsv": "HSV",

Settings/Localization/ModSettingsUi/zhs.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,8 @@
430430
"button.actionsShort": "操作",
431431
"button.actionsGlyph": "\u22ee",
432432
"button.resetDefault": "还原默认值",
433+
"button.resetPageDefaults": "还原本页默认值",
434+
"button.resetSectionDefaults": "还原本分组默认值",
433435
"color.title": "颜色编辑器",
434436
"color.mode.rgb": "RGB",
435437
"color.mode.hsv": "HSV",

0 commit comments

Comments
 (0)