Skip to content

Commit 869cade

Browse files
committed
chore(release): merge dev into main for v0.0.37
2 parents 0b2ab8a + f946f34 commit 869cade

19 files changed

Lines changed: 1128 additions & 138 deletions
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using MegaCrit.Sts2.Core.Entities.Cards;
2+
using MegaCrit.Sts2.Core.Entities.Creatures;
3+
using MegaCrit.Sts2.Core.Models;
4+
using STS2RitsuLib.Keywords;
5+
using STS2RitsuLib.Patching.Models;
6+
7+
namespace STS2RitsuLib.Cards.Patches
8+
{
9+
/// <summary>
10+
/// Postfixes vanilla card description builders so mod keywords with
11+
/// <see cref="ModKeywordDefinition.CardDescriptionPlacement" /> inject BBCode like vanilla keywords.
12+
/// </summary>
13+
public sealed class ModKeywordCardDescriptionPatches : IPatchMethod
14+
{
15+
/// <inheritdoc />
16+
public static string PatchId => "card_mod_keyword_description";
17+
18+
/// <inheritdoc />
19+
public static string Description => "Inject mod keyword BBCode into CardModel description rendering";
20+
21+
/// <inheritdoc />
22+
public static bool IsCritical => false;
23+
24+
/// <inheritdoc />
25+
public static ModPatchTarget[] GetTargets()
26+
{
27+
return
28+
[
29+
new(typeof(CardModel), nameof(CardModel.GetDescriptionForPile),
30+
[typeof(PileType), typeof(Creature)]),
31+
new(typeof(CardModel), nameof(CardModel.GetDescriptionForUpgradePreview), Type.EmptyTypes),
32+
];
33+
}
34+
35+
// ReSharper disable InconsistentNaming
36+
/// <summary>
37+
/// Appends or prepends keyword fragments after vanilla description composition.
38+
/// </summary>
39+
public static void Postfix(CardModel __instance, ref string __result)
40+
{
41+
ModKeywordCardDescriptionInjector.AppendFragments(__instance, ref __result);
42+
}
43+
// ReSharper restore InconsistentNaming
44+
}
45+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
using System.Reflection;
2+
using MegaCrit.Sts2.Core.Entities.Creatures;
3+
using STS2RitsuLib.Compat;
4+
5+
namespace STS2RitsuLib.Combat.HealthBars
6+
{
7+
/// <summary>
8+
/// When BaseLib is loaded, registers <see cref="HealthBarForecastRegistry.GetSegments" /> with BaseLib's
9+
/// <c>HealthBarForecastRegistry.RegisterForeign</c> so a single renderer can consume Ritsu-typed segments.
10+
/// </summary>
11+
/// <remarks>
12+
/// <see cref="ShouldRitsuRendererStandDown" /> becomes true after a successful bridge so duplicate overlays are
13+
/// not drawn.
14+
/// </remarks>
15+
internal static class BaseLibHealthBarForecastBridge
16+
{
17+
private const string SourceId = "ritsulib.registry";
18+
private static bool _registered;
19+
private static bool _baselibSupportsForecastInterop;
20+
private static bool _loggedMissingInterop;
21+
private static bool _loggedMissingRegisterForeign;
22+
private static bool _primaryAttemptIssued;
23+
private static bool _secondaryAttemptIssued;
24+
25+
/// <summary>
26+
/// When <see langword="true" />, Ritsu's <c>NHealthBar</c> forecast postfixes should skip drawing because BaseLib
27+
/// already merged this mod's segments.
28+
/// </summary>
29+
public static bool ShouldRitsuRendererStandDown()
30+
{
31+
return _registered && _baselibSupportsForecastInterop;
32+
}
33+
34+
/// <summary>
35+
/// Attempts foreign registration from <c>NHealthBar._Ready</c> (early load path).
36+
/// </summary>
37+
public static void TryRegisterPrimary()
38+
{
39+
if (_primaryAttemptIssued || _registered)
40+
return;
41+
_primaryAttemptIssued = true;
42+
TryRegisterCore();
43+
}
44+
45+
/// <summary>
46+
/// Attempts foreign registration from forecast render path if <see cref="TryRegisterPrimary" /> did not run yet.
47+
/// </summary>
48+
public static void TryRegisterSecondary()
49+
{
50+
if (_secondaryAttemptIssued || _registered)
51+
return;
52+
_secondaryAttemptIssued = true;
53+
TryRegisterCore();
54+
}
55+
56+
/// <summary>
57+
/// Alias for <see cref="TryRegisterPrimary" />.
58+
/// </summary>
59+
public static void TryRegister()
60+
{
61+
TryRegisterPrimary();
62+
}
63+
64+
private static void TryRegisterCore()
65+
{
66+
if (_registered)
67+
return;
68+
if (!IsBaseLibLoaded())
69+
return;
70+
71+
try
72+
{
73+
var registryType = ResolveBaseLibRegistryType();
74+
if (registryType == null)
75+
return;
76+
77+
var registerForeign = registryType.GetMethod(
78+
"RegisterForeign",
79+
BindingFlags.Public | BindingFlags.Static,
80+
null,
81+
[typeof(string), typeof(string), typeof(Func<Creature, IEnumerable<object>>)],
82+
null);
83+
84+
if (registerForeign == null)
85+
{
86+
_baselibSupportsForecastInterop = false;
87+
if (!_loggedMissingRegisterForeign)
88+
{
89+
_loggedMissingRegisterForeign = true;
90+
RitsuLibFramework.Logger.Warn(
91+
$"[HealthBarForecast] BaseLib registry type '{registryType.FullName}' does not expose " +
92+
"RegisterForeign(string, string, Func<Creature, IEnumerable<object>>); forecast interop unavailable.");
93+
}
94+
95+
return;
96+
}
97+
98+
var provider = GetSegmentsForCreature;
99+
registerForeign.Invoke(null, [Const.ModId, SourceId, provider]);
100+
_registered = true;
101+
_baselibSupportsForecastInterop = true;
102+
RitsuLibFramework.Logger.Info("[HealthBarForecast] Registered BaseLib bridge provider.");
103+
}
104+
catch (Exception ex)
105+
{
106+
RitsuLibFramework.Logger.Warn($"[HealthBarForecast] Failed to register BaseLib bridge provider: {ex}");
107+
}
108+
}
109+
110+
private static IEnumerable<object> GetSegmentsForCreature(Creature creature)
111+
{
112+
return HealthBarForecastRegistry.GetSegments(creature)
113+
.Select(registered => (object)registered.Segment)
114+
.ToArray();
115+
}
116+
117+
private static Type? ResolveBaseLibRegistryType()
118+
{
119+
var registryType = ResolveRegistryTypeFromLoadedAssemblies();
120+
_baselibSupportsForecastInterop = registryType != null;
121+
122+
if (!_baselibSupportsForecastInterop)
123+
{
124+
if (_loggedMissingInterop)
125+
return null;
126+
_loggedMissingInterop = true;
127+
RitsuLibFramework.Logger.Info(
128+
"[HealthBarForecast] BaseLib detected but forecast interop API is unavailable.");
129+
return null;
130+
}
131+
132+
_loggedMissingInterop = false;
133+
134+
return registryType;
135+
}
136+
137+
private static bool IsBaseLibLoaded()
138+
{
139+
foreach (var mod in Sts2ModManagerCompat.EnumerateLoadedModsWithAssembly())
140+
{
141+
var assembly = mod.assembly;
142+
if (assembly == null)
143+
continue;
144+
if (assembly.GetType("BaseLib.Hooks.HealthBarForecastRegistry") != null)
145+
return true;
146+
}
147+
148+
return false;
149+
}
150+
151+
private static Type? ResolveRegistryTypeFromLoadedAssemblies()
152+
{
153+
var byQualifiedName = Type.GetType("BaseLib.Hooks.HealthBarForecastRegistry, BaseLib");
154+
if (byQualifiedName != null)
155+
return byQualifiedName;
156+
157+
var loadedWithAssembly = Sts2ModManagerCompat.EnumerateLoadedModsWithAssembly();
158+
foreach (var mod in loadedWithAssembly)
159+
{
160+
var assembly = mod.assembly;
161+
if (assembly == null)
162+
continue;
163+
164+
var type = assembly.GetType("BaseLib.Hooks.HealthBarForecastRegistry");
165+
if (type != null)
166+
return type;
167+
}
168+
169+
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
170+
{
171+
var type = assembly.GetType("BaseLib.Hooks.HealthBarForecastRegistry");
172+
if (type != null)
173+
return type;
174+
}
175+
176+
return null;
177+
}
178+
}
179+
}

Combat/HealthBars/HealthBarForecastRegistry.cs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,56 @@ public enum HealthBarForecastGrowthDirection
2424
/// One forecast overlay segment for a creature health bar.
2525
/// </summary>
2626
/// <param name="Amount">HP amount represented by this segment.</param>
27-
/// <param name="Color">Overlay tint.</param>
27+
/// <param name="Color">
28+
/// Lethal HP label theming; also used as the forecast nine-patch <see cref="CanvasItem.SelfModulate" /> when
29+
/// <see cref="OverlaySelfModulate" /> is null.
30+
/// </param>
2831
/// <param name="Direction">Which edge the segment grows from.</param>
2932
/// <param name="Order">
3033
/// Lower values are rendered earlier in the chain.
3134
/// For <see cref="HealthBarForecastGrowthDirection.FromRight" />, earlier segments stay closer to the current HP
3235
/// edge; for <see cref="HealthBarForecastGrowthDirection.FromLeft" />, earlier segments stay closer to the empty
3336
/// edge.
3437
/// </param>
38+
/// <param name="OverlayMaterial">
39+
/// Optional Godot material (e.g. shader like vanilla doom). When null, only <see cref="Color" /> tint applies.
40+
/// </param>
41+
/// <param name="OverlaySelfModulate">
42+
/// Optional <see cref="CanvasItem.SelfModulate" /> for the forecast nine-patch. When null, <see cref="Color" /> is
43+
/// used
44+
/// for both overlay tint and lethal HP label; when set, <see cref="Color" /> is still used for lethal label theming.
45+
/// </param>
3546
public readonly record struct HealthBarForecastSegment(
3647
int Amount,
3748
Color Color,
3849
HealthBarForecastGrowthDirection Direction,
39-
int Order = 0);
50+
int Order,
51+
Material? OverlayMaterial,
52+
Color? OverlaySelfModulate = null)
53+
{
54+
/// <summary>
55+
/// Initializes a segment without overlay material or separate overlay modulate.
56+
/// </summary>
57+
public HealthBarForecastSegment(int amount, Color color, HealthBarForecastGrowthDirection direction,
58+
int order = 0)
59+
: this(amount, color, direction, order, null, null)
60+
{
61+
}
62+
63+
/// <summary>
64+
/// Initializes a segment with an optional <see cref="OverlayMaterial" /> and default overlay modulate.
65+
/// </summary>
66+
// ReSharper disable once RedundantOverload.Global
67+
public HealthBarForecastSegment(
68+
int amount,
69+
Color color,
70+
HealthBarForecastGrowthDirection direction,
71+
int order,
72+
Material? overlayMaterial)
73+
: this(amount, color, direction, order, overlayMaterial, null)
74+
{
75+
}
76+
}
4077

