Skip to content

Commit 549db3e

Browse files
committed
chore(release): merge dev into main for v0.4.54
2 parents 8df399b + 459d218 commit 549db3e

7 files changed

Lines changed: 245 additions & 22 deletions

File tree

STS2-RitsuLib.csproj

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

4848
<PropertyGroup Label="NuGet package">
4949
<IsPackable>true</IsPackable>
50-
<Version>0.4.53</Version>
50+
<Version>0.4.54</Version>
5151
<Authors>OLC</Authors>
5252
<Description>Shared framework library for Slay the Spire 2 mods.</Description>
5353
<PackageReadmeFile>README.md</PackageReadmeFile>

mod_manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"name": "RitsuLib",
55
"author": "OLC",
66
"description": "A shared Slay the Spire 2 mod framework library providing reusable patching, persistence, lifecycle, localization, and utility APIs for other mods.",
7-
"version": "0.4.53",
7+
"version": "0.4.54",
88
"has_pck": false,
99
"has_dll": true,
1010
"affects_gameplay": false,

src/Combat/HandSize/MaxHandSizePatchInstaller.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,21 @@ internal static void EnsurePatched()
7474
TryAddAsyncMoveNextPatch(builder, AccessTools.Method(typeof(CardPileCmd), nameof(CardPileCmd.Draw),
7575
[typeof(PlayerChoiceContext), typeof(decimal), typeof(Player), typeof(bool)]),
7676
transpilerStateMachine, "Patch CardPileCmd.Draw state machine max-hand-size constants");
77-
TryAddAsyncMoveNextPatch(builder, AccessTools.Method(typeof(CardPileCmd), nameof(CardPileCmd.Add),
78-
[
79-
typeof(IEnumerable<CardModel>), typeof(CardPile), typeof(CardPilePosition),
80-
typeof(AbstractModel), typeof(bool),
81-
]),
77+
TryAddAsyncMoveNextPatch(builder, ResolveCardPileCmdAddMethod(),
8278
transpilerStateMachine, "Patch CardPileCmd.Add state machine max-hand-size constants");
8379

8480
TryAddAsyncMoveNextPatch(builder, AccessTools.Method(typeof(Scrawl), "OnPlay",
8581
[typeof(PlayerChoiceContext), typeof(CardPlay)]),
8682
cardOnPlayTranspiler, "Patch Scrawl.OnPlay hand-size constant");
83+
TryAddAsyncMoveNextPatch(builder, AccessTools.Method(typeof(Anointed), "OnPlay",
84+
[typeof(PlayerChoiceContext), typeof(CardPlay)]),
85+
cardOnPlayTranspiler, "Patch Anointed.OnPlay hand-size constant");
8786
TryAddAsyncMoveNextPatch(builder, AccessTools.Method(typeof(Dredge), "OnPlay",
8887
[typeof(PlayerChoiceContext), typeof(CardPlay)]),
8988
cardOnPlayTranspiler, "Patch Dredge.OnPlay hand-size constant");
89+
TryAddAsyncMoveNextPatch(builder, AccessTools.Method(typeof(NeowsFury), "OnPlay",
90+
[typeof(PlayerChoiceContext), typeof(CardPlay)]),
91+
cardOnPlayTranspiler, "Patch NeowsFury.OnPlay hand-size constant");
9092
TryAddAsyncMoveNextPatch(builder, AccessTools.Method(typeof(CrashLanding), "OnPlay",
9193
[typeof(PlayerChoiceContext), typeof(CardPlay)]),
9294
cardOnPlayTranspiler, "Patch CrashLanding.OnPlay hand-size constant");
@@ -131,6 +133,20 @@ internal static void EnsurePatched()
131133
}
132134
}
133135

