Decompiled source of RecipeSync v1.0.0

RecipeSync.dll

Decompiled 2 weeks ago
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using UnityEngine;

[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyDescription("Keeps every player's Valheim recipe log in sync, including players who are offline.")]
[assembly: AssemblyProduct("RecipeSync")]
[assembly: AssemblyMetadata("AI_Assisted_Creation", "This assembly was fully created with the assistance of Generative AI (code, documentation and icon), directed by the mod author.")]
[assembly: AssemblyTitle("RecipeSync")]
[assembly: AssemblyMetadata("AI_Model_Vendor", "Anthropic")]
[assembly: AssemblyMetadata("AI_Model", "Claude Opus 5.5")]
[assembly: CompilationRelaxations(8)]
[assembly: AssemblyVersion("1.0.0.0")]
namespace RecipeSync;

internal sealed class LogData
{
	public readonly HashSet<string> Materials = new HashSet<string>(StringComparer.Ordinal);

	public readonly HashSet<string> Recipes = new HashSet<string>(StringComparer.Ordinal);

	public readonly Dictionary<string, int> Stations = new Dictionary<string, int>(StringComparer.Ordinal);

	public int Count => Materials.Count + Recipes.Count + Stations.Count;

	public bool IsEmpty => Count == 0;

	public void Clear()
	{
		Materials.Clear();
		Recipes.Clear();
		Stations.Clear();
	}

	public static bool IsValidName(string name)
	{
		return !string.IsNullOrEmpty(name);
	}

	public bool AddMaterial(string name)
	{
		if (IsValidName(name))
		{
			return Materials.Add(name);
		}
		return false;
	}

	public bool AddRecipe(string name)
	{
		if (IsValidName(name))
		{
			return Recipes.Add(name);
		}
		return false;
	}

	public bool AddStation(string name, int level)
	{
		if (!IsValidName(name))
		{
			return false;
		}
		if (Stations.TryGetValue(name, out var value) && value >= level)
		{
			return false;
		}
		Stations[name] = level;
		return true;
	}

	public int MergeFrom(LogData other, LogData added)
	{
		if (other == null)
		{
			return 0;
		}
		int num = 0;
		foreach (string material in other.Materials)
		{
			if (AddMaterial(material))
			{
				num++;
				added?.AddMaterial(material);
			}
		}
		foreach (string recipe in other.Recipes)
		{
			if (AddRecipe(recipe))
			{
				num++;
				added?.AddRecipe(recipe);
			}
		}
		foreach (KeyValuePair<string, int> station in other.Stations)
		{
			if (AddStation(station.Key, station.Value))
			{
				num++;
				added?.AddStation(station.Key, station.Value);
			}
		}
		return num;
	}

	public LogData Except(LogData other)
	{
		LogData logData = new LogData();
		foreach (string material in Materials)
		{
			if (other == null || !other.Materials.Contains(material))
			{
				logData.Materials.Add(material);
			}
		}
		foreach (string recipe in Recipes)
		{
			if (other == null || !other.Recipes.Contains(recipe))
			{
				logData.Recipes.Add(recipe);
			}
		}
		foreach (KeyValuePair<string, int> station in Stations)
		{
			if (other == null || !other.Stations.TryGetValue(station.Key, out var value) || value < station.Value)
			{
				logData.Stations[station.Key] = station.Value;
			}
		}
		return logData;
	}

	public LogData Copy()
	{
		LogData logData = new LogData();
		logData.MergeFrom(this, null);
		return logData;
	}
}
internal sealed class PlayerAccess
{
	private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

	private readonly FieldInfo _materials;

	private readonly FieldInfo _recipes;

	private readonly FieldInfo _stations;

	private readonly MethodInfo _updateKnownRecipesList;

	private readonly MethodInfo _updateAvailablePiecesList;

	private readonly MethodInfo _updateEvents;

	private bool _refreshErrorLogged;

	public readonly bool IsValid;