4178
/// <summary>
4279
/// Helpers for common turn-relative ordering of forecast segments.
@@ -74,6 +111,9 @@ public static class HealthBarForecastRegistry
74111
/// <summary>
75112
/// Registers or replaces a forecast provider for <paramref name="modId" />.
76113
/// </summary>
114+
/// <typeparam name="TSource">Concrete <see cref="IHealthBarForecastSource" /> with a parameterless constructor.</typeparam>
115+
/// <param name="modId">Owning mod identifier.</param>
116+
/// <param name="sourceId">Optional unique id; defaults to the type full name.</param>
77117
public static void Register<TSource>(string modId, string? sourceId = null)
78118
where TSource : IHealthBarForecastSource, new()
79119
{
@@ -83,6 +123,9 @@ public static void Register<TSource>(string modId, string? sourceId = null)
83123
/// <summary>
84124
/// Registers or replaces a forecast source instance for <paramref name="modId" />.
85125
/// </summary>
126+
/// <param name="modId">Owning mod identifier.</param>
127+
/// <param name="sourceId">Unique id for this source within the mod.</param>
128+
/// <param name="source">Provider instance.</param>
86129
public static void Register(
87130
string modId,
88131
string sourceId,
@@ -106,6 +149,9 @@ public static void Register(
106149
/// <summary>
107150
/// Removes a previously registered provider.
108151
/// </summary>
152+
/// <param name="modId">Mod identifier used at registration.</param>
153+
/// <param name="sourceId">Source id used at registration.</param>
154+
/// <returns><see langword="true" /> if an entry was removed.</returns>
109155
public static bool Unregister(string modId, string sourceId)
110156
{
111157
ArgumentException.ThrowIfNullOrWhiteSpace(modId);
@@ -117,6 +163,10 @@ public static bool Unregister(string modId, string sourceId)
117163
}
118164
}
119165

166+
/// <summary>
167+
/// Collects segments from powers implementing <see cref="IHealthBarForecastSource" /> and registered providers.
168+
/// </summary>
169+
/// <param name="creature">Creature whose bar is being evaluated.</param>
120170
internal static IReadOnlyList<RegisteredHealthBarForecastSegment> GetSegments(Creature creature)
121171
{
122172
ArgumentNullException.ThrowIfNull(creature);
@@ -176,6 +226,11 @@ where segment.Amount > 0
176226
}
177227
}
178228

229+
/// <summary>
230+
/// Segment plus a sequence key for stable ordering when <see cref="HealthBarForecastSegment.Order" /> ties.
231+
/// </summary>
232+
/// <param name="Segment">Forecast data.</param>
233+
/// <param name="SequenceOrder">Monotonic key (powers first, then registered sources).</param>
179234
internal readonly record struct RegisteredHealthBarForecastSegment(
180235
HealthBarForecastSegment Segment,
181236
long SequenceOrder);

0 commit comments

Comments
 (0)