Decompiled source of UEXP Bank v1.0.0

UEXP_Bank.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using CSync.Extensions;
using CSync.Lib;
using Entiffo.UEXPBank.Config;
using Entiffo.UEXPBank.Loan;
using Entiffo.UEXPBank.Network;
using Entiffo.UEXPBank.Terminal;
using Entiffo.UEXPBank.Util;
using HarmonyLib;
using LethalNetworkAPI;
using Microsoft.CodeAnalysis;
using TerminalApi;
using TerminalApi.Classes;
using TerminalApi.Events;
using Unity.Netcode;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp-firstpass")]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("UEXP_Bank")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("UEXP_Bank")]
[assembly: AssemblyTitle("UEXP_Bank")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace Entiffo.UEXPBank
{
	[BepInPlugin("Entiffo.UEXP_Bank", "UEXP_Bank", "1.0.0")]
	[BepInDependency("com.sigurd.csync", "5.0.0")]
	[BepInDependency("atomic.terminalapi", "1.5.0")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class Plugin : BaseUnityPlugin
	{
		public const string ModGuid = "Entiffo.UEXP_Bank";

		private readonly Harmony _harmony = new Harmony("Entiffo.UEXP_Bank");

		internal static Plugin Instance { get; private set; }

		internal static ModConfig Config { get; private set; }

		internal static ManualLogSource Log { get; private set; }

		internal static bool IsEnabled => Config.Enabled.Value;

		private void Awake()
		{
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			Config = new ModConfig(((BaseUnityPlugin)this).Config);
			WarnSoftIncompat();
			LoanNetwork.Initialize();
			BankCommands.Register();
			_harmony.PatchAll(typeof(Plugin).Assembly);
			Log.LogInfo((object)"UEXP_Bank 1.0.0 loaded.");
		}

		internal static void DebugLog(string message)
		{
			if (Config.DebugLogging.Value)
			{
				Log.LogInfo((object)message);
			}
		}

		private static void WarnSoftIncompat()
		{
			string[] array = new string[5] { "Entiffo.UEXPLoan", "tesinormed.FAndCDaveCo", "FAndCDaveCo", "JS03.LethalDebt", "LethalDebt" };
			foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos)
			{
				string key = pluginInfo.Key;
				string[] array2 = array;
				foreach (string value in array2)
				{
					if (key.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0)
					{
						if (key.IndexOf("UEXPLoan", StringComparison.OrdinalIgnoreCase) >= 0)
						{
							Log.LogWarning((object)("Remove old '" + key + "' — this mod was renamed to UEXP_Bank (Entiffo.UEXP_Bank)."));
						}
						else
						{
							Log.LogWarning((object)("Soft incompatibility: '" + key + "' also modifies credits/debt. Running with UEXP_Bank may double-charge or conflict. Prefer one money mod."));
						}
					}
				}
			}
		}
	}
	public static class MyPluginInfo
	{
		public const string PLUGIN_GUID = "Entiffo.UEXP_Bank";

		public const string PLUGIN_NAME = "UEXP_Bank";

		public const string PLUGIN_VERSION = "1.0.0";
	}
}
namespace Entiffo.UEXPBank.Util
{
	public static class ChatToast
	{
		public static void Broadcast(string message)
		{
			if (!string.IsNullOrEmpty(message))
			{
				LoanNetwork.BroadcastToast(message);
			}
		}

		internal static void ShowLocal(string message)
		{
			if (!string.IsNullOrEmpty(message))
			{
				if ((Object)(object)HUDManager.Instance != (Object)null)
				{
					HUDManager.Instance.DisplayTip("UEXP Bank", message, false, false, "UEXP_BankToast");
				}
				Plugin.DebugLog("Toast: " + message);
			}
		}
	}
	public static class SaveState
	{
		public const string PrincipalKey = "UEXP_Bank_principal";

		public const string RateKey = "UEXP_Bank_rate";

		public const string DaysElapsedKey = "UEXP_Bank_daysElapsed";

		public const string LastFeeKey = "UEXP_Bank_lastFeeDaysSpent";

		public const string PeakKey = "UEXP_Bank_peakPrincipal";

		public const string ActiveKey = "UEXP_Bank_active";

		public const string InterestCollectedKey = "UEXP_Bank_interestCollected";

		public const string LastFeeWasCashKey = "UEXP_Bank_lastFeeWasCash";

		public const string LockedLimitKey = "UEXP_Bank_lockedLimit";

		public const string DepositKey = "UEXP_Bank_deposit";

		private const string LegacyPrefix = "UEXPLoan_";

		public static string? CurrentSaveName()
		{
			if ((Object)(object)GameNetworkManager.Instance == (Object)null)
			{
				return null;
			}
			return GameNetworkManager.Instance.currentSaveFileName;
		}

		public static void LoadInto(LoanState state, int baselineRate)
		{
			string text = CurrentSaveName();
			if (string.IsNullOrEmpty(text))
			{
				Plugin.DebugLog("SaveState.LoadInto: no current save file name; skipping.");
				return;
			}
			MigrateLegacyIfNeeded(text);
			state.Principal = ES3.Load<int>("UEXP_Bank_principal", text, 0);
			state.Rate = ES3.Load<int>("UEXP_Bank_rate", text, baselineRate);
			state.DaysElapsed = ES3.Load<int>("UEXP_Bank_daysElapsed", text, 0);
			state.LastFeeDaysSpent = ES3.Load<int>("UEXP_Bank_lastFeeDaysSpent", text, -1);
			state.PeakPrincipal = ES3.Load<int>("UEXP_Bank_peakPrincipal", text, 0);
			state.Active = ES3.Load<bool>("UEXP_Bank_active", text, state.Principal > 0);
			state.InterestCollected = ES3.Load<bool>("UEXP_Bank_interestCollected", text, false);
			state.LastFeeWasCash = ES3.Load<bool>("UEXP_Bank_lastFeeWasCash", text, false);
			state.LockedLimit = ES3.Load<int>("UEXP_Bank_lockedLimit", text, 0);
			state.Deposit = ES3.Load<int>("UEXP_Bank_deposit", text, 0);
			if (state.Principal <= 0)
			{
				int nextRate = ((state.Rate > 0) ? state.Rate : baselineRate);
				int deposit = state.Deposit;
				state.ClearDebtKeepRate(nextRate);
				state.Deposit = deposit;
			}
			else if (state.LockedLimit <= 0)
			{
				int profitQuota = (((Object)(object)TimeOfDay.Instance != (Object)null) ? TimeOfDay.Instance.profitQuota : 0);
				state.LockedLimit = DeadlineScale.ComputeLimit(profitQuota, Plugin.Config.QuotaMultiplier.Value);
				state.LockedLimit = Mathf.Max(state.LockedLimit, state.Principal);
			}
		}

		public static void Save(LoanState state)
		{
			string text = CurrentSaveName();
			if (!string.IsNullOrEmpty(text))
			{
				ES3.Save<int>("UEXP_Bank_principal", state.Principal, text);
				ES3.Save<int>("UEXP_Bank_rate", state.Rate, text);
				ES3.Save<int>("UEXP_Bank_daysElapsed", state.DaysElapsed, text);
				ES3.Save<int>("UEXP_Bank_lastFeeDaysSpent", state.LastFeeDaysSpent, text);
				ES3.Save<int>("UEXP_Bank_peakPrincipal", state.PeakPrincipal, text);
				ES3.Save<bool>("UEXP_Bank_active", state.Active && state.Principal > 0, text);
				ES3.Save<bool>("UEXP_Bank_interestCollected", state.InterestCollected, text);
				ES3.Save<bool>("UEXP_Bank_lastFeeWasCash", state.LastFeeWasCash, text);
				ES3.Save<int>("UEXP_Bank_lockedLimit", (state.Principal > 0) ? state.LockedLimit : 0, text);
				ES3.Save<int>("UEXP_Bank_deposit", state.Deposit, text);
			}
		}

		public static void Clear()
		{
			string text = CurrentSaveName();
			if (!string.IsNullOrEmpty(text))
			{
				DeleteIfExists("UEXP_Bank_principal", text);
				DeleteIfExists("UEXP_Bank_rate", text);
				DeleteIfExists("UEXP_Bank_daysElapsed", text);
				DeleteIfExists("UEXP_Bank_lastFeeDaysSpent", text);
				DeleteIfExists("UEXP_Bank_peakPrincipal", text);
				DeleteIfExists("UEXP_Bank_active", text);
				DeleteIfExists("UEXP_Bank_interestCollected", text);
				DeleteIfExists("UEXP_Bank_lastFeeWasCash", text);
				DeleteIfExists("UEXP_Bank_lockedLimit", text);
				DeleteIfExists("UEXP_Bank_deposit", text);
				ClearLegacy(text);
			}
		}

		private static void MigrateLegacyIfNeeded(string save)
		{
			if (!ES3.KeyExists("UEXP_Bank_principal", save) && ES3.KeyExists("UEXPLoan_principal", save))
			{
				ES3.Save<int>("UEXP_Bank_principal", ES3.Load<int>("UEXPLoan_principal", save, 0), save);
				ES3.Save<int>("UEXP_Bank_rate", ES3.Load<int>("UEXPLoan_rate", save, 10), save);
				ES3.Save<int>("UEXP_Bank_daysElapsed", ES3.Load<int>("UEXPLoan_daysElapsed", save, 0), save);
				ES3.Save<int>("UEXP_Bank_lastFeeDaysSpent", ES3.Load<int>("UEXPLoan_lastFeeDaysSpent", save, -1), save);
				ES3.Save<int>("UEXP_Bank_peakPrincipal", ES3.Load<int>("UEXPLoan_peakPrincipal", save, 0), save);
				ES3.Save<bool>("UEXP_Bank_active", ES3.Load<bool>("UEXPLoan_active", save, false), save);
				ES3.Save<bool>("UEXP_Bank_interestCollected", ES3.Load<bool>("UEXPLoan_interestCollected", save, false), save);
				ES3.Save<int>("UEXP_Bank_lockedLimit", ES3.Load<int>("UEXPLoan_lockedLimit", save, 0), save);
				ClearLegacy(save);
				Plugin.Log.LogInfo((object)"Migrated UEXPLoan save keys to UEXP_Bank.");
			}
		}

		private static void ClearLegacy(string save)
		{
			string[] array = new string[8] { "principal", "rate", "daysElapsed", "lastFeeDaysSpent", "peakPrincipal", "active", "interestCollected", "lockedLimit" };
			foreach (string text in array)
			{
				DeleteIfExists("UEXPLoan_" + text, save);
			}
		}

		private static void DeleteIfExists(string key, string save)
		{
			if (ES3.KeyExists(key, save))
			{
				ES3.DeleteKey(key, save);
			}
		}
	}
}
namespace Entiffo.UEXPBank.Terminal
{
	public static class BankCommands
	{
		[Serializable]
		[CompilerGenerated]
		private sealed class <>c
		{
			public static readonly <>c <>9 = new <>c();

			public static Func<string> <>9__1_2;

			public static TerminalEventHandler <>9__1_0;

			public static TerminalEventHandler <>9__1_1;

			internal string <Register>b__1_2()
			{
				if (LoanService.IsHost())
				{
					LoanService.RefreshFromSave("bank-dashboard");
					return TerminalDisplay.Dashboard();
				}
				LoanNetwork.TryPullSnapshot();
				return LoanNetwork.ExecuteOrRequest("status");
			}

			internal void <Register>b__1_0(object _, TerminalEventArgs __)
			{
				TryInjectHelpNode();
			}

			internal void <Register>b__1_1(object _, TerminalEventArgs __)
			{
				TryInjectHelpNode();
			}
		}

		private static TerminalNode? _resultNode;

		public static void Register()
		{
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_0069: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Expected O, but got Unknown
			//IL_008f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0094: Unknown result type (might be due to invalid IL or missing references)
			//IL_009a: Expected O, but got Unknown
			//IL_00b3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00be: Expected O, but got Unknown
			_resultNode = ScriptableObject.CreateInstance<TerminalNode>();
			_resultNode.clearPreviousText = true;
			_resultNode.displayText = "";
			TerminalApi.AddCommand("bank", new CommandInfo
			{
				Category = "Other",
				Description = "UEXP_Bank — loans + vault. Try: bank terms",
				DisplayTextSupplier = delegate
				{
					if (LoanService.IsHost())
					{
						LoanService.RefreshFromSave("bank-dashboard");
						return TerminalDisplay.Dashboard();
					}
					LoanNetwork.TryPullSnapshot();
					return LoanNetwork.ExecuteOrRequest("status");
				},
				Title = "BANK"
			}, (string)null, true);
			object obj = <>c.<>9__1_0;
			if (obj == null)
			{
				TerminalEventHandler val = delegate
				{
					TryInjectHelpNode();
				};
				<>c.<>9__1_0 = val;
				obj = (object)val;
			}
			Events.TerminalAwake += (TerminalEventHandler)obj;
			object obj2 = <>c.<>9__1_1;
			if (obj2 == null)
			{
				TerminalEventHandler val2 = delegate
				{
					TryInjectHelpNode();
				};
				<>c.<>9__1_1 = val2;
				obj2 = (object)val2;
			}
			Events.TerminalStarted += (TerminalEventHandler)obj2;
		}

		internal static void TryInjectHelpNode()
		{
			try
			{
				TerminalKeyword keyword = TerminalApi.GetKeyword("help");
				if (!((Object)(object)keyword?.specialKeywordResult == (Object)null))
				{
					string text = keyword.specialKeywordResult.displayText ?? "";
					text = StripExistingBankHelp(text);
					keyword.specialKeywordResult.displayText = text + TerminalDisplay.HelpBlurb();
					Plugin.DebugLog("Injected short >BANK into terminal help node.");
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Could not inject bank help text: " + ex.Message));
			}
		}

		private static string StripExistingBankHelp(string text)
		{
			if (string.IsNullOrEmpty(text))
			{
				return text;
			}
			int num = text.IndexOf(">BANK", StringComparison.OrdinalIgnoreCase);
			if (num < 0)
			{
				return text;
			}
			int num2 = text.Length;
			int num3 = text.IndexOf("\n>", num + 1, StringComparison.Ordinal);
			if (num3 >= 0)
			{
				num2 = num3 + 1;
			}
			int num4 = text.IndexOf("\n\n", num, StringComparison.Ordinal);
			if (num4 >= 0 && num4 + 2 <= num2)
			{
				num2 = num4 + 2;
			}
			return text.Remove(num, num2 - num);
		}

		public static bool TryHandleSentence(Terminal terminal, string submitted, out TerminalNode? node)
		{
			node = null;
			if (string.IsNullOrWhiteSpace(submitted))
			{
				return false;
			}
			string[] array = submitted.Trim().ToLowerInvariant().Split(new char[2] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
			if (array.Length == 0)
			{
				return false;
			}
			if (array[0] == "bank")
			{
				if (array.Length == 1)
				{
					return false;
				}
				string[] array2 = new string[array.Length - 1];
				Array.Copy(array, 1, array2, 0, array2.Length);
				array = array2;
			}
			else if (!IsBankAliasVerb(array[0]))
			{
				return false;
			}
			string displayText = DispatchParts(array);
			if ((Object)(object)_resultNode == (Object)null)
			{
				_resultNode = ScriptableObject.CreateInstance<TerminalNode>();
				_resultNode.clearPreviousText = true;
			}
			_resultNode.displayText = displayText;
			node = _resultNode;
			return true;
		}

		private static bool IsBankAliasVerb(string verb)
		{
			switch (verb)
			{
			case "dep":
			case "pay":
			case "repay":
			case "terms":
			case "deposit":
			case "withdraw":
			case "wd":
			case "loan":
			case "borrow":
				return true;
			default:
				return false;
			}
		}

		private static string DispatchParts(string[] parts)
		{
			string text = parts[0];
			bool flag;
			switch (text)
			{
			case "terms":
				return TerminalDisplay.Terms();
			case "borrow":
			case "loan":
				flag = true;
				break;
			default:
				flag = false;
				break;
			}
			if (flag)
			{
				if (parts.Length >= 2 && int.TryParse(parts[1], out var result))
				{
					return LoanNetwork.ExecuteOrRequest($"borrow {result}");
				}
				return TerminalDisplay.WrapError("Usage: loan <amount>  (or: bank borrow <amount>)");
			}
			if ((text == "deposit" || text == "dep") ? true : false)
			{
				if (parts.Length >= 2 && int.TryParse(parts[1], out var result2))
				{
					return LoanNetwork.ExecuteOrRequest($"deposit {result2}");
				}
				return TerminalDisplay.WrapError("Usage: deposit <amount>");
			}
			if ((text == "withdraw" || text == "wd") ? true : false)
			{
				if (parts.Length >= 2 && int.TryParse(parts[1], out var result3))
				{
					return LoanNetwork.ExecuteOrRequest($"withdraw {result3}");
				}
				return TerminalDisplay.WrapError("Usage: withdraw <amount>");
			}
			if ((text == "pay" || text == "repay") ? true : false)
			{
				if (parts.Length >= 2 && parts[1] == "all")
				{
					return LoanNetwork.ExecuteOrRequest("pay all");
				}
				if (parts.Length >= 2 && int.TryParse(parts[1], out var result4))
				{
					return LoanNetwork.ExecuteOrRequest($"pay {result4}");
				}
				return TerminalDisplay.WrapError("Usage: pay <amount|all>");
			}
			return TerminalDisplay.WrapError("Unknown bank command. Type bank for help.");
		}

		internal static void ShowResultOnTerminal(string result)
		{
			if (!string.IsNullOrEmpty(result))
			{
				if ((Object)(object)_resultNode == (Object)null)
				{
					_resultNode = ScriptableObject.CreateInstance<TerminalNode>();
					_resultNode.clearPreviousText = true;
				}
				_resultNode.displayText = result;
				Terminal val = Object.FindObjectOfType<Terminal>();
				if (!((Object)(object)val == (Object)null) && val.terminalInUse)
				{
					val.LoadNewNode(_resultNode);
				}
			}
		}
	}
	[HarmonyPatch(typeof(Terminal), "ParsePlayerSentence")]
	internal static class TerminalParsePatch
	{
		[HarmonyPrefix]
		private static bool Prefix(Terminal __instance, ref TerminalNode __result)
		{
			string submitted = "";
			try
			{
				if ((Object)(object)__instance.screenText != (Object)null && __instance.textAdded > 0 && __instance.screenText.text.Length >= __instance.textAdded)
				{
					submitted = __instance.screenText.text.Substring(__instance.screenText.text.Length - __instance.textAdded);
				}
			}
			catch (Exception ex)
			{
				Plugin.DebugLog("ParsePlayerSentence text read failed: " + ex.Message);
				return true;
			}
			if (BankCommands.TryHandleSentence(__instance, submitted, out TerminalNode node) && (Object)(object)node != (Object)null)
			{
				__result = node;
				return false;
			}
			return true;
		}
	}
	[HarmonyPatch(typeof(Terminal), "Start")]
	internal static class TerminalStartHelpPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			BankCommands.TryInjectHelpNode();
		}
	}
	public static class TerminalDisplay
	{
		private const int BoxInnerWidth = 42;

		public static string Header()
		{
			string text = "+" + new string('-', 42) + "+";
			return text + "\n" + BoxLine("U E X P  B A N K") + "\n" + BoxLine("Loans & Vault") + "\n" + text + "\n\n";
		}

		internal static string BoxLine(string text)
		{
			if (text == null)
			{
				text = "";
			}
			if (text.Length > 42)
			{
				text = text.Substring(0, 42);
			}
			int num = 42 - text.Length;
			int num2 = num / 2;
			int count = num - num2;
			return "|" + new string(' ', num2) + text + new string(' ', count) + "|";
		}

		public static string HelpBlurb()
		{
			return ">BANK\nLoans & vault. Type BANK for status.\nShortcuts: deposit / withdraw / loan / pay\n\n";
		}

		public static string Dashboard()
		{
			LoanState state = LoanService.State;
			int num = LoanService.CurrentLimit();
			int num2 = LoanService.Available();
			int num3 = DeadlineScale.GraceDays();
			int num4 = DeadlineScale.HikeDay();
			int num5 = LoanService.ComputeFee(Mathf.Max(1, state.Principal), state.Rate);
			int value = Plugin.Config.DepositCap.Value;
			int value2 = Plugin.Config.WithdrawFee.Value;
			int num6 = ((state.Principal <= 0) ? num3 : Mathf.Max(0, num3 - state.DaysElapsed));
			int num7 = ((state.Principal <= 0) ? num4 : Mathf.Max(0, num4 - state.DaysElapsed));
			string text = ((state.Principal <= 0) ? "n/a (you're debt-free... for now)" : ((state.DaysElapsed <= num3) ? $"GRACE ({state.DaysElapsed}/{num3}) — enjoy it" : ((state.LastFeeDaysSpent == CurrentDaysSpent() && state.LastFeeWasCash) ? $"already collected today (~{num5})" : ((state.LastFeeDaysSpent != CurrentDaysSpent()) ? $"{num5} credits at {Plugin.Config.FeeHour.Value}:00" : $"capitalized today (~{num5} added to debt)"))));
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append(Header());
			stringBuilder.Append(" VAULT\n");
			stringBuilder.Append(" -----\n");
			stringBuilder.Append($" Deposited           {state.Deposit} / {value}\n");
			stringBuilder.Append($" Withdraw fee        {value2} from vault (on top of what you take)\n");
			stringBuilder.Append('\n');
			stringBuilder.Append(" CREDIT LINE\n");
			stringBuilder.Append(" -----------\n");
			stringBuilder.Append($" Credit limit        {num}");
			if (state.Principal > 0 && state.LockedLimit > 0)
			{
				stringBuilder.Append(" (LOCKED while owing)");
			}
			stringBuilder.Append('\n');
			stringBuilder.Append($" Still available     {num2}\n");
			stringBuilder.Append($" You currently owe   {state.Principal}\n");
			stringBuilder.Append($" Daily rate          {state.Rate}% (we round UP, always)\n");
			stringBuilder.Append($" Grace left          {num6} day(s)\n");
			stringBuilder.Append($" Until rate climbs   {num7} day(s)\n");
			stringBuilder.Append(" Today's fee         " + text + "\n");
			stringBuilder.Append('\n');
			stringBuilder.Append(" COMMANDS\n");
			stringBuilder.Append("  bank / bank terms       status / the deal\n");
			stringBuilder.Append("  deposit <n>             stash credits (fills to cap)\n");
			stringBuilder.Append("  withdraw <n>            take them back (fee from vault)\n");
			stringBuilder.Append("  loan <n>                borrow (alias: borrow)\n");
			stringBuilder.Append("  pay <n|all>             repay the loan\n");
			stringBuilder.Append("  (bank deposit / bank loan … still work)\n\n");
			return stringBuilder.ToString();
		}

		public static string Terms()
		{
			int num = DeadlineScale.DeadlineDays();
			int num2 = DeadlineScale.HikeDay();
			float value = Plugin.Config.QuotaMultiplier.Value;
			int value2 = Plugin.Config.BaselineRate.Value;
			int value3 = Plugin.Config.MaxRate.Value;
			int value4 = Plugin.Config.GiftMinPrincipal.Value;
			int value5 = Plugin.Config.FeeHour.Value;
			int value6 = Plugin.Config.DepositCap.Value;
			int value7 = Plugin.Config.WithdrawFee.Value;
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append(Header());
			stringBuilder.Append("EMPLOYEE BANKING AGREEMENT\n");
			stringBuilder.Append("(binding until you pay, die, or get fired into orbit)\n\n");
			stringBuilder.Append("0. THE VAULT (your own money)\n");
			stringBuilder.Append($"   Deposit up to {value6} credits for safekeeping. No interest on deposits.\n");
			stringBuilder.Append("   Asking for more than the vault can hold? We fill it to the cap and tell you.\n");
			stringBuilder.Append($"   Withdraw: you get the amount you asked for; the {value7} fee comes OUT OF THE VAULT on top.\n");
			stringBuilder.Append("   Vault can't cover the fee after your withdraw? Shortfall comes from what you receive.\n");
			stringBuilder.Append("   Deposits are separate from loans. Owing the bank does not touch your vault.\n");
			stringBuilder.Append("   Getting fired / new save: vault wiped with the books. Tragic.\n\n");
			stringBuilder.Append("1. HOW MUCH CAN YOU \"BORROW\"\n");
			stringBuilder.Append($"   Up to {value}x your current profit quota — but only when you open a loan.\n");
			stringBuilder.Append("   Example: quota 1000 → line of 2500.\n");
			stringBuilder.Append("   Take 700 today, another 500 tomorrow — until you hit the cap.\n");
			stringBuilder.Append("   While you still owe anything, the credit LINE IS FROZEN.\n");
			stringBuilder.Append("   Quota went up? Cute. Limit does not. Pay to zero first.\n");
			stringBuilder.Append("   This is a revolving line only after you clear the books.\n\n");
			stringBuilder.Append("2. GRACE PERIOD (the honeymoon)\n");
			stringBuilder.Append($"   Right now your quota lasts {num} day(s), so you get {num} day(s) free.\n");
			stringBuilder.Append("   No daily fee. No questions. Just vibes and bad decisions.\n");
			stringBuilder.Append("   (Longer deadlines from UnlimitedQuota = longer grace. We noticed.)\n\n");
			stringBuilder.Append("3. THE DAILY FEE (aka friendship tax)\n");
			stringBuilder.Append($"   After grace, every moon-day at {value5}:00 we skim credits.\n");
			stringBuilder.Append($"   Fee = debt x your CURRENT daily rate %, rounded UP (starts at {value2}%).\n");
			stringBuilder.Append("   Got the cash? We take it. Debt stays the same size.\n");
			stringBuilder.Append("   Broke? Cute. We ADD the fee onto your debt instead.\n");
			stringBuilder.Append("   Leave the moon before collection? We still charge you on the way out.\n\n");
			stringBuilder.Append("4. WHEN THE RATE GETS ANGRY\n");
			stringBuilder.Append($"   Still owing on day {num2}+? Rate climbs +1% per day.\n");
			stringBuilder.Append($"   Caps at {value3}%. We are not monsters. We are a bank.\n");
			stringBuilder.Append($"   Pay off with NO cash fee skimmed yet? Rate snaps to {value2}%.\n");
			stringBuilder.Append("   Pay off AFTER we've skimmed >=1 cash fee (any day, including after hike)?\n");
			stringBuilder.Append("   Standing rate drops by 1% for your NEXT loan (stacks, floor 1%).\n");
			stringBuilder.Append("   Partial payments shrink the debt. They do NOT reset the clock.\n");
			stringBuilder.Append("   Borrowing more also does NOT reset the clock. Nice try.\n\n");
			stringBuilder.Append("5. LOYALTY BRIBE (yes, really)\n");
			stringBuilder.Append($"   Peak debt over {value4}, AND we've already skimmed at least one daily fee\n");
			stringBuilder.Append("   from your credits (capitalizing onto the loan does NOT count — we want cash),\n");
			stringBuilder.Append("   THEN you pay the whole balance to zero?\n");
			stringBuilder.Append("   We buy you one random thing from the Company catalogue. Free.\n");
			stringBuilder.Append("   Dropship junk, ship toys — whatever's left that you don't already own.\n");
			stringBuilder.Append("   No suits. Fashion is a paid upgrade; this bank has standards.\n");
			stringBuilder.Append("   Clear it during grace with zero fees paid? Cute. No gift. Banks aren't charities.\n\n");
			stringBuilder.Append("6. GETTING FIRED\n");
			stringBuilder.Append("   Miss the quota, classic Company ejection: debt AND vault die with the job.\n");
			stringBuilder.Append("   New save, clean books. Old you was a liability anyway.\n\n");
			stringBuilder.Append("By typing BANK BORROW / DEPOSIT you agree you read this, understood none of it,\n");
			stringBuilder.Append($"and will blame your friends when the fee hits at {value5}.\n\n");
			stringBuilder.Append("— UEXP Bank Collections\n");
			stringBuilder.Append("   \"We'll get it back somehow.\"\n\n");
			return stringBuilder.ToString();
		}

		public static string BorrowReceipt(int amount)
		{
			return Header() + $"APPROVED. +{amount} credits hit the terminal.\n" + $"You now owe {LoanService.State.Principal}. Room left: {LoanService.Available()}.\n" + "Try not to feed it to a snare flea.\n\n";
		}

		public static string PayReceipt(int paid, bool fullyCleared, string giftNote)
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append(Header());
			stringBuilder.Append($"PAYMENT RECEIVED: {paid}\n");
			if (fullyCleared)
			{
				stringBuilder.Append("ZERO LOAN BALANCE. You're free. Temporarily.\n");
				stringBuilder.Append($"Credit line refreshed to {LoanService.CurrentLimit()}.\n");
				if (!string.IsNullOrEmpty(giftNote))
				{
					stringBuilder.Append(giftNote);
				}
			}
			else
			{
				stringBuilder.Append($"Still on the hook for {LoanService.State.Principal}.\n");
			}
			stringBuilder.Append('\n');
			return stringBuilder.ToString();
		}

		public static string DepositReceipt(int amount, string clampNote = "")
		{
			int value = Plugin.Config.DepositCap.Value;
			return Header() + $"VAULT DEPOSIT: +{amount}{clampNote}\n" + $"Balance {LoanService.State.Deposit} / {value}.\n" + "Safe from snare fleas. Not safe from getting fired.\n\n";
		}

		public static string WithdrawReceipt(int requested, int fee, int payout, int vaultTaken, int feeFromPayout)
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append(Header());
			stringBuilder.Append($"VAULT WITHDRAW: asked {requested}\n");
			stringBuilder.Append($"To terminal: +{payout}\n");
			stringBuilder.Append($"Vault charged: -{vaultTaken} (includes {fee} fee)\n");
			if (feeFromPayout > 0)
			{
				stringBuilder.Append($"Fee shortfall {feeFromPayout} came from your payout (vault couldn't cover it).\n");
			}
			stringBuilder.Append($"Vault left: {LoanService.State.Deposit}.\n\n");
			return stringBuilder.ToString();
		}

		public static string WrapError(string message)
		{
			return Header() + "DENIED.\n" + message + "\n\n";
		}

		private static int CurrentDaysSpent()
		{
			if (StartOfRound.Instance?.gameStats == null)
			{
				return -1;
			}
			return StartOfRound.Instance.gameStats.daysSpent;
		}
	}
}
namespace Entiffo.UEXPBank.Patches
{
	[HarmonyPatch(typeof(GameNetworkManager))]
	internal static class GameNetworkManagerPatches
	{
		[HarmonyPatch("ResetSavedGameValues")]
		[HarmonyPostfix]
		private static void ResetSavedGameValues_Postfix()
		{
			if (LoanService.IsHost())
			{
				SaveState.Clear();
				LoanService.State.ResetToBaseline(Plugin.Config.BaselineRate.Value);
				LoanService.MarkHydrated();
				LoanNetwork.PushState();
				Plugin.DebugLog("Cleared UEXP_Bank save keys after ResetSavedGameValues.");
			}
		}

