Skip to content

Commit f1141a5

Browse files
committed
feat(PortraitMaterials): add support for custom portrait materials in card assets
- Registered a new patch for handling card portrait materials, allowing mods to override portrait rendering. - Updated relevant classes and interfaces to include portrait material paths and overrides. - Enhanced documentation to reflect changes in asset profiles and patching mechanisms for portrait materials.
1 parent 37e3913 commit f1141a5

8 files changed

Lines changed: 258 additions & 22 deletions

RitsuLibFramework.PatcherSetup.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ private static void RegisterContentAssetPatches()
256256
patcher.RegisterPatch<CardTextureOverridePatch>();
257257
patcher.RegisterPatch<CardFrameMaterialPatch>();
258258
patcher.RegisterPatch<CardPoolFrameMaterialPatch>();
259+
patcher.RegisterPatch<CardPortraitMaterialPatch>();
259260
patcher.RegisterPatch<CardAllPortraitPathsPatch>();
260261
patcher.RegisterPatch<CardOverlayPathPatch>();
261262
patcher.RegisterPatch<CardOverlayAvailabilityPatch>();

Scaffolding/Characters/CharacterAssetProfile.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,19 +257,19 @@ public sealed record CharacterVanillaPotionVisualOverride(string PotionModelIdEn
257257
/// <summary>
258258
/// One entry in <see cref="CharacterAssetProfile.VanillaCardVisualOverrides" />: when this mod character
259259
/// encounters or holds a card whose <c>ModelId.Entry</c> equals <paramref name="CardModelIdEntry" />
260-
/// (ordinal ignore-case), use <paramref name="Assets" /> for portrait/frame/banner/overlay paths.
260+
/// (ordinal ignore-case), use <paramref name="Assets" /> for portrait/frame/banner/overlay paths and materials.
261261
/// <see cref="CharacterAssetProfile.VanillaCardVisualOverrides" /> 中的一个条目:当此 mod 角色
262262
/// 遇到或持有 <c>ModelId.Entry</c> 等于 <paramref name="CardModelIdEntry" />
263-
/// (ordinal ignore-case)的卡牌时,使用 <paramref name="Assets" /> 作为肖像/边框/banner/覆盖层路径
263+
/// (ordinal ignore-case)的卡牌时,使用 <paramref name="Assets" /> 作为肖像/边框/banner/覆盖层路径和材质
264264
/// </summary>
265265
/// <param name="CardModelIdEntry">
266266
/// Stable card id (same string as <c>CardModel.Id.Entry</c>).
267267
/// 稳定卡牌 id(与 <c>CardModel.Id.Entry</c> 相同的字符串)。
268268
/// </param>
269269
/// <param name="Assets">
270-
/// Card portrait and frame/border/material/overlay/banner path bundle (same shape as
270+
/// Card portrait and frame/border/material/overlay/banner path and material bundle (same shape as
271271
/// mod card <see cref="CardAssetProfile" />).
272-
/// 卡牌肖像和框/边框/材质/覆盖层/banner 路径包(与
272+
/// 卡牌肖像和框/边框/材质/覆盖层/banner 路径和材质包(与
273273
/// mod 卡牌 <see cref="CardAssetProfile" /> 形态相同)。
274274
/// </param>
275275
public sealed record CharacterVanillaCardVisualOverride(string CardModelIdEntry, CardAssetProfile Assets);

Scaffolding/Characters/CharacterAssetProfiles.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,9 @@ internal static CardAssetProfile MergeCardAssetProfiles(CardAssetProfile fallbac
361361
profile.BannerTexturePath ?? fallback.BannerTexturePath,
362362
profile.BannerMaterialPath ?? fallback.BannerMaterialPath,
363363
profile.FrameMaterial ?? fallback.FrameMaterial,
364-
profile.BannerMaterial ?? fallback.BannerMaterial);
364+
profile.BannerMaterial ?? fallback.BannerMaterial,
365+
profile.PortraitMaterialPath ?? fallback.PortraitMaterialPath,
366+
profile.PortraitMaterial ?? fallback.PortraitMaterial);
365367
}
366368

