using System; using System.Xml.Linq; using Barotrauma.Networking; namespace Barotrauma { internal readonly struct WalletChangedEvent { public readonly Option Owner; public readonly Wallet Wallet; public readonly WalletInfo Info; public readonly WalletChangedData ChangedData; public WalletChangedEvent(Wallet wallet, WalletChangedData changedData, WalletInfo info) { Wallet = wallet; Info = info; ChangedData = changedData; Owner = wallet.Owner; } } [NetworkSerialize] internal struct WalletInfo : INetSerializableStruct { public int RewardDistribution; public int Balance; } /// /// Network message for the server to update wallet values to clients /// internal struct NetWalletUpdate : INetSerializableStruct { [NetworkSerialize(ArrayMaxSize = 256)] public NetWalletTransaction[] Transactions; } /// /// Network message for the client to transfer money between wallets /// [NetworkSerialize] internal struct NetWalletTransfer : INetSerializableStruct { public Option Sender; public Option Receiver; public int Amount; } /// /// Network message for the client to set the salary of someone /// internal struct NetWalletSetSalaryUpdate : INetSerializableStruct { [NetworkSerialize] public Option Target; [NetworkSerialize(MinValueInt = 0, MaxValueInt = 100)] public int NewRewardDistribution; } /// /// Represents the difference in balance and salary when a wallet gets updated /// Not really used right now but could be used for notifications when receiving funds similar to how talents do it /// [NetworkSerialize] internal struct WalletChangedData : INetSerializableStruct { public Option RewardDistributionChanged; public Option BalanceChanged; public readonly WalletChangedData MergeInto(WalletChangedData other) { other.BalanceChanged = AddOptionalInt(other.BalanceChanged, BalanceChanged); other.RewardDistributionChanged = AddOptionalInt(other.RewardDistributionChanged, RewardDistributionChanged); other.BalanceChanged = TurnToNoneIfZero(other.BalanceChanged); other.RewardDistributionChanged = TurnToNoneIfZero(other.RewardDistributionChanged); return other; static Option AddOptionalInt(Option a, Option b) { bool hasValue1 = a.TryUnwrap(out var value1); bool hasValue2 = b.TryUnwrap(out var value2); return hasValue1 ? hasValue2 ? Option.Some(value1 + value2) : Option.Some(value1) : hasValue2 ? Option.Some(value2) : Option.None; } static Option TurnToNoneIfZero(Option option) { return option.Bind(i => i == 0 ? Option.None : Option.Some(i)); } } } /// /// Represents an update that changed the amount of money or salary of the wallet /// [NetworkSerialize] internal struct NetWalletTransaction : INetSerializableStruct { public Option CharacterID; public WalletChangedData ChangedData; public WalletInfo Info; } // ReSharper disable ValueParameterNotUsed internal sealed class InvalidWallet : Wallet { public InvalidWallet(): base(Option.None()) { } public override int Balance { get => 0; set => throw new InvalidOperationException("Tried to set the balance on an invalid wallet"); } public override int RewardDistribution { get => 0; set => throw new InvalidOperationException("Tried to set the reward distribution on an invalid wallet"); } } internal partial class Wallet { public static readonly Wallet Invalid = new InvalidWallet(); public const string LowerCaseSaveElementName = "wallet"; private const string AttributeNameBalance = "balance", AttributeNameRewardDistribution = "rewarddistribution", SaveElementName = "Wallet"; public readonly Option Owner; private int balance; public virtual int Balance { get => balance; set => balance = ClampBalance(value); } private int rewardDistribution; public virtual int RewardDistribution { get => rewardDistribution; set { rewardDistribution = ClampRewardDistribution(value); if (Owner.TryUnwrap(out var character) && character.Info is { } info) { info.LastRewardDistribution = Option.Some(rewardDistribution); } } } public Wallet(Option owner) { Owner = owner; } public Wallet(Option owner, XElement element): this(owner) { balance = ClampBalance(element.GetAttributeInt(AttributeNameBalance, 0)); rewardDistribution = ClampBalance(element.GetAttributeInt(AttributeNameRewardDistribution, 0)); } public XElement Save() { XElement element = new XElement(SaveElementName, new XAttribute(AttributeNameBalance, Balance), new XAttribute(AttributeNameRewardDistribution, RewardDistribution)); return element; } public bool TryDeduct(int price) { if (!CanAfford(price)) { return false; } Deduct(price); return true; } public bool CanAfford(int price) => Balance >= price; public void Refund(int price) => Give(price); public void Give(int amount) { Balance += amount; SettingsChanged(balanceChanged: Option.Some(amount), rewardChanged: Option.None()); } public void Deduct(int price) { Balance -= price; SettingsChanged(balanceChanged: Option.Some(-price), rewardChanged: Option.None()); } /// /// Sets how much salary the wallet owner should receive from mission rewards. /// Bank's salary determines the default salary for new characters. /// /// public void SetRewardDistribution(int value) { int oldValue = RewardDistribution; RewardDistribution = value; SettingsChanged(balanceChanged: Option.None(), rewardChanged: Option.Some(RewardDistribution - oldValue)); } public WalletInfo CreateWalletInfo() { return new WalletInfo { Balance = Balance, RewardDistribution = RewardDistribution }; } public string GetOwnerLogName() => Owner.TryUnwrap(out var character) ? character.Name : "the bank"; partial void SettingsChanged(Option balanceChanged, Option rewardChanged); private static int ClampBalance(int value) => Math.Clamp(value, 0, CampaignMode.MaxMoney); private static int ClampRewardDistribution(int value) => Math.Clamp(value, 0, 100); } }