		[HarmonyPatch("SaveGameValues")]
		[HarmonyPostfix]
		private static void SaveGameValues_Postfix()
		{
			if (LoanService.IsHost() && !((Object)(object)StartOfRound.Instance == (Object)null) && StartOfRound.Instance.inShipPhase)
			{
				LoanService.PersistToDisk();
			}
		}
	}
	[HarmonyPatch(typeof(StartOfRound))]
	internal static class StartOfRoundPatches
	{
		[HarmonyPatch("SetTimeAndPlanetToSavedSettings")]
		[HarmonyPostfix]
		private static void SetTimeAndPlanetToSavedSettings_Postfix()
		{
			LoanService.LoadFromSave();
			FeeScheduler.ResetDayTracking();
		}

		[HarmonyPatch("OnClientConnect")]
		[HarmonyPostfix]
		private static void OnClientConnect_Postfix(ulong clientId)
		{
			LoanService.RepublishState($"OnClientConnect:{clientId}");
		}

		[HarmonyPatch("OnPlayerConnectedClientRpc")]
		[HarmonyPostfix]
		private static void OnPlayerConnectedClientRpc_Postfix()
		{
			LoanService.RepublishState("OnPlayerConnectedClientRpc");
		}

		[HarmonyPatch("ResetShip")]
		[HarmonyPostfix]
		private static void ResetShip_Postfix()
		{
			LoanService.ClearAll("ResetShip");
			FeeScheduler.ResetDayTracking();
		}