136+
private static MethodInfo? ResolveCardPileCmdAddMethod()
137+
{
138+
return AccessTools.Method(typeof(CardPileCmd), nameof(CardPileCmd.Add),
139+
[
140+
typeof(IEnumerable<CardModel>), typeof(CardPile), typeof(CardPilePosition),
141+
typeof(AbstractModel), typeof(bool), typeof(bool),
142+
])
143+
?? AccessTools.Method(typeof(CardPileCmd), nameof(CardPileCmd.Add),
144+
[
145+
typeof(IEnumerable<CardModel>), typeof(CardPile), typeof(CardPilePosition),
146+
typeof(AbstractModel), typeof(bool),
147+
]);
148+
}
149+
134150
private static HarmonyMethod FromMethodAfterBaseLib(string methodName)
135151
{
136152
var method = DynamicPatchBuilder.FromMethod(typeof(MaxHandSizePatchInstaller), methodName);

src/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.4.53";
25+
public const string Version = "0.4.54";
2626

2727
/// <summary>
2828
/// Steam Workshop item id for the official RitsuLib release.

src/Patching/Builders/DynamicPatchBuilder.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,83 @@ public DynamicPatchBuilder AddMethod(
200200
patchId);
201201
}
202202

203+
/// <summary>
204+
/// Attempts to resolve and append a method patch. Returns false when the target type is null or the target method
205+
/// is missing, instead of throwing. Use this for optional compatibility patches against another mod or a
206+
/// version-varying game API.
207+
/// 尝试解析并追加方法 patch。目标类型为 null 或目标方法缺失时返回 false,而不是抛出异常。
208+
/// 适用于针对其他 mod 或跨版本游戏 API 的可选兼容 patch。
209+
/// </summary>
210+
public bool TryAddMethod(
211+
Type? targetType,
212+
string methodName,
213+
Type[]? parameterTypes = null,
214+
HarmonyMethod? prefix = null,
215+
HarmonyMethod? postfix = null,
216+
HarmonyMethod? transpiler = null,
217+
HarmonyMethod? finalizer = null,
218+
bool isCritical = false,
219+
string? description = null,
220+
string? patchId = null,
221+
MethodType harmonyMethodType = MethodType.Normal)
222+
{
223+
ArgumentException.ThrowIfNullOrWhiteSpace(methodName);
224+
225+
if (targetType == null)
226+
return false;
227+
228+
var method = PatchTargetMethodResolver.Resolve(targetType, methodName, parameterTypes, harmonyMethodType);
229+
if (method == null)
230+
return false;
231+
232+
Add(
233+
method,
234+
prefix,
235+
postfix,
236+
transpiler,
237+
finalizer,
238+
isCritical,
239+
description ?? $"Patch method {targetType.Name}.{methodName}",
240+
patchId);
241+
return true;
242+
}
243+
244+
/// <summary>
245+
/// Attempts to resolve <paramref name="targetTypeName" /> with Harmony <see cref="AccessTools.TypeByName" /> and
246+
/// append a method patch. Returns false when the type or method is absent. This keeps optional inter-mod patches
247+
/// from needing their own reflection boilerplate.
248+
/// 使用 Harmony <see cref="AccessTools.TypeByName" /> 尝试解析 <paramref name="targetTypeName" /> 并追加方法
249+
/// patch。类型或方法不存在时返回 false,用于减少可选跨 mod patch 的反射样板代码。
250+
/// </summary>
251+
public bool TryAddMethodByName(
252+
string targetTypeName,
253+
string methodName,
254+
Type[]? parameterTypes = null,
255+
HarmonyMethod? prefix = null,
256+
HarmonyMethod? postfix = null,
257+
HarmonyMethod? transpiler = null,
258+
HarmonyMethod? finalizer = null,
259+
bool isCritical = false,
260+
string? description = null,
261+
string? patchId = null,
262+
MethodType harmonyMethodType = MethodType.Normal)
263+
{
264+
ArgumentException.ThrowIfNullOrWhiteSpace(targetTypeName);
265+
266+
return TryAddMethod(
267+
AccessTools.TypeByName(targetTypeName),
268+
methodName,
269+
parameterTypes,
270+
prefix,
271+
postfix,
272+
transpiler,
273+
finalizer,
274+
isCritical,
275+
description,
276+
patchId,
277+
harmonyMethodType);
278+
}
279+
203280
/// <summary>
204281
/// Wraps a static patch method on <paramref name="patchType" /> as a <see cref="HarmonyMethod" />.
205282
/// 将 <paramref name="patchType" /> 上的静态 patch 方法包装为 <see cref="HarmonyMethod" />。

