From 4de736dd839a4c3918e445aeebc676ba2740b760 Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Thu, 6 Aug 2026 09:49:56 +0800 Subject: [PATCH] fix(tooltips): handle unreferenced aura upgrade values --- .../Tooltips/UpgradePreviewValueRegistry.cs | 34 +++++++ .../Tooltips/UpgradePreviewValuePatch.cs | 27 ++++++ tests/UpgradePreviewTooltip.Tests/Program.cs | 95 +++++++++++++++++++ 3 files changed, 156 insertions(+) diff --git a/src/BazaarPlusPlus/Game/Tooltips/UpgradePreviewValueRegistry.cs b/src/BazaarPlusPlus/Game/Tooltips/UpgradePreviewValueRegistry.cs index 132cb2f2..cb9bd832 100644 --- a/src/BazaarPlusPlus/Game/Tooltips/UpgradePreviewValueRegistry.cs +++ b/src/BazaarPlusPlus/Game/Tooltips/UpgradePreviewValueRegistry.cs @@ -8,6 +8,12 @@ namespace BazaarPlusPlus.Game.Tooltips; internal static class UpgradePreviewValueRegistry { + [ThreadStatic] + private static int _tooltipRenderDepth; + + [ThreadStatic] + private static ITooltipComponent? _fallbackComponent; + private static readonly ConditionalWeakTable< CardTooltipData, UpgradePreviewValueProjection @@ -25,11 +31,39 @@ out float value ) { value = default; + component ??= TakeFallbackComponent(); return tooltipData != null && Projections.TryGetValue(tooltipData, out var projection) && projection.TryResolve(component, out value); } + internal static void BeginTooltipRender() => _tooltipRenderDepth++; + + internal static void EndTooltipRender() + { + if (_tooltipRenderDepth > 0) + _tooltipRenderDepth--; + + if (_tooltipRenderDepth == 0) + _fallbackComponent = null; + } + + internal static void CaptureFallbackComponent(ITooltipComponent component) + { + // Native RenderTooltip omits the component argument for styled tokens whose + // ReferencedAttribute is null. Resolve() runs immediately before that formatter, + // so retain the token only for the duration of this synchronous render scope. + if (_tooltipRenderDepth > 0) + _fallbackComponent = component; + } + + private static ITooltipComponent? TakeFallbackComponent() + { + var component = _fallbackComponent; + _fallbackComponent = null; + return component; + } + internal static bool TryResolveEffectiveCooldowns( CardTooltipData tooltipData, out float currentSeconds, diff --git a/src/BazaarPlusPlus/Patches/Tooltips/UpgradePreviewValuePatch.cs b/src/BazaarPlusPlus/Patches/Tooltips/UpgradePreviewValuePatch.cs index e264eb19..01045957 100644 --- a/src/BazaarPlusPlus/Patches/Tooltips/UpgradePreviewValuePatch.cs +++ b/src/BazaarPlusPlus/Patches/Tooltips/UpgradePreviewValuePatch.cs @@ -2,6 +2,7 @@ using System.Reflection; using BazaarGameClient.Domain.Tooltips; using BazaarGameShared.Domain.Core.Types; +using BazaarGameShared.Domain.Effect.AuraActions; using BazaarPlusPlus.Game.Tooltips; using HarmonyLib; using TheBazaar; @@ -9,6 +10,32 @@ namespace BazaarPlusPlus.Patches.Tooltips; +[HarmonyPatch(typeof(CardTooltipData), "RenderTooltip", [typeof(TooltipBuilder)])] +internal static class UpgradePreviewTooltipRenderScopePatch +{ + [HarmonyPrefix] + private static void Prefix() => UpgradePreviewValueRegistry.BeginTooltipRender(); + + [HarmonyFinalizer] + private static void Finalizer() => UpgradePreviewValueRegistry.EndTooltipRender(); +} + +[HarmonyPatch(typeof(TooltipComponentAura), nameof(TooltipComponentAura.Resolve))] +internal static class UpgradePreviewAuraValueContextPatch +{ + [HarmonyPostfix] + private static void Postfix(TooltipComponentAura __instance) + { + if ( + !__instance.ReferencedAttribute.HasValue + && __instance.Aura.Action + is TAuraActionCardModifyAttribute + or TAuraActionPlayerModifyAttribute + ) + UpgradePreviewValueRegistry.CaptureFallbackComponent(__instance); + } +} + [HarmonyPatch] internal static class UpgradePreviewValuePatch { diff --git a/tests/UpgradePreviewTooltip.Tests/Program.cs b/tests/UpgradePreviewTooltip.Tests/Program.cs index c0e4d572..2d465328 100644 --- a/tests/UpgradePreviewTooltip.Tests/Program.cs +++ b/tests/UpgradePreviewTooltip.Tests/Program.cs @@ -104,6 +104,101 @@ out var projection ); AssertFloatEqual(3f, upgradedBurn, "Derived Burn should be recomputed as 20% of 15, not 2 + 5."); +var guzhengAura = new TCardAura +{ + Id = "guzheng-damage-per-tempo", + Action = new TAuraActionCardModifyAttribute + { + AttributeType = ECardAttributeType.DamageAmount, + Operation = EAttributeModifierOperation.Add, + Target = new TTargetCardSelf(), + Value = new TReferenceValuePlayerAttribute + { + AttributeType = EPlayerAttributeType.Tempo, + Modifier = new TValueModifier + { + ModifyMode = EValueModifierMode.Multiply, + Value = new TReferenceValueCardAttribute + { + AttributeType = ECardAttributeType.Custom_0, + Target = new TTargetCardSelf(), + Modifier = new TValueModifier + { + ModifyMode = EValueModifierMode.Multiply, + Value = new TFixedValue { Value = 1f }, + }, + }, + }, + }, + }, +}; +var guzhengTemplate = new TCardItem +{ + Type = ECardType.Item, + StartingTier = ETier.Gold, + Auras = new Dictionary { [guzhengAura.Id] = guzhengAura }, + Tiers = new Dictionary + { + [ETier.Gold] = new TCardTier + { + Attributes = new Dictionary + { + [ECardAttributeType.Custom_0] = 15, + }, + }, + [ETier.Diamond] = new TCardTier + { + Attributes = new Dictionary + { + [ECardAttributeType.Custom_0] = 30, + }, + }, + }, +}; +var guzhengCard = new ItemCard +{ + Template = guzhengTemplate, + Type = ECardType.Item, + Tier = ETier.Gold, + Attributes = new Dictionary { [ECardAttributeType.Custom_0] = 15 }, +}; +var guzhengContext = new TooltipContext( + guzhengCard, + guzhengTemplate, + new ValueContext(null!, guzhengCard) +); +var guzhengToken = TooltipComponentAura.Create( + guzhengContext, + guzhengAura.Id, + ETooltipAccessorType.Mod, + 0 +); + +AssertNotNull(guzhengToken, "Guzheng aura token should be created."); +AssertTrue( + !guzhengToken!.ReferencedAttribute.HasValue, + "Guzheng's player-attribute aura intentionally has no native referenced card attribute." +); +AssertFloatEqual(15f, guzhengToken.Resolve()!.Value, "Current Guzheng multiplier should be 15."); +AssertTrue( + UpgradePreviewValueProjection.TryCreate( + guzhengCard, + guzhengTemplate, + guzhengContext.ValueContext, + out var guzhengProjection + ), + "Guzheng upgrade projection should be created." +); +AssertTrue( + guzhengProjection.TryResolve(guzhengToken, out var upgradedGuzhengMultiplier), + "Projected Guzheng aura token should resolve." +); +AssertFloatEqual( + 30f, + upgradedGuzhengMultiplier, + "Guzheng Damage and Shield per Tempo should upgrade from 15 to 30." +); + card.Attributes[ECardAttributeType.Custom_1] = 12; AssertTrue( UpgradePreviewValueProjection.TryCreate(