Skip to content

Commit ca395cc

Browse files
committed
chore(release): merge dev into main for v0.2.38
2 parents 57ae075 + 69fab7f commit ca395cc

21 files changed

Lines changed: 586 additions & 237 deletions

Compat/Sts2ModManagerCompat.cs

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Reflection;
2+
using MegaCrit.Sts2.Core.Localization;
13
using MegaCrit.Sts2.Core.Modding;
24

35
namespace STS2RitsuLib.Compat
@@ -8,6 +10,29 @@ namespace STS2RitsuLib.Compat
810
/// </summary>
911
internal static class Sts2ModManagerCompat
1012
{
13+
private const BindingFlags InstanceMemberFlags =
14+
BindingFlags.Instance |
15+
BindingFlags.Public |
16+
BindingFlags.NonPublic;
17+
18+
private static readonly Func<Mod, ModManifest?> ReadManifest = CreateModManifestAccessor();
19+
private static readonly Func<Mod, Assembly?> ReadAssembly = CreateModAssemblyAccessor();
20+
private static readonly Func<Mod, IReadOnlyList<LocString>> ReadErrors = CreateModErrorsAccessor();
21+
private static readonly Func<Mod, string> ReadSource = CreateModSourceAccessor();
22+
private static readonly Func<Mod, int, string> ReadLoadState = CreateLoadStateAccessor();
23+
24+
private static readonly Func<ModManifest, string?> ReadManifestId =
25+
CreateManifestStringAccessor("id", static manifest => manifest.id);
26+
27+
private static readonly Func<ModManifest, string?> ReadManifestName =
28+
CreateManifestStringAccessor("name", static manifest => manifest.name);
29+
30+
private static readonly Func<ModManifest, string?> ReadManifestVersion =
31+
CreateManifestStringAccessor("version", static manifest => manifest.version);
32+
33+
private static readonly Func<ModManifest, bool> ReadManifestAffectsGameplay =
34+
CreateManifestBoolAccessor("affectsGameplay", static manifest => manifest.affectsGameplay, true);
35+
1136
internal static IEnumerable<Mod> EnumerateLoadedModsWithAssembly()
1237
{
1338
return ModManager.GetLoadedMods();
@@ -21,5 +46,206 @@ internal static IEnumerable<Mod> EnumerateModsForManifestLookup()
2146
{
2247
return ModManager.Mods;
2348
}
49+
50+
internal static IReadOnlyList<Sts2ModInventoryEntry> BuildModInventoryEntries()
51+
{
52+
return EnumerateModsForManifestLookup()
53+
.Select(TryBuildModInventoryEntry)
54+
.Where(entry => entry != null)
55+
.Select(entry => entry!)
56+
.OrderBy(entry => entry.Id, StringComparer.OrdinalIgnoreCase)
57+
.ThenBy(entry => entry.AssemblyName ?? "", StringComparer.OrdinalIgnoreCase)
58+
.ToArray();
59+
}
60+
61+
private static Sts2ModInventoryEntry? TryBuildModInventoryEntry(Mod mod)
62+
{
63+
try
64+
{
65+
var manifest = ReadManifest(mod);
66+
var assembly = ReadAssembly(mod);
67+
var assemblyName = ResolveAssemblyName(assembly);
68+
var errors = ReadErrors(mod);
69+
var fallbackName = assemblyName?.Name ?? "<unknown>";
70+
return new(
71+
manifest == null ? fallbackName : ReadManifestId(manifest) ?? fallbackName,
72+
manifest == null ? fallbackName : ReadManifestName(manifest) ?? fallbackName,
73+
manifest == null ? null : ReadManifestVersion(manifest),
74+
ReadLoadState(mod, errors.Count),
75+
ReadSource(mod),
76+
manifest == null || ReadManifestAffectsGameplay(manifest),
77+
assemblyName?.Name,
78+
assemblyName?.Version?.ToString(),
79+
errors);
80+
}
81+
catch (Exception ex)
82+
{
83+
RitsuLibFramework.Logger.Warn(
84+
$"[Compat] Failed to describe a registered mod for inventory telemetry: {ex.Message}");
85+
return null;
86+
}
87+
}
88+
89+
private static Func<Mod, ModManifest?> CreateModManifestAccessor()
90+
{
91+
if (typeof(Mod).GetField("manifest", InstanceMemberFlags) != null)
92+
return static mod => mod.manifest;
93+
94+
var getter = CreateUntypedMemberGetter(typeof(Mod), "manifest");
95+
return mod => getter?.Invoke(mod) as ModManifest;
96+
}
97+
98+
private static Func<Mod, Assembly?> CreateModAssemblyAccessor()
99+
{
100+
if (typeof(Mod).GetField("assembly", InstanceMemberFlags) != null)
101+
return static mod => mod.assembly;
102+
103+
var getter = CreateUntypedMemberGetter(typeof(Mod), "assembly");
104+
return mod => getter?.Invoke(mod) as Assembly;
105+
}
106+
107+
private static Func<Mod, IReadOnlyList<LocString>> CreateModErrorsAccessor()
108+
{
109+
if (typeof(Mod).GetField("errors", InstanceMemberFlags) != null)
110+
return static mod => NormalizeErrors(mod.errors);
111+
112+
var getter = CreateUntypedMemberGetter(typeof(Mod), "errors");
113+
return mod => NormalizeErrors(getter?.Invoke(mod) as IEnumerable<LocString>);
114+
}
115+
116+
private static Func<Mod, string> CreateModSourceAccessor()
117+
{
118+
if (typeof(Mod).GetField("modSource", InstanceMemberFlags) != null)
119+
return static mod => mod.modSource.ToString();
120+
121+
var getter = CreateUntypedMemberGetter(typeof(Mod), "modSource");
122+
return mod => getter?.Invoke(mod)?.ToString() ?? "None";
123+
}
124+
125+
private static Func<Mod, int, string> CreateLoadStateAccessor()
126+
{
127+
if (typeof(Mod).GetField("state", InstanceMemberFlags) != null)
128+
return static (mod, _) => mod.state.ToString();
129+
130+
var stateGetter = CreateUntypedMemberGetter(typeof(Mod), "state");
131+
var wasLoadedGetter = CreateUntypedMemberGetter(typeof(Mod), "wasLoaded");
132+
var assemblyLoadedSuccessfullyGetter =
133+
CreateUntypedMemberGetter(typeof(Mod), "assemblyLoadedSuccessfully");
134+
return (mod, errorCount) =>
135+
{
136+
if (stateGetter?.Invoke(mod) is { } stateValue)
137+
return stateValue.ToString() ?? "None";
138+
139+
if (ReadBool(wasLoadedGetter, mod) == true)
140+
return "Loaded";
141+
142+
if (ReadBool(assemblyLoadedSuccessfullyGetter, mod) == false || errorCount > 0)
143+
return "Failed";
144+
145+
return "None";
146+
};
147+
}
148+
149+
private static Func<ModManifest, string?> CreateManifestStringAccessor(
150+
string memberName,
151+
Func<ModManifest, string?> directAccessor)
152+
{
153+
if (typeof(ModManifest).GetField(memberName, InstanceMemberFlags) != null)
154+
return directAccessor;
155+
156+
var getter = CreateUntypedMemberGetter(typeof(ModManifest), memberName);
157+
return manifest => getter?.Invoke(manifest) as string;
158+
}
159+
160+
private static Func<ModManifest, bool> CreateManifestBoolAccessor(
161+
string memberName,
162+
Func<ModManifest, bool> directAccessor,
163+
bool defaultValue)
164+
{
165+
if (typeof(ModManifest).GetField(memberName, InstanceMemberFlags) != null)
166+
return directAccessor;
167+
168+
var getter = CreateUntypedMemberGetter(typeof(ModManifest), memberName);
169+
return manifest => ReadBool(getter, manifest) ?? defaultValue;
170+
}
171+
172+
private static AssemblyName? ResolveAssemblyName(Assembly? assembly)
173+
{
174+
if (assembly == null)
175+
return null;
176+
177+
try
178+
{
179+
return assembly.GetName();
180+
}
181+
catch
182+
{
183+
return null;
184+
}
185+
}
186+
187+
private static IReadOnlyList<LocString> NormalizeErrors(IEnumerable<LocString>? errors)
188+
{
189+
return errors?.Where(error => error != null).ToArray() ?? [];
190+
}
191+
192+
private static bool? ReadBool(Func<object, object?>? getter, object target)
193+
{
194+
return getter?.Invoke(target) is bool value ? value : null;
195+
}
196+
197+
private static Func<object, object?>? CreateUntypedMemberGetter(Type type, string memberName)
198+
{
199+
var field = type.GetField(memberName, InstanceMemberFlags);
200+
if (field != null)
201+
return field.GetValue;
202+
203+
var property = type.GetProperty(memberName, InstanceMemberFlags);
204+
if (property == null)
205+
return null;
206+
207+
var getter = property.GetGetMethod(true);
208+
if (getter == null)
209+
return property.GetValue;
210+
211+
try
212+
{
213+
return CreateUntypedPropertyGetter(type, property.PropertyType, getter);
214+
}
215+
catch
216+
{
217+
return target => getter.Invoke(target, null);
218+
}
219+
}
220+
221+
private static Func<object, object?> CreateUntypedPropertyGetter(
222+
Type declaringType,
223+
Type valueType,
224+
MethodInfo getter)
225+
{
226+
var method = typeof(Sts2ModManagerCompat)
227+
.GetMethod(nameof(CreateUntypedPropertyGetterCore), BindingFlags.Static | BindingFlags.NonPublic)!
228+
.MakeGenericMethod(declaringType, valueType);
229+
return (Func<object, object?>)method.Invoke(null, [getter])!;
230+
}
231+
232+
private static Func<object, object?> CreateUntypedPropertyGetterCore<TDeclaring, TValue>(
233+
MethodInfo getter)
234+
{
235+
var typedGetter = getter.CreateDelegate<Func<TDeclaring, TValue>>();
236+
237+
return target => typedGetter((TDeclaring)target);
238+
}
24239
}
240+
241+
internal sealed record Sts2ModInventoryEntry(
242+
string Id,
243+
string Name,
244+
string? Version,
245+
string State,
246+
string Source,
247+
bool AffectsGameplay,
248+
string? AssemblyName,
249+
string? AssemblyVersion,
250+
IReadOnlyList<LocString> Errors);
25251
}

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

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

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.2.37</Version>
36+
<Version>0.2.38</Version>
3737
<Authors>OLC</Authors>
3838
<Description>Shared framework library for Slay the Spire 2 mods.</Description>
3939
<PackageReadmeFile>README.md</PackageReadmeFile>

