Decompiled source of Chipmunks Shared Storage v0.1.0

BepInEx/plugins/ChipmunksSharedStorage/SharedStorageLedger.dll

Decompiled 2 weeks ago
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using BepInEx;
using HarmonyLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[BepInPlugin("local.chipmunks.storage.ledger", "Chipmunks Shared Storage", "0.1.0")]
public sealed class SharedStorageLedger : BaseUnityPlugin
{
	private sealed class Scope
	{
		public Container container;

		public string id;

		public Dictionary<string, int> before;
	}

	[HarmonyPatch]
	private static class Transfers
	{
		private static IEnumerable<MethodBase> TargetMethods()
		{
			try
			{
				string[] array = new string[2] { "OnSelectedItem", "OnDropOutside" };
				foreach (string method in array)
				{
					yield return AccessTools.Method(typeof(InventoryGui), method, (Type[])null, (Type[])null);
				}
			}
			finally
			{
			}
			try
			{
				string[] array2 = new string[2] { "RPC_TakeAllResponse", "RPC_StackResponse" };
				foreach (string method2 in array2)
				{
					yield return AccessTools.Method(typeof(Container), method2, (Type[])null, (Type[])null);
				}
			}
			finally
			{
			}
		}

		[HarmonyPrefix]
		private static void Prefix(object __instance, out Scope __state)
		{
			__state = null;
			try
			{
				Container val = (Container)((__instance is Container) ? __instance : null);
				if ((Object)(object)val == (Object)null)
				{
					object? value = AccessTools.Field(typeof(InventoryGui), "m_currentContainer").GetValue(__instance);
					val = (Container)((value is Container) ? value : null);
				}
				__state = Begin(val);
			}
			catch (Exception ex)
			{
				if ((Object)(object)self != (Object)null)
				{
					self.Fail(ex);
				}
			}
		}

		[HarmonyFinalizer]
		private static void Finalizer(Scope __state)
		{
			if (__state == null)
			{
				return;
			}
			try
			{
				self.Record(__state);
			}
			catch (Exception ex)
			{
				if ((Object)(object)self != (Object)null)
				{
					self.Fail(ex);
				}
			}
			finally
			{
				depth--;
			}
		}
	}

	private const string Hello = "ChipmunksLedgerHello1";

	private const string Welcome = "ChipmunksLedgerWelcome1";

	private const string Event = "ChipmunksLedgerEvent1";

	private const string Ack = "ChipmunksLedgerAck1";

	private static SharedStorageLedger self;

	private Harmony patches;

	private ZRoutedRpc registered;

	private float nextTick;

	private float nextHello;

	private float nextWarning;

	private long serverPeer;

	private string world = "";

	private string pendingPath = "";

	private string journalPath = "";

	private bool ready;

	private bool serverReady;

	private bool dirty;

	private readonly List<string> pending = new List<string>();

	private LedgerJournal journal;

	private readonly Dictionary<long, float> helloTimes = new Dictionary<long, float>();

	private readonly Dictionary<long, float> rateStart = new Dictionary<long, float>();

	private readonly Dictionary<long, int> rateCount = new Dictionary<long, int>();

	private static int depth;

	private readonly string directory = Path.Combine(Paths.ConfigPath, "ChipmunksSharedStorage");