src/Patching/Core/ModPatcher.cs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,96 @@ public void ApplyLateStaticPatches(ReadOnlySpan<ModPatchInfo> patches)
289289
}
290290
}
291291

292+
/// <summary>
293+
/// Removes Harmony patches owned by another Harmony id from a resolved target method. This is intended for narrow
294+
/// compatibility fixes where a dependency's patch is unsafe on the current game version and the caller replaces it
295+
/// with a safe patch through this patcher.
296+
/// 从已解析的目标方法上移除另一个 Harmony id 拥有的 Harmony patch。用于依赖 mod 的 patch 在当前游戏版本
297+
/// 不安全、调用方需要用本 patcher 的安全 patch 替换它的窄范围兼容修复。
298+
/// </summary>
299+
/// <param name="originalMethod">
300+
/// Original method whose patch list should be inspected.
301+
/// 要检查 patch 列表的原始方法。
302+
/// </param>
303+
/// <param name="owner">
304+
/// Harmony owner id to remove.
305+
/// 要移除的 Harmony owner id。
306+
/// </param>
307+
/// <param name="patchDeclaringType">
308+
/// Optional declaring type filter for the patch method.
309+
/// 可选的 patch 方法声明类型过滤器。
310+
/// </param>
311+
/// <param name="patchMethodName">
312+
/// Optional patch method name filter.
313+
/// 可选的 patch 方法名过滤器。
314+
/// </param>
315+
/// <param name="patchType">
316+
/// Harmony patch kind to inspect; <see cref="HarmonyPatchType.All" /> checks every kind.
317+
/// 要检查的 Harmony patch 类型;<see cref="HarmonyPatchType.All" /> 会检查所有类型。
318+
/// </param>
319+
/// <returns>
320+
/// Number of patch methods removed.
321+
/// 已移除的 patch 方法数量。
322+
/// </returns>
323+
public int UnpatchExternalPatches(
324+
MethodBase originalMethod,
325+
string owner,
326+
Type? patchDeclaringType = null,
327+
string? patchMethodName = null,
328+
HarmonyPatchType patchType = HarmonyPatchType.All)
329+
{
330+
ArgumentNullException.ThrowIfNull(originalMethod);
331+
ArgumentException.ThrowIfNullOrWhiteSpace(owner);
332+
333+
var patchInfo = Harmony.GetPatchInfo(originalMethod);
334+
if (patchInfo == null)
335+
{
336+
logger.Info($"{_logPrefix}No Harmony patches found on {FormatMethod(originalMethod)}");
337+
return 0;
338+
}
339+
340+
var patches = EnumeratePatches(patchInfo, patchType)
341+
.Where(patch => MatchesPatch(patch, owner, patchDeclaringType, patchMethodName))
342+
.ToArray();
343+
344+
foreach (var patch in patches) _harmony.Unpatch(originalMethod, patch.PatchMethod);
345+
346+
logger.Info(
347+
$"{_logPrefix}Removed {patches.Length} external patch(es) from {FormatMethod(originalMethod)} owned by '{owner}'");
348+
return patches.Length;
349+
}
350+
351+
/// <summary>
352+
/// Resolves <paramref name="target" /> and removes matching external Harmony patches. Missing targets can be
353+
/// ignored for optional compatibility paths.
354+
/// 解析 <paramref name="target" /> 并移除匹配的外部 Harmony patch。可选兼容路径可以忽略缺失目标。
355+
/// </summary>
356+
public int UnpatchExternalPatches(
357+
ModPatchTarget target,
358+
string owner,
359+
Type? patchDeclaringType = null,
360+
string? patchMethodName = null,
361+
HarmonyPatchType patchType = HarmonyPatchType.All,
362+
bool ignoreIfTargetMissing = true)
363+
{
364+
ArgumentNullException.ThrowIfNull(target);
365+
366+
var originalMethod = PatchTargetMethodResolver.Resolve(target);
367+
if (originalMethod != null)
368+
return UnpatchExternalPatches(
369+
originalMethod,
370+
owner,
371+
patchDeclaringType,
372+
patchMethodName,
373+
patchType);
374+
375+
var message = $"{_logPrefix}External unpatch target not found: {target}";
376+
if (!ignoreIfTargetMissing && !target.IgnoreIfMissing)
377+
throw new MissingMethodException(target.TargetType.FullName, target.MethodName);
378+
logger.Info(message);
379+
return 0;
380+
}
381+
292382
/// <summary>
293383
/// Removes all applied patches tracked by this instance from the underlying Harmony id.
294384
/// 从底层 Harmony id 移除此实例跟踪的所有已应用补丁。
@@ -498,6 +588,46 @@ private bool ProcessPatchResults(ReadOnlySpan<ModPatchResult> results)
498588
);
499589
}
500590