367369
internal static CardAssetProfile? MergeCardAssetProfilesPreferSecond(CardAssetProfile? fallback,

Scaffolding/Content/ContentAssetProfiles.cs

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
namespace STS2RitsuLib.Scaffolding.Content
66
{
77
/// <summary>
8-
/// Bundle of optional resource paths for mod card portraits, frames, energy icon, overlay scene, and banner.
9-
/// Mod 卡牌肖像、边框、能量图标、覆盖场景和横幅的可选ResourcePath集合。
8+
/// Bundle of optional resource paths and materials for mod card portraits, frames, energy icon, overlay scene,
9+
/// and banner.
10+
/// Mod 卡牌肖像、边框、能量图标、覆盖场景和横幅的可选ResourcePath和材质集合。
1011
/// </summary>
1112
/// <param name="PortraitPath">
1213
/// Main card portrait image path.
@@ -52,6 +53,14 @@ namespace STS2RitsuLib.Scaffolding.Content
5253
/// Direct banner material override.
5354
/// 直接覆盖横幅材质。
5455
/// </param>
56+
/// <param name="PortraitMaterialPath">
57+
/// Material path for portrait rendering.
58+
/// 卡图渲染使用的材质路径。
59+
/// </param>
60+
/// <param name="PortraitMaterial">
61+
/// Direct portrait material override.
62+
/// 直接覆盖卡图材质。
63+
/// </param>
5564
public sealed record CardAssetProfile(
5665
string? PortraitPath = null,
5766
string? BetaPortraitPath = null,
@@ -63,7 +72,9 @@ public sealed record CardAssetProfile(
6372
string? BannerTexturePath = null,
6473
string? BannerMaterialPath = null,
6574
Material? FrameMaterial = null,
66-
Material? BannerMaterial = null)
75+
Material? BannerMaterial = null,
76+
string? PortraitMaterialPath = null,
77+
Material? PortraitMaterial = null)
6778
{
6879
/// <summary>
6980
/// Backward-compatible constructor preserving the original parameter list.
@@ -94,8 +105,40 @@ public CardAssetProfile(
94105
}
95106

96107
/// <summary>
97-
/// Default empty profile (no custom paths).
98-
/// 默认空 profile(无自定义路径)。
108+
/// Backward-compatible constructor preserving the direct material parameter list.
109+
/// 保留直接材质参数列表的向后兼容构造函数。
110+
/// </summary>
111+
public CardAssetProfile(
112+
string? PortraitPath,
113+
string? BetaPortraitPath,
114+
string? FramePath,
115+
string? PortraitBorderPath,
116+
string? EnergyIconPath,
117+
string? FrameMaterialPath,
118+
string? OverlayScenePath,
119+
string? BannerTexturePath,
120+
string? BannerMaterialPath,
121+
Material? FrameMaterial,
122+
Material? BannerMaterial)
123+
: this(
124+
PortraitPath,
125+
BetaPortraitPath,
126+
FramePath,
127+
PortraitBorderPath,
128+
EnergyIconPath,
129+
FrameMaterialPath,
130+
OverlayScenePath,
131+
BannerTexturePath,
132+
BannerMaterialPath,
133+
FrameMaterial,
134+
BannerMaterial,
135+
null)
136+
{
137+
}
138+
139+
/// <summary>
140+
/// Default empty profile (no custom paths or materials).
141+
/// 默认空 profile(无自定义路径或材质)。
99142
/// </summary>
100143
public static CardAssetProfile Empty { get; } = new();
101144
}

Scaffolding/Content/ModCardTemplate.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public abstract class ModCardTemplate(
2929
TargetType target,
3030
bool showInCardLibrary = true)
3131
: CardModel(baseCost, type, rarity, target, showInCardLibrary), IModCardAssetOverrides,
32-
IModCardFrameMaterialOverride, IModCardBannerMaterialOverride
32+
IModCardPortraitMaterialOverride, IModCardFrameMaterialOverride, IModCardBannerMaterialOverride
3333
{
3434
/// <summary>
3535
/// Legacy constructor overload; <paramref name="autoAdd" /> is ignored.
@@ -91,6 +91,9 @@ protected ModCardTemplate(
9191
/// <inheritdoc />
9292
public virtual string? CustomBetaPortraitPath => AssetProfile.BetaPortraitPath;
9393

94+
/// <inheritdoc />
95+
public virtual string? CustomPortraitMaterialPath => AssetProfile.PortraitMaterialPath;
96+
9497
/// <inheritdoc />
9598
public virtual string? CustomFramePath => AssetProfile.FramePath;
9699

@@ -118,6 +121,9 @@ protected ModCardTemplate(
118121
/// <inheritdoc />
119122
public virtual Material? CustomFrameMaterial => AssetProfile.FrameMaterial;
120123

124+
/// <inheritdoc />
125+
public virtual Material? CustomPortraitMaterial => AssetProfile.PortraitMaterial;
126+
121127
/// <summary>
122128
/// Internal accessor for the mod-keyword seeding patch.
123129
/// 供 mod 关键词种入补丁使用的内部访问器。

Scaffolding/Content/Patches/ContentAssetOverridePatches.cs

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
using Godot;
33
using HarmonyLib;
44
using MegaCrit.Sts2.Core.Assets;
5+
using MegaCrit.Sts2.Core.Entities.Cards;
6+
using MegaCrit.Sts2.Core.Entities.UI;
57
using MegaCrit.Sts2.Core.Events;
68
using MegaCrit.Sts2.Core.Helpers;
79
using MegaCrit.Sts2.Core.Models;
10+
using MegaCrit.Sts2.Core.Nodes.Cards;
811
using MegaCrit.Sts2.Core.Runs;
912
using MegaCrit.Sts2.Core.Timeline;
1013
using STS2RitsuLib.Patching.Models;
1114
using STS2RitsuLib.Scaffolding.Characters;
15+
using STS2RitsuLib.Timeline.Scaffolding;
1216
using STS2RitsuLib.Utils;
1317

1418
namespace STS2RitsuLib.Scaffolding.Content.Patches
@@ -282,8 +286,8 @@ internal static bool TryUseCompressedTextureAsTexture2DOverride<TOverrides>(
282286
}
283287

284288
/// <summary>
285-
/// Optional card art paths consumed by content asset Harmony patches on <see cref="CardModel" />.
286-
/// 由 <see cref="CardModel" /> 上的 content asset Harmony 补丁使用的可选卡牌美术路径
289+
/// Optional card art paths and materials consumed by content asset Harmony patches.
290+
/// 由 content asset Harmony 补丁使用的可选卡牌美术路径和材质
287291
/// </summary>
288292
public interface IModCardAssetOverrides
289293
{
@@ -305,6 +309,12 @@ public interface IModCardAssetOverrides
305309
/// </summary>
306310
string? CustomBetaPortraitPath { get; }
307311

312+
/// <summary>
313+
/// Override for card portrait <see cref="Material" /> resource path.
314+
/// 卡图 <see cref="Material" /> 资源路径覆盖。
315+
/// </summary>
316+
string? CustomPortraitMaterialPath => AssetProfile.PortraitMaterialPath;
317+
308318
/// <summary>
309319
/// Override for card frame texture path.
310320
/// 卡牌边框纹理路径覆盖。
@@ -348,6 +358,23 @@ public interface IModCardAssetOverrides
348358
string? CustomBannerMaterialPath { get; }
349359
}
350360

361+
/// <summary>
362+
/// Optional direct portrait <see cref="Material" /> override for cards.
363+
/// This is applied to the portrait TextureRect after <see cref="NCard" /> reloads its vanilla visuals.
364+
/// 用于卡牌的可选直接卡图 <see cref="Material" /> 覆盖。
365+
/// 会在 <see cref="NCard" /> 重载原版视觉后应用到卡图 TextureRect。
366+
/// </summary>
367+
public interface IModCardPortraitMaterialOverride
368+
{
369+
/// <summary>
370+
/// Direct portrait material override.
371+
/// Return <c>null</c> to continue with other override layers.
372+
/// 直接的卡图材质覆盖。
373+
/// 返回 <c>null</c> 以继续使用其它覆盖层。
374+
/// </summary>
375+
Material? CustomPortraitMaterial => null;
376+
}
377+
351378
/// <summary>
352379
/// Optional direct frame <see cref="Material" /> override for cards.
353380
/// This bypasses resource-path loading and is checked before
@@ -733,8 +760,9 @@ public static ModPatchTarget[] GetTargets()
733760
/// <summary>
734761
/// Suppresses the vanilla placeholder label for mod epochs with custom artwork.
735762
/// </summary>
736-
// ReSharper disable once InconsistentNaming
763+
// ReSharper disable InconsistentNaming
737764
public static bool Prefix(EpochModel __instance, ref bool __result)
765+
// ReSharper restore InconsistentNaming
738766
{
739767
if (__instance is IModEpochAssetOverrides overrides &&
740768
!string.IsNullOrWhiteSpace(overrides.CustomBigPortraitPath) &&
@@ -757,12 +785,10 @@ public static bool Prefix(EpochModel __instance, ref bool __result)
757785
private static bool IsCharacterUnlockEpochTemplate(Type type)
758786
{
759787
for (var current = type; current != null; current = current.BaseType)
760-
{
761788
if (current.IsGenericType &&
762789
current.GetGenericTypeDefinition() ==
763-
typeof(STS2RitsuLib.Timeline.Scaffolding.CharacterUnlockEpochTemplate<>))
790+
typeof(CharacterUnlockEpochTemplate<>))
764791
return true;
765-
}
766792

767793
return false;
768794
}
@@ -1079,6 +1105,76 @@ public static bool Prefix(CardPoolModel __instance, ref Material __result)
10791105
}
10801106
}
10811107

1108+
/// <summary>
1109+
/// Applies custom portrait <see cref="Material" /> overrides after <see cref="NCard" /> reloads vanilla visuals.
1110+
/// 在 <see cref="NCard" /> 重载原版视觉后应用自定义卡图 <see cref="Material" /> 覆盖。
1111+
/// </summary>
1112+
public class CardPortraitMaterialPatch : IPatchMethod
1113+
{
1114+
/// <inheritdoc cref="IPatchMethod.PatchId" />
1115+
public static string PatchId => "content_asset_override_card_portrait_material";
1116+
1117+
/// <inheritdoc cref="IPatchMethod.Description" />
1118+
public static string Description => "Allow mod cards to override the NCard portrait material";
1119+
1120+
/// <inheritdoc cref="IPatchMethod.IsCritical" />
1121+
public static bool IsCritical => false;
1122+
1123+
/// <inheritdoc cref="IPatchMethod.GetTargets" />
1124+
public static ModPatchTarget[] GetTargets()
1125+
{
1126+
return [new(typeof(NCard), "Reload")];
1127+
}
1128+
1129+
// ReSharper disable InconsistentNaming
1130+
/// <summary>
1131+
/// Reapplies the custom material because vanilla <c>Reload</c> clears portrait materials for visible cards.
1132+
/// 重新应用自定义材质,因为原版 <c>Reload</c> 会清空可见卡牌的卡图材质。
1133+
/// </summary>
1134+
public static void Postfix(NCard __instance)
1135+
// ReSharper restore InconsistentNaming
1136+
{
1137+
var model = __instance.Model;
1138+
if (model == null || __instance.Visibility != ModelVisibility.Visible)
1139+
return;
1140+
1141+
if (!TryGetPortraitMaterial(model, out var material))
1142+
return;
1143+
1144+
var portrait = GetPortraitNode(__instance, model);
1145+
if (portrait == null)
1146+
return;
1147+
1148+
portrait.Material = material;
1149+
}
1150+
1151+
private static TextureRect? GetPortraitNode(NCard card, CardModel model)
1152+
{
1153+
var path = model.Rarity == CardRarity.Ancient ? "%AncientPortrait" : "%Portrait";
1154+
return card.GetNodeOrNull<TextureRect>(path);
1155+
}
1156+
1157+
private static bool TryGetPortraitMaterial(CardModel card, out Material material)
1158+
{
1159+
material = null!;
1160+
if (!ContentAssetOverridePatchHelper.TryUseDirectMaterialOverride<IModCardPortraitMaterialOverride>(
1161+
card, ref material, static o => o.CustomPortraitMaterial))
1162+
return true;
1163+
1164+
if (ExternalCardMaterialOverrideRegistry.TryGetPortraitMaterial(card, out material))
1165+
return true;
1166+
1167+
if (!ModCharacterOwnedVisualOverrideHelper.TryCardPortraitMaterial(card, ref material))
1168+
return true;
1169+
1170+
return !ContentAssetOverridePatchHelper.TryUseMaterialOverride<IModCardAssetOverrides>(
1171+
card,
1172+
ref material,
1173+
static o => o.CustomPortraitMaterialPath,
1174+
nameof(IModCardAssetOverrides.CustomPortraitMaterialPath));
1175+
}
1176+
}
1177+
10821178
/// <summary>
10831179
/// Patches <see cref="CardModel.AllPortraitPaths" /> so custom portrait/beta paths participate in preload lists.
10841180
/// 修补<see cref="CardModel.AllPortraitPaths" />,使自定义 portrait/beta 路径 participate in 预加载 列表。

0 commit comments

Comments
 (0)