		[HarmonyPatch("FirePlayersAfterDeadlineClientRpc")]
		[HarmonyPostfix]
		private static void FirePlayersAfterDeadlineClientRpc_Postfix()
		{
			if (LoanService.IsHost())
			{
				LoanService.ClearAll("eject");
				FeeScheduler.ResetDayTracking();
			}
		}

		[HarmonyPatch("ShipLeaveAutomatically")]
		[HarmonyPrefix]
		private static void ShipLeaveAutomatically_Prefix()
		{
			if ((Object)(object)TimeOfDay.Instance != (Object)null)
			{
				FeeScheduler.OnTimeOfDayUpdate(TimeOfDay.Instance);
			}
		}
	}
	[HarmonyPatch(typeof(TimeOfDay))]
	internal static class TimeOfDayPatches
	{
		[HarmonyPatch("Update")]
		[HarmonyPostfix]
		private static void Update_Postfix(TimeOfDay __instance)
		{
			FeeScheduler.OnTimeOfDayUpdate(__instance);
		}

		[HarmonyPatch("SetNewProfitQuota")]
		[HarmonyPostfix]
		private static void SetNewProfitQuota_Postfix()
		{
			if (LoanService.IsHost())
			{
				LoanService.SyncOnly();
				Plugin.DebugLog($"New quota; limit now {LoanService.CurrentLimit()}, principal {LoanService.State.Principal}");
			}
		}
	}
}
namespace Entiffo.UEXPBank.Network
{
	public static class LoanNetwork
	{
		private static LNetworkVariable<string>? _snapshot;