591+
private static IEnumerable<Patch> EnumeratePatches(Patches patchInfo, HarmonyPatchType patchType)
592+
{
593+
if (patchType is HarmonyPatchType.All or HarmonyPatchType.Prefix)
594+
foreach (var patch in patchInfo.Prefixes)
595+
yield return patch;
596+
597+
if (patchType is HarmonyPatchType.All or HarmonyPatchType.Postfix)
598+
foreach (var patch in patchInfo.Postfixes)
599+
yield return patch;
600+
601+
if (patchType is HarmonyPatchType.All or HarmonyPatchType.Transpiler)
602+
foreach (var patch in patchInfo.Transpilers)
603+
yield return patch;
604+
605+
// ReSharper disable once InvertIf
606+
if (patchType is HarmonyPatchType.All or HarmonyPatchType.Finalizer)
607+
foreach (var patch in patchInfo.Finalizers)
608+
yield return patch;
609+
}
610+
611+
private static bool MatchesPatch(
612+
Patch patch,
613+
string owner,
614+
Type? patchDeclaringType,
615+
string? patchMethodName)
616+
{
617+
if (patch.owner != owner)
618+
return false;
619+
620+
if (patchDeclaringType != null && patch.PatchMethod.DeclaringType != patchDeclaringType)
621+
return false;
622+
623+
return string.IsNullOrEmpty(patchMethodName) || patch.PatchMethod.Name == patchMethodName;
624+
}
625+
626+
private static string FormatMethod(MethodBase method)
627+
{
628+
return $"{method.DeclaringType?.FullName}.{method.Name}";
629+
}
630+
501631
/// <summary>
502632
/// Validate that patch type implements IPatchMethod interface (optional but recommended)
503633
/// 验证 patch 类型是否实现 IPatchMethod 接口(可选但推荐)

src/Scaffolding/Godot/NodeFactories/RitsuNEnergyCounterNodeFactory.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,11 @@ protected override void GenerateNode(NEnergyCounter target, IRitsuGodotNodeSlot
6868
target.AddChild(CreateDefaultLabel());
6969
break;
7070
case "%RotationLayers":
71-
{
72-
var control = CreateFullRectControl(null);
73-
target.AddUniqueChild(control, "RotationLayers");
74-
break;
75-
}
71+
{
72+
var control = CreateFullRectControl(null);
73+
target.AddUniqueChild(control, "RotationLayers");
74+
break;
75+
}
7676
case "%EnergyVfxBack":
7777
target.AddUniqueChild(CreateParticlesContainer(null, "EnergyVfxBack"));
7878
break;
@@ -131,18 +131,18 @@ private static NParticlesContainer CreateParticlesContainer(Node? source, String
131131
if (source is CanvasItem sourceCanvas)
132132
CopyCanvasItemProperties(container, sourceCanvas);
133133

134-
if (source is GpuParticles2D singleParticle)
134+
switch (source)
135135
{
136-
container.AddChild(singleParticle);
137-
singleParticle.Owner = container;
138-
return container;
136+
case GpuParticles2D singleParticle:
137+
container.AddChild(singleParticle);
138+
singleParticle.Owner = container;
139+
return container;
140+
case null:
141+
return container;
139142
}
140143

141-
if (source != null)
142-
{
143-
source.GetParent()?.RemoveChild(source);
144-
container.AddChild(source);
145-
}
144+
source.GetParent()?.RemoveChild(source);
145+
container.AddChild(source);
146146

147147
return container;
148148
}

0 commit comments

Comments
 (0)