	public PlayerAccess()
	{
		Type typeFromHandle = typeof(Player);
		_materials = FindField(typeFromHandle, "m_knownMaterial", typeof(HashSet<string>));
		_recipes = FindField(typeFromHandle, "m_knownRecipes", typeof(HashSet<string>));
		_stations = FindField(typeFromHandle, "m_knownStations", typeof(Dictionary<string, int>));
		_updateKnownRecipesList = FindMethod(typeFromHandle, "UpdateKnownRecipesList");
		_updateAvailablePiecesList = FindMethod(typeFromHandle, "UpdateAvailablePiecesList");
		_updateEvents = FindMethod(typeFromHandle, "UpdateEvents");
		IsValid = _materials != null && _recipes != null && _stations != null;
	}

	public HashSet<string> Materials(Player p)
	{
		return (HashSet<string>)_materials.GetValue(p);
	}

	public HashSet<string> Recipes(Player p)
	{
		return (HashSet<string>)_recipes.GetValue(p);
	}

	public Dictionary<string, int> Stations(Player p)
	{
		return (Dictionary<string, int>)_stations.GetValue(p);
	}

	public void Collect(Player p, LogData known, LogData into, bool materials, bool recipes, bool stations)
	{
		if (materials)
		{
			HashSet<string> hashSet = Materials(p);
			if (hashSet != null)
			{
				foreach (string item in hashSet)
				{
					if (known == null || !known.Materials.Contains(item))
					{
						into.AddMaterial(item);
					}
				}
			}
		}
		if (recipes)
		{
			HashSet<string> hashSet2 = Recipes(p);
			if (hashSet2 != null)
			{
				foreach (string item2 in hashSet2)
				{
					if (known == null || !known.Recipes.Contains(item2))
					{
						into.AddRecipe(item2);
					}
				}
			}
		}
		if (!stations)
		{
			return;
		}
		Dictionary<string, int> dictionary = Stations(p);
		if (dictionary == null)
		{
			return;
		}
		foreach (KeyValuePair<string, int> item3 in dictionary)
		{
			if (known == null || !known.Stations.TryGetValue(item3.Key, out var value) || value < item3.Value)
			{
				into.AddStation(item3.Key, item3.Value);
			}
		}
	}

	public void Refresh(Player p)
	{
		Invoke(_updateKnownRecipesList, p);
		Invoke(_updateAvailablePiecesList, p);
		Invoke(_updateEvents, p);
	}

	public string DescribeMissing()
	{
		List<string> list = new List<string>();
		if (_materials == null)
		{
			list.Add("Player.m_knownMaterial");
		}
		if (_recipes == null)
		{
			list.Add("Player.m_knownRecipes");
		}
		if (_stations == null)
		{
			list.Add("Player.m_knownStations");
		}
		if (_updateKnownRecipesList == null)
		{
			list.Add("Player.UpdateKnownRecipesList()");
		}
		if (_updateAvailablePiecesList == null)
		{
			list.Add("Player.UpdateAvailablePiecesList()");
		}
		if (_updateEvents == null)
		{
			list.Add("Player.UpdateEvents()");
		}
		return string.Join(", ", list.ToArray());
	}

	private void Invoke(MethodInfo method, Player p)
	{
		if (method == null)
		{
			return;
		}
		try
		{
			method.Invoke(p, null);
		}
		catch (Exception ex)
		{
			if (!_refreshErrorLogged)
			{
				_refreshErrorLogged = true;
				Exception ex2 = ((ex is TargetInvocationException && ex.InnerException != null) ? ex.InnerException : ex);
				RecipeSyncPlugin.Log.LogWarning((object)("Player." + method.Name + "() threw after a sync (entries were still added): " + ex2));
			}
		}
	}

	private static FieldInfo FindField(Type t, string name, Type expected)
	{
		FieldInfo field = t.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
		if (!(field != null) || !expected.IsAssignableFrom(field.FieldType))
		{
			return null;
		}
		return field;
	}

	private static MethodInfo FindMethod(Type t, string name)
	{
		return t.GetMethod(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
	}
}
[BepInPlugin("Rock3t.RecipeSync", "RecipeSync", "1.0.0")]
public sealed class RecipeSyncPlugin : BaseUnityPlugin
{
	public const string Guid = "Rock3t.RecipeSync";

	public const string ModName = "RecipeSync";

	public const string ModVersion = "1.0.0";

	private const float TickInterval = 1f;

	internal static ManualLogSource Log;

	private ConfigEntry<bool> _syncMaterials;

	private ConfigEntry<bool> _syncRecipes;