		private static LNetworkMessage<string>? _request;

		private static LNetworkMessage<string>? _toast;

		public static void Initialize()
		{
			_snapshot = LNetworkVariable<string>.Connect("UEXP_BankState", (string)null, (LNetworkVariableWritePerms)0, (Action<string, string>)OnSnapshotChanged);
			_request = LNetworkMessage<string>.Connect("UEXP_BankRequest", (Action<string, ulong>)OnServerRequest, (Action<string>)OnClientResult, (Action<string, ulong>)null);
			_toast = LNetworkMessage<string>.Connect("UEXP_BankToast", (Action<string, ulong>)null, (Action<string>)ChatToast.ShowLocal, (Action<string, ulong>)null);
		}

		public static void PushState()
		{
			if (_snapshot != null && LoanService.IsHost())
			{
				_snapshot.Value = Encode(LoanService.State);
			}
		}

		public static void TryPullSnapshot()
		{
			if (LoanService.IsHost() || _snapshot == null)
			{
				return;
			}
			try
			{
				string value = _snapshot.Value;
				if (!string.IsNullOrEmpty(value))
				{
					DecodeInto(value, LoanService.State);
				}
			}
			catch (Exception ex)
			{
				Plugin.DebugLog("TryPullSnapshot: " + ex.Message);
			}
		}

