Skip to content

Commit 607d773

Browse files
committed
chore(release): merge dev into main for v0.3.0
2 parents f80d8a6 + 37e3913 commit 607d773

49 files changed

Lines changed: 602 additions & 1279 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Audio/FmodStudioDirectOneShots.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ public static class FmodStudioDirectOneShots
2525
/// </summary>
2626
public static bool TryPlay(string eventPath)
2727
{
28-
return FmodStudioGateway.TryCall(FmodStudioMethodNames.PlayOneShot, eventPath);
28+
return !string.IsNullOrWhiteSpace(eventPath) &&
29+
FmodStudioGateway.TryCall(FmodStudioMethodNames.PlayOneShot, eventPath);
2930
}
3031

3132
/// <summary>
@@ -34,6 +35,9 @@ public static bool TryPlay(string eventPath)
3435
/// </summary>
3536
public static bool TryPlay(string eventPath, IReadOnlyDictionary<string, float> parameters)
3637
{
38+
if (string.IsNullOrWhiteSpace(eventPath))
39+
return false;
40+
3741
var server = FmodStudioGateway.TryGetServer();
3842
if (server is null)
3943
return false;

Audio/FmodStudioEventInstances.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ public static class FmodStudioEventInstances
3434
/// </summary>
3535
public static GodotObject? TryCreate(string eventOrSnapshotPath)
3636
{
37+
if (string.IsNullOrWhiteSpace(eventOrSnapshotPath))
38+
return null;
39+
3740
if (!FmodStudioGuidPathTable.TryGetStudioGuidForEventPath(eventOrSnapshotPath, out var mappedGuid))
3841
return TryCreateByPathOnly(eventOrSnapshotPath);
3942

@@ -56,6 +59,9 @@ public static class FmodStudioEventInstances
5659
/// </summary>
5760
private static bool? ProbeStudioHasEventPath(string eventPath)
5861
{
62+
if (string.IsNullOrWhiteSpace(eventPath))
63+
return false;
64+
5965
if (!FmodStudioGateway.TryCall(out var v, FmodStudioMethodNames.CheckEventPath, eventPath))
6066
return null;
6167

@@ -89,6 +95,9 @@ public static class FmodStudioEventInstances
8995

9096
private static GodotObject? TryCreateByPathOnly(string eventOrSnapshotPath)
9197
{
98+
if (string.IsNullOrWhiteSpace(eventOrSnapshotPath))
99+
return null;
100+
92101
return !FmodStudioGateway.TryCall(out var v, FmodStudioMethodNames.CreateEventInstance, eventOrSnapshotPath)
93102
? null
94103
: v.AsGodotObject();

Audio/FmodStudioServer.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ private static void WarnIfMappedEventGuidsUnresolved()
223223
/// </summary>
224224
public static bool? TryCheckEventPath(string eventPath)
225225
{
226+
if (string.IsNullOrWhiteSpace(eventPath))
227+
return false;
228+
226229
if (FmodStudioGuidPathTable.TryGetStudioGuidForEventPath(eventPath, out _))
227230
return true;
228231

Audio/Patches/NAudioManagerGuidMappedStudioEventsPatches.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ public static bool Prefix(NAudioManager __instance, string path, Dictionary<stri
8383
if (TestMode.IsOn)
8484
return true;
8585

86-
if (string.IsNullOrEmpty(path))
87-
return true;
86+
if (string.IsNullOrWhiteSpace(path))
87+
return false;
8888

8989
if (!FmodStudioGuidPathTable.TryGetStudioGuidForEventPath(path, out var mappedGuid))
9090
{
@@ -137,8 +137,8 @@ public static bool Prefix(NAudioManager __instance, string path, bool usesLoopPa
137137
if (TestMode.IsOn)
138138
return true;
139139

140-
if (string.IsNullOrEmpty(path))
141-
return true;
140+
if (string.IsNullOrWhiteSpace(path))
141+
return false;
142142

143143
if (!GuidMappedNaudioStudioProxy.IsMappedPath(path))
144144
{
@@ -314,8 +314,8 @@ public static bool Prefix(NAudioManager __instance, string music)
314314
if (TestMode.IsOn)
315315
return true;
316316

317-
if (string.IsNullOrEmpty(music))
318-
return true;
317+
if (string.IsNullOrWhiteSpace(music))
318+
return false;
319319

320320
if (!GuidMappedNaudioStudioProxy.IsMappedPath(music))
321321
{

Combat/CardTargeting/CustomTargetType.cs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using MegaCrit.Sts2.Core.Entities.Cards;
2+
using MegaCrit.Sts2.Core.Entities.Creatures;
23
using STS2RitsuLib.Content;
34
using STS2RitsuLib.Utils;
45

@@ -117,9 +118,77 @@ public static bool IsCustomMultiTargetType(TargetType type)
117118
return CustomTargetTypeRegistry.IsCustomMultiTargetType(type);
118119
}
119120

121+
/// <summary>
122+
/// Registers a mod-scoped single-target <see cref="TargetType" /> and returns the deterministic enum value.
123+
/// The returned value is stable for the same <paramref name="modId" /> and <paramref name="localStem" />.
124+
/// 注册一个 mod 作用域的单体目标 <see cref="TargetType" />,并返回确定性的枚举值。
125+
/// 相同 <paramref name="modId" /> 与 <paramref name="localStem" /> 会得到稳定相同的返回值。
126+
/// </summary>
127+
/// <param name="modId">
128+
/// Owning mod id.
129+
/// 所属 mod ID。
130+
/// </param>
131+
/// <param name="localStem">
132+
/// Stable local id stem, normalized into <c>MODID_TARGETTYPE_STEM</c>.
133+
/// 稳定的本地 ID 词干,会规范化为 <c>MODID_TARGETTYPE_STEM</c>。
134+
/// </param>
135+
/// <param name="canTarget">
136+
/// Predicate used by mouse/controller targeting and card validation for candidate creatures.
137+
/// 鼠标 / 手柄选目标与卡牌目标校验使用的候选生物谓词。
138+
/// </param>
139+
public static TargetType RegisterSingleTargetType(
140+
string modId,
141+
string localStem,
142+
Func<Creature, bool> canTarget)
143+
{
144+
ArgumentException.ThrowIfNullOrWhiteSpace(modId);
145+
ArgumentException.ThrowIfNullOrWhiteSpace(localStem);
146+
ArgumentNullException.ThrowIfNull(canTarget);
147+
148+
var id = ModContentRegistry.GetQualifiedTargetTypeId(modId, localStem);
149+
var type = TargetTypeMinter.Mint(id);
150+
CustomTargetTypeRegistry.RegisterSingleTargetType(type, id, canTarget);
151+
return type;
152+
}
153+
154+
/// <summary>
155+
/// Registers a mod-scoped multi-target <see cref="TargetType" /> and returns the deterministic enum value.
156+
/// Cards using this target type play once with a null selected target; use
157+
/// <c>CardModelTargetingExtensions.GetTargets(...)</c> to resolve the affected creatures in card logic.
158+
/// 注册一个 mod 作用域的群体目标 <see cref="TargetType" />,并返回确定性的枚举值。
159+
/// 使用此目标类型的卡牌会以 null 已选目标打出一次;卡牌逻辑中可用
160+
/// <c>CardModelTargetingExtensions.GetTargets(...)</c> 解析实际影响的生物。
161+
/// </summary>
162+
/// <param name="modId">
163+
/// Owning mod id.
164+
/// 所属 mod ID。
165+
/// </param>
166+
/// <param name="localStem">
167+
/// Stable local id stem, normalized into <c>MODID_TARGETTYPE_STEM</c>.
168+
/// 稳定的本地 ID 词干,会规范化为 <c>MODID_TARGETTYPE_STEM</c>。
169+
/// </param>
170+
/// <param name="includeTarget">
171+
/// Predicate used for multi-target reticles and target resolution.
172+
/// 群体目标指示器与目标解析使用的谓词。
173+
/// </param>
174+
public static TargetType RegisterMultiTargetType(
175+
string modId,
176+
string localStem,
177+
Func<Creature, bool> includeTarget)
178+
{
179+
ArgumentException.ThrowIfNullOrWhiteSpace(modId);
180+
ArgumentException.ThrowIfNullOrWhiteSpace(localStem);
181+
ArgumentNullException.ThrowIfNull(includeTarget);
182+
183+
var id = ModContentRegistry.GetQualifiedTargetTypeId(modId, localStem);
184+
var type = TargetTypeMinter.Mint(id);
185+
CustomTargetTypeRegistry.RegisterMultiTargetType(type, id, includeTarget);
186+
return type;
187+
}
188+
120189
private static TargetType Mint(string localStem)
121190
{
122-
var id = ModContentRegistry.GetCompoundId(Const.ModId, "TARGETTYPE", localStem);
191+
var id = ModContentRegistry.GetQualifiedTargetTypeId(Const.ModId, localStem);
123192
return TargetTypeMinter.Mint(id);
124193
}
125194
}

Combat/CardTargeting/CustomTargetTypeRegistry.cs

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ namespace STS2RitsuLib.Combat.CardTargeting
99
/// </summary>
1010
internal static class CustomTargetTypeRegistry
1111
{
12+
private static readonly Lock SyncRoot = new();
13+
1214
/// <summary>
1315
/// Predicate map for custom single-target types.
1416
/// 自定义单体目标类型的谓词映射。
@@ -21,13 +23,18 @@ internal static class CustomTargetTypeRegistry
2123
/// </summary>
2224
private static readonly Dictionary<TargetType, Func<Creature, bool>> MultiTargetPredicates = [];
2325

26+
private static readonly Dictionary<TargetType, CustomTargetTypeRegistration> Registrations = [];
27+
2428
/// <summary>
2529
/// Returns whether <paramref name="type" /> belongs to any registered custom target category.
2630
/// 返回 <paramref name="type" /> 是否属于任一已注册的自定义目标类别。
2731
/// </summary>
2832
internal static bool IsRitsuCustom(TargetType type)
2933
{
30-
return SingleTargetPredicates.ContainsKey(type) || MultiTargetPredicates.ContainsKey(type);
34+
lock (SyncRoot)
35+
{
36+
return Registrations.ContainsKey(type);
37+
}
3138
}
3239

3340
/// <summary>
@@ -36,7 +43,10 @@ internal static bool IsRitsuCustom(TargetType type)
3643
/// </summary>
3744
internal static bool IsCustomSingleTargetType(TargetType type)
3845
{
39-
return SingleTargetPredicates.ContainsKey(type);
46+
lock (SyncRoot)
47+
{
48+
return SingleTargetPredicates.ContainsKey(type);
49+
}
4050
}
4151

4252
/// <summary>
@@ -45,7 +55,10 @@ internal static bool IsCustomSingleTargetType(TargetType type)
4555
/// </summary>
4656
internal static bool IsCustomMultiTargetType(TargetType type)
4757
{
48-
return MultiTargetPredicates.ContainsKey(type);
58+
lock (SyncRoot)
59+
{
60+
return MultiTargetPredicates.ContainsKey(type);
61+
}
4962
}
5063

5164
/// <summary>
@@ -54,7 +67,13 @@ internal static bool IsCustomMultiTargetType(TargetType type)
5467
/// </summary>
5568
internal static bool TryIsAllowedSingleTarget(TargetType type, Creature creature, out bool allowed)
5669
{
57-
if (!SingleTargetPredicates.TryGetValue(type, out var predicate))
70+
Func<Creature, bool>? predicate;
71+
lock (SyncRoot)
72+
{
73+
SingleTargetPredicates.TryGetValue(type, out predicate);
74+
}
75+
76+
if (predicate == null)
5877
{
5978
allowed = false;
6079
return false;
@@ -70,7 +89,13 @@ internal static bool TryIsAllowedSingleTarget(TargetType type, Creature creature
7089
/// </summary>
7190
internal static bool TryShouldIncludeMultiTarget(TargetType type, Creature creature, out bool include)
7291
{
73-
if (!MultiTargetPredicates.TryGetValue(type, out var predicate))
92+
Func<Creature, bool>? predicate;
93+
lock (SyncRoot)
94+
{
95+
MultiTargetPredicates.TryGetValue(type, out predicate);
96+
}
97+
98+
if (predicate == null)
7499
{
75100
include = false;
76101
return false;
@@ -86,7 +111,17 @@ internal static bool TryShouldIncludeMultiTarget(TargetType type, Creature creat
86111
/// </summary>
87112
internal static void RegisterSingleTargetType(TargetType type, Func<Creature, bool> predicate)
88113
{
89-
SingleTargetPredicates[type] = predicate;
114+
Register(type, null, CustomTargetTypeKind.Single, predicate);
115+
}
116+
117+
/// <summary>
118+
/// Registers or replaces a custom single-target predicate with a diagnostic id.
119+
/// 使用诊断 ID 注册或替换一个自定义单体目标谓词。
120+
/// </summary>
121+
internal static void RegisterSingleTargetType(TargetType type, string id, Func<Creature, bool> predicate)
122+
{
123+
ArgumentException.ThrowIfNullOrWhiteSpace(id);
124+
Register(type, id, CustomTargetTypeKind.Single, predicate);
90125
}
91126

92127
/// <summary>
@@ -95,18 +130,25 @@ internal static void RegisterSingleTargetType(TargetType type, Func<Creature, bo
95130
/// </summary>
96131
internal static void RegisterMultiTargetType(TargetType type, Func<Creature, bool> predicate)
97132
{
98-
MultiTargetPredicates[type] = predicate;
133+
Register(type, null, CustomTargetTypeKind.Multi, predicate);
99134
}
100135

101136
/// <summary>
102-
/// Clears and re-registers all built-in custom target definitions.
103-
/// 清空并重新注册全部内置自定义目标定义
137+
/// Registers or replaces a custom multi-target predicate with a diagnostic id.
138+
/// 使用诊断 ID 注册或替换一个自定义群体目标谓词
104139
/// </summary>
105-
internal static void RegisterBuiltIns()
140+
internal static void RegisterMultiTargetType(TargetType type, string id, Func<Creature, bool> predicate)
106141
{
107-
SingleTargetPredicates.Clear();
108-
MultiTargetPredicates.Clear();
142+
ArgumentException.ThrowIfNullOrWhiteSpace(id);
143+
Register(type, id, CustomTargetTypeKind.Multi, predicate);
144+
}
109145

146+
/// <summary>
147+
/// Registers all built-in custom target definitions while preserving mod-registered predicates.
148+
/// 注册全部内置自定义目标定义,同时保留 mod 注册的谓词。
149+
/// </summary>
150+
internal static void RegisterBuiltIns()
151+
{
110152
RegisterSingleTargetType(CustomTargetType.Anyone, target => target is { IsAlive: true, IsPet: false });
111153
RegisterMultiTargetType(CustomTargetType.Everyone, target => target is { IsAlive: true, IsPet: false });
112154

@@ -159,5 +201,52 @@ private static bool IsEnemyHpExtremum(Creature target, bool lowest)
159201
var extremum = lowest ? enemies.Min(e => e.CurrentHp) : enemies.Max(e => e.CurrentHp);
160202
return target.CurrentHp == extremum;
161203
}
204+
205+
private static void Register(
206+
TargetType type,
207+
string? id,
208+
CustomTargetTypeKind kind,
209+
Func<Creature, bool> predicate)
210+
{
211+
ArgumentNullException.ThrowIfNull(predicate);
212+
213+
lock (SyncRoot)
214+
{
215+
var diagnosticId = id;
216+
if (Registrations.TryGetValue(type, out var existing))
217+
{
218+
diagnosticId ??= existing.Id;
219+
if (existing.Kind != kind)
220+
throw new InvalidOperationException(
221+
$"TargetType '{diagnosticId}' is already registered as "
222+
+ $"{existing.Kind}; it cannot also be registered as {kind}.");
223+
}
224+
225+
diagnosticId ??= ((int)type).ToString();
226+
Registrations[type] = new(diagnosticId, kind);
227+
228+
switch (kind)
229+
{
230+
case CustomTargetTypeKind.Single:
231+
SingleTargetPredicates[type] = predicate;
232+
MultiTargetPredicates.Remove(type);
233+
break;
234+
case CustomTargetTypeKind.Multi:
235+
MultiTargetPredicates[type] = predicate;
236+
SingleTargetPredicates.Remove(type);
237+
break;
238+
default:
239+
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
240+
}
241+
}
242+
}
243+
244+
private enum CustomTargetTypeKind
245+
{
246+
Single,
247+
Multi,
248+
}
249+
250+
private readonly record struct CustomTargetTypeRegistration(string Id, CustomTargetTypeKind Kind);
162251
}
163252
}

Combat/Powers/ModTemporaryPowerTemplate.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,12 @@ await ApplyInternalPower(new ThrowingPlayerChoiceContext(), Owner, SignedAmount(
195195
}
196196

197197
/// <inheritdoc />
198+
#if STS2_AT_LEAST_0_106_0
199+
public override async Task AfterSideTurnEnd(PlayerChoiceContext choiceContext, CombatSide side,
200+
IEnumerable<Creature> participants)
201+
#else
198202
public override async Task AfterTurnEnd(PlayerChoiceContext choiceContext, CombatSide side)
203+
#endif
199204
{
200205
var expiresOnThisSide = UntilEndOfOtherSideTurn ? side != Owner.Side : side == Owner.Side;
201206
if (!expiresOnThisSide)

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.2.40";
25+
public const string Version = "0.3.0";
2626

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

0 commit comments

Comments
 (0)