	private bool IsServer => (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer();

	private void Awake()
	{
		//IL_0019: Unknown result type (might be due to invalid IL or missing references)
		//IL_0023: Expected O, but got Unknown
		self = this;
		Directory.CreateDirectory(directory);
		patches = new Harmony("local.chipmunks.storage.ledger");
		patches.PatchAll(typeof(SharedStorageLedger).Assembly);
		((BaseUnityPlugin)this).Logger.LogInfo((object)"Shared storage hooks installed; waiting for server connection");
	}

	private void OnDestroy()
	{
		try
		{
			if (dirty && pendingPath != "")
			{
				SavePending();
			}
		}
		catch (Exception ex)
		{
			Fail(ex);
		}
		if (patches != null)
		{
			patches.UnpatchSelf();
		}
		self = null;
	}

	private void Fail(Exception ex)
	{
		if (!(Time.unscaledTime < nextWarning))
		{
			nextWarning = Time.unscaledTime + 30f;
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Shared storage telemetry: " + ex.GetType().Name + ": " + ex.Message));
		}
	}

	private void Update()
	{
		if (Time.unscaledTime < nextTick)
		{
			return;
		}
		nextTick = Time.unscaledTime + 2f;
		try
		{
			if (dirty && pendingPath != "")
			{
				SavePending();
			}
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance == null || (Object)(object)ZNet.instance == (Object)null)
			{
				ready = false;
				return;
			}
			if (!object.ReferenceEquals(registered, instance))
			{
				registered = instance;
				ready = false;
				serverReady = false;
				serverPeer = 0L;
				world = "";
				nextHello = 0f;
				journal = null;
				helloTimes.Clear();
				rateStart.Clear();
				rateCount.Clear();
				instance.Register<string>("ChipmunksLedgerHello1", (Action<long, string>)OnHello);
				instance.Register<string>("ChipmunksLedgerWelcome1", (Action<long, string>)OnWelcome);
				instance.Register<string>("ChipmunksLedgerEvent1", (Action<long, string>)OnEvent);
				instance.Register<string>("ChipmunksLedgerAck1", (Action<long, string>)OnAck);
			}
			if (IsServer)
			{
				if (!serverReady)
				{
					OpenJournal();
				}
				return;
			}
			ZNetPeer val = ZNet.instance.GetServerPeer();
			if (val == null || val.m_uid == 0 || (Object)(object)Player.m_localPlayer == (Object)null)
			{
				ready = false;
				return;
			}
			if (val.m_uid != serverPeer)
			{
				serverPeer = val.m_uid;
				ready = false;
				world = "";
				nextHello = 0f;
			}
			if (Time.unscaledTime >= nextHello)
			{
				nextHello = Time.unscaledTime + 60f;
				instance.InvokeRoutedRPC(serverPeer, "ChipmunksLedgerHello1", new object[1] { "0.1.0" });
			}
			if (ready)
			{
				for (int i = 0; i < Math.Min(20, pending.Count); i++)
				{
					instance.InvokeRoutedRPC(serverPeer, "ChipmunksLedgerEvent1", new object[1] { pending[i] });
				}
			}
		}
		catch (Exception ex)
		{
			if (IsServer)
			{
				nextTick = Time.unscaledTime + 60f;
			}
			Fail(ex);
		}
	}

	private void OpenJournal()
	{
		journalPath = Path.Combine(directory, "events.tsv");
		journal = new LedgerJournal(journalPath);
		serverReady = true;
		((BaseUnityPlugin)this).Logger.LogInfo((object)("CHIPMUNKS_LEDGER_READY\t0.1.0\t" + ZNet.instance.GetWorldUID()));
	}

	private string Steam(long sender)
	{
		ZNetPeer peer = ZNet.instance.GetPeer(sender);
		return (peer == null || peer.m_socket == null) ? null : SteamIdentity.Extract(peer.m_socket.GetHostName());
	}

	private void OnHello(long sender, string version)
	{
		try
		{
			if (IsServer && serverReady && !(version != "0.1.0"))
			{
				string text = Steam(sender);
				if (text != null && (!helloTimes.TryGetValue(sender, out var value) || !(Time.unscaledTime - value < 20f)))
				{
					helloTimes[sender] = Time.unscaledTime;
					ZRoutedRpc.instance.InvokeRoutedRPC(sender, "ChipmunksLedgerWelcome1", new object[1] { ZNet.instance.GetWorldUID().ToString() });
					((BaseUnityPlugin)this).Logger.LogInfo((object)("CHIPMUNKS_LEDGER_CLIENT\t" + text + "\t0.1.0"));
				}
			}
		}
		catch (Exception ex)
		{
			Fail(ex);
		}
	}

	private void OnWelcome(long sender, string worldId)
	{
		try
		{
			if (IsServer || sender != serverPeer || !long.TryParse(worldId, out var _))
			{
				return;
			}
			if (world != worldId)
			{
				string text = Path.Combine(directory, "pending-" + worldId + ".tsv");
				List<string> list = new List<string>();
				if (File.Exists(text))
				{
					if (new FileInfo(text).Length > 4194304)
					{
						throw new IOException("Pending ledger too large");
					}
					string[] array = File.ReadAllLines(text);
					foreach (string text2 in array)
					{
						if (!LedgerProtocol.Valid(text2))
						{
							throw new IOException("Invalid pending ledger; preserve for diagnosis");
						}
						if (list.Count >= 10000)
						{
							throw new IOException("Pending queue exceeds limit");
						}
						list.Add(text2);
					}
				}
				pending.Clear();
				pending.AddRange(list);
				pendingPath = text;
				world = worldId;
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Shared storage connected; pending events=" + pending.Count));
			}
			ready = true;
		}
		catch (Exception ex)
		{
			ready = false;
			Fail(ex);
		}
	}

