From 1861710ed78b947c5e97acd1db6fa160ed292411 Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Mon, 6 Apr 2026 15:15:25 +0800 Subject: [PATCH 01/15] Take 2 on customReward, unfinished --- Abstracts/CustomMessage.cs | 19 ++++++++ Abstracts/CustomReward.cs | 14 ++++++ Abstracts/CustomRewardMessage.cs | 19 ++++++++ Common/Rewards/CardTransformReward.cs | 35 ++++++++++++++ Patches/Content/RewardSynchronizerPatches.cs | 49 ++++++++++++++++++++ Patches/Content/RunManagerPatches.cs | 22 +++++++++ 6 files changed, 158 insertions(+) create mode 100644 Abstracts/CustomMessage.cs create mode 100644 Abstracts/CustomReward.cs create mode 100644 Abstracts/CustomRewardMessage.cs create mode 100644 Common/Rewards/CardTransformReward.cs create mode 100644 Patches/Content/RewardSynchronizerPatches.cs create mode 100644 Patches/Content/RunManagerPatches.cs diff --git a/Abstracts/CustomMessage.cs b/Abstracts/CustomMessage.cs new file mode 100644 index 0000000..484071c --- /dev/null +++ b/Abstracts/CustomMessage.cs @@ -0,0 +1,19 @@ +using MegaCrit.Sts2.Core.Logging; +using MegaCrit.Sts2.Core.Multiplayer.Messages.Game; +using MegaCrit.Sts2.Core.Multiplayer.Serialization; +using MegaCrit.Sts2.Core.Multiplayer.Transport; +using MegaCrit.Sts2.Core.Runs; + +namespace BaseLib.Abstracts; + +public abstract class CustomMessage : INetMessage, IRunLocationTargetedMessage +{ + public abstract void MessageHandler(T message, ulong senderID) where T : CustomMessage; + public abstract bool ShouldBroadcast { get; } + public abstract NetTransferMode Mode { get; } + public abstract LogLevel LogLevel { get; } + public abstract RunLocation Location { get; } + + public abstract void Deserialize(PacketReader reader); + public abstract void Serialize(PacketWriter writer); +} diff --git a/Abstracts/CustomReward.cs b/Abstracts/CustomReward.cs new file mode 100644 index 0000000..948e533 --- /dev/null +++ b/Abstracts/CustomReward.cs @@ -0,0 +1,14 @@ +using MegaCrit.Sts2.Core.Entities.Players; +using MegaCrit.Sts2.Core.Rewards; + +namespace BaseLib.Abstracts; + +public abstract class CustomReward(Player player) : Reward(player) +{ + /// + /// Set the reward index after vanilla rewards by default + /// + public override int RewardsSetIndex => 9; + + // TODO: per-mod id prefixing for localisation +} diff --git a/Abstracts/CustomRewardMessage.cs b/Abstracts/CustomRewardMessage.cs new file mode 100644 index 0000000..24e89ce --- /dev/null +++ b/Abstracts/CustomRewardMessage.cs @@ -0,0 +1,19 @@ +using MegaCrit.Sts2.Core.Logging; +using MegaCrit.Sts2.Core.Multiplayer.Transport; +using MegaCrit.Sts2.Core.Rewards; +using MegaCrit.Sts2.Core.Runs; + +namespace BaseLib.Abstracts; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public abstract class CustomRewardMessage : CustomMessage +{ + public required RewardType rewardType; + public required RunLocation location; + public required bool wasSkipped; + + public override bool ShouldBroadcast => true; + public override NetTransferMode Mode => NetTransferMode.Reliable; + public override LogLevel LogLevel => LogLevel.VeryDebug; + public override RunLocation Location => location; +} diff --git a/Common/Rewards/CardTransformReward.cs b/Common/Rewards/CardTransformReward.cs new file mode 100644 index 0000000..6c53d9c --- /dev/null +++ b/Common/Rewards/CardTransformReward.cs @@ -0,0 +1,35 @@ +using BaseLib.Abstracts; +using MegaCrit.Sts2.Core.Entities.Players; +using MegaCrit.Sts2.Core.Helpers; +using MegaCrit.Sts2.Core.Localization; +using MegaCrit.Sts2.Core.Rewards; +using MegaCrit.Sts2.Core.Runs; + +namespace BaseLib.Common.Rewards; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public class CardTransformReward(Player player) : CustomReward(player) +{ + public static RewardType CardTransform; + protected override RewardType RewardType => CardTransform; + public override LocString Description => new LocString("gameplay_ui", "COMBAT_REWARD_CARD_TRANSFORM"); + public override bool IsPopulated => true; + + public static string RewardIcon => ImageHelper.GetImagePath("ui/reward_screen/reward_icon_card_removal.png"); + protected override string IconPath => RewardIcon; + + public override void MarkContentAsSeen() + { + } + + public override Task Populate() + { + return Task.CompletedTask; + } + + protected override async Task OnSelect() + { + BaseLibMain.Logger.Info("Obtained card transformation from reward"); + return await RunManager.Instance.RewardSynchronizer.DoLocalCardTransform(); + } +} diff --git a/Patches/Content/RewardSynchronizerPatches.cs b/Patches/Content/RewardSynchronizerPatches.cs new file mode 100644 index 0000000..0a29703 --- /dev/null +++ b/Patches/Content/RewardSynchronizerPatches.cs @@ -0,0 +1,49 @@ +using BaseLib.Abstracts; +using HarmonyLib; +using MegaCrit.Sts2.Core.Entities.Players; +using MegaCrit.Sts2.Core.Helpers; +using MegaCrit.Sts2.Core.Multiplayer.Game; + +namespace BaseLib.Patches.Content; + +[HarmonyPatch(typeof(RewardSynchronizer))] +public static class RewardSynchronizerExtensions +{ + /// + /// Struct to save a custom reward message until combat ends + /// + public struct BufferedCustomRewardMessage + { + /// + /// the id of the player who sent the message + /// + public ulong senderId; + /// + /// The message being sent + /// + public CustomRewardMessage message; + } + + /// + /// Reference list of buffered messages + /// + public static List BufferedCustomRewardMessages = []; + + /// + /// Exposes the private LocalPlayer property from + /// + public static Player? LocalPlayerRef(this RewardSynchronizer rewardSynchronizer) => rewardSynchronizer._playerCollection.GetPlayer(rewardSynchronizer._localPlayerId); + + internal static readonly List _rewardMessageCache = [..ReflectionHelper.GetSubtypesInMods()]; + + [HarmonyPatch(nameof(RewardSynchronizer.OnCombatEnded))] + [HarmonyPrefix] + internal static void OnCombatEndedHandleCustomBufferedMessages() + { + foreach (BufferedCustomRewardMessage bufferedMessage in BufferedCustomRewardMessages) + { + bufferedMessage.message.MessageHandler(bufferedMessage.message, bufferedMessage.senderId); + } + BufferedCustomRewardMessages.Clear(); + } +} diff --git a/Patches/Content/RunManagerPatches.cs b/Patches/Content/RunManagerPatches.cs new file mode 100644 index 0000000..157090c --- /dev/null +++ b/Patches/Content/RunManagerPatches.cs @@ -0,0 +1,22 @@ +using BaseLib.Abstracts; +using HarmonyLib; +using MegaCrit.Sts2.Core.Helpers; +using MegaCrit.Sts2.Core.Runs; + +namespace BaseLib.Patches.Content; + +[HarmonyPatch(typeof(RunManager))] +public static class RunManagerPatches +{ + internal static List customMessageTypes = new Type[] [..ReflectionHelper.GetSubtypesInMods()]; + [HarmonyPatch(nameof(RunManager.InitializeShared))] + [HarmonyPostfix] + public static void RegisterCustomMessageHandlers(RunManager __instance) + { + var runMessageBuffer = __instance.RunLocationTargetedBuffer; + foreach (var type in customMessageTypes) + { + + } + } +} From e3a43bfa096503e3946eac4895d854b348b4e4ba Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Tue, 7 Apr 2026 00:55:16 +0800 Subject: [PATCH 02/15] Works locally; checking mp in morning --- Abstracts/CustomMessage.cs | 23 +++- Abstracts/CustomRewardMessage.cs | 2 +- BaseLib.csproj | 1 + Common/Rewards/CardTransformReward.cs | 4 +- Common/Rewards/CardTransformRewardMessage.cs | 34 ++++++ Patches/Content/RewardSynchronizerPatches.cs | 120 +++++++++++++++++-- Patches/Content/RunManagerPatches.cs | 36 +++++- 7 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 Common/Rewards/CardTransformRewardMessage.cs diff --git a/Abstracts/CustomMessage.cs b/Abstracts/CustomMessage.cs index 484071c..29c48fb 100644 --- a/Abstracts/CustomMessage.cs +++ b/Abstracts/CustomMessage.cs @@ -1,4 +1,5 @@ using MegaCrit.Sts2.Core.Logging; +using MegaCrit.Sts2.Core.Multiplayer.Game; using MegaCrit.Sts2.Core.Multiplayer.Messages.Game; using MegaCrit.Sts2.Core.Multiplayer.Serialization; using MegaCrit.Sts2.Core.Multiplayer.Transport; @@ -6,14 +7,34 @@ namespace BaseLib.Abstracts; +/// +/// The type to inherit from to add a custom message +/// public abstract class CustomMessage : INetMessage, IRunLocationTargetedMessage { - public abstract void MessageHandler(T message, ulong senderID) where T : CustomMessage; + /// + /// Register your message type here + /// Needs to be a function that takes ( message, senderId) + /// + public abstract void Initialize(RunLocationTargetedMessageBuffer messageBuffer); + + /// + /// Unregister your message type here
+ /// Reference the same function you registered in + ///
+ public abstract void Dispose(RunLocationTargetedMessageBuffer messageBuffer); + public abstract bool ShouldBroadcast { get; } public abstract NetTransferMode Mode { get; } public abstract LogLevel LogLevel { get; } public abstract RunLocation Location { get; } + /// + /// Read out the necessary data from the saved info, in the order it was written + /// public abstract void Deserialize(PacketReader reader); + /// + /// Save necessary data, to be read out when reconstructing the message + /// public abstract void Serialize(PacketWriter writer); } diff --git a/Abstracts/CustomRewardMessage.cs b/Abstracts/CustomRewardMessage.cs index 24e89ce..3e87bf7 100644 --- a/Abstracts/CustomRewardMessage.cs +++ b/Abstracts/CustomRewardMessage.cs @@ -10,7 +10,7 @@ public abstract class CustomRewardMessage : CustomMessage { public required RewardType rewardType; public required RunLocation location; - public required bool wasSkipped; + // public required bool wasSkipped; public override bool ShouldBroadcast => true; public override NetTransferMode Mode => NetTransferMode.Reliable; diff --git a/BaseLib.csproj b/BaseLib.csproj index fc5627c..a09cb6b 100644 --- a/BaseLib.csproj +++ b/BaseLib.csproj @@ -3,6 +3,7 @@ net9.0 + 14.0 true enable true diff --git a/Common/Rewards/CardTransformReward.cs b/Common/Rewards/CardTransformReward.cs index 6c53d9c..e801eb0 100644 --- a/Common/Rewards/CardTransformReward.cs +++ b/Common/Rewards/CardTransformReward.cs @@ -1,4 +1,5 @@ using BaseLib.Abstracts; +using BaseLib.Patches.Content; using MegaCrit.Sts2.Core.Entities.Players; using MegaCrit.Sts2.Core.Helpers; using MegaCrit.Sts2.Core.Localization; @@ -11,6 +12,7 @@ namespace BaseLib.Common.Rewards; public class CardTransformReward(Player player) : CustomReward(player) { public static RewardType CardTransform; + public required bool Upgrade; protected override RewardType RewardType => CardTransform; public override LocString Description => new LocString("gameplay_ui", "COMBAT_REWARD_CARD_TRANSFORM"); public override bool IsPopulated => true; @@ -30,6 +32,6 @@ public override Task Populate() protected override async Task OnSelect() { BaseLibMain.Logger.Info("Obtained card transformation from reward"); - return await RunManager.Instance.RewardSynchronizer.DoLocalCardTransform(); + return await RunManager.Instance.RewardSynchronizer.DoLocalCardTransform(true); } } diff --git a/Common/Rewards/CardTransformRewardMessage.cs b/Common/Rewards/CardTransformRewardMessage.cs new file mode 100644 index 0000000..339713a --- /dev/null +++ b/Common/Rewards/CardTransformRewardMessage.cs @@ -0,0 +1,34 @@ +using BaseLib.Abstracts; +using MegaCrit.Sts2.Core.Combat; +using MegaCrit.Sts2.Core.Multiplayer.Game; +using MegaCrit.Sts2.Core.Multiplayer.Serialization; +using MegaCrit.Sts2.Core.Runs; + +namespace BaseLib.Common.Rewards; + +public sealed class CardTransformRewardMessage : CustomRewardMessage +{ + private void HandleCardTransformedMessage(CardTransformRewardMessage message, ulong senderId) + { + if (CombatManager.Instance.IsInProgress) + { + // RunManager.Instance.RewardSynchronizer; + } + } + public required bool Upgrade; + public override void Dispose(RunLocationTargetedMessageBuffer messageBuffer) + { + } + + public override void Initialize(RunLocationTargetedMessageBuffer messageBuffer) + { + } + public override void Deserialize(PacketReader reader) + { + } + + + public override void Serialize(PacketWriter writer) + { + } +} diff --git a/Patches/Content/RewardSynchronizerPatches.cs b/Patches/Content/RewardSynchronizerPatches.cs index 0a29703..28152ac 100644 --- a/Patches/Content/RewardSynchronizerPatches.cs +++ b/Patches/Content/RewardSynchronizerPatches.cs @@ -1,8 +1,15 @@ using BaseLib.Abstracts; +using BaseLib.Common.Rewards; using HarmonyLib; +using MegaCrit.Sts2.Core.CardSelection; +using MegaCrit.Sts2.Core.Commands; using MegaCrit.Sts2.Core.Entities.Players; +using MegaCrit.Sts2.Core.Factories; using MegaCrit.Sts2.Core.Helpers; +using MegaCrit.Sts2.Core.Localization; +using MegaCrit.Sts2.Core.Models; using MegaCrit.Sts2.Core.Multiplayer.Game; +using MegaCrit.Sts2.Core.Runs; namespace BaseLib.Patches.Content; @@ -12,7 +19,7 @@ public static class RewardSynchronizerExtensions /// /// Struct to save a custom reward message until combat ends /// - public struct BufferedCustomRewardMessage + internal struct BufferedCustomRewardMessage { /// /// the id of the player who sent the message @@ -27,23 +34,116 @@ public struct BufferedCustomRewardMessage /// /// Reference list of buffered messages /// - public static List BufferedCustomRewardMessages = []; + internal static List _bufferedCustomRewardMessages = []; + + extension(RewardSynchronizer rewardSynchronizer) + { + + internal List BufferedCustomRewardMessages { get { return _bufferedCustomRewardMessages; } } + + public void BufferCustomRewardMessage(CustomRewardMessage message, ulong senderId) + { + var bufferedMessage = new BufferedCustomRewardMessage + { + senderId = senderId, + message = message + }; + rewardSynchronizer.BufferedCustomRewardMessages.Add(bufferedMessage); + } + + /// + /// Exposes the private LocalPlayer property from + /// + public Player? LocalPlayerRef => rewardSynchronizer._playerCollection.GetPlayer(rewardSynchronizer._localPlayerId); + /// + /// Exposes the private IPlayerCollection property + /// + public IPlayerCollection? PlayerCollection => rewardSynchronizer._playerCollection; + /// + /// Exposes the private RunLocationTargetedMessageBuffer property + /// + public RunLocationTargetedMessageBuffer? MessageBuffer => rewardSynchronizer._messageBuffer; + + + public async Task DoLocalCardTransform(bool upgrade = false) + { + CardTransformRewardMessage message = new CardTransformRewardMessage + { + location = rewardSynchronizer._messageBuffer.CurrentLocation, + rewardType = CardTransformReward.CardTransform, + Upgrade = upgrade, + + }; + rewardSynchronizer._gameService.SendMessage(message); + return await rewardSynchronizer.DoCardTransform(rewardSynchronizer.LocalPlayer, upgrade); + } + + public async Task DoCardTransform(Player player, bool upgrade = false) + { + CardSelectorPrefs prefs = new CardSelectorPrefs(new LocString("gameplay_ui", "COMBAT_REWARD_CARD_REMOVAL.selection_screen_prompt"), 1) + { + Cancelable = false, + RequireManualConfirmation = true + }; + CardModel card = (await CardSelectCmd.FromDeckForTransformation(player, prefs)).FirstOrDefault(); + if (card != null) + { + CardModel newCard = CardFactory.CreateRandomCardForTransform(card, isInCombat: false, player.RunState.Rng.Niche); + if (upgrade) + { + newCard.UpgradeInternal(); + } + await CardCmd.Transform(card, newCard); + BaseLibMain.Logger.Debug($"Player {player.NetId} transformed {card.Id} in their deck into {newCard.Id}" + (upgrade ? " and upgraded it." : ".")); + return true; + } + return false; + } + } + - /// - /// Exposes the private LocalPlayer property from - /// - public static Player? LocalPlayerRef(this RewardSynchronizer rewardSynchronizer) => rewardSynchronizer._playerCollection.GetPlayer(rewardSynchronizer._localPlayerId); internal static readonly List _rewardMessageCache = [..ReflectionHelper.GetSubtypesInMods()]; + [HarmonyPatch(MethodType.Constructor, [typeof(RunLocationTargetedMessageBuffer), typeof(INetGameService), typeof(IPlayerCollection), typeof(ulong)])] + [HarmonyPostfix] + public static void InitializeCustomRewardHandlers(RewardSynchronizer __instance) + { + foreach (var rewardMessageType in _rewardMessageCache) + { + if (rewardMessageType.CreateInstance() is not CustomRewardMessage dummyMessage) + { + BaseLibMain.Logger.Error($"Message instance creation for type {rewardMessageType.GetType()} from {rewardMessageType.Assembly} failed during Initialize"); + continue; + } + dummyMessage.Initialize(__instance._messageBuffer); + } + } + + [HarmonyPatch(nameof(RewardSynchronizer.Dispose))] + [HarmonyPostfix] + public static void UnregisterCustomRewardHandlers(RewardSynchronizer __instance) + { + foreach (var rewardMessageType in _rewardMessageCache) + { + if (rewardMessageType.CreateInstance() is not CustomRewardMessage dummyMessage) + { + BaseLibMain.Logger.Error($"Message instance creation for type {rewardMessageType.GetType()} from {rewardMessageType.Assembly} failed during Dispose"); + continue; + } + dummyMessage.Dispose(__instance._messageBuffer); + } + } + [HarmonyPatch(nameof(RewardSynchronizer.OnCombatEnded))] [HarmonyPrefix] - internal static void OnCombatEndedHandleCustomBufferedMessages() + public static void HandleCustomBufferedMessages(RewardSynchronizer __instance) { - foreach (BufferedCustomRewardMessage bufferedMessage in BufferedCustomRewardMessages) + foreach (BufferedCustomRewardMessage bufferedMessage in __instance.BufferedCustomRewardMessages) { - bufferedMessage.message.MessageHandler(bufferedMessage.message, bufferedMessage.senderId); + // TODO: Get reference to appropriate message handler from the message type? Call handle method? + // bufferedMessage.message.MessageHandler(bufferedMessage.message, bufferedMessage.senderId); } - BufferedCustomRewardMessages.Clear(); + __instance.BufferedCustomRewardMessages.Clear(); } } diff --git a/Patches/Content/RunManagerPatches.cs b/Patches/Content/RunManagerPatches.cs index 157090c..8868994 100644 --- a/Patches/Content/RunManagerPatches.cs +++ b/Patches/Content/RunManagerPatches.cs @@ -8,15 +8,49 @@ namespace BaseLib.Patches.Content; [HarmonyPatch(typeof(RunManager))] public static class RunManagerPatches { - internal static List customMessageTypes = new Type[] [..ReflectionHelper.GetSubtypesInMods()]; + + internal static List customMessageTypes = [..ReflectionHelper.GetSubtypesInMods()]; + [HarmonyPatch(nameof(RunManager.InitializeShared))] [HarmonyPostfix] + public static void InitializeCustomMessageHandlers(RunManager __instance) + { + foreach (var messageType in customMessageTypes) + { + if (messageType.CreateInstance() is not CustomMessage dummyMessage) + { + BaseLibMain.Logger.Error($"Message instance creation for type {messageType.GetType()} from {messageType.Assembly} failed during Initialize"); + continue; + } + dummyMessage.Initialize(__instance.RunLocationTargetedBuffer); + } + } + + [HarmonyPatch(nameof(RunManager.CleanUp))] + [HarmonyPostfix] + public static void UnregisterCustomRewardHandlers(RunManager __instance) + { + foreach (var messageType in customMessageTypes) + { + if (messageType.CreateInstance() is not CustomMessage dummyMessage) + { + BaseLibMain.Logger.Error($"Message instance creation for type {messageType.GetType()} from {messageType.Assembly} failed during Dispose"); + continue; + } + dummyMessage.Dispose(__instance.RunLocationTargetedBuffer); + } + } + // [HarmonyPatch(nameof(RunManager.InitializeShared))] + // [HarmonyPostfix] public static void RegisterCustomMessageHandlers(RunManager __instance) { var runMessageBuffer = __instance.RunLocationTargetedBuffer; foreach (var type in customMessageTypes) { + // var registerMethod = AccessTools.Method(typeof(RunLocationTargetedMessageBuffer), nameof(RunLocationTargetedMessageBuffer.RegisterMessageHandler)); + // var typedMethod = registerMethod.MakeGenericMethod(type); + // typedMethod.Invoke(__instance, [AccessTools.FieldRef(type, nameof(CustomMessage.MessageHandler))]); } } } From 19b28c410ed26432da2d9368cf7f36bd27d2ec20 Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Tue, 7 Apr 2026 14:18:00 +0800 Subject: [PATCH 03/15] small additions to message registering and disposal --- Patches/Content/RewardSynchronizerPatches.cs | 24 +++++++++++++------- Patches/Content/RunManagerPatches.cs | 16 ++----------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/Patches/Content/RewardSynchronizerPatches.cs b/Patches/Content/RewardSynchronizerPatches.cs index 28152ac..334e822 100644 --- a/Patches/Content/RewardSynchronizerPatches.cs +++ b/Patches/Content/RewardSynchronizerPatches.cs @@ -19,7 +19,7 @@ public static class RewardSynchronizerExtensions /// /// Struct to save a custom reward message until combat ends /// - internal struct BufferedCustomRewardMessage + public struct BufferedCustomRewardMessage { /// /// the id of the player who sent the message @@ -32,7 +32,8 @@ internal struct BufferedCustomRewardMessage } /// - /// Reference list of buffered messages + /// Reference list of buffered messages
+ /// Hopefully there is only ever one instance of at a time on each client? ///
internal static List _bufferedCustomRewardMessages = []; @@ -63,19 +64,26 @@ public void BufferCustomRewardMessage(CustomRewardMessage message, ulong senderI /// Exposes the private RunLocationTargetedMessageBuffer property ///
public RunLocationTargetedMessageBuffer? MessageBuffer => rewardSynchronizer._messageBuffer; + /// + /// Exposes the private INetGameService property + /// + public INetGameService? GameService => rewardSynchronizer._gameService; + /// + /// Method to handle transforming a card as a combat reward + /// public async Task DoLocalCardTransform(bool upgrade = false) { CardTransformRewardMessage message = new CardTransformRewardMessage { - location = rewardSynchronizer._messageBuffer.CurrentLocation, + location = rewardSynchronizer.MessageBuffer.CurrentLocation, rewardType = CardTransformReward.CardTransform, Upgrade = upgrade, }; - rewardSynchronizer._gameService.SendMessage(message); - return await rewardSynchronizer.DoCardTransform(rewardSynchronizer.LocalPlayer, upgrade); + rewardSynchronizer.GameService?.SendMessage(message); + return await rewardSynchronizer.DoCardTransform(rewardSynchronizer.LocalPlayerRef, upgrade); } public async Task DoCardTransform(Player player, bool upgrade = false) @@ -85,7 +93,7 @@ public async Task DoCardTransform(Player player, bool upgrade = false) Cancelable = false, RequireManualConfirmation = true }; - CardModel card = (await CardSelectCmd.FromDeckForTransformation(player, prefs)).FirstOrDefault(); + CardModel? card = (await CardSelectCmd.FromDeckForTransformation(player, prefs)).FirstOrDefault(); if (card != null) { CardModel newCard = CardFactory.CreateRandomCardForTransform(card, isInCombat: false, player.RunState.Rng.Niche); @@ -116,7 +124,7 @@ public static void InitializeCustomRewardHandlers(RewardSynchronizer __instance) BaseLibMain.Logger.Error($"Message instance creation for type {rewardMessageType.GetType()} from {rewardMessageType.Assembly} failed during Initialize"); continue; } - dummyMessage.Initialize(__instance._messageBuffer); + dummyMessage.Initialize(__instance.MessageBuffer); } } @@ -131,7 +139,7 @@ public static void UnregisterCustomRewardHandlers(RewardSynchronizer __instance) BaseLibMain.Logger.Error($"Message instance creation for type {rewardMessageType.GetType()} from {rewardMessageType.Assembly} failed during Dispose"); continue; } - dummyMessage.Dispose(__instance._messageBuffer); + dummyMessage.Dispose(__instance.MessageBuffer); } } diff --git a/Patches/Content/RunManagerPatches.cs b/Patches/Content/RunManagerPatches.cs index 8868994..4f8f59c 100644 --- a/Patches/Content/RunManagerPatches.cs +++ b/Patches/Content/RunManagerPatches.cs @@ -11,6 +11,7 @@ public static class RunManagerPatches internal static List customMessageTypes = [..ReflectionHelper.GetSubtypesInMods()]; + // currently duplicates the registration for CustomRewardMessages? maybe remove that part since it happens at the same time [HarmonyPatch(nameof(RunManager.InitializeShared))] [HarmonyPostfix] public static void InitializeCustomMessageHandlers(RunManager __instance) @@ -28,7 +29,7 @@ public static void InitializeCustomMessageHandlers(RunManager __instance) [HarmonyPatch(nameof(RunManager.CleanUp))] [HarmonyPostfix] - public static void UnregisterCustomRewardHandlers(RunManager __instance) + public static void DisposeCustomMessageHandlers(RunManager __instance) { foreach (var messageType in customMessageTypes) { @@ -40,17 +41,4 @@ public static void UnregisterCustomRewardHandlers(RunManager __instance) dummyMessage.Dispose(__instance.RunLocationTargetedBuffer); } } - // [HarmonyPatch(nameof(RunManager.InitializeShared))] - // [HarmonyPostfix] - public static void RegisterCustomMessageHandlers(RunManager __instance) - { - var runMessageBuffer = __instance.RunLocationTargetedBuffer; - foreach (var type in customMessageTypes) - { - - // var registerMethod = AccessTools.Method(typeof(RunLocationTargetedMessageBuffer), nameof(RunLocationTargetedMessageBuffer.RegisterMessageHandler)); - // var typedMethod = registerMethod.MakeGenericMethod(type); - // typedMethod.Invoke(__instance, [AccessTools.FieldRef(type, nameof(CustomMessage.MessageHandler))]); - } - } } From 92ede508e826503c9ee26d5abde4785d4ae8ae98 Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Wed, 8 Apr 2026 17:07:39 +0800 Subject: [PATCH 04/15] Fix serializing and loading from save --- Abstracts/CustomReward.cs | 24 +++++++++++++++ Common/Rewards/CardTransformReward.cs | 25 +++++++++++++-- Patches/Content/CustomEnums.cs | 32 ++++++++++++++------ Patches/Content/CustomRewardPatches.cs | 42 ++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 Patches/Content/CustomRewardPatches.cs diff --git a/Abstracts/CustomReward.cs b/Abstracts/CustomReward.cs index 948e533..9c283b2 100644 --- a/Abstracts/CustomReward.cs +++ b/Abstracts/CustomReward.cs @@ -1,8 +1,12 @@ +using Baselib.Patches.Content; using MegaCrit.Sts2.Core.Entities.Players; using MegaCrit.Sts2.Core.Rewards; +using MegaCrit.Sts2.Core.Saves.Runs; namespace BaseLib.Abstracts; +public delegate T SerializableCustomReward(SerializableReward save, Player player) where T : CustomReward; + public abstract class CustomReward(Player player) : Reward(player) { /// @@ -10,5 +14,25 @@ public abstract class CustomReward(Player player) : Reward(player) /// public override int RewardsSetIndex => 9; + public abstract SerializableCustomReward SerializeMethod { get; } + + public virtual void Initialize() + { + if (SerializeMethod?.Method.IsStatic == true) + { + BaseLibMain.Logger.Info($"Registering CustomReward serializer for {GetType()}"); + CustomRewardPatches.RegisterCustomReward(RewardType, SerializeMethod); + } + else if (SerializeMethod != null) + { + throw new FieldAccessException($"Custom Reward {this.GetType()} has assigned a non-static method to SerializeMethod property"); + } + else + { + throw new NotImplementedException($"Custom Reward {this.GetType()} has not implemented an Initialize() method to register a serializer for itself"); + } + } + // TODO: per-mod id prefixing for localisation } + diff --git a/Common/Rewards/CardTransformReward.cs b/Common/Rewards/CardTransformReward.cs index e801eb0..6778d92 100644 --- a/Common/Rewards/CardTransformReward.cs +++ b/Common/Rewards/CardTransformReward.cs @@ -5,21 +5,40 @@ using MegaCrit.Sts2.Core.Localization; using MegaCrit.Sts2.Core.Rewards; using MegaCrit.Sts2.Core.Runs; +using MegaCrit.Sts2.Core.Saves.Runs; namespace BaseLib.Common.Rewards; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -public class CardTransformReward(Player player) : CustomReward(player) +public class CardTransformReward(Player player, bool upgrade = false) : CustomReward(player) { + [CustomEnum] public static RewardType CardTransform; - public required bool Upgrade; protected override RewardType RewardType => CardTransform; + + public required bool Upgrade; + public override LocString Description => new LocString("gameplay_ui", "COMBAT_REWARD_CARD_TRANSFORM"); public override bool IsPopulated => true; - public static string RewardIcon => ImageHelper.GetImagePath("ui/reward_screen/reward_icon_card_removal.png"); protected override string IconPath => RewardIcon; + public static CardTransformReward CreateFromSerializable(SerializableReward save, Player player) + { + return new CardTransformReward(player) { Upgrade = save.WasGoldStolenBack}; // temp hack before worrying about extending the serialized values + } + + public override SerializableReward ToSerializable() + { + return new SerializableReward() + { + RewardType = CardTransform, + WasGoldStolenBack = Upgrade + }; + } + + public override SerializableCustomReward SerializeMethod => CreateFromSerializable; + public override void MarkContentAsSeen() { } diff --git a/Patches/Content/CustomEnums.cs b/Patches/Content/CustomEnums.cs index e0088af..5aea4fc 100644 --- a/Patches/Content/CustomEnums.cs +++ b/Patches/Content/CustomEnums.cs @@ -9,6 +9,7 @@ using MegaCrit.Sts2.Core.Entities.Cards; using MegaCrit.Sts2.Core.Helpers; using MegaCrit.Sts2.Core.Models; +using MegaCrit.Sts2.Core.Rewards; namespace BaseLib.Patches.Content; @@ -339,15 +340,26 @@ static void FindAndGenerate() } //Following code is exclusively for CustomPile - if (field.FieldType != typeof(PileType)) continue; - if (!t.IsAssignableTo(typeof(CustomPile))) continue; - - var constructor = t.GetConstructor(BindingFlags.Instance | BindingFlags.Public, []) ?? throw new Exception($"CustomPile {t.FullName} with custom PileType does not have an accessible no-parameter constructor"); - - var pileType = (PileType?)field.GetValue(null); - if (pileType == null) throw new Exception($"Failed to be set up custom PileType in {t.FullName}"); - - CustomPiles.RegisterCustomPile((PileType) pileType, () => (CustomPile) constructor.Invoke(null)); + if (field.FieldType == typeof(PileType) && t.IsAssignableTo(typeof(CustomPile))) + { + var constructor = t.GetConstructor(BindingFlags.Instance | BindingFlags.Public, []) ?? throw new Exception($"CustomPile {t.FullName} with custom PileType does not have an accessible no-parameter constructor"); + + var pileType = (PileType?)field.GetValue(null); + if (pileType == null) throw new Exception($"Failed to be set up custom PileType in {t.FullName}"); + + CustomPiles.RegisterCustomPile((PileType) pileType, () => (CustomPile) constructor.Invoke(null)); + } + + if (field.FieldType == typeof(RewardType) && t.IsAssignableTo(typeof(CustomReward))) + { + if (t.CreateInstance() is not CustomReward dummyReward) + { + BaseLibMain.Logger.Error($"Reward instance creation for type {t.GetType()} from {t.Assembly} failed during Initialize"); + continue; + } + BaseLibMain.Logger.Debug($"Initializing CustomReward inheriting class {dummyReward.GetType()}"); + dummyReward.Initialize(); + } } } -} \ No newline at end of file +} diff --git a/Patches/Content/CustomRewardPatches.cs b/Patches/Content/CustomRewardPatches.cs new file mode 100644 index 0000000..4d8ae89 --- /dev/null +++ b/Patches/Content/CustomRewardPatches.cs @@ -0,0 +1,42 @@ +using BaseLib; +using BaseLib.Abstracts; +using HarmonyLib; +using MegaCrit.Sts2.Core.Entities.Players; +using MegaCrit.Sts2.Core.Rewards; +using MegaCrit.Sts2.Core.Saves.Runs; + +namespace Baselib.Patches.Content; + +[HarmonyPatch(typeof(Reward))] +public static class CustomRewardPatches +{ + internal static readonly Dictionary> _RewardTypeSerializers = []; + + public static void RegisterCustomReward(RewardType type, SerializableCustomReward serializer) + { + if (!_RewardTypeSerializers.ContainsKey(type)) + { + BaseLibMain.Logger.Info($"Registering RewardType {nameof(type)}"); + _RewardTypeSerializers.Add(type, serializer); + } + } + + [HarmonyPatch(nameof(Reward.FromSerializable))] + [HarmonyPrefix] + public static bool FromSerializablePrefix(SerializableReward save, Player player, ref Reward __result) + { + foreach (var pair in _RewardTypeSerializers.Keys) + { + BaseLibMain.Logger.Debug($"{pair}: {_RewardTypeSerializers[pair].Method}"); + } + BaseLibMain.Logger.Debug(_RewardTypeSerializers.ToString()); + if (_RewardTypeSerializers.Keys.Contains(save.RewardType)) + { + BaseLibMain.Logger.Debug($"Found RewardType {save.RewardType} in registry from mod {_RewardTypeSerializers[save.RewardType].Method.GetType().Assembly}"); + __result = _RewardTypeSerializers[save.RewardType].Invoke(save, player); + return false; + } + BaseLibMain.Logger.Debug($"No CustomReward found for RewardType {save.RewardType}, proceeding to vanilla method"); + return true; + } +} From c5f54e350210da38318b4cfc5add17f84a4dca5f Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Thu, 9 Apr 2026 00:22:53 +0800 Subject: [PATCH 05/15] fixup to messaging (currently causes state divergence) --- Common/Rewards/CardTransformReward.cs | 2 +- Common/Rewards/CardTransformRewardMessage.cs | 21 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Common/Rewards/CardTransformReward.cs b/Common/Rewards/CardTransformReward.cs index 6778d92..cc5597b 100644 --- a/Common/Rewards/CardTransformReward.cs +++ b/Common/Rewards/CardTransformReward.cs @@ -16,7 +16,7 @@ public class CardTransformReward(Player player, bool upgrade = false) : CustomRe public static RewardType CardTransform; protected override RewardType RewardType => CardTransform; - public required bool Upgrade; + public bool Upgrade = upgrade; public override LocString Description => new LocString("gameplay_ui", "COMBAT_REWARD_CARD_TRANSFORM"); public override bool IsPopulated => true; diff --git a/Common/Rewards/CardTransformRewardMessage.cs b/Common/Rewards/CardTransformRewardMessage.cs index 339713a..8cab828 100644 --- a/Common/Rewards/CardTransformRewardMessage.cs +++ b/Common/Rewards/CardTransformRewardMessage.cs @@ -1,8 +1,12 @@ using BaseLib.Abstracts; +using BaseLib.Patches.Content; using MegaCrit.Sts2.Core.Combat; +using MegaCrit.Sts2.Core.Entities.Players; +using MegaCrit.Sts2.Core.Helpers; using MegaCrit.Sts2.Core.Multiplayer.Game; using MegaCrit.Sts2.Core.Multiplayer.Serialization; using MegaCrit.Sts2.Core.Runs; +using MegaCrit.Sts2.Core.Runs.History; namespace BaseLib.Common.Rewards; @@ -10,19 +14,32 @@ public sealed class CardTransformRewardMessage : CustomRewardMessage { private void HandleCardTransformedMessage(CardTransformRewardMessage message, ulong senderId) { + var rs = RunManager.Instance.RewardSynchronizer; if (CombatManager.Instance.IsInProgress) { - // RunManager.Instance.RewardSynchronizer; + rs.BufferCustomRewardMessage(message, senderId); + return; } + + Player player = rs.PlayerCollection.GetPlayer(senderId); + if (player == rs.LocalPlayer) + { + throw new InvalidOperationException($"CardTransformRewardMessage should not be sent to the player transforming the card"); + } + TaskHelper.RunSafely(rs.DoCardTransform(player)); } - public required bool Upgrade; + public override void Dispose(RunLocationTargetedMessageBuffer messageBuffer) { + messageBuffer.UnregisterMessageHandler(HandleCardTransformedMessage); } public override void Initialize(RunLocationTargetedMessageBuffer messageBuffer) { + messageBuffer.RegisterMessageHandler(HandleCardTransformedMessage); } + + public required bool Upgrade; public override void Deserialize(PacketReader reader) { } From 2581ddd1ed03cda09dec7f9e9baa617b71c3f778 Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Thu, 9 Apr 2026 18:38:57 +0800 Subject: [PATCH 06/15] try handling end combat end? --- Patches/Content/RewardSynchronizerPatches.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Patches/Content/RewardSynchronizerPatches.cs b/Patches/Content/RewardSynchronizerPatches.cs index 334e822..229b1d3 100644 --- a/Patches/Content/RewardSynchronizerPatches.cs +++ b/Patches/Content/RewardSynchronizerPatches.cs @@ -9,6 +9,7 @@ using MegaCrit.Sts2.Core.Localization; using MegaCrit.Sts2.Core.Models; using MegaCrit.Sts2.Core.Multiplayer.Game; +using MegaCrit.Sts2.Core.Multiplayer.Serialization; using MegaCrit.Sts2.Core.Runs; namespace BaseLib.Patches.Content; @@ -151,6 +152,8 @@ public static void HandleCustomBufferedMessages(RewardSynchronizer __instance) { // TODO: Get reference to appropriate message handler from the message type? Call handle method? // bufferedMessage.message.MessageHandler(bufferedMessage.message, bufferedMessage.senderId); + __instance.MessageBuffer.CallHandlersOfType(bufferedMessage.message.GetType(), bufferedMessage.message, bufferedMessage.senderId); + } __instance.BufferedCustomRewardMessages.Clear(); } From 6937aee0d7f1586f8dde6a4a1f1c74bcaa349e2a Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Fri, 10 Apr 2026 20:20:24 +0800 Subject: [PATCH 07/15] remove runlocation from base message wrapper, add to customRewardMessage --- Abstracts/CustomMessage.cs | 5 +++-- Abstracts/CustomRewardMessage.cs | 12 +++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Abstracts/CustomMessage.cs b/Abstracts/CustomMessage.cs index 29c48fb..254a44f 100644 --- a/Abstracts/CustomMessage.cs +++ b/Abstracts/CustomMessage.cs @@ -4,17 +4,19 @@ using MegaCrit.Sts2.Core.Multiplayer.Serialization; using MegaCrit.Sts2.Core.Multiplayer.Transport; using MegaCrit.Sts2.Core.Runs; +using BaseLib.Patches.Content; namespace BaseLib.Abstracts; /// /// The type to inherit from to add a custom message /// -public abstract class CustomMessage : INetMessage, IRunLocationTargetedMessage +public abstract class CustomMessage : INetMessage { /// /// Register your message type here /// Needs to be a function that takes ( message, senderId) + /// See for an example. You probably want to use an Extension Method /// public abstract void Initialize(RunLocationTargetedMessageBuffer messageBuffer); @@ -27,7 +29,6 @@ public abstract class CustomMessage : INetMessage, IRunLocationTargetedMessage public abstract bool ShouldBroadcast { get; } public abstract NetTransferMode Mode { get; } public abstract LogLevel LogLevel { get; } - public abstract RunLocation Location { get; } /// /// Read out the necessary data from the saved info, in the order it was written diff --git a/Abstracts/CustomRewardMessage.cs b/Abstracts/CustomRewardMessage.cs index 3e87bf7..a27f304 100644 --- a/Abstracts/CustomRewardMessage.cs +++ b/Abstracts/CustomRewardMessage.cs @@ -1,19 +1,17 @@ -using MegaCrit.Sts2.Core.Logging; +using MegaCrit.Sts2.Core.Multiplayer.Messages.Game; using MegaCrit.Sts2.Core.Multiplayer.Transport; using MegaCrit.Sts2.Core.Rewards; using MegaCrit.Sts2.Core.Runs; namespace BaseLib.Abstracts; -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -public abstract class CustomRewardMessage : CustomMessage +public abstract class CustomRewardMessage : CustomMessage, IRunLocationTargetedMessage { public required RewardType rewardType; - public required RunLocation location; - // public required bool wasSkipped; + public required bool wasSkipped; public override bool ShouldBroadcast => true; public override NetTransferMode Mode => NetTransferMode.Reliable; - public override LogLevel LogLevel => LogLevel.VeryDebug; - public override RunLocation Location => location; + + public RunLocation Location { get; set; } } From d38ae1ff1c1d6449ae1bd1a2ab056cbea415c34c Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Fri, 10 Apr 2026 21:19:32 +0800 Subject: [PATCH 08/15] General cleanup, change CardTransformReward to accept an amount of cards --- Abstracts/CustomRewardMessage.cs | 25 ++++-- Common/Rewards/CardTransformReward.cs | 7 +- Common/Rewards/CardTransformRewardMessage.cs | 28 +++++-- Patches/Content/RewardSynchronizerPatches.cs | 86 ++++++++------------ Patches/Content/RunManagerPatches.cs | 10 +-- 5 files changed, 83 insertions(+), 73 deletions(-) diff --git a/Abstracts/CustomRewardMessage.cs b/Abstracts/CustomRewardMessage.cs index a27f304..f809649 100644 --- a/Abstracts/CustomRewardMessage.cs +++ b/Abstracts/CustomRewardMessage.cs @@ -1,17 +1,32 @@ using MegaCrit.Sts2.Core.Multiplayer.Messages.Game; using MegaCrit.Sts2.Core.Multiplayer.Transport; -using MegaCrit.Sts2.Core.Rewards; using MegaCrit.Sts2.Core.Runs; namespace BaseLib.Abstracts; +/// +/// Abstract class to inherit for syncing rewards +/// public abstract class CustomRewardMessage : CustomMessage, IRunLocationTargetedMessage { - public required RewardType rewardType; + /// + /// Include whether the reward was skipped or not + /// Not required to actually check, namely if being used for a per-player reward like + /// public required bool wasSkipped; - public override bool ShouldBroadcast => true; - public override NetTransferMode Mode => NetTransferMode.Reliable; + /// + /// You probably want to broadcast the message + /// + public sealed override bool ShouldBroadcast => true; - public RunLocation Location { get; set; } + /// + /// Rewards should prabably be sent reliably too + /// + public sealed override NetTransferMode Mode => NetTransferMode.Reliable; + + /// + /// Set when instantiating, afaik needed for saving to the run? + /// + public required RunLocation Location { get; set; } } diff --git a/Common/Rewards/CardTransformReward.cs b/Common/Rewards/CardTransformReward.cs index cc5597b..0c88e14 100644 --- a/Common/Rewards/CardTransformReward.cs +++ b/Common/Rewards/CardTransformReward.cs @@ -10,13 +10,14 @@ namespace BaseLib.Common.Rewards; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -public class CardTransformReward(Player player, bool upgrade = false) : CustomReward(player) +public class CardTransformReward(Player player, int amount, bool upgrade = false) : CustomReward(player) { [CustomEnum] public static RewardType CardTransform; protected override RewardType RewardType => CardTransform; public bool Upgrade = upgrade; + public int Amount = amount; public override LocString Description => new LocString("gameplay_ui", "COMBAT_REWARD_CARD_TRANSFORM"); public override bool IsPopulated => true; @@ -25,7 +26,7 @@ public class CardTransformReward(Player player, bool upgrade = false) : CustomRe public static CardTransformReward CreateFromSerializable(SerializableReward save, Player player) { - return new CardTransformReward(player) { Upgrade = save.WasGoldStolenBack}; // temp hack before worrying about extending the serialized values + return new CardTransformReward(player, save.GoldAmount, save.WasGoldStolenBack); // temp hack before worrying about extending the serialized values } public override SerializableReward ToSerializable() @@ -51,6 +52,6 @@ public override Task Populate() protected override async Task OnSelect() { BaseLibMain.Logger.Info("Obtained card transformation from reward"); - return await RunManager.Instance.RewardSynchronizer.DoLocalCardTransform(true); + return await RunManager.Instance.RewardSynchronizer.DoLocalCardTransform(Amount, true); } } diff --git a/Common/Rewards/CardTransformRewardMessage.cs b/Common/Rewards/CardTransformRewardMessage.cs index 8cab828..dd39476 100644 --- a/Common/Rewards/CardTransformRewardMessage.cs +++ b/Common/Rewards/CardTransformRewardMessage.cs @@ -3,48 +3,64 @@ using MegaCrit.Sts2.Core.Combat; using MegaCrit.Sts2.Core.Entities.Players; using MegaCrit.Sts2.Core.Helpers; +using MegaCrit.Sts2.Core.Logging; using MegaCrit.Sts2.Core.Multiplayer.Game; using MegaCrit.Sts2.Core.Multiplayer.Serialization; using MegaCrit.Sts2.Core.Runs; -using MegaCrit.Sts2.Core.Runs.History; namespace BaseLib.Common.Rewards; +/// +/// Message for transforming a card from a new reward type +/// public sealed class CardTransformRewardMessage : CustomRewardMessage { private void HandleCardTransformedMessage(CardTransformRewardMessage message, ulong senderId) { + BaseLibMain.Logger.Debug($"Handling message {message}"); var rs = RunManager.Instance.RewardSynchronizer; if (CombatManager.Instance.IsInProgress) { rs.BufferCustomRewardMessage(message, senderId); + BaseLibMain.Logger.Debug($"Buffered card transform message for {rs.PlayerCollection?.GetPlayer(senderId)}"); return; } - Player player = rs.PlayerCollection.GetPlayer(senderId); - if (player == rs.LocalPlayer) + Player? player = rs.PlayerCollection?.GetPlayer(senderId); + if (player == rs.LocalPlayerRef) { - throw new InvalidOperationException($"CardTransformRewardMessage should not be sent to the player transforming the card"); + throw new InvalidOperationException("CardTransformRewardMessage should not be sent to the player transforming the card"); } - TaskHelper.RunSafely(rs.DoCardTransform(player)); + TaskHelper.RunSafely(rs.DoCardTransform(player, message.Amount, message.Upgrade)); } public override void Dispose(RunLocationTargetedMessageBuffer messageBuffer) { + BaseLibMain.Logger.Debug($"Unregistering handler for {GetType()}"); messageBuffer.UnregisterMessageHandler(HandleCardTransformedMessage); } public override void Initialize(RunLocationTargetedMessageBuffer messageBuffer) { + BaseLibMain.Logger.Debug($"Registering handler for {GetType()}"); messageBuffer.RegisterMessageHandler(HandleCardTransformedMessage); } + /// + /// Whether to upgrade the card as well as transforming + /// public required bool Upgrade; + /// + /// The amount of cards to select from + /// + public required int Amount; + + public override LogLevel LogLevel => LogLevel.Debug; + public override void Deserialize(PacketReader reader) { } - public override void Serialize(PacketWriter writer) { } diff --git a/Patches/Content/RewardSynchronizerPatches.cs b/Patches/Content/RewardSynchronizerPatches.cs index 229b1d3..affd9c1 100644 --- a/Patches/Content/RewardSynchronizerPatches.cs +++ b/Patches/Content/RewardSynchronizerPatches.cs @@ -2,6 +2,7 @@ using BaseLib.Common.Rewards; using HarmonyLib; using MegaCrit.Sts2.Core.CardSelection; +using MegaCrit.Sts2.Core.Combat; using MegaCrit.Sts2.Core.Commands; using MegaCrit.Sts2.Core.Entities.Players; using MegaCrit.Sts2.Core.Factories; @@ -9,16 +10,19 @@ using MegaCrit.Sts2.Core.Localization; using MegaCrit.Sts2.Core.Models; using MegaCrit.Sts2.Core.Multiplayer.Game; -using MegaCrit.Sts2.Core.Multiplayer.Serialization; using MegaCrit.Sts2.Core.Runs; namespace BaseLib.Patches.Content; +/// +/// Extensions to to provide public getters to internal properties and common reward functions +/// [HarmonyPatch(typeof(RewardSynchronizer))] public static class RewardSynchronizerExtensions { /// /// Struct to save a custom reward message until combat ends + /// Prefer creating with /// public struct BufferedCustomRewardMessage { @@ -43,6 +47,9 @@ public struct BufferedCustomRewardMessage internal List BufferedCustomRewardMessages { get { return _bufferedCustomRewardMessages; } } + /// + /// Add a to the combat buffer + /// public void BufferCustomRewardMessage(CustomRewardMessage message, ulong senderId) { var bufferedMessage = new BufferedCustomRewardMessage @@ -74,86 +81,59 @@ public void BufferCustomRewardMessage(CustomRewardMessage message, ulong senderI /// /// Method to handle transforming a card as a combat reward /// - public async Task DoLocalCardTransform(bool upgrade = false) + public async Task DoLocalCardTransform(int amount = 1, bool upgrade = false) { CardTransformRewardMessage message = new CardTransformRewardMessage { - location = rewardSynchronizer.MessageBuffer.CurrentLocation, - rewardType = CardTransformReward.CardTransform, + Location = rewardSynchronizer.MessageBuffer!.CurrentLocation, + wasSkipped = false, Upgrade = upgrade, - + Amount = amount }; + BaseLibMain.Logger.Debug($"Transforming card for local player {rewardSynchronizer.LocalPlayerRef}"); + rewardSynchronizer.GameService?.SendMessage(message); - return await rewardSynchronizer.DoCardTransform(rewardSynchronizer.LocalPlayerRef, upgrade); + return await rewardSynchronizer.DoCardTransform(rewardSynchronizer.LocalPlayerRef!, amount, upgrade); } - public async Task DoCardTransform(Player player, bool upgrade = false) + /// + /// Transform a card for a specific player as a combat reward + /// + public async Task DoCardTransform(Player player, int amount = 1, bool upgrade = false) { - CardSelectorPrefs prefs = new CardSelectorPrefs(new LocString("gameplay_ui", "COMBAT_REWARD_CARD_REMOVAL.selection_screen_prompt"), 1) + CardSelectorPrefs prefs = new CardSelectorPrefs(new LocString("gameplay_ui", "COMBAT_REWARD_CARD_TRANSFORM"), amount) { - Cancelable = false, + Cancelable = true, RequireManualConfirmation = true }; - CardModel? card = (await CardSelectCmd.FromDeckForTransformation(player, prefs)).FirstOrDefault(); - if (card != null) + + List cards = (await CardSelectCmd.FromDeckForTransformation(player, prefs)).ToList(); + + BaseLibMain.Logger.Debug($"Current combat state for transform rewards is: IsEnding={CombatManager.Instance.IsEnding}"); + foreach (CardModel card in cards) { CardModel newCard = CardFactory.CreateRandomCardForTransform(card, isInCombat: false, player.RunState.Rng.Niche); - if (upgrade) + + if (upgrade || card.IsUpgraded) // need a more robust handler for multi-upgrade at some point { - newCard.UpgradeInternal(); + CardCmd.Upgrade(newCard); } + await CardCmd.Transform(card, newCard); BaseLibMain.Logger.Debug($"Player {player.NetId} transformed {card.Id} in their deck into {newCard.Id}" + (upgrade ? " and upgraded it." : ".")); - return true; - } - return false; - } - } - - - - internal static readonly List _rewardMessageCache = [..ReflectionHelper.GetSubtypesInMods()]; - - [HarmonyPatch(MethodType.Constructor, [typeof(RunLocationTargetedMessageBuffer), typeof(INetGameService), typeof(IPlayerCollection), typeof(ulong)])] - [HarmonyPostfix] - public static void InitializeCustomRewardHandlers(RewardSynchronizer __instance) - { - foreach (var rewardMessageType in _rewardMessageCache) - { - if (rewardMessageType.CreateInstance() is not CustomRewardMessage dummyMessage) - { - BaseLibMain.Logger.Error($"Message instance creation for type {rewardMessageType.GetType()} from {rewardMessageType.Assembly} failed during Initialize"); - continue; } - dummyMessage.Initialize(__instance.MessageBuffer); - } - } - [HarmonyPatch(nameof(RewardSynchronizer.Dispose))] - [HarmonyPostfix] - public static void UnregisterCustomRewardHandlers(RewardSynchronizer __instance) - { - foreach (var rewardMessageType in _rewardMessageCache) - { - if (rewardMessageType.CreateInstance() is not CustomRewardMessage dummyMessage) - { - BaseLibMain.Logger.Error($"Message instance creation for type {rewardMessageType.GetType()} from {rewardMessageType.Assembly} failed during Dispose"); - continue; - } - dummyMessage.Dispose(__instance.MessageBuffer); + return cards.Count > 0; } } [HarmonyPatch(nameof(RewardSynchronizer.OnCombatEnded))] [HarmonyPrefix] - public static void HandleCustomBufferedMessages(RewardSynchronizer __instance) + private static void HandleCustomBufferedMessages(RewardSynchronizer __instance) { foreach (BufferedCustomRewardMessage bufferedMessage in __instance.BufferedCustomRewardMessages) { - // TODO: Get reference to appropriate message handler from the message type? Call handle method? - // bufferedMessage.message.MessageHandler(bufferedMessage.message, bufferedMessage.senderId); - __instance.MessageBuffer.CallHandlersOfType(bufferedMessage.message.GetType(), bufferedMessage.message, bufferedMessage.senderId); - + __instance.MessageBuffer?.CallHandlersOfType(bufferedMessage.message.GetType(), bufferedMessage.message, bufferedMessage.senderId); } __instance.BufferedCustomRewardMessages.Clear(); } diff --git a/Patches/Content/RunManagerPatches.cs b/Patches/Content/RunManagerPatches.cs index 4f8f59c..0060550 100644 --- a/Patches/Content/RunManagerPatches.cs +++ b/Patches/Content/RunManagerPatches.cs @@ -6,15 +6,13 @@ namespace BaseLib.Patches.Content; [HarmonyPatch(typeof(RunManager))] -public static class RunManagerPatches +internal static class RunManagerPatches { + private static readonly List customMessageTypes = [..ReflectionHelper.GetSubtypesInMods()]; - internal static List customMessageTypes = [..ReflectionHelper.GetSubtypesInMods()]; - - // currently duplicates the registration for CustomRewardMessages? maybe remove that part since it happens at the same time [HarmonyPatch(nameof(RunManager.InitializeShared))] [HarmonyPostfix] - public static void InitializeCustomMessageHandlers(RunManager __instance) + private static void InitializeCustomMessageHandlers(RunManager __instance) { foreach (var messageType in customMessageTypes) { @@ -29,7 +27,7 @@ public static void InitializeCustomMessageHandlers(RunManager __instance) [HarmonyPatch(nameof(RunManager.CleanUp))] [HarmonyPostfix] - public static void DisposeCustomMessageHandlers(RunManager __instance) + private static void DisposeCustomMessageHandlers(RunManager __instance) { foreach (var messageType in customMessageTypes) { From ece959b11cd7dfe00bbe1823125dd5f3a1228e13 Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Fri, 10 Apr 2026 21:56:21 +0800 Subject: [PATCH 09/15] move back to c# 13.0 syntax --- BaseLib.csproj | 1 - Patches/Content/RewardSynchronizerPatches.cs | 141 +++++++++---------- 2 files changed, 68 insertions(+), 74 deletions(-) diff --git a/BaseLib.csproj b/BaseLib.csproj index a09cb6b..fc5627c 100644 --- a/BaseLib.csproj +++ b/BaseLib.csproj @@ -3,7 +3,6 @@ net9.0 - 14.0 true enable true diff --git a/Patches/Content/RewardSynchronizerPatches.cs b/Patches/Content/RewardSynchronizerPatches.cs index affd9c1..0f85997 100644 --- a/Patches/Content/RewardSynchronizerPatches.cs +++ b/Patches/Content/RewardSynchronizerPatches.cs @@ -6,7 +6,6 @@ using MegaCrit.Sts2.Core.Commands; using MegaCrit.Sts2.Core.Entities.Players; using MegaCrit.Sts2.Core.Factories; -using MegaCrit.Sts2.Core.Helpers; using MegaCrit.Sts2.Core.Localization; using MegaCrit.Sts2.Core.Models; using MegaCrit.Sts2.Core.Multiplayer.Game; @@ -42,99 +41,95 @@ public struct BufferedCustomRewardMessage /// internal static List _bufferedCustomRewardMessages = []; - extension(RewardSynchronizer rewardSynchronizer) - { - - internal List BufferedCustomRewardMessages { get { return _bufferedCustomRewardMessages; } } + internal static List BufferedCustomRewardMessages(this RewardSynchronizer rewardSynchronizer) => _bufferedCustomRewardMessages; - /// - /// Add a to the combat buffer - /// - public void BufferCustomRewardMessage(CustomRewardMessage message, ulong senderId) + /// + /// Add a to the combat buffer + /// + public static void BufferCustomRewardMessage(this RewardSynchronizer rewardSynchronizer, CustomRewardMessage message, ulong senderId) + { + var bufferedMessage = new BufferedCustomRewardMessage { - var bufferedMessage = new BufferedCustomRewardMessage - { - senderId = senderId, - message = message - }; - rewardSynchronizer.BufferedCustomRewardMessages.Add(bufferedMessage); - } + senderId = senderId, + message = message + }; + rewardSynchronizer.BufferedCustomRewardMessages().Add(bufferedMessage); + } - /// - /// Exposes the private LocalPlayer property from - /// - public Player? LocalPlayerRef => rewardSynchronizer._playerCollection.GetPlayer(rewardSynchronizer._localPlayerId); - /// - /// Exposes the private IPlayerCollection property - /// - public IPlayerCollection? PlayerCollection => rewardSynchronizer._playerCollection; - /// - /// Exposes the private RunLocationTargetedMessageBuffer property - /// - public RunLocationTargetedMessageBuffer? MessageBuffer => rewardSynchronizer._messageBuffer; - /// - /// Exposes the private INetGameService property - /// - public INetGameService? GameService => rewardSynchronizer._gameService; + /// + /// Exposes the private LocalPlayer property from + /// + public static Player? LocalPlayerRef(this RewardSynchronizer rewardSynchronizer) => rewardSynchronizer._playerCollection.GetPlayer(rewardSynchronizer._localPlayerId); + /// + /// Exposes the private IPlayerCollection property + /// + public static IPlayerCollection? PlayerCollection(this RewardSynchronizer rewardSynchronizer) => rewardSynchronizer._playerCollection; + /// + /// Exposes the private RunLocationTargetedMessageBuffer property + /// + public static RunLocationTargetedMessageBuffer? MessageBuffer(this RewardSynchronizer rewardSynchronizer) => rewardSynchronizer._messageBuffer; + /// + /// Exposes the private INetGameService property + /// + public static INetGameService? GameService(this RewardSynchronizer rewardSynchronizer) => rewardSynchronizer._gameService; - /// - /// Method to handle transforming a card as a combat reward - /// - public async Task DoLocalCardTransform(int amount = 1, bool upgrade = false) + /// + /// Method to handle transforming a card as a combat reward + /// + public static async Task DoLocalCardTransform(this RewardSynchronizer rewardSynchronizer, int amount = 1, bool upgrade = false) + { + CardTransformRewardMessage message = new CardTransformRewardMessage { - CardTransformRewardMessage message = new CardTransformRewardMessage - { - Location = rewardSynchronizer.MessageBuffer!.CurrentLocation, - wasSkipped = false, - Upgrade = upgrade, - Amount = amount - }; - BaseLibMain.Logger.Debug($"Transforming card for local player {rewardSynchronizer.LocalPlayerRef}"); - - rewardSynchronizer.GameService?.SendMessage(message); - return await rewardSynchronizer.DoCardTransform(rewardSynchronizer.LocalPlayerRef!, amount, upgrade); - } + Location = rewardSynchronizer.MessageBuffer()!.CurrentLocation, + wasSkipped = false, + Upgrade = upgrade, + Amount = amount + }; + BaseLibMain.Logger.Debug($"Transforming card for local player {rewardSynchronizer.LocalPlayerRef}"); + + rewardSynchronizer.GameService()?.SendMessage(message); + return await rewardSynchronizer.DoCardTransform(rewardSynchronizer.LocalPlayerRef()!, amount, upgrade); + } - /// - /// Transform a card for a specific player as a combat reward - /// - public async Task DoCardTransform(Player player, int amount = 1, bool upgrade = false) + /// + /// Transform a card for a specific player as a combat reward + /// + public static async Task DoCardTransform(this RewardSynchronizer rewardSynchronizer, Player player, int amount = 1, bool upgrade = false) + { + CardSelectorPrefs prefs = new CardSelectorPrefs(new LocString("gameplay_ui", "COMBAT_REWARD_CARD_TRANSFORM"), amount) { - CardSelectorPrefs prefs = new CardSelectorPrefs(new LocString("gameplay_ui", "COMBAT_REWARD_CARD_TRANSFORM"), amount) - { - Cancelable = true, - RequireManualConfirmation = true - }; + Cancelable = true, + RequireManualConfirmation = true + }; - List cards = (await CardSelectCmd.FromDeckForTransformation(player, prefs)).ToList(); + List cards = (await CardSelectCmd.FromDeckForTransformation(player, prefs)).ToList(); - BaseLibMain.Logger.Debug($"Current combat state for transform rewards is: IsEnding={CombatManager.Instance.IsEnding}"); - foreach (CardModel card in cards) - { - CardModel newCard = CardFactory.CreateRandomCardForTransform(card, isInCombat: false, player.RunState.Rng.Niche); - - if (upgrade || card.IsUpgraded) // need a more robust handler for multi-upgrade at some point - { - CardCmd.Upgrade(newCard); - } + BaseLibMain.Logger.Debug($"Current combat state for transform rewards is: IsEnding={CombatManager.Instance.IsEnding}"); + foreach (CardModel card in cards) + { + CardModel newCard = CardFactory.CreateRandomCardForTransform(card, isInCombat: false, player.RunState.Rng.Niche); - await CardCmd.Transform(card, newCard); - BaseLibMain.Logger.Debug($"Player {player.NetId} transformed {card.Id} in their deck into {newCard.Id}" + (upgrade ? " and upgraded it." : ".")); + if (upgrade || card.IsUpgraded) // need a more robust handler for multi-upgrade at some point + { + CardCmd.Upgrade(newCard); } - return cards.Count > 0; + await CardCmd.Transform(card, newCard); + BaseLibMain.Logger.Debug($"Player {player.NetId} transformed {card.Id} in their deck into {newCard.Id}" + (upgrade ? " and upgraded it." : ".")); } + + return cards.Count > 0; } [HarmonyPatch(nameof(RewardSynchronizer.OnCombatEnded))] [HarmonyPrefix] private static void HandleCustomBufferedMessages(RewardSynchronizer __instance) { - foreach (BufferedCustomRewardMessage bufferedMessage in __instance.BufferedCustomRewardMessages) + foreach (BufferedCustomRewardMessage bufferedMessage in __instance.BufferedCustomRewardMessages()) { - __instance.MessageBuffer?.CallHandlersOfType(bufferedMessage.message.GetType(), bufferedMessage.message, bufferedMessage.senderId); + __instance.MessageBuffer()?.CallHandlersOfType(bufferedMessage.message.GetType(), bufferedMessage.message, bufferedMessage.senderId); } - __instance.BufferedCustomRewardMessages.Clear(); + __instance.BufferedCustomRewardMessages().Clear(); } } From 5e97b221ebd58b7dfeedbe3a153cd3d2eaf62d4b Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Fri, 10 Apr 2026 22:09:55 +0800 Subject: [PATCH 10/15] doc cleanup, fix last errors from c# 13 refactor --- Common/Rewards/CardTransformRewardMessage.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Common/Rewards/CardTransformRewardMessage.cs b/Common/Rewards/CardTransformRewardMessage.cs index dd39476..ecbaf1f 100644 --- a/Common/Rewards/CardTransformRewardMessage.cs +++ b/Common/Rewards/CardTransformRewardMessage.cs @@ -22,24 +22,26 @@ private void HandleCardTransformedMessage(CardTransformRewardMessage message, ul if (CombatManager.Instance.IsInProgress) { rs.BufferCustomRewardMessage(message, senderId); - BaseLibMain.Logger.Debug($"Buffered card transform message for {rs.PlayerCollection?.GetPlayer(senderId)}"); + BaseLibMain.Logger.Debug($"Buffered card transform message for {rs.PlayerCollection()?.GetPlayer(senderId)}"); return; } - Player? player = rs.PlayerCollection?.GetPlayer(senderId); - if (player == rs.LocalPlayerRef) + Player? player = rs.PlayerCollection()?.GetPlayer(senderId); + if (player == rs.LocalPlayerRef()) { throw new InvalidOperationException("CardTransformRewardMessage should not be sent to the player transforming the card"); } TaskHelper.RunSafely(rs.DoCardTransform(player, message.Amount, message.Upgrade)); } + /// public override void Dispose(RunLocationTargetedMessageBuffer messageBuffer) { BaseLibMain.Logger.Debug($"Unregistering handler for {GetType()}"); messageBuffer.UnregisterMessageHandler(HandleCardTransformedMessage); } + /// public override void Initialize(RunLocationTargetedMessageBuffer messageBuffer) { BaseLibMain.Logger.Debug($"Registering handler for {GetType()}"); @@ -55,12 +57,15 @@ public override void Initialize(RunLocationTargetedMessageBuffer messageBuffer) ///
public required int Amount; + /// public override LogLevel LogLevel => LogLevel.Debug; + /// public override void Deserialize(PacketReader reader) { } + /// public override void Serialize(PacketWriter writer) { } From 978cc379bf90198c4db05608339574e3e0bb536b Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Mon, 13 Apr 2026 16:37:16 +0800 Subject: [PATCH 11/15] Midway refactor to interface pattern for CustomMessage --- Abstracts/CustomMessage.cs | 36 ++++++++------------ Abstracts/CustomRewardMessage.cs | 4 +-- Abstracts/CustomTargetedMessage.cs | 13 +++++++ Abstracts/ICustomMessage.cs | 22 ++++++++++++ Abstracts/ICustomTargetedMessage.cs | 14 ++++++++ Common/Rewards/CardTransformReward.cs | 1 - Patches/Content/RewardSynchronizerPatches.cs | 2 +- 7 files changed, 66 insertions(+), 26 deletions(-) create mode 100644 Abstracts/CustomTargetedMessage.cs create mode 100644 Abstracts/ICustomMessage.cs create mode 100644 Abstracts/ICustomTargetedMessage.cs diff --git a/Abstracts/CustomMessage.cs b/Abstracts/CustomMessage.cs index 254a44f..b8ddd3b 100644 --- a/Abstracts/CustomMessage.cs +++ b/Abstracts/CustomMessage.cs @@ -1,41 +1,33 @@ using MegaCrit.Sts2.Core.Logging; using MegaCrit.Sts2.Core.Multiplayer.Game; -using MegaCrit.Sts2.Core.Multiplayer.Messages.Game; using MegaCrit.Sts2.Core.Multiplayer.Serialization; using MegaCrit.Sts2.Core.Multiplayer.Transport; -using MegaCrit.Sts2.Core.Runs; -using BaseLib.Patches.Content; namespace BaseLib.Abstracts; /// -/// The type to inherit from to add a custom message +/// The type to inherit from to add a custom message. +/// Not actually necessary, just provides some helpful abstract methods as reminders/hints for setting up a message /// -public abstract class CustomMessage : INetMessage +public abstract class CustomMessage : INetMessage, ICustomMessage { - /// - /// Register your message type here - /// Needs to be a function that takes ( message, senderId) - /// See for an example. You probably want to use an Extension Method - /// - public abstract void Initialize(RunLocationTargetedMessageBuffer messageBuffer); + public abstract void Serialize(PacketWriter writer); + public abstract void Deserialize(PacketReader reader); + + public abstract void Initialize(INetGameService netService); + public abstract void Dispose(INetGameService netService); /// - /// Unregister your message type here
- /// Reference the same function you registered in + /// Whether to broadcast the message ///
- public abstract void Dispose(RunLocationTargetedMessageBuffer messageBuffer); - public abstract bool ShouldBroadcast { get; } - public abstract NetTransferMode Mode { get; } - public abstract LogLevel LogLevel { get; } - /// - /// Read out the necessary data from the saved info, in the order it was written + /// The way to transfer the message /// - public abstract void Deserialize(PacketReader reader); + public abstract NetTransferMode Mode { get; } /// - /// Save necessary data, to be read out when reconstructing the message + /// What log level to output to (referenced when calling the vanilla handler(s) for messages) /// - public abstract void Serialize(PacketWriter writer); + public abstract LogLevel LogLevel { get; } } + diff --git a/Abstracts/CustomRewardMessage.cs b/Abstracts/CustomRewardMessage.cs index f809649..e169af6 100644 --- a/Abstracts/CustomRewardMessage.cs +++ b/Abstracts/CustomRewardMessage.cs @@ -7,7 +7,7 @@ namespace BaseLib.Abstracts; /// /// Abstract class to inherit for syncing rewards /// -public abstract class CustomRewardMessage : CustomMessage, IRunLocationTargetedMessage +public abstract class CustomRewardMessage : CustomTargetedMessage { /// /// Include whether the reward was skipped or not @@ -28,5 +28,5 @@ public abstract class CustomRewardMessage : CustomMessage, IRunLocationTargetedM /// /// Set when instantiating, afaik needed for saving to the run? /// - public required RunLocation Location { get; set; } + public required override RunLocation Location { get; set; } } diff --git a/Abstracts/CustomTargetedMessage.cs b/Abstracts/CustomTargetedMessage.cs new file mode 100644 index 0000000..efa83bf --- /dev/null +++ b/Abstracts/CustomTargetedMessage.cs @@ -0,0 +1,13 @@ +using MegaCrit.Sts2.Core.Multiplayer.Game; +using MegaCrit.Sts2.Core.Multiplayer.Messages.Game; +using MegaCrit.Sts2.Core.Runs; + +namespace BaseLib.Abstracts; + +public abstract class CustomTargetedMessage : ICustomTargetedMessage +{ + public abstract RunLocation Location { get; set; } + + public abstract void Dispose(RunLocationTargetedMessageBuffer messageBuffer); + public abstract void Initialize(RunLocationTargetedMessageBuffer messageBuffer); +} diff --git a/Abstracts/ICustomMessage.cs b/Abstracts/ICustomMessage.cs new file mode 100644 index 0000000..4669daf --- /dev/null +++ b/Abstracts/ICustomMessage.cs @@ -0,0 +1,22 @@ +using BaseLib.Patches.Content; +using MegaCrit.Sts2.Core.Multiplayer.Game; +using MegaCrit.Sts2.Core.Multiplayer.Serialization; + +namespace BaseLib.Abstracts; + +public interface ICustomMessage : INetMessage +{ + /// + /// Register your message type here. + /// Needs to be a function that takes ( message, senderId) + /// See for an example. You probably want to use an Extension Method + /// + public abstract void Initialize(INetGameService netService); + + /// + /// Unregister your message type here
+ /// Reference the same function you registered in + ///
+ public abstract void Dispose(INetGameService netService); +} + diff --git a/Abstracts/ICustomTargetedMessage.cs b/Abstracts/ICustomTargetedMessage.cs new file mode 100644 index 0000000..6e66ade --- /dev/null +++ b/Abstracts/ICustomTargetedMessage.cs @@ -0,0 +1,14 @@ +using MegaCrit.Sts2.Core.Multiplayer.Game; +using MegaCrit.Sts2.Core.Multiplayer.Messages.Game; + +namespace BaseLib.Abstracts; + +/// Message interface +public interface ICustomTargetedMessage : ICustomMessage, IRunLocationTargetedMessage +{ + /// + abstract void Initialize(RunLocationTargetedMessageBuffer messageBuffer); + + /// + abstract void Dispose(RunLocationTargetedMessageBuffer messageBuffer); +} diff --git a/Common/Rewards/CardTransformReward.cs b/Common/Rewards/CardTransformReward.cs index 0c88e14..be6a8be 100644 --- a/Common/Rewards/CardTransformReward.cs +++ b/Common/Rewards/CardTransformReward.cs @@ -9,7 +9,6 @@ namespace BaseLib.Common.Rewards; -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public class CardTransformReward(Player player, int amount, bool upgrade = false) : CustomReward(player) { [CustomEnum] diff --git a/Patches/Content/RewardSynchronizerPatches.cs b/Patches/Content/RewardSynchronizerPatches.cs index 0f85997..a01b6a5 100644 --- a/Patches/Content/RewardSynchronizerPatches.cs +++ b/Patches/Content/RewardSynchronizerPatches.cs @@ -88,7 +88,7 @@ public static async Task DoLocalCardTransform(this RewardSynchronizer rewa }; BaseLibMain.Logger.Debug($"Transforming card for local player {rewardSynchronizer.LocalPlayerRef}"); - rewardSynchronizer.GameService()?.SendMessage(message); + rewardSynchronizer.GameService().SendMessage(message); return await rewardSynchronizer.DoCardTransform(rewardSynchronizer.LocalPlayerRef()!, amount, upgrade); } From 13e31d90539e251b53973882003dcf358f26b75d Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Fri, 17 Apr 2026 21:47:07 +0800 Subject: [PATCH 12/15] General structure cleanup; Refactor interface again again to just be for checking inheritance --- Abstracts/ICustomMessage.cs | 18 +------- Abstracts/ICustomTargetedMessage.cs | 14 ------ Patches/Content/RewardSynchronizerPatches.cs | 2 +- Patches/Content/RunManagerPatches.cs | 48 ++++++++++++++++++-- 4 files changed, 46 insertions(+), 36 deletions(-) delete mode 100644 Abstracts/ICustomTargetedMessage.cs diff --git a/Abstracts/ICustomMessage.cs b/Abstracts/ICustomMessage.cs index 4669daf..d900c43 100644 --- a/Abstracts/ICustomMessage.cs +++ b/Abstracts/ICustomMessage.cs @@ -1,22 +1,6 @@ -using BaseLib.Patches.Content; -using MegaCrit.Sts2.Core.Multiplayer.Game; using MegaCrit.Sts2.Core.Multiplayer.Serialization; namespace BaseLib.Abstracts; -public interface ICustomMessage : INetMessage -{ - /// - /// Register your message type here. - /// Needs to be a function that takes ( message, senderId) - /// See for an example. You probably want to use an Extension Method - /// - public abstract void Initialize(INetGameService netService); - - /// - /// Unregister your message type here
- /// Reference the same function you registered in - ///
- public abstract void Dispose(INetGameService netService); -} +public interface ICustomMessage : INetMessage { } diff --git a/Abstracts/ICustomTargetedMessage.cs b/Abstracts/ICustomTargetedMessage.cs deleted file mode 100644 index 6e66ade..0000000 --- a/Abstracts/ICustomTargetedMessage.cs +++ /dev/null @@ -1,14 +0,0 @@ -using MegaCrit.Sts2.Core.Multiplayer.Game; -using MegaCrit.Sts2.Core.Multiplayer.Messages.Game; - -namespace BaseLib.Abstracts; - -/// Message interface -public interface ICustomTargetedMessage : ICustomMessage, IRunLocationTargetedMessage -{ - /// - abstract void Initialize(RunLocationTargetedMessageBuffer messageBuffer); - - /// - abstract void Dispose(RunLocationTargetedMessageBuffer messageBuffer); -} diff --git a/Patches/Content/RewardSynchronizerPatches.cs b/Patches/Content/RewardSynchronizerPatches.cs index a01b6a5..26087af 100644 --- a/Patches/Content/RewardSynchronizerPatches.cs +++ b/Patches/Content/RewardSynchronizerPatches.cs @@ -124,7 +124,7 @@ public static async Task DoCardTransform(this RewardSynchronizer rewardSyn [HarmonyPatch(nameof(RewardSynchronizer.OnCombatEnded))] [HarmonyPrefix] - private static void HandleCustomBufferedMessages(RewardSynchronizer __instance) + private static void OnCombat_HandleCustomBufferedMessages(RewardSynchronizer __instance) { foreach (BufferedCustomRewardMessage bufferedMessage in __instance.BufferedCustomRewardMessages()) { diff --git a/Patches/Content/RunManagerPatches.cs b/Patches/Content/RunManagerPatches.cs index 0060550..89cb5c5 100644 --- a/Patches/Content/RunManagerPatches.cs +++ b/Patches/Content/RunManagerPatches.cs @@ -8,7 +8,12 @@ namespace BaseLib.Patches.Content; [HarmonyPatch(typeof(RunManager))] internal static class RunManagerPatches { + /// + /// Potentially for future usage if we get the basegame messages to not automatically include mod messages + /// + private static readonly List allCustomMessages = [..ReflectionHelper.GetSubtypesInMods()]; private static readonly List customMessageTypes = [..ReflectionHelper.GetSubtypesInMods()]; + private static readonly List customTargetedMessageTypes = [..ReflectionHelper.GetSubtypesInMods()]; [HarmonyPatch(nameof(RunManager.InitializeShared))] [HarmonyPostfix] @@ -16,12 +21,35 @@ private static void InitializeCustomMessageHandlers(RunManager __instance) { foreach (var messageType in customMessageTypes) { - if (messageType.CreateInstance() is not CustomMessage dummyMessage) + var dummyMessage = messageType.CreateInstance(); + if (dummyMessage == null) { - BaseLibMain.Logger.Error($"Message instance creation for type {messageType.GetType()} from {messageType.Assembly} failed during Initialize"); + BaseLibMain.Logger.Error( + $"CustomMessage instance creation for type {messageType.GetType()} from {messageType.Assembly} failed during Initialize"); continue; } - dummyMessage.Initialize(__instance.RunLocationTargetedBuffer); + + if (dummyMessage is CustomMessage customMessage) + { + customMessage.Initialize(__instance.NetService); + } + } + + + foreach (var targetedMessageType in customTargetedMessageTypes) + { + var dummyTargetedMessage = targetedMessageType.CreateInstance(); + if (dummyTargetedMessage == null) + { + BaseLibMain.Logger.Error( + $"CustomTargetedMessage instance creation for type {targetedMessageType.GetType()} from {targetedMessageType.Assembly} failed during Initialize"); + continue; + } + // Need to double check that all the targeted messages are sent to this one handler in the base game + if (dummyTargetedMessage is CustomTargetedMessage targetedMessage) + { + targetedMessage.Initialize(__instance.RunLocationTargetedBuffer); + } } } @@ -33,7 +61,19 @@ private static void DisposeCustomMessageHandlers(RunManager __instance) { if (messageType.CreateInstance() is not CustomMessage dummyMessage) { - BaseLibMain.Logger.Error($"Message instance creation for type {messageType.GetType()} from {messageType.Assembly} failed during Dispose"); + BaseLibMain.Logger.Error( + $"CustomMessage instance creation for type {messageType.GetType()} from {messageType.Assembly} failed during Dispose"); + continue; + } + dummyMessage.Dispose(__instance.NetService); + } + + foreach (var targetedMessageType in customTargetedMessageTypes) + { + if (targetedMessageType.CreateInstance() is not CustomTargetedMessage dummyMessage) + { + BaseLibMain.Logger.Error( + $"CustomTargetedMessage instance creation for type {targetedMessageType.GetType()} from {targetedMessageType.Assembly} failed during Dispose"); continue; } dummyMessage.Dispose(__instance.RunLocationTargetedBuffer); From 2361450d3e58abaeb127ff125ee8e3969127997f Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Sat, 18 Apr 2026 02:41:11 +0800 Subject: [PATCH 13/15] Finishing touches, almost ready --- Abstracts/CustomMessage.cs | 31 +++++-- Abstracts/CustomReward.cs | 45 +++++++++-- Abstracts/CustomTargetedMessage.cs | 16 +++- Common/Rewards/CardTransformReward.cs | 85 +++++++++++++++----- Common/Rewards/CardTransformRewardMessage.cs | 2 +- 5 files changed, 148 insertions(+), 31 deletions(-) diff --git a/Abstracts/CustomMessage.cs b/Abstracts/CustomMessage.cs index b8ddd3b..0e45476 100644 --- a/Abstracts/CustomMessage.cs +++ b/Abstracts/CustomMessage.cs @@ -1,3 +1,4 @@ +using BaseLib.Common.Rewards; using MegaCrit.Sts2.Core.Logging; using MegaCrit.Sts2.Core.Multiplayer.Game; using MegaCrit.Sts2.Core.Multiplayer.Serialization; @@ -7,15 +8,36 @@ namespace BaseLib.Abstracts; /// /// The type to inherit from to add a custom message. -/// Not actually necessary, just provides some helpful abstract methods as reminders/hints for setting up a message +/// Not actually necessary to inherit from, just provides some helpful abstract methods as reminders/hints for setting up a message /// public abstract class CustomMessage : INetMessage, ICustomMessage { - public abstract void Serialize(PacketWriter writer); - public abstract void Deserialize(PacketReader reader); - + /// + /// Register your message type here. + /// Needs to be a function that takes ( message, senderId) + /// See for an example. + /// You probably want to use an Extension Method + /// public abstract void Initialize(INetGameService netService); + + /// + /// Unregister your message type here
+ /// Reference the same function you registered in + ///
public abstract void Dispose(INetGameService netService); + /// + /// How your message is "written" to be sent over the internet + /// + /// The + /// writer. + public abstract void Serialize(PacketWriter writer); + /// + /// Read out your message into whatever variables it was created from + /// + /// Parameter description. + /// Type and description of the returned object. + /// Write me later. + public abstract void Deserialize(PacketReader reader); /// /// Whether to broadcast the message @@ -30,4 +52,3 @@ public abstract class CustomMessage : INetMessage, ICustomMessage /// public abstract LogLevel LogLevel { get; } } - diff --git a/Abstracts/CustomReward.cs b/Abstracts/CustomReward.cs index 9c283b2..ea9826f 100644 --- a/Abstracts/CustomReward.cs +++ b/Abstracts/CustomReward.cs @@ -1,12 +1,24 @@ using Baselib.Patches.Content; +using BaseLib.Common.Rewards; using MegaCrit.Sts2.Core.Entities.Players; +using MegaCrit.Sts2.Core.Multiplayer.Game; using MegaCrit.Sts2.Core.Rewards; using MegaCrit.Sts2.Core.Saves.Runs; namespace BaseLib.Abstracts; -public delegate T SerializableCustomReward(SerializableReward save, Player player) where T : CustomReward; +/// +/// Delegate handler to indicate the expected structure of CreateFromSerializable methods +/// +public delegate T SerializableCustomReward(SerializableReward save, Player player) where T : CustomReward; +/// +/// Class to inherit for creation a new type of reward.\n +/// "New type" does not mean this should be used for card pool rewards, or single card rewards. +/// Use and respectively, +/// though be mindful that you don't use a constructor that is unsupported by the base-game structure. +/// +/// public abstract class CustomReward(Player player) : Reward(player) { /// @@ -14,25 +26,48 @@ public abstract class CustomReward(Player player) : Reward(player) /// public override int RewardsSetIndex => 9; + /// + /// Delegate to create your reward type from the saved data. + /// The method reference must be of a method + /// + /// + /// + /// // in MyCustomReward.cs + /// public static MyCustomReward CreateFromSerializable(SerializableReward save, Player player) + /// { + /// return new MyCustomReward(player) { + /// MyCustomNumber = save.GoldAmount + /// } + /// } + /// public override SerializableCustomReward<CustomReward> SerializeMethod => CreateFromSerializable; + /// + /// public abstract SerializableCustomReward SerializeMethod { get; } + + /// + /// Base method to handle registering your reward for serializing and deserializing in + /// Override this if you wish to manually register your reward with + /// or by getting your own reference to the used for the instance + /// public virtual void Initialize() { - if (SerializeMethod?.Method.IsStatic == true) + // if (SerializeMethod?.Method.IsStatic == true) + if (SerializeMethod != null) // TODO: test that the constructor doesn't have to be static? { BaseLibMain.Logger.Info($"Registering CustomReward serializer for {GetType()}"); CustomRewardPatches.RegisterCustomReward(RewardType, SerializeMethod); } else if (SerializeMethod != null) { - throw new FieldAccessException($"Custom Reward {this.GetType()} has assigned a non-static method to SerializeMethod property"); + throw new FieldAccessException($"Custom Reward {GetType()} has assigned a non-static method to SerializeMethod property"); } else { - throw new NotImplementedException($"Custom Reward {this.GetType()} has not implemented an Initialize() method to register a serializer for itself"); + throw new NotImplementedException($"Custom Reward {GetType()} has not implemented an Initialize() method to register a serializer for itself"); } } - // TODO: per-mod id prefixing for localisation + // TODO: per-mod id prefixing for localisation? } diff --git a/Abstracts/CustomTargetedMessage.cs b/Abstracts/CustomTargetedMessage.cs index efa83bf..997d0fa 100644 --- a/Abstracts/CustomTargetedMessage.cs +++ b/Abstracts/CustomTargetedMessage.cs @@ -1,13 +1,25 @@ +using MegaCrit.Sts2.Core.Logging; using MegaCrit.Sts2.Core.Multiplayer.Game; using MegaCrit.Sts2.Core.Multiplayer.Messages.Game; +using MegaCrit.Sts2.Core.Multiplayer.Serialization; +using MegaCrit.Sts2.Core.Multiplayer.Transport; using MegaCrit.Sts2.Core.Runs; namespace BaseLib.Abstracts; -public abstract class CustomTargetedMessage : ICustomTargetedMessage + +public abstract class CustomTargetedMessage : INetMessage, IRunLocationTargetedMessage, ICustomMessage { public abstract RunLocation Location { get; set; } - public abstract void Dispose(RunLocationTargetedMessageBuffer messageBuffer); + public abstract bool ShouldBroadcast { get; } + public abstract NetTransferMode Mode { get; } + public abstract LogLevel LogLevel { get; } + public abstract void Initialize(RunLocationTargetedMessageBuffer messageBuffer); + + public abstract void Dispose(RunLocationTargetedMessageBuffer messageBuffer); + + public abstract void Serialize(PacketWriter writer); + public abstract void Deserialize(PacketReader reader); } diff --git a/Common/Rewards/CardTransformReward.cs b/Common/Rewards/CardTransformReward.cs index be6a8be..dbc57a2 100644 --- a/Common/Rewards/CardTransformReward.cs +++ b/Common/Rewards/CardTransformReward.cs @@ -3,51 +3,100 @@ using MegaCrit.Sts2.Core.Entities.Players; using MegaCrit.Sts2.Core.Helpers; using MegaCrit.Sts2.Core.Localization; +using MegaCrit.Sts2.Core.Models.Cards; using MegaCrit.Sts2.Core.Rewards; using MegaCrit.Sts2.Core.Runs; using MegaCrit.Sts2.Core.Saves.Runs; namespace BaseLib.Common.Rewards; -public class CardTransformReward(Player player, int amount, bool upgrade = false) : CustomReward(player) +/// +/// A reward class similar to the card removal one created by , only for transforming instead of removing cards +/// +/// +/// room.AddExtraReward(Owner.Player, new CardTransformReward(Owner.Player) {Amount = Amount, Upgrade = true}); +/// +public sealed class CardTransformReward(Player player) : CustomReward(player) { - [CustomEnum] - public static RewardType CardTransform; + /// + /// A new defined with the attribute + /// + [CustomEnum] public static RewardType CardTransform; + /// + /// Reference to the defined earlier + /// protected override RewardType RewardType => CardTransform; - public bool Upgrade = upgrade; - public int Amount = amount; + /// + /// Whether the card rewards should be upgraded or not + /// + public required bool Upgrade; + /// + /// How many cards can be selected in this reward screen + /// + public required int Amount; - public override LocString Description => new LocString("gameplay_ui", "COMBAT_REWARD_CARD_TRANSFORM"); + /// + /// The description to show in the reward screen, + /// switches based on whether the reward will upgrade the transformed cards + /// + public override LocString Description + { + get + { + LocString locString = Upgrade + ? new LocString("gameplay_ui", "COMBAT_REWARD_CARD_TRANSFORM_AND_UPGRADE") + : new LocString("gameplay_ui", "COMBAT_REWARD_CARD_TRANSFORM"); + locString.Add("cards", Amount); + return locString; + } + } + /// public override bool IsPopulated => true; - public static string RewardIcon => ImageHelper.GetImagePath("ui/reward_screen/reward_icon_card_removal.png"); + // TODO: make asset for this + public static string RewardIcon => ImageHelper.GetImagePath("ui/reward_screen/reward_icon_card_transform.png"); + /// protected override string IconPath => RewardIcon; - public static CardTransformReward CreateFromSerializable(SerializableReward save, Player player) - { - return new CardTransformReward(player, save.GoldAmount, save.WasGoldStolenBack); // temp hack before worrying about extending the serialized values - } + /// + /// Serializing the reward, saving whether to upgrade and how many cards to transform in the vanilla fields + /// public override SerializableReward ToSerializable() { return new SerializableReward() { RewardType = CardTransform, + GoldAmount = Amount, WasGoldStolenBack = Upgrade }; } - public override SerializableCustomReward SerializeMethod => CreateFromSerializable; - - public override void MarkContentAsSeen() + /// + /// Recreates the reward from the saved + /// + /// The that was created and saved from + /// + /// The the reward belongs to + public CardTransformReward CreateFromSerializable(SerializableReward save, Player player) { + return new CardTransformReward(player) { + // hijacking the gold amounts as a temp hack before worrying about extending the serialized values + Amount = save.GoldAmount, + Upgrade = save.WasGoldStolenBack + }; } - public override Task Populate() - { - return Task.CompletedTask; - } + /// + public override SerializableCustomReward SerializeMethod => CreateFromSerializable; + + /// + public override void MarkContentAsSeen() { } + + /// + public override Task Populate() { return Task.CompletedTask; } + /// protected override async Task OnSelect() { BaseLibMain.Logger.Info("Obtained card transformation from reward"); diff --git a/Common/Rewards/CardTransformRewardMessage.cs b/Common/Rewards/CardTransformRewardMessage.cs index ecbaf1f..c6e35ea 100644 --- a/Common/Rewards/CardTransformRewardMessage.cs +++ b/Common/Rewards/CardTransformRewardMessage.cs @@ -15,7 +15,7 @@ namespace BaseLib.Common.Rewards; ///
public sealed class CardTransformRewardMessage : CustomRewardMessage { - private void HandleCardTransformedMessage(CardTransformRewardMessage message, ulong senderId) + internal void HandleCardTransformedMessage(CardTransformRewardMessage message, ulong senderId) { BaseLibMain.Logger.Debug($"Handling message {message}"); var rs = RunManager.Instance.RewardSynchronizer; From a69f19fe7bd692f6f527fe0f75699672d7e44b08 Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Sat, 18 Apr 2026 02:42:38 +0800 Subject: [PATCH 14/15] reward handling cleanup --- Patches/Content/CustomRewardPatches.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Patches/Content/CustomRewardPatches.cs b/Patches/Content/CustomRewardPatches.cs index 4d8ae89..30a4dfd 100644 --- a/Patches/Content/CustomRewardPatches.cs +++ b/Patches/Content/CustomRewardPatches.cs @@ -8,7 +8,7 @@ namespace Baselib.Patches.Content; [HarmonyPatch(typeof(Reward))] -public static class CustomRewardPatches +internal static class CustomRewardPatches { internal static readonly Dictionary> _RewardTypeSerializers = []; @@ -25,17 +25,15 @@ public static void RegisterCustomReward(RewardType type, SerializableCustomRewar [HarmonyPrefix] public static bool FromSerializablePrefix(SerializableReward save, Player player, ref Reward __result) { - foreach (var pair in _RewardTypeSerializers.Keys) - { - BaseLibMain.Logger.Debug($"{pair}: {_RewardTypeSerializers[pair].Method}"); - } - BaseLibMain.Logger.Debug(_RewardTypeSerializers.ToString()); if (_RewardTypeSerializers.Keys.Contains(save.RewardType)) { BaseLibMain.Logger.Debug($"Found RewardType {save.RewardType} in registry from mod {_RewardTypeSerializers[save.RewardType].Method.GetType().Assembly}"); - __result = _RewardTypeSerializers[save.RewardType].Invoke(save, player); + + var method = _RewardTypeSerializers[save.RewardType]; + __result = method.Invoke(save, player); return false; } + BaseLibMain.Logger.Debug($"No CustomReward found for RewardType {save.RewardType}, proceeding to vanilla method"); return true; } From 0b962242fd6e9a34f076fb06f6a8251dfc5c292c Mon Sep 17 00:00:00 2001 From: Mangochicken Date: Sat, 18 Apr 2026 02:42:54 +0800 Subject: [PATCH 15/15] temporary transform reward localization --- BaseLib/localization/eng/gameplay_ui.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 BaseLib/localization/eng/gameplay_ui.json diff --git a/BaseLib/localization/eng/gameplay_ui.json b/BaseLib/localization/eng/gameplay_ui.json new file mode 100644 index 0000000..faaafca --- /dev/null +++ b/BaseLib/localization/eng/gameplay_ui.json @@ -0,0 +1,6 @@ +{ + "COMBAT_REWARD_CARD_TRANSFORM": "Transform {cards:plural:a card|{cards} cards}", + "COMBAT_REWARD_CARD_TRANSFORM.selectionScreenPrompt": "Choose {Amount:plural:a card:[blue]{}[/blue] cards} to [gold]Transform[/gold]", + "COMBAT_REWARD_CARD_TRANSFORM_AND_UPGRADE": "Transform and Upgrade {cards:plural:a card|{cards} cards}", + "COMBAT_REWARD_CARD_TRANSFORM_AND_UPGRADE.selectionScreenPrompt": "Choose {Amount:plural:a card:[blue]{}[/blue] cards} to [gold]Transform[/gold] and [gold]Upgrade[/gold]", +}