Skip to content

Commit c21ad99

Browse files
committed
chore(release): merge dev into main for v0.0.33
2 parents 16fbd8b + 610bf15 commit c21ad99

18 files changed

Lines changed: 511 additions & 13 deletions

Const.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public static class Const
1818
/// <summary>
1919
/// Assembly / manifest version string.
2020
/// </summary>
21-
public const string Version = "0.0.32";
21+
public const string Version = "0.0.33";
2222

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

Data/Models/RitsuLibSettings.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public sealed class RitsuLibSettings
1111
/// <summary>
1212
/// Current schema version written by the library when creating or normalizing settings.
1313
/// </summary>
14-
public const int CurrentSchemaVersion = 2;
14+
public const int CurrentSchemaVersion = 3;
1515

1616
/// <summary>
1717
/// Persisted schema version used by the migration pipeline
@@ -48,5 +48,17 @@ public sealed class RitsuLibSettings
4848
/// </summary>
4949
[JsonPropertyName("debug_compat_ancient_architect")]
5050
public bool DebugCompatAncientArchitect { get; set; } = true;
51+
52+
/// <summary>
53+
/// Absolute path or Godot <c>user://</c> path for Harmony patch dump output (text log).
54+
/// </summary>
55+
[JsonPropertyName("harmony_patch_dump_output_path")]
56+
public string HarmonyPatchDumpOutputPath { get; set; } = string.Empty;
57+
58+
/// <summary>
59+
/// When true, writes a dump once when the main menu first finishes loading this session (deferred).
60+
/// </summary>
61+
[JsonPropertyName("harmony_patch_dump_on_first_main_menu")]
62+
public bool HarmonyPatchDumpOnFirstMainMenu { get; set; }
5163
}
5264
}

Data/RitsuLibSettingsStore.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ private static RitsuLibSettings GetSettings()
8989
return Store.Get<RitsuLibSettings>(Const.SettingsKey);
9090
}
9191

92+
/// <summary>
93+
/// Harmony patch dump UI / lifecycle reads paths and flags without exposing the store surface publicly.
94+
/// </summary>
95+
internal static (string OutputPath, bool DumpOnFirstMainMenu) GetHarmonyPatchDumpOptions()
96+
{
97+
Initialize();
98+
var s = GetSettings();
99+
return (s.HarmonyPatchDumpOutputPath, s.HarmonyPatchDumpOnFirstMainMenu);
100+
}
101+
92102
private static void NormalizeSchemaVersionIfNeeded()
93103
{
94104
var settings = GetSettings();
@@ -104,6 +114,8 @@ private static void NormalizeSchemaVersionIfNeeded()
104114
model.DebugCompatAncientArchitect = true;
105115
}
106116

