diff --git a/Abstracts/CustomMessage.cs b/Abstracts/CustomMessage.cs
new file mode 100644
index 0000000..0e45476
--- /dev/null
+++ b/Abstracts/CustomMessage.cs
@@ -0,0 +1,54 @@
+using BaseLib.Common.Rewards;
+using MegaCrit.Sts2.Core.Logging;
+using MegaCrit.Sts2.Core.Multiplayer.Game;
+using MegaCrit.Sts2.Core.Multiplayer.Serialization;
+using MegaCrit.Sts2.Core.Multiplayer.Transport;
+
+namespace BaseLib.Abstracts;
+
+///
+/// The type to inherit from to add a custom 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
+{
+ ///
+ /// 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
+ ///
+ public abstract bool ShouldBroadcast { get; }
+ ///
+ /// The way to transfer the message
+ ///
+ public abstract NetTransferMode Mode { get; }
+ ///
+ /// What log level to output to (referenced when calling the vanilla handler(s) for messages)
+ ///
+ public abstract LogLevel LogLevel { get; }
+}
diff --git a/Abstracts/CustomReward.cs b/Abstracts/CustomReward.cs
new file mode 100644
index 0000000..ea9826f
--- /dev/null
+++ b/Abstracts/CustomReward.cs
@@ -0,0 +1,73 @@
+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;
+
+///
+/// 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)
+{
+ ///
+ /// Set the reward index after vanilla rewards by default
+ ///
+ 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 != 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 {GetType()} has assigned a non-static method to SerializeMethod property");
+ }
+ else
+ {
+ 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?
+}
+
diff --git a/Abstracts/CustomRewardMessage.cs b/Abstracts/CustomRewardMessage.cs
new file mode 100644
index 0000000..e169af6
--- /dev/null
+++ b/Abstracts/CustomRewardMessage.cs
@@ -0,0 +1,32 @@
+using MegaCrit.Sts2.Core.Multiplayer.Messages.Game;
+using MegaCrit.Sts2.Core.Multiplayer.Transport;
+using MegaCrit.Sts2.Core.Runs;
+
+namespace BaseLib.Abstracts;
+
+///
+/// Abstract class to inherit for syncing rewards
+///
+public abstract class CustomRewardMessage : CustomTargetedMessage
+{
+ ///
+ /// 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;
+
+ ///
+ /// You probably want to broadcast the message
+ ///
+ public sealed override bool ShouldBroadcast => true;
+
+ ///
+ /// 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 override RunLocation Location { get; set; }
+}
diff --git a/Abstracts/CustomTargetedMessage.cs b/Abstracts/CustomTargetedMessage.cs
new file mode 100644
index 0000000..997d0fa
--- /dev/null
+++ b/Abstracts/CustomTargetedMessage.cs
@@ -0,0 +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 : INetMessage, IRunLocationTargetedMessage, ICustomMessage
+{
+ public abstract RunLocation Location { get; set; }
+
+ 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/Abstracts/ICustomMessage.cs b/Abstracts/ICustomMessage.cs
new file mode 100644
index 0000000..d900c43
--- /dev/null
+++ b/Abstracts/ICustomMessage.cs
@@ -0,0 +1,6 @@
+using MegaCrit.Sts2.Core.Multiplayer.Serialization;
+
+namespace BaseLib.Abstracts;
+
+
+public interface ICustomMessage : INetMessage { }
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]",
+}
diff --git a/Common/Rewards/CardTransformReward.cs b/Common/Rewards/CardTransformReward.cs
new file mode 100644
index 0000000..dbc57a2
--- /dev/null
+++ b/Common/Rewards/CardTransformReward.cs
@@ -0,0 +1,105 @@
+using BaseLib.Abstracts;
+using BaseLib.Patches.Content;
+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;
+
+///
+/// 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)
+{
+ ///
+ /// A new defined with the attribute
+ ///
+ [CustomEnum] public static RewardType CardTransform;
+ ///
+ /// Reference to the defined earlier
+ ///
+ protected override RewardType RewardType => CardTransform;
+
+ ///
+ /// 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;
+
+ ///
+ /// 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;
+ // TODO: make asset for this
+ public static string RewardIcon => ImageHelper.GetImagePath("ui/reward_screen/reward_icon_card_transform.png");
+ ///
+ protected override string IconPath => RewardIcon;
+
+
+ ///
+ /// 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
+ };
+ }
+
+ ///
+ /// 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 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");
+ return await RunManager.Instance.RewardSynchronizer.DoLocalCardTransform(Amount, true);
+ }
+}
diff --git a/Common/Rewards/CardTransformRewardMessage.cs b/Common/Rewards/CardTransformRewardMessage.cs
new file mode 100644
index 0000000..c6e35ea
--- /dev/null
+++ b/Common/Rewards/CardTransformRewardMessage.cs
@@ -0,0 +1,72 @@
+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.Logging;
+using MegaCrit.Sts2.Core.Multiplayer.Game;
+using MegaCrit.Sts2.Core.Multiplayer.Serialization;
+using MegaCrit.Sts2.Core.Runs;
+
+namespace BaseLib.Common.Rewards;
+
+///
+/// Message for transforming a card from a new reward type
+///
+public sealed class CardTransformRewardMessage : CustomRewardMessage
+{
+ internal 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.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()}");
+ 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/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..30a4dfd
--- /dev/null
+++ b/Patches/Content/CustomRewardPatches.cs
@@ -0,0 +1,40 @@
+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))]
+internal 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)
+ {
+ if (_RewardTypeSerializers.Keys.Contains(save.RewardType))
+ {
+ BaseLibMain.Logger.Debug($"Found RewardType {save.RewardType} in registry from mod {_RewardTypeSerializers[save.RewardType].Method.GetType().Assembly}");
+
+ 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;
+ }
+}
diff --git a/Patches/Content/RewardSynchronizerPatches.cs b/Patches/Content/RewardSynchronizerPatches.cs
new file mode 100644
index 0000000..26087af
--- /dev/null
+++ b/Patches/Content/RewardSynchronizerPatches.cs
@@ -0,0 +1,135 @@
+using BaseLib.Abstracts;
+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;
+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;
+
+///
+/// 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
+ {
+ ///
+ /// the id of the player who sent the message
+ ///
+ public ulong senderId;
+ ///
+ /// The message being sent
+ ///
+ public CustomRewardMessage message;
+ }
+
+ ///
+ /// Reference list of buffered messages
+ /// Hopefully there is only ever one instance of at a time on each client?
+ ///
+ internal static List _bufferedCustomRewardMessages = [];
+
+ internal static List BufferedCustomRewardMessages(this RewardSynchronizer rewardSynchronizer) => _bufferedCustomRewardMessages;
+
+ ///
+ /// Add a to the combat buffer
+ ///
+ public static void BufferCustomRewardMessage(this RewardSynchronizer rewardSynchronizer, CustomRewardMessage message, ulong senderId)
+ {
+ var bufferedMessage = new BufferedCustomRewardMessage
+ {
+ senderId = senderId,
+ message = message
+ };
+ rewardSynchronizer.BufferedCustomRewardMessages().Add(bufferedMessage);
+ }
+
+ ///
+ /// 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 static async Task DoLocalCardTransform(this RewardSynchronizer rewardSynchronizer, int amount = 1, bool upgrade = false)
+ {
+ 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);
+ }
+
+ ///
+ /// 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)
+ {
+ Cancelable = true,
+ RequireManualConfirmation = true
+ };
+
+ 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);
+ }
+
+ 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 OnCombat_HandleCustomBufferedMessages(RewardSynchronizer __instance)
+ {
+ foreach (BufferedCustomRewardMessage bufferedMessage in __instance.BufferedCustomRewardMessages())
+ {
+ __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
new file mode 100644
index 0000000..89cb5c5
--- /dev/null
+++ b/Patches/Content/RunManagerPatches.cs
@@ -0,0 +1,82 @@
+using BaseLib.Abstracts;
+using HarmonyLib;
+using MegaCrit.Sts2.Core.Helpers;
+using MegaCrit.Sts2.Core.Runs;
+
+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]
+ private static void InitializeCustomMessageHandlers(RunManager __instance)
+ {
+ foreach (var messageType in customMessageTypes)
+ {
+ var dummyMessage = messageType.CreateInstance();
+ if (dummyMessage == null)
+ {
+ BaseLibMain.Logger.Error(
+ $"CustomMessage instance creation for type {messageType.GetType()} from {messageType.Assembly} failed during Initialize");
+ continue;
+ }
+
+ 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);
+ }
+ }
+ }
+
+ [HarmonyPatch(nameof(RunManager.CleanUp))]
+ [HarmonyPostfix]
+ private static void DisposeCustomMessageHandlers(RunManager __instance)
+ {
+ foreach (var messageType in customMessageTypes)
+ {
+ if (messageType.CreateInstance() is not CustomMessage dummyMessage)
+ {
+ 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);
+ }
+ }
+}