		public static void BroadcastToast(string message)
		{
			if (!LoanService.IsHost())
			{
				ChatToast.ShowLocal(message);
				return;
			}
			ChatToast.ShowLocal(message);
			_toast?.SendOtherClients(message);
		}

		public static string ExecuteOrRequest(string action)
		{
			if (LoanService.IsHost())
			{
				return RunAction(action);
			}
			_request?.SendServer(action);
			return TerminalDisplay.Header() + "Request sent to host.\nWaiting for confirmation...\n\n";
		}

		private static void OnServerRequest(string action, ulong clientId)
		{
			string text = RunAction(action);
			_request?.SendClient(text, clientId);
			PushState();
		}

		private static void OnClientResult(string result)
		{
			BankCommands.ShowResultOnTerminal(result);
			Plugin.DebugLog("Received loan result from host.");
		}

		private static string RunAction(string action)
		{
			if (string.IsNullOrWhiteSpace(action))
			{
				return TerminalDisplay.WrapError("Empty request.");
			}
			string[] array = action.Trim().Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
			if (array.Length == 0)
			{
				return TerminalDisplay.Dashboard();
			}
			switch (array[0].ToLowerInvariant())
			{
			case "status":
			case "bank":
				if (LoanService.IsHost())
				{
					LoanService.RefreshFromSave("status-request");
				}
				else
				{
					TryPullSnapshot();
				}
				return TerminalDisplay.Dashboard();
			case "terms":
				return TerminalDisplay.Terms();
			case "borrow":
			case "loan":
			{
				if (array.Length >= 2 && int.TryParse(array[1], out var result3))
				{
					return LoanService.TryBorrow(result3);
				}
				return TerminalDisplay.WrapError("Usage: loan <amount>");
			}
			case "deposit":
			case "dep":
			{
				if (array.Length >= 2 && int.TryParse(array[1], out var result4))
				{
					return LoanService.TryDeposit(result4);
				}
				return TerminalDisplay.WrapError("Usage: deposit <amount>");
			}
			case "withdraw":
			case "wd":
			{
				if (array.Length >= 2 && int.TryParse(array[1], out var result2))
				{
					return LoanService.TryWithdraw(result2);
				}
				return TerminalDisplay.WrapError("Usage: withdraw <amount>");
			}
			case "pay":
			case "repay":
			{
				if (array.Length >= 2 && array[1].Equals("all", StringComparison.OrdinalIgnoreCase))
				{
					return LoanService.TryPay(0, payAll: true);
				}
				if (array.Length >= 2 && int.TryParse(array[1], out var result))
				{
					return LoanService.TryPay(result, payAll: false);
				}
				return TerminalDisplay.WrapError("Usage: pay <n|all>");
			}
			default:
				return TerminalDisplay.WrapError("Unknown bank action.");
			}
		}

		private static void OnSnapshotChanged(string previous, string current)
		{
			if (!LoanService.IsHost() && !string.IsNullOrEmpty(current))
			{
				DecodeInto(current, LoanService.State);
			}
		}

		public static string Encode(LoanState s)
		{
			return string.Join("|", s.Principal, s.Rate, s.DaysElapsed, s.LastFeeDaysSpent, s.PeakPrincipal, s.Active ? 1 : 0, s.InterestCollected ? 1 : 0, s.LockedLimit, s.Deposit, s.LastFeeWasCash ? 1 : 0);
		}

		public static void DecodeInto(string raw, LoanState s)
		{
			string[] array = raw.Split('|');
			if (array.Length >= 6)
			{
				s.Principal = ParseInt(array[0]);
				s.Rate = ParseInt(array[1], Plugin.Config.BaselineRate.Value);
				s.DaysElapsed = ParseInt(array[2]);
				s.LastFeeDaysSpent = ParseInt(array[3], -1);
				s.PeakPrincipal = ParseInt(array[4]);
				s.Active = ParseInt(array[5]) != 0;
				s.InterestCollected = array.Length >= 7 && ParseInt(array[6]) != 0;
				s.LockedLimit = ((array.Length >= 8) ? ParseInt(array[7]) : 0);
				s.Deposit = ((array.Length >= 9) ? ParseInt(array[8]) : 0);
				s.LastFeeWasCash = array.Length >= 10 && ParseInt(array[9]) != 0;
			}
		}

		private static int ParseInt(string s, int fallback = 0)
		{
			if (!int.TryParse(s, out var result))
			{
				return fallback;
			}
			return result;
		}
	}
}
namespace Entiffo.UEXPBank.Loan
{
	public static class DeadlineScale
	{
		public static int DeadlineDays()
		{
			if (TimeOfDay.Instance?.quotaVariables == null)
			{
				return 3;
			}
			return Mathf.Max(1, TimeOfDay.Instance.quotaVariables.deadlineDaysAmount);
		}

		public static int GraceDays()
		{
			return DeadlineDays();
		}

		public static int HikeDay()
		{
			return DeadlineDays() + 4;
		}

		public static int ComputeLimit(int profitQuota, float multiplier)
		{
			return Mathf.FloorToInt((float)profitQuota * multiplier);
		}
	}
	public static class FeeScheduler
	{
		private static float _prevNormalized = -1f;

		private static bool _wasInShipPhase = true;

		public static float FeeHourNormalized(TimeOfDay tod)
		{
			float num = (float)Mathf.Clamp(Plugin.Config.FeeHour.Value, 6, 23) * 60f;
			float num2 = 60f * (float)Mathf.Max(1, tod.numberOfHours);
			return Mathf.Clamp01((num - 360f) / num2);
		}

		public static void OnTimeOfDayUpdate(TimeOfDay tod)
		{
			if (Plugin.IsEnabled && LoanService.IsHost() && !((Object)(object)tod == (Object)null) && !((Object)(object)StartOfRound.Instance == (Object)null))
			{
				bool inShipPhase = StartOfRound.Instance.inShipPhase;
				float normalizedTimeOfDay = tod.normalizedTimeOfDay;
				float num = FeeHourNormalized(tod);
				int num2 = Mathf.Clamp(Plugin.Config.FeeHour.Value, 6, 23);
				if (!inShipPhase && _prevNormalized >= 0f && _prevNormalized < num && normalizedTimeOfDay >= num)
				{
					TryFee($"{num2}:00 clock");
				}
				if (!_wasInShipPhase && inShipPhase && LoanService.State.Principal > 0 && _prevNormalized >= 0f && _prevNormalized < num)
				{
					TryFee("early leave");
				}
				_wasInShipPhase = inShipPhase;
				_prevNormalized = normalizedTimeOfDay;
			}
		}

