Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions Abstracts/CustomMessage.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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
/// </summary>
public abstract class CustomMessage : INetMessage, ICustomMessage
{
/// <summary>
/// Register your message type here.
/// Needs to be a function that takes <c>(<see cref="ICustomMessage"/> message, <see langword="ulong"/> senderId)</c>
/// See <seealso cref="CardTransformRewardMessage.HandleCardTransformedMessage"/> for an example.
/// You probably want to use an <see href="https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods">Extension Method</see>
/// </summary>
public abstract void Initialize(INetGameService netService);

/// <summary>
/// Unregister your message type here<br/>
/// Reference the same function you registered in <see cref="Initialize(INetGameService)"/>
/// </summary>
public abstract void Dispose(INetGameService netService);
/// <summary>
/// How your message is "written" to be sent over the internet
/// </summary>
/// <param name="writer">The </param>
/// <example>writer.</example>
public abstract void Serialize(PacketWriter writer);
/// <summary>
/// Read out your message into whatever variables it was created from
/// </summary>
/// <param name="reader">Parameter description.</param>
/// <returns>Type and description of the returned object.</returns>
/// <example>Write me later.</example>
public abstract void Deserialize(PacketReader reader);

/// <summary>
/// Whether to broadcast the message
/// </summary>
public abstract bool ShouldBroadcast { get; }
/// <summary>
/// The way to transfer the message
/// </summary>
public abstract NetTransferMode Mode { get; }
/// <summary>
/// What log level to output to (referenced when calling the vanilla handler(s) for messages)
/// </summary>
public abstract LogLevel LogLevel { get; }
}
73 changes: 73 additions & 0 deletions Abstracts/CustomReward.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Delegate handler to indicate the expected structure of <c>CreateFromSerializable</c> methods
/// </summary>
public delegate T SerializableCustomReward<out T>(SerializableReward save, Player player) where T : CustomReward;

/// <summary>
/// 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 <see cref="CardReward"/> and <see cref="SpecialCardReward"/> respectively,
/// though be mindful that you don't use a constructor that is unsupported by the base-game structure.
/// <seealso cref="CardTransformReward"/>
/// </summary>
public abstract class CustomReward(Player player) : Reward(player)
{
/// <summary>
/// Set the reward index after vanilla rewards by default
/// </summary>
public override int RewardsSetIndex => 9;

/// <summary>
/// Delegate to create your reward type from the saved data.
/// The method reference must be of a <see langword="static"/> method
/// </summary>
/// <example>
/// <code>
/// // in MyCustomReward.cs
/// public static MyCustomReward CreateFromSerializable(SerializableReward save, Player player)
/// {
/// return new MyCustomReward(player) {
/// MyCustomNumber = save.GoldAmount
/// }
/// }
/// public override SerializableCustomReward&lt;CustomReward&gt; SerializeMethod => CreateFromSerializable;
/// </code>
/// </example>
public abstract SerializableCustomReward<CustomReward> SerializeMethod { get; }


/// <summary>
/// Base method to handle registering your reward for serializing and deserializing in <see cref="RewardSynchronizer"/>
/// Override this if you wish to manually register your reward with <see cref="CustomRewardPatches.RegisterCustomReward(RewardType, SerializableCustomReward{CustomReward})"/>
/// or by getting your own reference to the <see cref="RunLocationTargetedMessageBuffer"/> used for the <see cref="RewardSynchronizer"/> instance
/// </summary>
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?
}

32 changes: 32 additions & 0 deletions Abstracts/CustomRewardMessage.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Abstract class to inherit for syncing rewards
/// </summary>
public abstract class CustomRewardMessage : CustomTargetedMessage
{
/// <summary>
/// Include whether the reward was skipped or not
/// Not required to actually check, namely if being used for a per-player reward like <seealso cref="PaelsWingSacrificeMessage"/>
/// </summary>
public required bool wasSkipped;

/// <summary>
/// You probably want to broadcast the message
/// </summary>
public sealed override bool ShouldBroadcast => true;

/// <summary>
/// Rewards should prabably be sent reliably too
/// </summary>
public sealed override NetTransferMode Mode => NetTransferMode.Reliable;

/// <summary>
/// Set when instantiating, afaik needed for saving to the run?
/// </summary>
public required override RunLocation Location { get; set; }
}
25 changes: 25 additions & 0 deletions Abstracts/CustomTargetedMessage.cs
Original file line number Diff line number Diff line change
@@ -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);
}
6 changes: 6 additions & 0 deletions Abstracts/ICustomMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using MegaCrit.Sts2.Core.Multiplayer.Serialization;