	private ConfigEntry<bool> _syncStations;

	private ConfigEntry<bool> _showNotifications;

	private PlayerAccess _access;

	private string _lastErrorMessage;

	private ZRoutedRpc _registeredRpc;

	private ServerStore _store;

	private readonly LogData _shared = new LogData();

	private readonly Dictionary<long, LogData> _joinSnapshots = new Dictionary<long, LogData>();

	private Player _lastPlayer;

	private bool _announced;

	private bool _serverHasMod;

	private bool _reapply;

	private float _nextTick;

	private void Awake()
	{
		Log = ((BaseUnityPlugin)this).Logger;
		_syncMaterials = ((BaseUnityPlugin)this).Config.Bind<bool>("Sync", "Items", true, "Share which items/materials have been picked up. This is what unlocks most recipes.");
		_syncRecipes = ((BaseUnityPlugin)this).Config.Bind<bool>("Sync", "RecipesAndPieces", true, "Share known crafting recipes and build pieces.");
		_syncStations = ((BaseUnityPlugin)this).Config.Bind<bool>("Sync", "CraftingStations", true, "Share which crafting stations (and upgrade levels) have been discovered. Some recipes only unlock after the required station level has been seen.");
		_showNotifications = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "ShowNotifications", true, "Show a message in the top-left corner when entries from other players are added to your log.");
		EventHandler eventHandler = delegate
		{
			_reapply = true;
		};
		_syncMaterials.SettingChanged += eventHandler;
		_syncRecipes.SettingChanged += eventHandler;
		_syncStations.SettingChanged += eventHandler;
		_access = new PlayerAccess();
		if (!_access.IsValid)
		{
			Log.LogError((object)("This game version's Player class is missing " + _access.DescribeMissing() + ". Player logs won't be synced, but a server will still keep the shared log."));
		}
		Log.LogInfo((object)"RecipeSync 1.0.0 loaded.");
	}

	private void Update()
	{
		try
		{
			Tick();
		}
		catch (Exception e)
		{
			LogErrorOnce("Update", e);
		}
	}

	private void OnApplicationQuit()
	{
		SaveStore();
	}

	private void OnDestroy()
	{
		SaveStore();
	}

	private void Tick()
	{
		ZNet instance = ZNet.instance;
		ZRoutedRpc instance2 = ZRoutedRpc.instance;
		if ((Object)(object)instance == (Object)null || instance2 == null)
		{
			if (_registeredRpc != null)
			{
				EndSession();
			}
			return;
		}
		if (instance2 != _registeredRpc)
		{
			if (_registeredRpc != null)
			{
				EndSession();
			}
			BeginSession(instance2);
		}
		float unscaledTime = Time.unscaledTime;
		if (!(unscaledTime < _nextTick))
		{
			_nextTick = unscaledTime + 1f;
			if (instance.IsServer())
			{
				EnsureStore(instance)?.SaveIfDue(unscaledTime);
			}
			if (!instance.IsDedicated())
			{
				ClientTick();
			}
		}
	}

	private void BeginSession(ZRoutedRpc rpc)
	{
		ResetSessionState();
		rpc.Register<ZPackage>("RecipeSync_Data", (Action<long, ZPackage>)OnData);
		rpc.Register<ZPackage>("RecipeSync_Hello", (Action<long, ZPackage>)OnHello);
		_registeredRpc = rpc;
		Log.LogDebug((object)"Registered sync RPCs for new session.");
	}

	private void EndSession()
	{
		SaveStore();
		ResetSessionState();
	}

	private void ResetSessionState()
	{
		_registeredRpc = null;
		_store = null;
		_shared.Clear();
		_joinSnapshots.Clear();
		_lastPlayer = null;
		_announced = false;
		_serverHasMod = false;
		_reapply = false;
		_nextTick = 0f;
	}

	private void SaveStore()
	{
		if (_store == null)
		{
			return;
		}
		try
		{
			_store.SaveIfDirty();
		}
		catch (Exception e)
		{
			LogErrorOnce("Save", e);
		}
	}

	private ServerStore EnsureStore(ZNet znet)
	{
		if (_store != null || (Object)(object)znet == (Object)null || !znet.IsServer())
		{
			return _store;
		}
		string worldName = znet.GetWorldName();
		if (string.IsNullOrEmpty(worldName))
		{
			return null;
		}
		_store = ServerStore.Load(worldName, znet.GetWorldUID());
		return _store;
	}