		public static void ResetDayTracking()
		{
			_prevNormalized = -1f;
		}

		private static void TryFee(string reason)
		{
			int daysSpentKey = 0;
			if (StartOfRound.Instance?.gameStats != null)
			{
				daysSpentKey = StartOfRound.Instance.gameStats.daysSpent;
			}
			LoanService.ProcessDailyFeeTick(daysSpentKey, reason);
		}
	}
	public static class GiftService
	{
		private enum GiftKind
		{
			Item,
			Unlockable
		}

		private struct GiftCandidate
		{
			public GiftKind Kind;

			public int Index;

			public string Name;
		}

		public static string TryGrantLoyaltyGift()
		{
			Terminal terminal = LoanService.GetTerminal();
			if ((Object)(object)terminal == (Object)null || (Object)(object)StartOfRound.Instance == (Object)null)
			{
				return "\nLoyalty gift skipped (no terminal).\n";
			}
			List<GiftCandidate> list = new List<GiftCandidate>();
			if (terminal.buyableItemsList != null)
			{
				for (int i = 0; i < terminal.buyableItemsList.Length; i++)
				{
					Item val = terminal.buyableItemsList[i];
					if (!((Object)(object)val == (Object)null))
					{
						list.Add(new GiftCandidate
						{
							Kind = GiftKind.Item,
							Index = i,
							Name = (string.IsNullOrEmpty(val.itemName) ? $"Item#{i}" : val.itemName)
						});
					}
				}
			}
			List<UnlockableItem> list2 = StartOfRound.Instance.unlockablesList?.unlockables;
			if (list2 != null)
			{
				for (int j = 0; j < list2.Count; j++)
				{
					UnlockableItem val2 = list2[j];
					if (val2 != null && !IsSuitUnlockable(val2) && !val2.hasBeenUnlockedByPlayer && !val2.alreadyUnlocked && (StartOfRound.Instance.SpawnedShipUnlockables == null || !StartOfRound.Instance.SpawnedShipUnlockables.ContainsKey(j)))
					{
						list.Add(new GiftCandidate
						{
							Kind = GiftKind.Unlockable,
							Index = j,
							Name = (string.IsNullOrEmpty(val2.unlockableName) ? $"Unlockable#{j}" : val2.unlockableName)
						});
					}
				}
			}
			if (list.Count == 0)
			{
				ChatToast.Broadcast("Loyalty gift: nothing left to give.");
				return "\nLoyalty gift: catalog empty.\n";
			}
			GiftCandidate giftCandidate = list[Random.Range(0, list.Count)];
			int groupCredits = terminal.groupCredits;
			try
			{
				if (giftCandidate.Kind == GiftKind.Item)
				{
					int num = terminal.numberOfItemsInDropship + 1;
					terminal.BuyItemsServerRpc(new int[1] { giftCandidate.Index }, groupCredits, num);
				}
				else
				{
					StartOfRound.Instance.BuyShipUnlockableServerRpc(giftCandidate.Index, groupCredits);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Gift purchase failed: " + ex.Message));
				return "\nLoyalty gift failed for " + giftCandidate.Name + ".\n";
			}
			ChatToast.Broadcast("Loyalty gift: " + giftCandidate.Name + " (on the house).");
			return "\nLOYALTY GIFT: " + giftCandidate.Name + "\n(Paid by UEXP_Bank — dropship/ship delivery as normal.)\n";
		}

		private static bool IsSuitUnlockable(UnlockableItem u)
		{
			if (u.unlockableType == 0)
			{
				return true;
			}
			if ((Object)(object)u.suitMaterial != (Object)null)
			{
				return true;
			}
			return (u.unlockableName ?? "").IndexOf("suit", StringComparison.OrdinalIgnoreCase) >= 0;
		}
	}
	public static class LoanService
	{
		public static LoanState State { get; } = new LoanState();

		public static bool StateHydrated { get; private set; }

		internal static void MarkHydrated()
		{
			StateHydrated = true;
		}

		public static bool IsHost()
		{
			return NetworkManagerIsServer();
		}

		private static bool NetworkManagerIsServer()
		{
			if ((Object)(object)StartOfRound.Instance != (Object)null)
			{
				return ((NetworkBehaviour)StartOfRound.Instance).IsServer;
			}
			return false;
		}

		public static Terminal? GetTerminal()
		{
			return Object.FindObjectOfType<Terminal>();
		}

		public static int CurrentLimit()
		{
			int num = DeadlineScale.ComputeLimit(((Object)(object)TimeOfDay.Instance != (Object)null) ? TimeOfDay.Instance.profitQuota : 0, Plugin.Config.QuotaMultiplier.Value);
			if (State.Principal > 0)
			{
				if (State.LockedLimit <= 0)
				{
					State.LockedLimit = Mathf.Max(num, State.Principal);
				}
				return State.LockedLimit;
			}
			return num;
		}

		public static int Available()
		{
			return Mathf.Max(0, CurrentLimit() - State.Principal);
		}

		public static int ComputeFee(int principal, int ratePercent)
		{
			if (principal <= 0)
			{
				return 0;
			}
			return Mathf.Max(1, (int)Math.Ceiling((double)principal * ((double)ratePercent / 100.0)));
		}

		public static void LoadFromSave()
		{
			if (IsHost())
			{
				if (string.IsNullOrEmpty(SaveState.CurrentSaveName()))
				{
					Plugin.DebugLog("LoadFromSave deferred — GameNetworkManager has no save file name yet.");
					return;
				}
				SaveState.LoadInto(State, Plugin.Config.BaselineRate.Value);
				StateHydrated = true;
				LoanNetwork.PushState();
				Plugin.DebugLog($"Loaded loan principal={State.Principal} rate={State.Rate} days={State.DaysElapsed} " + $"deposit={State.Deposit}");
			}
		}

		public static void RefreshFromSave(string reason)
		{
			if (IsHost())
			{
				LoadFromSave();
				if (!StateHydrated)
				{
					Plugin.DebugLog("RefreshFromSave still not hydrated (" + reason + ").");
				}
			}
		}

		public static void RepublishState(string reason)
		{
			if (IsHost() && StateHydrated)
			{
				LoanNetwork.PushState();
				Plugin.DebugLog($"Republished bank state ({reason}) deposit={State.Deposit}");
			}
		}

		public static void SyncOnly()
		{
			if (IsHost())
			{
				LoanNetwork.PushState();
			}
		}

		public static void PersistToDisk()
		{
			if (IsHost() && StateHydrated)
			{
				SaveState.Save(State);
				Plugin.DebugLog($"Persisted loan to disk principal={State.Principal} deposit={State.Deposit}");
			}
		}

		public static void SyncAndFlushGameSave()
		{
			if (!IsHost())
			{
				return;
			}
			LoanNetwork.PushState();
			try
			{
				if ((Object)(object)GameNetworkManager.Instance != (Object)null)
				{
					GameNetworkManager.Instance.SaveGame();
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("SaveGame after loan change failed (loan left in memory only): " + ex.Message));
			}
		}

		public static void ClearAll(string reason)
		{
			if (IsHost())
			{
				State.ResetToBaseline(Plugin.Config.BaselineRate.Value);
				MarkHydrated();
				SaveState.Clear();
				LoanNetwork.PushState();
				Plugin.DebugLog("Cleared loan (" + reason + ")");
			}
		}