namespace BaseLib.Abstracts;


public interface ICustomMessage : INetMessage { }
6 changes: 6 additions & 0 deletions BaseLib/localization/eng/gameplay_ui.json
Original file line number Diff line number Diff line change
@@ -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]",
}
105 changes: 105 additions & 0 deletions Common/Rewards/CardTransformReward.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A reward class similar to the card removal one created by <see cref="ForbiddenGrimoire"/>, only for transforming instead of removing cards
/// </summary>
/// <example> <code>
/// room.AddExtraReward(Owner.Player, new CardTransformReward(Owner.Player) {Amount = Amount, Upgrade = true});
/// </code> </example>
public sealed class CardTransformReward(Player player) : CustomReward(player)
{
/// <summary>
/// A new <see cref="RewardType"/> defined with the <see cref="CustomEnumAttribute"/> attribute
/// </summary>
[CustomEnum] public static RewardType CardTransform;
/// <summary>
/// Reference to the <see cref="RewardType"/> <see cref="CardTransform"/> defined earlier
/// </summary>
protected override RewardType RewardType => CardTransform;

/// <summary>
/// Whether the card rewards should be upgraded or not
/// </summary>
public required bool Upgrade;
/// <summary>
/// How many cards can be selected in this reward screen
/// </summary>
public required int Amount;

/// <summary>
/// The description to show in the reward screen,
/// switches based on whether the reward will upgrade the transformed cards
/// </summary>
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;
}
}
/// <inheritdoc/>
public override bool IsPopulated => true;
// TODO: make asset for this
public static string RewardIcon => ImageHelper.GetImagePath("ui/reward_screen/reward_icon_card_transform.png");
/// <inheritdoc/>
protected override string IconPath => RewardIcon;


/// <summary>
/// Serializing the reward, saving whether to upgrade and how many cards to transform in the vanilla fields
/// </summary>
public override SerializableReward ToSerializable()
{
return new SerializableReward()
{
RewardType = CardTransform,
GoldAmount = Amount,
WasGoldStolenBack = Upgrade
};
}

/// <summary>
/// Recreates the reward from the saved <see cref="SerializableReward"/>
/// </summary>
/// <param name="save">The <see cref="SerializableReward"/> that was created and saved from
/// <see cref="ToSerializable"/></param>
/// <param name="player">The <see cref="Player"/> the reward belongs to</param>
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
};
}

/// <inheritdoc/>
public override SerializableCustomReward<CustomReward> SerializeMethod => CreateFromSerializable;

/// <inheritdoc/>
public override void MarkContentAsSeen() { }

/// <inheritdoc />
public override Task Populate() { return Task.CompletedTask; }

/// <inheritdoc/>
protected override async Task<bool> OnSelect()
{
BaseLibMain.Logger.Info("Obtained card transformation from reward");
return await RunManager.Instance.RewardSynchronizer.DoLocalCardTransform(Amount, true);
}
}
72 changes: 72 additions & 0 deletions Common/Rewards/CardTransformRewardMessage.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Message for transforming a card from a new reward type
/// </summary>
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));
}

/// <inheritdoc/>
public override void Dispose(RunLocationTargetedMessageBuffer messageBuffer)
{
BaseLibMain.Logger.Debug($"Unregistering handler for {GetType()}");
messageBuffer.UnregisterMessageHandler<CardTransformRewardMessage>(HandleCardTransformedMessage);
}

/// <inheritdoc/>
public override void Initialize(RunLocationTargetedMessageBuffer messageBuffer)
{
BaseLibMain.Logger.Debug($"Registering handler for {GetType()}");
messageBuffer.RegisterMessageHandler<CardTransformRewardMessage>(HandleCardTransformedMessage);
}

/// <summary>
/// Whether to upgrade the card as well as transforming
/// </summary>
public required bool Upgrade;
/// <summary>
/// The amount of cards to select from
/// </summary>
public required int Amount;

/// <inheritdoc/>
public override LogLevel LogLevel => LogLevel.Debug;

/// <inheritdoc/>
public override void Deserialize(PacketReader reader)
{
}

/// <inheritdoc/>
public override void Serialize(PacketWriter writer)
{
}
}
Loading