	private void OnEvent(long sender, string text)
	{
		try
		{
			if (!IsServer || !serverReady || !LedgerProtocol.Valid(text) || !helloTimes.ContainsKey(sender))
			{
				return;
			}
			string text2 = Steam(sender);
			if (text2 == null)
			{
				return;
			}
			if (!rateStart.TryGetValue(sender, out var value) || Time.unscaledTime - value >= 60f)
			{
				rateStart[sender] = Time.unscaledTime;
				rateCount[sender] = 0;
			}
			if (++rateCount[sender] <= 1200)
			{
				string[] array = text.Split(new char[1] { '\t' });
				long num = long.Parse(array[1]);
				if (num <= LedgerProtocol.Now() + 300)
				{
					journal.Append(ZNet.instance.GetWorldUID(), text2, text);
					ZRoutedRpc.instance.InvokeRoutedRPC(sender, "ChipmunksLedgerAck1", new object[1] { array[0] });
				}
			}
		}
		catch (Exception ex)
		{
			Fail(ex);
		}
	}

	private void SavePending()
	{
		string text = pendingPath + ".tmp";
		File.WriteAllLines(text, pending.ToArray());
		if (File.Exists(pendingPath))
		{
			File.Replace(text, pendingPath, null);
		}
		else
		{
			File.Move(text, pendingPath);
		}
		dirty = false;
	}

	private void OnAck(long sender, string id)
	{
		if (!IsServer && ready && sender == serverPeer && LedgerProtocol.Id.IsMatch(id ?? ""))
		{
			int num = pending.FindIndex((string row) => row.StartsWith(id + "\t", StringComparison.Ordinal));
			if (num >= 0)
			{
				pending.RemoveAt(num);
				dirty = true;
			}
		}
	}

	private void Record(Scope scope)
	{
		int count = pending.Count;
		foreach (KeyValuePair<string, int> item in LedgerProtocol.Difference(scope.before, Count(scope.container.GetInventory())))
		{
			if (item.Value != 0 && LedgerProtocol.Item.IsMatch(item.Key))
			{
				if (pending.Count >= 10000)
				{
					throw new IOException("Pending queue full; collection incomplete");
				}
				pending.Add(LedgerProtocol.Encode(Guid.NewGuid().ToString("N"), LedgerProtocol.Now(), scope.id, item.Key, item.Value));
			}
		}
		if (pending.Count != count)
		{
			SavePending();
		}
	}

	private static Dictionary<string, int> Count(Inventory inventory)
	{
		Dictionary<string, int> dictionary = new Dictionary<string, int>();
		foreach (ItemData allItem in inventory.GetAllItems())
		{
			if (!((Object)(object)allItem.m_dropPrefab == (Object)null) && allItem.m_stack > 0)
			{
				string key = ((Object)allItem.m_dropPrefab).name.Replace("(Clone)", "");
				dictionary.TryGetValue(key, out var value);
				dictionary[key] = value + allItem.m_stack;
			}
		}
		return dictionary;
	}

	private static Scope Begin(Container container)
	{
		if ((Object)(object)self == (Object)null || self.IsServer || !self.ready || (Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)container == (Object)null || depth != 0)
		{
			return null;
		}
		if ((Object)(object)((Component)container).GetComponent<TombStone>() != (Object)null || (Object)(object)((Component)container).GetComponentInParent<TombStone>() != (Object)null)
		{
			return null;
		}
		ZNetView component = ((Component)container).GetComponent<ZNetView>();
		if ((Object)(object)component == (Object)null || component.GetZDO() == null)
		{
			return null;
		}
		Scope scope = new Scope();
		scope.container = container;
		scope.id = ((object)Unsafe.As<ZDOID, ZDOID>(ref component.GetZDO().m_uid)/*cast due to .constrained prefix*/).ToString();
		scope.before = Count(container.GetInventory());
		Scope scope2 = scope;
		if (!LedgerProtocol.ContainerId.IsMatch(scope2.id))
		{
			return null;
		}
		depth++;
		return scope2;
	}
}
public static class LedgerProtocol
{
	public const int MaxPending = 10000;