Telemetry/Adapters/HttpJsonTelemetryAdapter.cs

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ namespace STS2RitsuLib.Telemetry
99
/// </summary>
1010
public sealed class HttpJsonTelemetryAdapter : ITelemetryAdapter
1111
{
12-
private static readonly HttpClient Client = new();
12+
private static readonly HttpClient Client = new()
13+
{
14+
Timeout = TimeSpan.FromSeconds(60),
15+
};
16+
1317
private readonly IReadOnlyDictionary<string, string> _headers;
1418

1519
/// <summary>
@@ -48,21 +52,32 @@ public async ValueTask<TelemetrySendResult> SendAsync(
4852
events,
4953
}, TelemetryJson.Options);
5054

51-
using var request = new HttpRequestMessage(HttpMethod.Post, Endpoint);
52-
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
55+
try
56+
{
57+
using var request = new HttpRequestMessage(HttpMethod.Post, Endpoint);
58+
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
5359

54-
foreach (var header in _headers)
55-
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
60+
foreach (var header in _headers)
61+
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
5662

57-
using var response = await Client.SendAsync(request, cancellationToken);
58-
if (response.IsSuccessStatusCode)
59-
return TelemetrySendResult.Ok();
63+
using var response = await Client.SendAsync(request, cancellationToken);
64+
if (response.IsSuccessStatusCode)
65+
return TelemetrySendResult.Ok();
6066

61-
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
62-
var reason = string.IsNullOrWhiteSpace(responseBody)
63-
? $"{(int)response.StatusCode} {response.ReasonPhrase}"
64-
: $"{(int)response.StatusCode} {response.ReasonPhrase}: {responseBody}";
65-
return TelemetrySendResult.Fail(reason);
67+
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
68+
var reason = string.IsNullOrWhiteSpace(responseBody)
69+
? $"{(int)response.StatusCode} {response.ReasonPhrase}"
70+
: $"{(int)response.StatusCode} {response.ReasonPhrase}: {responseBody}";
71+
return TelemetrySendResult.Fail(reason);
72+
}
73+
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
74+
{
75+
return TelemetrySendResult.Fail($"Timed out posting telemetry to {Endpoint}.");
76+
}
77+
catch (Exception ex)
78+
{
79+
return TelemetrySendResult.Fail(ex.Message);
80+
}
6681
}
6782
}
6883
}