117+
// Schema 3 adds Harmony patch dump options; new properties default via type initializers / JSON.
118+
107119
model.SchemaVersion = RitsuLibSettings.CurrentSchemaVersion;
108120
});
109121
Store.Save(Const.SettingsKey);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using STS2RitsuLib.Data;
2+
3+
namespace STS2RitsuLib.Diagnostics
4+
{
5+
/// <summary>
6+
/// Orchestrates manual and first-main-menu Harmony patch dumps using persisted RitsuLib settings.
7+
/// </summary>
8+
internal static class HarmonyPatchDumpCoordinator
9+
{
10+
private static int _autoDumpIssuedForSession;
11+
12+
/// <summary>
13+
/// Invoked deferred from <see cref="MegaCrit.Sts2.Core.Nodes.Screens.MainMenu.NMainMenu" /> readiness; runs at
14+
/// most once per process when the setting is enabled.
15+
/// </summary>
16+
internal static void TryAutoDumpOnFirstMainMenu()
17+
{
18+
var (path, onFirstMainMenu) = RitsuLibSettingsStore.GetHarmonyPatchDumpOptions();
19+
if (!onFirstMainMenu)
20+
return;
21+
22+
if (Interlocked.CompareExchange(ref _autoDumpIssuedForSession, 1, 0) != 0)
23+
return;
24+
25+
TryDumpToConfiguredPath(path, "[HarmonyDump][Auto]");
26+
}
27+
28+
internal static void TryManualDumpFromSettings()
29+
{
30+
var (path, _) = RitsuLibSettingsStore.GetHarmonyPatchDumpOptions();
31+
TryDumpToConfiguredPath(path, "[HarmonyDump][Manual]");
32+
}
33+
34+
private static void TryDumpToConfiguredPath(string rawPath, string logPrefix)
35+
{
36+
var resolved = HarmonyPatchDumpWriter.TryResolveFilesystemPath(rawPath);
37+
if (string.IsNullOrEmpty(resolved))
38+
{
39+
RitsuLibFramework.Logger.Warn(
40+
$"{logPrefix} Output path is empty or invalid. Set a path in RitsuLib settings (or use Browse).");
41+
return;
42+
}
43+
44+
if (!HarmonyPatchDumpWriter.TryWrite(resolved, out var err))
45+
{
46+
RitsuLibFramework.Logger.Warn($"{logPrefix} Failed to write dump: {err}");
47+
return;
48+
}
49+
50+
RitsuLibFramework.Logger.Info($"{logPrefix} Wrote Harmony patch dump to: {resolved}");
51+
}
52+
}
53+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
using System.Reflection;
2+
using System.Text;
3+
using Godot;
4+
using HarmonyLib;
5+
using FileAccess = System.IO.FileAccess;
6+
7+
namespace STS2RitsuLib.Diagnostics
8+
{
9+
/// <summary>
10+
/// Writes a UTF-8 text report of all Harmony-patched methods (similar to standalone dump mods).
11+
/// </summary>
12+
internal static class HarmonyPatchDumpWriter
13+
{
14+
/// <summary>
15+
/// Resolves <c>user://</c> / <c>res://</c> via Godot and returns an absolute filesystem path.
16+
/// </summary>
17+
internal static string? TryResolveFilesystemPath(string rawPath)
18+
{
19+
if (string.IsNullOrWhiteSpace(rawPath))
20+
return null;
21+
22+
var trimmed = rawPath.Trim();
23+
if (trimmed.StartsWith("user://", StringComparison.OrdinalIgnoreCase) ||
24+
trimmed.StartsWith("res://", StringComparison.OrdinalIgnoreCase))
25+
return ProjectSettings.GlobalizePath(trimmed);
26+
27+
return Path.GetFullPath(trimmed);
28+
}
29+
30+
internal static bool TryWrite(string filesystemPath, out string? errorMessage)
31+
{
32+
errorMessage = null;
33+
try
34+
{
35+
var dir = Path.GetDirectoryName(filesystemPath);
36+
if (!string.IsNullOrEmpty(dir))
37+
Directory.CreateDirectory(dir);
38+
39+
using var fileStream =
40+
new FileStream(filesystemPath, FileMode.Create, FileAccess.Write, FileShare.Read);
41+
using var streamWriter = new StreamWriter(fileStream, Encoding.UTF8);
42+
WriteReport(streamWriter);
43+
return true;
44+
}
45+
catch (Exception ex)
46+
{
47+
errorMessage = ex.Message;
48+
return false;
49+
}
50+
}
51+
52+
private static void WriteReport(StreamWriter streamWriter)
53+
{
54+
streamWriter.WriteLine("=======================================================");
55+
streamWriter.WriteLine("=== Harmony Patch Dump Report ===");
56+
streamWriter.WriteLine("=======================================================");
57+
streamWriter.WriteLine($"Generated at: {DateTime.Now:O}");
58+
streamWriter.WriteLine($"User data dir: {OS.GetUserDataDir()}");
59+
streamWriter.WriteLine("=======================================================");
60+
streamWriter.WriteLine();
61+
62+
var allPatchedMethods = Harmony.GetAllPatchedMethods()
63+
.OrderBy(m => m.DeclaringType?.FullName ?? "Unknown")
64+
.ThenBy(m => m.Name)
65+
.ToList();
66+
67+
var methodCount = 0;
68+
var totalPrefixes = 0;
69+
var totalPostfixes = 0;
70+
var totalTranspilers = 0;
71+
var totalFinalizers = 0;
72+
73+
foreach (var patchedMethod in allPatchedMethods)
74+
{
75+
methodCount++;
76+
var counts = LogPatchedMethodInfo(patchedMethod, streamWriter);
77+
totalPrefixes += counts.prefixes;
78+
totalPostfixes += counts.postfixes;
79+
totalTranspilers += counts.transpilers;
80+
totalFinalizers += counts.finalizers;
81+
streamWriter.WriteLine();
82+
}
83+
84+
streamWriter.WriteLine("=======================================================");
85+
streamWriter.WriteLine("=== Summary ===");
86+
streamWriter.WriteLine("=======================================================");
87+
streamWriter.WriteLine($"Total Patched Methods: {methodCount}");
88+
streamWriter.WriteLine($" - Prefix patches: {totalPrefixes}");
89+
streamWriter.WriteLine($" - Postfix patches: {totalPostfixes}");
90+
streamWriter.WriteLine($" - Transpiler patches: {totalTranspilers}");
91+
streamWriter.WriteLine($" - Finalizer patches: {totalFinalizers}");
92+
streamWriter.WriteLine(
93+
$" - Total patches: {totalPrefixes + totalPostfixes + totalTranspilers + totalFinalizers}");
94+
streamWriter.WriteLine("=======================================================");
95+
}
96+
97+
private static (int prefixes, int postfixes, int transpilers, int finalizers) LogPatchedMethodInfo(
98+
MethodBase methodBase, StreamWriter streamWriter)
99+
{
100+
var patchInfo = Harmony.GetPatchInfo(methodBase);
101+
if (patchInfo == null) return (0, 0, 0, 0);
102+
103+
var declaringType = methodBase.DeclaringType?.FullName ?? "Unknown";
104+
var methodSignature = GetMethodSignature(methodBase);
105+
var returnType = methodBase is MethodInfo mi ? mi.ReturnType.Name : "void";
106+
107+
streamWriter.WriteLine($"┌─ [{declaringType}]");
108+
streamWriter.WriteLine($"│ Method: {returnType} {methodSignature}");
109+
streamWriter.WriteLine("│");
110+
111+
var prefixCount = 0;
112+
var postfixCount = 0;
113+
var transpilerCount = 0;
114+
var finalizerCount = 0;
115+
116+
if (patchInfo.Prefixes.Count > 0)
117+
{
118+
streamWriter.WriteLine($"│ ├─ Prefixes ({patchInfo.Prefixes.Count}):");
119+
foreach (var patch in patchInfo.Prefixes.OrderBy(p => p.priority).ThenBy(p => p.owner))
120+
{
121+
streamWriter.WriteLine($"│ │ {FormatPatchInfo(patch)}");
122+
prefixCount++;
123+
}
124+
}
125+
126+
if (patchInfo.Postfixes.Count > 0)
127+
{
128+
streamWriter.WriteLine($"│ ├─ Postfixes ({patchInfo.Postfixes.Count}):");
129+
foreach (var patch in patchInfo.Postfixes.OrderBy(p => p.priority).ThenBy(p => p.owner))
130+
{
131+
streamWriter.WriteLine($"│ │ {FormatPatchInfo(patch)}");
132+
postfixCount++;
133+
}
134+
}
135+
136+
if (patchInfo.Transpilers.Count > 0)
137+
{
138+
streamWriter.WriteLine($"│ ├─ Transpilers ({patchInfo.Transpilers.Count}):");
139+
foreach (var patch in patchInfo.Transpilers.OrderBy(p => p.priority).ThenBy(p => p.owner))
140+
{
141+
streamWriter.WriteLine($"│ │ {FormatPatchInfo(patch)}");
142+
transpilerCount++;
143+
}
144+
}
145+
146+
if (patchInfo.Finalizers.Count > 0)
147+
{
148+
streamWriter.WriteLine($"│ └─ Finalizers ({patchInfo.Finalizers.Count}):");
149+
foreach (var patch in patchInfo.Finalizers.OrderBy(p => p.priority).ThenBy(p => p.owner))
150+
{
151+
streamWriter.WriteLine($"│ {FormatPatchInfo(patch)}");
152+
finalizerCount++;
153+
}
154+
}
155+
156+
streamWriter.WriteLine("└─────────────────────────────────────────────────────────────────");
157+
158+
return (prefixCount, postfixCount, transpilerCount, finalizerCount);
159+
}
160+
161+
private static string GetMethodSignature(MethodBase methodBase)
162+
{
163+
var parameters = methodBase.GetParameters();
164+
var paramString = string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"));
165+
return $"{methodBase.Name}({paramString})";
166+
}
167+
168+
private static string FormatPatchInfo(Patch patch)
169+
{
170+
var sb = new StringBuilder();
171+
sb.Append($"├─ [Priority: {patch.priority}] ");
172+
sb.Append($"[{patch.owner}] ");
173+
var patchClass = patch.PatchMethod.DeclaringType?.FullName ?? "Unknown";
174+
var patchMethodName = patch.PatchMethod.Name;
175+
sb.Append($"{patchClass}.{patchMethodName}");
176+
177+
try
178+
{
179+
var moduleName = Path.GetFileName(patch.PatchMethod.Module.FullyQualifiedName);
180+
if (!string.IsNullOrEmpty(moduleName) && moduleName != "<Unknown>")
181+
sb.Append($" (from {moduleName})");
182+
}
183+
catch
184+
{
185+
// ignored
186+
}
187+
188+
return sb.ToString();
189+
}
190+
}
191+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using Godot;
2+
using MegaCrit.Sts2.Core.Nodes.Screens.MainMenu;
3+
using STS2RitsuLib.Diagnostics;
4+
using STS2RitsuLib.Patching.Models;
5+
6+
namespace STS2RitsuLib.Lifecycle.Patches
7+
{
8+
/// <summary>
9+
/// After the main menu node is ready, optionally dumps Harmony patch info once per session (deferred).
10+
/// </summary>
11+
public class NMainMenuHarmonyPatchDumpPatch : IPatchMethod
12+
{
13+
/// <inheritdoc />
14+
public static string PatchId => "nmain_menu_harmony_patch_dump";
15+
16+
/// <inheritdoc />
17+
public static string Description =>
18+
"Main menu: deferred optional Harmony patch dump when enabled in RitsuLib settings";
19+
20+
/// <inheritdoc />
21+
public static bool IsCritical => false;
22+
23+
/// <inheritdoc />
24+
public static ModPatchTarget[] GetTargets()
25+
{
26+
return [new(typeof(NMainMenu), "_Ready")];
27+
}
28+
29+
/// <summary>
30+
/// Harmony postfix: schedule auto-dump after the menu finishes its ready work.
31+
/// </summary>
32+
public static void Postfix()
33+
{
34+
Callable.From(HarmonyPatchDumpCoordinator.TryAutoDumpOnFirstMainMenu).CallDeferred();
35+
}
36+
}
37+
}

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ Shared framework library for Slay the Spire 2 mods.
44

55
Chinese README: [README.zh.md](README.zh.md)
66

7-
RitsuLib is maintained as a practical authoring library. API growth is demand-driven and focused on the patterns used by the bundled mods.
7+
RitsuLib is maintained as a practical authoring library. API growth is demand-driven and focused on the patterns used by
8+
the bundled mods.
89

910
The library exists alongside [BaseLib](https://github.com/Alchyr/BaseLib-StS2) and currently does not conflict with it.
1011

@@ -25,13 +26,14 @@ Guide: [Docs/en/ModSettings.md](Docs/en/ModSettings.md)
2526

2627
`debug_compatibility_mode` defaults to **off**. In that state, patched systems keep vanilla behavior.
2728

28-
When the master toggle is **on**, the settings page exposes per-feature compatibility fallbacks. Sub-toggles default to **on**.
29+
When the master toggle is **on**, the settings page exposes per-feature compatibility fallbacks. Sub-toggles default to
30+
**on**.
2931

30-
| Sub-setting | Effect when enabled |
31-
|---|---|
32-
| LocTable missing keys | Resolve to placeholder `LocString` values and log one `[Localization][DebugCompat]` warning per key |
33-
| Invalid unlock epochs | Skip invalid epoch grants and log one `[Unlocks][DebugCompat]` warning per stable key |
34-
| THE_ARCHITECT missing dialogue | Inject empty `Lines` entries for `ModContentRegistry` characters when vanilla provides no dialogue |
32+
| Sub-setting | Effect when enabled |
33+
|--------------------------------|-----------------------------------------------------------------------------------------------------|
34+
| LocTable missing keys | Resolve to placeholder `LocString` values and log one `[Localization][DebugCompat]` warning per key |
35+
| Invalid unlock epochs | Skip invalid epoch grants and log one `[Unlocks][DebugCompat]` warning per stable key |
36+
| THE_ARCHITECT missing dialogue | Inject empty `Lines` entries for `ModContentRegistry` characters when vanilla provides no dialogue |
3537

3638
Disabling a sub-toggle removes only that fallback.
3739

RitsuLibFramework.PatcherSetup.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ private static void RegisterLifecyclePatches()
5454
patcher.RegisterPatch<ModTypeDiscoveryPatch>();
5555
patcher.RegisterPatch<CoreInitializationLifecyclePatch>();
5656
patcher.RegisterPatch<NMainMenuContinueRunMissingCharacterPatch>();
57+
patcher.RegisterPatch<NMainMenuHarmonyPatchDumpPatch>();
5758
patcher.RegisterPatch<NContinueRunInfoShowInfoModelNotFoundPatch>();
5859
patcher.RegisterPatch<NMultiplayerLoadGameScreenBeginRunMissingCharacterPatch>();
5960
patcher.RegisterPatch<NMultiplayerTestCharacterPaginatorAllCharactersPatch>();

RitsuLibFramework.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
using Godot;
44
using MegaCrit.Sts2.Core.Logging;
55
using MegaCrit.Sts2.Core.Modding;
6-
using STS2RitsuLib.Content;
76
using STS2RitsuLib.Combat.HealthBars;
7+
using STS2RitsuLib.Content;
88
using STS2RitsuLib.Data;
99
using STS2RitsuLib.Interop;
1010
using STS2RitsuLib.Keywords;

0 commit comments

Comments
 (0)