	private void ClientTick()
	{
		if (!_access.IsValid)
		{
			return;
		}
		Player localPlayer = Player.m_localPlayer;
		if ((Object)(object)localPlayer == (Object)null)
		{
			return;
		}
		if ((Object)(object)localPlayer != (Object)(object)_lastPlayer)
		{
			_lastPlayer = localPlayer;
			_reapply = true;
		}
		if (_reapply)
		{
			_reapply = false;
			ApplyToPlayer(localPlayer, _shared);
		}
		if (!_announced)
		{
			LogData logData = new LogData();
			_access.Collect(localPlayer, null, logData, _syncMaterials.Value, _syncRecipes.Value, _syncStations.Value);
			_shared.MergeFrom(logData, null);
			SendData(0L, logData, 2, alwaysSendOne: false);
			ZRoutedRpc.instance.InvokeRoutedRPC(0L, "RecipeSync_Hello", new object[1] { Protocol.BuildHello() });
			_announced = true;
			Log.LogInfo((object)("Shared this character's log (" + logData.Count + " entries) and requested the world's shared log."));
		}
		else
		{
			LogData logData2 = new LogData();
			_access.Collect(localPlayer, _shared, logData2, _syncMaterials.Value, _syncRecipes.Value, _syncStations.Value);
			if (!logData2.IsEmpty)
			{
				_shared.MergeFrom(logData2, null);
				SendData(0L, logData2, 0, alwaysSendOne: false);
				Log.LogDebug((object)("Shared " + logData2.Count + " new log entries."));
			}
		}
	}