Telemetry/Adapters/PostHogTelemetryAdapter.cs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ namespace STS2RitsuLib.Telemetry
99
/// </summary>
1010
public sealed class PostHogTelemetryAdapter : ITelemetryAdapter
1111
{
12-
private static readonly HttpClient Client = new();
12+
private static readonly HttpClient Client = new()
13+
{
14+
Timeout = TimeSpan.FromSeconds(60),
15+
};
1316

1417
/// <summary>
1518
/// Creates a PostHog adapter for a fixed host and project API key.
@@ -63,14 +66,25 @@ public async ValueTask<TelemetrySendResult> SendAsync(
6366
batch,
6467
}, TelemetryJson.Options);
6568

66-
using var response = await Client.PostAsync(
67-
new Uri(Host, "/batch/"),
68-
new StringContent(body, Encoding.UTF8, "application/json"),
69-
cancellationToken);
69+
try
70+
{
71+
using var response = await Client.PostAsync(
72+
new Uri(Host, "/batch/"),
73+
new StringContent(body, Encoding.UTF8, "application/json"),
74+
cancellationToken);
7075

71-
return response.IsSuccessStatusCode
72-
? TelemetrySendResult.Ok()
73-
: TelemetrySendResult.Fail($"{(int)response.StatusCode} {response.ReasonPhrase}");
76+
return response.IsSuccessStatusCode
77+
? TelemetrySendResult.Ok()
78+
: TelemetrySendResult.Fail($"{(int)response.StatusCode} {response.ReasonPhrase}");
79+
}
80+
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
81+
{
82+
return TelemetrySendResult.Fail($"Timed out posting telemetry to {Host}.");
83+
}
84+
catch (Exception ex)
85+
{
86+
return TelemetrySendResult.Fail(ex.Message);
87+
}
7488
}
7589

7690
private static Dictionary<string, object?> BuildProperties(TelemetryEnvelope evt)

0 commit comments

Comments
 (0)