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); + } + } +}