	private void ReceiveShared(LogData incoming)
	{
		LogData logData = new LogData();
		_shared.MergeFrom(incoming, logData);
		if (!logData.IsEmpty && _access.IsValid)
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer != (Object)null)
			{
				ApplyToPlayer(localPlayer, logData);
			}
		}
	}

	private void ApplyToPlayer(Player player, LogData data)
	{
		if (data.IsEmpty)
		{
			return;
		}
		int num = 0;
		int num2 = 0;
		int num3 = 0;
		if (_syncMaterials.Value)
		{
			HashSet<string> hashSet = _access.Materials(player);
			if (hashSet != null)
			{
				foreach (string material in data.Materials)
				{
					if (hashSet.Add(material))
					{
						num++;
					}
				}
			}
		}
		if (_syncRecipes.Value)
		{
			HashSet<string> hashSet2 = _access.Recipes(player);
			if (hashSet2 != null)
			{
				foreach (string recipe in data.Recipes)
				{
					if (hashSet2.Add(recipe))
					{
						num2++;
					}
				}
			}
		}
		if (_syncStations.Value)
		{
			Dictionary<string, int> dictionary = _access.Stations(player);
			if (dictionary != null)
			{
				foreach (KeyValuePair<string, int> station in data.Stations)
				{
					if (!dictionary.TryGetValue(station.Key, out var value) || value < station.Value)
					{
						dictionary[station.Key] = station.Value;
						num3++;
					}
				}
			}
		}
		if (num + num2 + num3 == 0)
		{
			return;
		}
		_access.Refresh(player);
		string text = Describe(num2, num, num3);
		Log.LogInfo((object)("Added from shared log: " + text + "."));
		if (!_showNotifications.Value)
		{
			return;
		}
		try
		{
			ShowMessage(player, "Shared recipe log: +" + text);
		}
		catch (Exception e)
		{
			LogErrorOnce("Message", e);
		}
	}

	[MethodImpl(MethodImplOptions.NoInlining)]
	private static void ShowMessage(Player player, string text)
	{
		((Character)player).Message((MessageType)1, text, 0, (Sprite)null, false);
	}

	private static string Describe(int recipes, int materials, int stations)
	{
		List<string> list = new List<string>();
		if (recipes > 0)
		{
			list.Add(recipes + ((recipes == 1) ? " recipe/piece" : " recipes/pieces"));
		}
		if (materials > 0)
		{
			list.Add(materials + ((materials == 1) ? " item" : " items"));
		}
		if (stations > 0)
		{
			list.Add(stations + ((stations == 1) ? " station" : " stations"));
		}
		return string.Join(", ", list.ToArray());
	}

	private void SendData(long target, LogData data, byte flags, bool alwaysSendOne)
	{
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance == null)
		{
			return;
		}
		foreach (ZPackage item in Protocol.BuildData(data, flags, alwaysSendOne))
		{
			instance.InvokeRoutedRPC(target, "RecipeSync_Data", new object[1] { item });
		}
	}

	private void OnData(long sender, ZPackage pkg)
	{
		try
		{
			if (!Protocol.TryReadData(pkg, out var flags, out var data))
			{
				return;
			}
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			if (instance.IsServer())
			{
				ServerStore serverStore = EnsureStore(instance);
				if (serverStore != null)
				{
					int num = serverStore.Merge(data, Time.unscaledTime);
					if (num > 0)
					{
						Log.LogDebug((object)("Shared log for this world gained " + num + " entries."));
					}
				}
			}
			if (sender == ZNet.GetUID())
			{
				return;
			}
			if ((flags & 2) != 0)
			{
				if (!_joinSnapshots.TryGetValue(sender, out var value))
				{
					value = new LogData();
					_joinSnapshots[sender] = value;
				}
				value.MergeFrom(data, null);
			}
			if ((flags & 1) != 0)
			{
				_serverHasMod = true;
			}
			if (!instance.IsDedicated())
			{
				ReceiveShared(data);
			}
		}
		catch (Exception e)
		{
			LogErrorOnce("OnData", e);
		}
	}

	private void OnHello(long sender, ZPackage pkg)
	{
		try
		{
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			bool flag = sender == ZNet.GetUID();
			if (_joinSnapshots.TryGetValue(sender, out var value))
			{
				_joinSnapshots.Remove(sender);
			}
			if (instance.IsServer())
			{
				ServerStore serverStore = EnsureStore(instance);
				if (serverStore == null)
				{
					return;
				}
				if (flag)
				{
					if (!instance.IsDedicated())
					{
						ReceiveShared(serverStore.Data);
					}
					return;
				}
				LogData logData = serverStore.Data.Except(value);
				SendData(sender, logData, 1, alwaysSendOne: true);
				Log.LogInfo((object)("Sent " + logData.Count + " shared log entries to joining player " + sender + "."));
			}
			else if (!flag && !_serverHasMod && !instance.IsDedicated() && _announced)
			{
				LogData logData2 = _shared.Except(value);
				if (!logData2.IsEmpty)
				{
					SendData(sender, logData2, 0, alwaysSendOne: false);
				}
			}
		}
		catch (Exception e)
		{
			LogErrorOnce("OnHello", e);
		}
	}

	private void LogErrorOnce(string where, Exception e)
	{
		string text = where + ": " + e.Message;
		if (!(text == _lastErrorMessage))
		{
			_lastErrorMessage = text;
			Log.LogError((object)("Error in " + where + " (sync keeps running): " + e));
		}
	}
}
internal static class Protocol
{
	public const string RpcData = "RecipeSync_Data";

	public const string RpcHello = "RecipeSync_Hello";

	public const int Version = 1;

	public const byte FlagFromServer = 1;

	public const byte FlagJoinSnapshot = 2;

	private const int MaxEntriesPerPackage = 250;

	public static List<ZPackage> BuildData(LogData data, byte flags, bool alwaysSendOne)
	{
		List<ZPackage> list = new List<ZPackage>();
		List<string> list2 = new List<string>();
		List<string> list3 = new List<string>();
		List<KeyValuePair<string, int>> list4 = new List<KeyValuePair<string, int>>();
		int pending = 0;
		foreach (string material in data.Materials)
		{
			list2.Add(material);
			if (++pending >= 250)
			{
				Flush(list, flags, list2, list3, list4, ref pending);
			}
		}
		foreach (string recipe in data.Recipes)
		{
			list3.Add(recipe);
			if (++pending >= 250)
			{
				Flush(list, flags, list2, list3, list4, ref pending);
			}
		}
		foreach (KeyValuePair<string, int> station in data.Stations)
		{
			list4.Add(station);
			if (++pending >= 250)
			{
				Flush(list, flags, list2, list3, list4, ref pending);
			}
		}
		if (pending > 0 || (alwaysSendOne && list.Count == 0))
		{
			Flush(list, flags, list2, list3, list4, ref pending);
		}
		return list;
	}