	public static readonly Regex Item = new Regex("\\A[A-Za-z0-9_]{1,100}\\z");

	public static readonly Regex Id = new Regex("\\A[a-f0-9]{32}\\z");

	public static readonly Regex ContainerId = new Regex("\\A-?[0-9]{1,20}:[0-9]{1,20}\\z");

	public static long Now()
	{
		return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
	}

	public static string Encode(string id, long stamp, string container, string item, int delta)
	{
		return id + "\t" + stamp.ToString(CultureInfo.InvariantCulture) + "\t" + container + "\t" + item + "\t" + delta.ToString(CultureInfo.InvariantCulture);
	}

	public static bool Valid(string text)
	{
		if (text == null || text.Length > 256)
		{
			return false;
		}
		string[] array = text.Split(new char[1] { '\t' });
		long result;
		int result2;
		return array.Length == 5 && Id.IsMatch(array[0]) && long.TryParse(array[1], out result) && result > 0 && ContainerId.IsMatch(array[2]) && Item.IsMatch(array[3]) && int.TryParse(array[4], out result2) && result2 != 0 && result2 >= -10000000 && result2 <= 10000000;
	}

	public static Dictionary<string, int> Difference(Dictionary<string, int> before, Dictionary<string, int> after)
	{
		Dictionary<string, int> dictionary = new Dictionary<string, int>();
		foreach (string key in before.Keys)
		{
			dictionary[key] = -before[key];
		}
		foreach (string key2 in after.Keys)
		{
			dictionary.TryGetValue(key2, out var value);
			dictionary[key2] = value + after[key2];
		}
		return dictionary;
	}
}
public sealed class LedgerJournal
{
	private const int MaxBytes = 67108864;

	private readonly string path;

	private readonly HashSet<string> seen = new HashSet<string>();

	private static bool Sid(string value)
	{
		return Regex.IsMatch(value, "\\A[0-9]{17}\\z");
	}

	public LedgerJournal(string path)
	{
		this.path = path;
		if (!File.Exists(path))
		{
			File.WriteAllText(path, "");
		}
		if (new FileInfo(path).Length > 67108864)
		{
			throw new IOException("Ledger exceeds 64 MiB; archive before continuing");
		}
		using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
		{
			long num = fileStream.Length;
			if (num > 0)
			{
				fileStream.Position = num - 1;
				if (fileStream.ReadByte() != 10)
				{
					File.Copy(path, path + ".partial-" + DateTime.UtcNow.Ticks);
					while (num > 0)
					{
						num = (fileStream.Position = num - 1);
						if (fileStream.ReadByte() == 10)
						{
							num++;
							break;
						}
					}
					fileStream.SetLength(num);
					fileStream.Flush(flushToDisk: true);
				}
			}
		}
		foreach (string item in File.ReadLines(path))
		{
			string[] array = item.Split(new char[1] { '\t' });
			if (array.Length != 7 || !long.TryParse(array[0], out var _) || !Sid(array[1]) || !LedgerProtocol.Valid(string.Join("\t", array, 2, 5)))
			{
				throw new IOException("Invalid journal row; preserve for diagnosis");
			}
			seen.Add(array[0] + ":" + array[1] + ":" + array[2]);
		}
	}

	public bool Append(long world, string sid, string text)
	{
		if (!Sid(sid) || !LedgerProtocol.Valid(text))
		{
			throw new ArgumentException("Invalid ledger event");
		}
		string item = world + ":" + sid + ":" + text.Split(new char[1] { '\t' })[0];
		if (seen.Contains(item))
		{
			return false;
		}
		byte[] bytes = Encoding.UTF8.GetBytes(world + "\t" + sid + "\t" + text + "\n");
		using (FileStream fileStream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read))
		{
			if (fileStream.Length + bytes.Length > 67108864)
			{
				throw new IOException("Ledger full; events retained on client");
			}
			fileStream.Write(bytes, 0, bytes.Length);
			fileStream.Flush(flushToDisk: true);
		}
		seen.Add(item);
		return true;
	}
}
internal static class SteamIdentity
{
	private static readonly Regex HostId = new Regex("^(?:Steam_)?(\\d{17})$", RegexOptions.CultureInvariant);

	internal static string Extract(string host)
	{
		Match match = HostId.Match(host ?? "");
		return match.Success ? match.Groups[1].Value : null;
	}
}