		public static string TryBorrow(int amount)
		{
			if (!Plugin.IsEnabled)
			{
				return Fail("UEXP_Bank is disabled.");
			}
			if (!IsHost())
			{
				return Fail("Only the host session can process loans. Ask the host / ensure you are host.");
			}
			RefreshFromSave("borrow");
			if (amount < 1)
			{
				return Fail("Borrow amount must be at least 1.");
			}
			Terminal terminal = GetTerminal();
			if ((Object)(object)terminal == (Object)null)
			{
				return Fail("Terminal not found.");
			}
			int num = Available();
			if (amount > num)
			{
				return Fail($"Insufficient credit line. Available: {num}.");
			}
			bool num2 = State.Principal <= 0;
			State.Principal += amount;
			State.PeakPrincipal = Mathf.Max(State.PeakPrincipal, State.Principal);
			State.Active = true;
			if (num2)
			{
				State.DaysElapsed = 0;
				State.LastFeeDaysSpent = -1;
				State.InterestCollected = false;
				State.LastFeeWasCash = false;
				if (State.Rate < 1)
				{
					State.Rate = Plugin.Config.BaselineRate.Value;
				}
				State.PeakPrincipal = State.Principal;
				int num3 = DeadlineScale.ComputeLimit(((Object)(object)TimeOfDay.Instance != (Object)null) ? TimeOfDay.Instance.profitQuota : 0, Plugin.Config.QuotaMultiplier.Value);
				State.LockedLimit = Mathf.Max(num3, State.Principal);
			}
			else if (State.LockedLimit < State.Principal)
			{
				State.LockedLimit = State.Principal;
			}
			terminal.groupCredits += amount;
			terminal.SyncGroupCreditsServerRpc(terminal.groupCredits, terminal.numberOfItemsInDropship);
			SyncAndFlushGameSave();
			ChatToast.Broadcast($"Borrowed {amount}. Principal {State.Principal}. Available {Available()}.");
			return TerminalDisplay.BorrowReceipt(amount);
		}

		public static string TryPay(int amount, bool payAll)
		{
			if (!Plugin.IsEnabled)
			{
				return Fail("UEXP_Bank is disabled.");
			}
			if (!IsHost())
			{
				return Fail("Only the host session can process loans.");
			}
			RefreshFromSave("pay");
			if (State.Principal <= 0)
			{
				return Fail("No outstanding loan.");
			}
			Terminal terminal = GetTerminal();
			if ((Object)(object)terminal == (Object)null)
			{
				return Fail("Terminal not found.");
			}
			if (payAll)
			{
				amount = State.Principal;
			}
			if (amount < 1)
			{
				return Fail("Pay amount must be at least 1.");
			}
			amount = Mathf.Min(amount, State.Principal);
			if (terminal.groupCredits < amount)
			{
				return Fail($"Not enough credits. Need {amount}, have {terminal.groupCredits}.");
			}
			int peakPrincipal = State.PeakPrincipal;
			bool interestCollected = State.InterestCollected;
			int rate = State.Rate;
			int num = amount;
			State.Principal -= amount;
			terminal.groupCredits -= amount;
			terminal.SyncGroupCreditsServerRpc(terminal.groupCredits, terminal.numberOfItemsInDropship);
			string giftNote = "";
			if (State.Principal <= 0)
			{
				int num2 = (interestCollected ? Mathf.Max(1, rate - 1) : Plugin.Config.BaselineRate.Value);
				bool flag = peakPrincipal > Plugin.Config.GiftMinPrincipal.Value && interestCollected;
				State.ClearDebtKeepRate(num2);
				SyncAndFlushGameSave();
				if (interestCollected)
				{
					ChatToast.Broadcast($"Loan paid in full ({num}). Loyalty rate {rate}% -> {num2}%.");
				}
				else
				{
					ChatToast.Broadcast($"Loan paid in full ({num}). Rate reset to {num2}%.");
				}
				if (flag)
				{
					giftNote = GiftService.TryGrantLoyaltyGift();
				}
				return TerminalDisplay.PayReceipt(num, fullyCleared: true, giftNote);
			}
			SyncAndFlushGameSave();
			ChatToast.Broadcast($"Paid {num}. Remaining principal {State.Principal}.");
			return TerminalDisplay.PayReceipt(num, fullyCleared: false, giftNote);
		}

		public static string TryDeposit(int amount)
		{
			if (!Plugin.IsEnabled)
			{
				return Fail("UEXP_Bank is disabled.");
			}
			if (!IsHost())
			{
				return Fail("Only the host session can process deposits.");
			}
			RefreshFromSave("deposit");
			if (amount < 1)
			{
				return Fail("Deposit amount must be at least 1.");
			}
			int value = Plugin.Config.DepositCap.Value;
			int num = Mathf.Max(0, value - State.Deposit);
			if (num <= 0)
			{
				return Fail($"Savings vault full ({value} max). Withdraw some first.");
			}
			Terminal terminal = GetTerminal();
			if ((Object)(object)terminal == (Object)null)
			{
				return Fail("Terminal not found.");
			}
			if (terminal.groupCredits < 1)
			{
				return Fail("No credits on the terminal to deposit.");
			}
			int num2 = num;
			int groupCredits = terminal.groupCredits;
			int num3 = Mathf.Min(new int[3] { amount, num2, groupCredits });
			if (num3 < 1)
			{
				return Fail("Nothing to deposit.");
			}
			terminal.groupCredits -= num3;
			terminal.SyncGroupCreditsServerRpc(terminal.groupCredits, terminal.numberOfItemsInDropship);
			State.Deposit += num3;
			SyncAndFlushGameSave();
			string clampNote = "";
			if (num3 < amount)
			{
				clampNote = ((num3 != num2) ? $" (asked {amount}; only {num3} credits available)" : $" (asked {amount}; vault only had room for {num3})");
			}
			ChatToast.Broadcast($"Deposited {num3}. Vault balance {State.Deposit}/{value}.");
			return TerminalDisplay.DepositReceipt(num3, clampNote);
		}

		public static string TryWithdraw(int amount)
		{
			if (!Plugin.IsEnabled)
			{
				return Fail("UEXP_Bank is disabled.");
			}
			if (!IsHost())
			{
				return Fail("Only the host session can process withdrawals.");
			}
			RefreshFromSave("withdraw");
			if (amount < 1)
			{
				return Fail("Withdraw amount must be at least 1.");
			}
			if (State.Deposit <= 0)
			{
				return Fail("Vault is empty.");
			}
			if (amount > State.Deposit)
			{
				return Fail($"Vault only holds {State.Deposit}.");
			}
			int num = Mathf.Max(0, Plugin.Config.WithdrawFee.Value);
			Terminal terminal = GetTerminal();
			if ((Object)(object)terminal == (Object)null)
			{
				return Fail("Terminal not found.");
			}
			int num2;
			int num3;
			int num4;
			if (State.Deposit >= amount + num)
			{
				num2 = amount;
				num3 = amount + num;
				num4 = 0;
			}
			else
			{
				int num5 = Mathf.Max(0, State.Deposit - amount);
				num4 = num - num5;
				num2 = amount - num4;
				num3 = State.Deposit;
				if (num2 < 1)
				{
					return Fail($"Withdraw fee ({num}) would eat the whole payout. Leave more in the vault or withdraw less.");
				}
			}
			State.Deposit -= num3;
			terminal.groupCredits += num2;
			terminal.SyncGroupCreditsServerRpc(terminal.groupCredits, terminal.numberOfItemsInDropship);
			SyncAndFlushGameSave();
			ChatToast.Broadcast($"Withdrew {num2} to terminal (fee {num} from vault). Vault left {State.Deposit}.");
			return TerminalDisplay.WithdrawReceipt(amount, num, num2, num3, num4);
		}

		public static void ProcessDailyFeeTick(int daysSpentKey, string reason)
		{
			if (!Plugin.IsEnabled || !IsHost() || State.Principal <= 0)
			{
				return;
			}
			if (State.LastFeeDaysSpent == daysSpentKey)
			{
				Plugin.DebugLog($"Fee skip duplicate day key={daysSpentKey} ({reason})");
				return;
			}
			State.LastFeeDaysSpent = daysSpentKey;
			State.DaysElapsed++;
			int num = DeadlineScale.GraceDays();
			int num2 = DeadlineScale.HikeDay();
			if (State.DaysElapsed <= num)
			{
				SyncOnly();
				Plugin.DebugLog($"Grace day {State.DaysElapsed}/{num} ({reason})");
				return;
			}
			if (State.DaysElapsed >= num2)
			{
				int rate = State.Rate;
				State.Rate = Mathf.Min(Plugin.Config.MaxRate.Value, State.Rate + 1);
				if (State.Rate != rate)
				{
					ChatToast.Broadcast($"Delinquent! Rate {rate}% -> {State.Rate}%.");
				}
			}
			int num3 = ComputeFee(State.Principal, State.Rate);
			Terminal terminal = GetTerminal();
			if ((Object)(object)terminal == (Object)null || terminal.groupCredits < num3)
			{
				State.Principal += num3;
				State.PeakPrincipal = Mathf.Max(State.PeakPrincipal, State.Principal);
				if (State.LockedLimit < State.Principal)
				{
					State.LockedLimit = State.Principal;
				}
				State.LastFeeWasCash = false;
				SyncAndFlushGameSave();
				if ((Object)(object)terminal == (Object)null)
				{
					ChatToast.Broadcast($"Terminal missing. Fee {num3} capitalized. Principal now {State.Principal}.");
					Plugin.DebugLog($"Fee capitalize {num3} (no terminal) via {reason}");
				}
				else
				{
					ChatToast.Broadcast($"Insufficient credits. Fee {num3} capitalized. Principal now {State.Principal}.");
					Plugin.DebugLog($"Fee capitalize {num3} via {reason}");
				}
			}
			else
			{
				terminal.groupCredits -= num3;
				terminal.SyncGroupCreditsServerRpc(terminal.groupCredits, terminal.numberOfItemsInDropship);
				State.InterestCollected = true;
				State.LastFeeWasCash = true;
				SyncAndFlushGameSave();
				ChatToast.Broadcast($"Daily fee -{num3} (rate {State.Rate}%). Principal still {State.Principal}.");
				Plugin.DebugLog($"Fee skim {num3} via {reason}");
			}
		}