	private static void Flush(List<ZPackage> packages, byte flags, List<string> materials, List<string> recipes, List<KeyValuePair<string, int>> stations, ref int pending)
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		//IL_0006: Expected O, but got Unknown
		ZPackage val = new ZPackage();
		val.Write(1);
		val.Write(flags);
		val.Write(materials.Count);
		foreach (string material in materials)
		{
			val.Write(material);
		}
		val.Write(recipes.Count);
		foreach (string recipe in recipes)
		{
			val.Write(recipe);
		}
		val.Write(stations.Count);
		foreach (KeyValuePair<string, int> station in stations)
		{
			val.Write(station.Key);
			val.Write(station.Value);
		}
		packages.Add(val);
		materials.Clear();
		recipes.Clear();
		stations.Clear();
		pending = 0;
	}

	public static bool TryReadData(ZPackage pkg, out byte flags, out LogData data)
	{
		flags = 0;
		data = null;
		try
		{
			int num = pkg.ReadInt();
			if (num < 1)
			{
				return false;
			}
			flags = pkg.ReadByte();
			LogData logData = new LogData();
			int num2 = ReadCount(pkg);
			for (int i = 0; i < num2; i++)
			{
				logData.AddMaterial(pkg.ReadString());
			}
			num2 = ReadCount(pkg);
			for (int j = 0; j < num2; j++)
			{
				logData.AddRecipe(pkg.ReadString());
			}
			num2 = ReadCount(pkg);
			for (int k = 0; k < num2; k++)
			{
				string name = pkg.ReadString();
				int level = pkg.ReadInt();
				logData.AddStation(name, level);
			}
			data = logData;
			return true;
		}
		catch (Exception ex)
		{
			RecipeSyncPlugin.Log.LogWarning((object)("Ignoring malformed sync message: " + ex.Message));
			return false;
		}
	}

	public static ZPackage BuildHello()
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		//IL_0006: Expected O, but got Unknown
		ZPackage val = new ZPackage();
		val.Write(1);
		return val;
	}

	private static int ReadCount(ZPackage pkg)
	{
		int num = pkg.ReadInt();
		if (num < 0 || num > pkg.Size() - pkg.GetPos())
		{
			throw new InvalidOperationException("bad entry count " + num);
		}
		return num;
	}
}
internal sealed class ServerStore
{
	private const float SaveDelaySeconds = 5f;

	private const float MaxSaveDelaySeconds = 60f;

	public readonly LogData Data = new LogData();

	private readonly string _path;

	private readonly string _worldLabel;

	private bool _dirty;

	private float _dirtySince;

	private float _lastChange;

	public string Path => _path;

	private ServerStore(string path, string worldLabel)
	{
		_path = path;
		_worldLabel = worldLabel;
	}

	public static ServerStore Load(string worldName, long worldUid)
	{
		string path = System.IO.Path.Combine(Paths.ConfigPath, "RecipeSync");
		string path2 = SanitizeFileName(worldName) + "_" + worldUid + ".txt";
		ServerStore serverStore = new ServerStore(System.IO.Path.Combine(path, path2), worldName + " (" + worldUid + ")");
		serverStore.ReadFile(serverStore._path);
		serverStore.ReadFile(serverStore._path + ".tmp");
		serverStore.ReadFile(serverStore._path + ".bak");
		return serverStore;
	}

	public int Merge(LogData incoming, float now)
	{
		int num = Data.MergeFrom(incoming, null);
		if (num > 0)
		{
			if (!_dirty)
			{
				_dirty = true;
				_dirtySince = now;
			}
			_lastChange = now;
		}
		return num;
	}

	public void SaveIfDue(float now)
	{
		if (_dirty && (now - _lastChange >= 5f || now - _dirtySince >= 60f))
		{
			Save();
		}
	}

	public void SaveIfDirty()
	{
		if (_dirty)
		{
			Save();
		}
	}