		private static string Fail(string message)
		{
			return TerminalDisplay.WrapError(message);
		}
	}
	[Serializable]
	public class LoanState
	{
		public int Principal;

		public int Rate = 10;

		public int DaysElapsed;

		public int LastFeeDaysSpent = -1;

		public int PeakPrincipal;

		public bool Active;

		public bool InterestCollected;

		public bool LastFeeWasCash;

		public int LockedLimit;

		public int Deposit;

		public void ResetToBaseline(int baselineRate)
		{
			Principal = 0;
			Rate = baselineRate;
			DaysElapsed = 0;
			LastFeeDaysSpent = -1;
			PeakPrincipal = 0;
			Active = false;
			InterestCollected = false;
			LastFeeWasCash = false;
			LockedLimit = 0;
			Deposit = 0;
		}

		public void ClearDebtKeepRate(int nextRate)
		{
			Principal = 0;
			Rate = Math.Max(1, nextRate);
			DaysElapsed = 0;
			LastFeeDaysSpent = -1;
			PeakPrincipal = 0;
			Active = false;
			InterestCollected = false;
			LastFeeWasCash = false;
			LockedLimit = 0;
		}
	}
}
namespace Entiffo.UEXPBank.Config
{
	public class ModConfig : SyncedConfig2<ModConfig>
	{
		public ConfigEntry<bool> DebugLogging { get; private set; }

		[field: SyncedEntryField]
		public SyncedEntry<bool> Enabled { get; private set; }

		[field: SyncedEntryField]
		public SyncedEntry<float> QuotaMultiplier { get; private set; }

		[field: SyncedEntryField]
		public SyncedEntry<int> BaselineRate { get; private set; }

		[field: SyncedEntryField]
		public SyncedEntry<int> MaxRate { get; private set; }

		[field: SyncedEntryField]
		public SyncedEntry<int> GiftMinPrincipal { get; private set; }

		[field: SyncedEntryField]
		public SyncedEntry<int> FeeHour { get; private set; }

		[field: SyncedEntryField]
		public SyncedEntry<int> DepositCap { get; private set; }

		[field: SyncedEntryField]
		public SyncedEntry<int> WithdrawFee { get; private set; }

		public ModConfig(ConfigFile cfg)
			: base("Entiffo.UEXP_Bank")
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: Expected O, but got Unknown
			//IL_0032: Expected O, but got Unknown
			//IL_0043: Unknown result type (might be due to invalid IL or missing references)
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: Expected O, but got Unknown
			//IL_0062: Expected O, but got Unknown
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			//IL_008f: Expected O, but got Unknown
			//IL_008f: Expected O, but got Unknown
			//IL_00a0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bc: Expected O, but got Unknown
			//IL_00bc: Expected O, but got Unknown
			//IL_00cd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ec: Expected O, but got Unknown
			//IL_00ec: Expected O, but got Unknown
			//IL_00fd: Unknown result type (might be due to invalid IL or missing references)
			//IL_010f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0119: Expected O, but got Unknown
			//IL_0119: Expected O, but got Unknown
			//IL_012a: Unknown result type (might be due to invalid IL or missing references)
			//IL_013f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0149: Expected O, but got Unknown
			//IL_0149: Expected O, but got Unknown
			//IL_015a: Unknown result type (might be due to invalid IL or missing references)
			//IL_016c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0176: Expected O, but got Unknown
			//IL_0176: Expected O, but got Unknown
			//IL_0187: Unknown result type (might be due to invalid IL or missing references)
			//IL_0198: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a2: Expected O, but got Unknown
			//IL_01a2: Expected O, but got Unknown
			Enabled = SyncedBindingExtensions.BindSyncedEntry<bool>(cfg, new ConfigDefinition("General", "Enabled"), true, new ConfigDescription("Master toggle for UEXP_Bank.", (AcceptableValueBase)null, Array.Empty<object>()));
			QuotaMultiplier = SyncedBindingExtensions.BindSyncedEntry<float>(cfg, new ConfigDefinition("Loan", "QuotaMultiplier"), 2.5f, new ConfigDescription("Loan limit = floor(profitQuota * this).", (AcceptableValueBase)null, Array.Empty<object>()));
			BaselineRate = SyncedBindingExtensions.BindSyncedEntry<int>(cfg, new ConfigDefinition("Loan", "BaselineRate"), 10, new ConfigDescription("Daily interest rate percent after full clear / new loan.", (AcceptableValueBase)null, Array.Empty<object>()));
			MaxRate = SyncedBindingExtensions.BindSyncedEntry<int>(cfg, new ConfigDefinition("Loan", "MaxRate"), 20, new ConfigDescription("Maximum daily interest rate percent when delinquent.", (AcceptableValueBase)null, Array.Empty<object>()));
			GiftMinPrincipal = SyncedBindingExtensions.BindSyncedEntry<int>(cfg, new ConfigDefinition("Loan", "GiftMinPrincipal"), 500, new ConfigDescription("Peak principal must exceed this (and at least one fee must have been skimmed) to earn a loyalty gift on full repay.", (AcceptableValueBase)null, Array.Empty<object>()));
			FeeHour = SyncedBindingExtensions.BindSyncedEntry<int>(cfg, new ConfigDefinition("Loan", "FeeHour"), 10, new ConfigDescription("In-game clock hour when the daily fee is charged on a moon (effective range 6-23; hours below 6 fire at day-start).", (AcceptableValueBase)null, Array.Empty<object>()));
			DepositCap = SyncedBindingExtensions.BindSyncedEntry<int>(cfg, new ConfigDefinition("Vault", "DepositCap"), 2000, new ConfigDescription("Maximum credits players can deposit for safekeeping.", (AcceptableValueBase)null, Array.Empty<object>()));
			WithdrawFee = SyncedBindingExtensions.BindSyncedEntry<int>(cfg, new ConfigDefinition("Vault", "WithdrawFee"), 50, new ConfigDescription("Credits taken from the vault on each withdraw, on top of the amount you take. If the vault can't cover the fee, the shortfall comes from your payout.", (AcceptableValueBase)null, Array.Empty<object>()));
			DebugLogging = cfg.Bind<bool>(new ConfigDefinition("General", "DebugLogging"), false, new ConfigDescription("Extra LogInfo (local only, not synced).", (AcceptableValueBase)null, Array.Empty<object>()));
			ConfigManager.Register<ModConfig>((SyncedConfig2<ModConfig>)(object)this);
			if (FeeHour.Value < 6 || FeeHour.Value > 23)
			{
				Plugin.Log.LogWarning((object)$"FeeHour={FeeHour.Value} is outside 6-23; daily fee clock clamps to that range (hours below 6 = day-start).");
			}
			if (MaxRate.Value < BaselineRate.Value)
			{
				Plugin.Log.LogWarning((object)$"MaxRate={MaxRate.Value} is below BaselineRate={BaselineRate.Value}; delinquency hike can lower the rate.");
			}
		}
	}
}
namespace System.Runtime.CompilerServices
{
	[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
	internal sealed class IgnoresAccessChecksToAttribute : Attribute
	{
		public IgnoresAccessChecksToAttribute(string assemblyName)
		{
		}
	}
}