	private void Save()
	{
		try
		{
			Directory.CreateDirectory(System.IO.Path.GetDirectoryName(_path));
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append("# RecipeSync shared discovery log for world: ").Append(Escape(_worldLabel)).Append('\n');
			stringBuilder.Append("# Entries are only ever added. M = material/item, R = recipe/build piece, S = crafting station + level.\n");
			foreach (string item in Sorted(Data.Materials))
			{
				stringBuilder.Append("M\t").Append(Escape(item)).Append('\n');
			}
			foreach (string item2 in Sorted(Data.Recipes))
			{
				stringBuilder.Append("R\t").Append(Escape(item2)).Append('\n');
			}
			List<string> list = new List<string>(Data.Stations.Keys);
			list.Sort(StringComparer.Ordinal);
			foreach (string item3 in list)
			{
				stringBuilder.Append("S\t").Append(Escape(item3)).Append('\t')
					.Append(Data.Stations[item3].ToString())
					.Append('\n');
			}
			string text = _path + ".tmp";
			File.WriteAllText(text, stringBuilder.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
			if (File.Exists(_path))
			{
				File.Copy(_path, _path + ".bak", overwrite: true);
				File.Delete(_path);
			}
			File.Move(text, _path);
			_dirty = false;
			RecipeSyncPlugin.Log.LogDebug((object)("Saved shared log (" + Data.Count + " entries) to " + _path));
		}
		catch (Exception ex)
		{
			RecipeSyncPlugin.Log.LogWarning((object)("Could not save shared log to " + _path + ": " + ex.Message));
			_lastChange = (_dirtySince = Time.unscaledTime);
		}
	}

	private void ReadFile(string path)
	{
		try
		{
			if (!File.Exists(path))
			{
				return;
			}
			string[] array = File.ReadAllLines(path, Encoding.UTF8);
			foreach (string text in array)
			{
				string text2 = text.TrimEnd('\r');
				if (text2.Length < 3 || text2[0] == '#' || text2[1] != '\t')
				{
					continue;
				}
				string[] array2 = text2.Split('\t');
				switch (text2[0])
				{
				case 'M':
					Data.AddMaterial(Unescape(array2[1]));
					break;
				case 'R':
					Data.AddRecipe(Unescape(array2[1]));
					break;
				case 'S':
				{
					if (array2.Length >= 3 && int.TryParse(array2[2], out var result))
					{
						Data.AddStation(Unescape(array2[1]), result);
					}
					break;
				}
				}
			}
			RecipeSyncPlugin.Log.LogInfo((object)("Loaded shared log for " + _worldLabel + " from " + path + " (" + Data.Count + " entries total)"));
		}
		catch (Exception ex)
		{
			RecipeSyncPlugin.Log.LogWarning((object)("Could not read " + path + ": " + ex.Message));
		}
	}

	private static List<string> Sorted(HashSet<string> set)
	{
		List<string> list = new List<string>(set);
		list.Sort(StringComparer.Ordinal);
		return list;
	}

	private static string SanitizeFileName(string name)
	{
		if (string.IsNullOrEmpty(name))
		{
			return "world";
		}
		char[] invalidFileNameChars = System.IO.Path.GetInvalidFileNameChars();
		StringBuilder stringBuilder = new StringBuilder(name.Length);
		foreach (char c in name)
		{
			stringBuilder.Append((Array.IndexOf(invalidFileNameChars, c) >= 0) ? '_' : c);
		}
		return stringBuilder.ToString();
	}

	private static string Escape(string s)
	{
		return s.Replace("\\", "\\\\").Replace("\t", "\\t").Replace("\n", "\\n")
			.Replace("\r", "\\r");
	}

	private static string Unescape(string s)
	{
		if (s.IndexOf('\\') < 0)
		{
			return s;
		}
		StringBuilder stringBuilder = new StringBuilder(s.Length);
		for (int i = 0; i < s.Length; i++)
		{
			char c = s[i];
			if (c == '\\' && i + 1 < s.Length)
			{
				char c2 = s[++i];
				switch (c2)
				{
				case 't':
					stringBuilder.Append('\t');
					break;
				case 'n':
					stringBuilder.Append('\n');
					break;
				case 'r':
					stringBuilder.Append('\r');
					break;
				default:
					stringBuilder.Append(c2);
					break;
				}
			}
			else
			{
				stringBuilder.Append(c);
			}
		}
		return stringBuilder.ToString();
	}
}