Decompiled source of Keep Rested v1.0.1

PreserveRest.dll

Decompiled 2 weeks ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]
[assembly: AssemblyCompany("PreserveRest")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+63fa8a1f3ebbab4e1fec68dd6be1b4774fc18bed")]
[assembly: AssemblyProduct("PreserveRest")]
[assembly: AssemblyTitle("PreserveRest")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[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 PreserveRest
{
	internal static class Game
	{
		private const BindingFlags Any = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;

		internal static ManualLogSource Log;

		internal static Type PlayerType;

		private static FieldInfo _customData;

		private static FieldInfo _comfortLevel;

		private static MethodInfo _isDead;

		private static MethodInfo _getSEMan;

		private static MethodInfo _getStatusEffect;

		private static MethodInfo _addStatusEffect;

		private static bool _addTakesEffect;

		private static PropertyInfo _objectDbInstance;

		private static MethodInfo _objectDbGetStatusEffect;

		private static FieldInfo _ttl;

		private static FieldInfo _time;

		private static PropertyInfo _znetInstance;

		private static MethodInfo _worldUid;

		private static MethodInfo _worldName;

		internal static int RestedHash;

		private static readonly HashSet<string> Reported = new HashSet<string>();

		internal static bool CanSaveAndRestore
		{
			get
			{
				if (PlayerType != null && _customData != null && _getSEMan != null && _getStatusEffect != null && _ttl != null)
				{
					return CanReadWorld;
				}
				return false;
			}
		}

		internal static bool CanRestore
		{
			get
			{
				if (CanSaveAndRestore)
				{
					return _addStatusEffect != null;
				}
				return false;
			}
		}

		private static bool CanReadWorld
		{
			get
			{
				if (_znetInstance != null)
				{
					if (!(_worldUid != null))
					{
						return _worldName != null;
					}
					return true;
				}
				return false;
			}
		}

		public static void Init(ManualLogSource log)
		{
			Log = log;
			PlayerType = FindType("Player", "nothing can be saved or restored");
			Type type = FindType("Character", "nothing can be saved or restored");
			Type type2 = FindType("SEMan", "nothing can be saved or restored");
			Type type3 = FindType("StatusEffect", "nothing can be saved or restored");
			Type type4 = FindType("ZNet", "the world can't be told apart, so nothing is saved");
			_customData = FindField(PlayerType, "m_customData", typeof(Dictionary<string, string>), "nothing can be saved or restored");
			_comfortLevel = FindField(PlayerType, "m_comfortLevel", typeof(int), "comfort isn't saved; Rested still comes back with its time");
			_isDead = FindMethod(type, "IsDead", typeof(bool), new Type[0], "a dead character's Rested could be saved");
			_getSEMan = FindMethod(type, "GetSEMan", type2, new Type[0], "nothing can be saved or restored");
			_getStatusEffect = FindMethod(type2, "GetStatusEffect", type3, new Type[1] { typeof(int) }, "nothing can be saved or restored");
			FindAddStatusEffect(type2, type3);
			_ttl = FindField(type3, "m_ttl", typeof(float), "nothing can be saved or restored");
			_time = FindField(type3, "m_time", typeof(float), "Rested comes back with the right time left, but its bar starts full");
			if (type4 != null)
			{
				_znetInstance = type4.GetProperty("instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				Report("ZNet.instance", _znetInstance != null && _znetInstance.PropertyType == type4, "the world can't be told apart, so nothing is saved");
				if (_znetInstance?.PropertyType != type4)
				{
					_znetInstance = null;
				}
			}
			_worldUid = FindMethod(type4, "GetWorldUID", typeof(long), new Type[0], "worlds are told apart by name instead");
			_worldName = FindMethod(type4, "GetWorldName", typeof(string), new Type[0], "worlds are told apart by their ID only");
			FieldInfo fieldInfo = type2?.GetField("s_statusEffectRested", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
			if (fieldInfo != null && fieldInfo.IsStatic && fieldInfo.FieldType == typeof(int))
			{
				RestedHash = (int)fieldInfo.GetValue(null);
				Log.LogInfo((object)"Found SEMan.s_statusEffectRested");
			}
			else
			{
				RestedHash = StableHash("Rested");
				Log.LogWarning((object)"Missing SEMan.s_statusEffectRested, so Rested is looked up by the hash of its name instead");
			}
		}

		public static IDictionary<string, string> CustomData(object player)
		{
			return Guard("Player.m_customData", () => (IDictionary<string, string>)_customData.GetValue(player), null);
		}

		public static bool IsDead(object player)
		{
			if (_isDead != null)
			{
				return Guard("Character.IsDead", () => (bool)_isDead.Invoke(player, null), fallback: false);
			}
			return false;
		}

		public static object Rested(object player)
		{
			return Guard("SEMan.GetStatusEffect", delegate
			{
				object obj = _getSEMan.Invoke(player, null);
				return (obj != null) ? _getStatusEffect.Invoke(obj, new object[1] { RestedHash }) : null;
			}, null);
		}

		public static object AddRested(object player)
		{
			return Guard("SEMan.AddStatusEffect", delegate
			{
				object obj = _getSEMan.Invoke(player, null);
				if (obj == null)
				{
					return (object)null;
				}
				object obj2 = RestedHash;
				if (_addTakesEffect)
				{
					object value = _objectDbInstance.GetValue(null, null);
					obj2 = ((value == null) ? null : _objectDbGetStatusEffect.Invoke(value, new object[1] { RestedHash }));
					if (obj2 == null)
					{
						return (object)null;
					}
				}
				return _addStatusEffect.Invoke(obj, Arguments(_addStatusEffect, obj2)) ?? _getStatusEffect.Invoke(obj, new object[1] { RestedHash });
			}, null);
		}

		public static float Remaining(object effect)
		{
			return Total(effect) - Elapsed(effect);
		}

		public static float Total(object effect)
		{
			return Guard("StatusEffect.m_ttl", () => (float)_ttl.GetValue(effect), float.NaN);
		}

		public static float Elapsed(object effect)
		{
			if (!(_time == null))
			{
				return Guard("StatusEffect.m_time", () => (float)_time.GetValue(effect), 0f);
			}
			return 0f;
		}

		public static bool SetTimer(object effect, float remaining, float total)
		{
			return Guard("StatusEffect.m_ttl", delegate
			{
				if (_time != null && total >= remaining)
				{
					_ttl.SetValue(effect, total);
					_time.SetValue(effect, total - remaining);
				}
				else
				{
					_ttl.SetValue(effect, Elapsed(effect) + remaining);
				}
				return true;
			}, fallback: false);
		}

		public static int Comfort(object player)
		{
			if (!(_comfortLevel == null))
			{
				return Guard("Player.m_comfortLevel", () => (int)_comfortLevel.GetValue(player), 0);
			}
			return 0;
		}

		public static void SetComfort(object player, int comfort)
		{
			if (_comfortLevel != null && comfort > 0)
			{
				Guard("Player.m_comfortLevel", delegate
				{
					_comfortLevel.SetValue(player, comfort);
					return true;
				}, fallback: false);
			}
		}

		public static string WorldId()
		{
			return Guard("ZNet.GetWorldUID", delegate
			{
				object obj = _znetInstance?.GetValue(null, null);
				if (obj == null)
				{
					return (string)null;
				}
				if (_worldUid != null)
				{
					long num = (long)_worldUid.Invoke(obj, null);
					if (num != 0L)
					{
						return num.ToString(CultureInfo.InvariantCulture);
					}
				}
				if (_worldName != null)
				{
					string text = (string)_worldName.Invoke(obj, null);
					if (!string.IsNullOrEmpty(text))
					{
						return "name-" + text;
					}
				}
				return (string)null;
			}, null);
		}

		private static void FindAddStatusEffect(Type seManType, Type statusEffectType)
		{
			if (seManType == null || statusEffectType == null)
			{
				return;
			}
			List<MethodInfo> list = (from m in seManType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
				where m.Name == "AddStatusEffect" && !m.IsStatic
				select m).ToList();
			Log.LogInfo((object)("SEMan.AddStatusEffect overloads: " + ((list.Count == 0) ? "none" : string.Join("; ", list.Select(Describe).ToArray()))));
			List<MethodInfo> source = (from m in list
				where statusEffectType.IsAssignableFrom(m.ReturnType) && m.GetParameters().Length != 0
				where m.GetParameters().All((ParameterInfo p) => !p.ParameterType.IsByRef && !p.IsOut)
				orderby m.GetParameters().Length
				select m).ToList();
			_addStatusEffect = source.FirstOrDefault((MethodInfo m) => m.GetParameters()[0].ParameterType == typeof(int));
			if (_addStatusEffect == null)
			{
				MethodInfo methodInfo = source.FirstOrDefault((MethodInfo m) => m.GetParameters()[0].ParameterType.IsAssignableFrom(statusEffectType));
				Type type = AccessTools.TypeByName("ObjectDB");
				_objectDbInstance = type?.GetProperty("instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				_objectDbGetStatusEffect = type?.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "GetStatusEffect" && !m.IsStatic && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == typeof(int));
				if (methodInfo != null && _objectDbInstance != null && _objectDbGetStatusEffect != null)
				{
					_addStatusEffect = methodInfo;
					_addTakesEffect = true;
				}
			}
			if (_addStatusEffect != null)
			{
				Log.LogInfo((object)("Found SEMan." + Describe(_addStatusEffect) + ", used to give Rested back"));
			}
			else
			{
				Log.LogWarning((object)"Missing a usable SEMan.AddStatusEffect, so Rested is saved but can't be given back");
			}
		}

		private static object[] Arguments(MethodInfo method, object first)
		{
			ParameterInfo[] parameters = method.GetParameters();
			object[] array = new object[parameters.Length];
			array[0] = first;
			for (int i = 1; i < parameters.Length; i++)
			{
				Type parameterType = parameters[i].ParameterType;
				object obj = (parameters[i].HasDefaultValue ? parameters[i].DefaultValue : null);
				if (obj == null || obj == DBNull.Value || obj == Missing.Value || !parameterType.IsInstanceOfType(obj))
				{
					obj = (parameterType.IsValueType ? Activator.CreateInstance(parameterType) : null);
				}
				array[i] = obj;
			}
			return array;
		}

		private static string Describe(MethodInfo m)
		{
			return m.Name + "(" + string.Join(", ", (from p in m.GetParameters()
				select p.ParameterType.Name + " " + p.Name).ToArray()) + ")";
		}

		public static T Guard<T>(string what, Func<T> call, T fallback)
		{
			try
			{
				return call();
			}
			catch (Exception ex)
			{
				if (Reported.Add(what))
				{
					Log.LogWarning((object)$"{what} failed, so the mod carries on without it: {(ex as TargetInvocationException)?.InnerException ?? ex}");
				}
				return fallback;
			}
		}

		internal static int StableHash(string text)
		{
			int num = 5381;
			int num2 = num;
			for (int i = 0; i < text.Length && text[i] != 0; i += 2)
			{
				num = ((num << 5) + num) ^ text[i];
				if (i == text.Length - 1 || text[i + 1] == '\0')
				{
					break;
				}
				num2 = ((num2 << 5) + num2) ^ text[i + 1];
			}
			return num + num2 * 1566083941;
		}

		private static Type FindType(string name, string withoutIt)
		{
			Type type = AccessTools.TypeByName(name);
			Report(name, type != null, withoutIt);
			return type;
		}

		private static FieldInfo FindField(Type type, string name, Type fieldType, string withoutIt)
		{
			if (type == null)
			{
				return null;
			}
			FieldInfo fieldInfo = AccessTools.Field(type, name);
			bool flag = fieldInfo != null && !fieldInfo.IsStatic && fieldInfo.FieldType == fieldType;
			Report(type.Name + "." + name, flag, withoutIt);
			if (!flag)
			{
				return null;
			}
			return fieldInfo;
		}

		private static MethodInfo FindMethod(Type type, string name, Type returns, Type[] parameters, string withoutIt)
		{
			if (type == null)
			{
				return null;
			}
			MethodInfo methodInfo = (from m in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
				where m.Name == name && !m.IsStatic
				select m).FirstOrDefault((MethodInfo m) => (from p in m.GetParameters()
				select p.ParameterType).SequenceEqual(parameters)) ?? AccessTools.Method(type, name, parameters, (Type[])null);
			bool flag = methodInfo != null && (returns == null || returns.IsAssignableFrom(methodInfo.ReturnType));
			Report(type.Name + "." + name + "(" + string.Join(", ", parameters.Select((Type p) => p.Name).ToArray()) + ")", flag, withoutIt);
			if (!flag)
			{
				return null;
			}
			return methodInfo;
		}

		private static void Report(string what, bool found, string withoutIt)
		{
			if (found)
			{
				Log.LogInfo((object)("Found " + what));
			}
			else
			{
				Log.LogWarning((object)("Missing " + what + ", so " + withoutIt));
			}
		}
	}
	internal static class Patches
	{
		private static bool _warnedNoWorld;

		private static ManualLogSource Log => Game.Log;

		private static bool FollowWorlds => PreserveRestPlugin.RestoreInOtherWorlds?.Value ?? false;

		public static void Apply(Harmony harmony, ManualLogSource log)
		{
			if (!Game.CanSaveAndRestore)
			{
				log.LogWarning((object)"Some of what saving Rested needs is missing (see above), so the mod stays idle and the game plays as usual.");
			}
			else
			{
				if (!Hook(harmony, "Save", (MethodInfo m) => m.GetParameters().Length == 1, "SavePrefix", null, "Rested can't be saved"))
				{
					return;
				}
				Hook(harmony, "OnDeath", (MethodInfo m) => m.GetParameters().Length == 0, "DeathPrefix", null, "Rested isn't cleared the moment you die, but a save while dead still drops it");
				if (Game.CanRestore)
				{
					Hook(harmony, "OnSpawned", (MethodInfo m) => true, null, "SpawnedPostfix", "Rested is saved but never given back");
				}
				else
				{
					Log.LogWarning((object)"Player.OnSpawned hook: not set, because Rested can't be added back (see above). Rested is still saved.");
				}
			}
		}

		private static bool Hook(Harmony harmony, string name, Func<MethodInfo, bool> matches, string prefix, string postfix, string withoutIt)
		{
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_0089: Unknown result type (might be due to invalid IL or missing references)
			MethodInfo methodInfo = Game.PlayerType.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == name && matches(m));
			if (methodInfo == null)
			{
				Log.LogWarning((object)("Missing Player." + name + " hook, so " + withoutIt));
				return false;
			}
			try
			{
				harmony.Patch((MethodBase)methodInfo, (prefix == null) ? ((HarmonyMethod)null) : new HarmonyMethod(typeof(Patches), prefix, (Type[])null), (postfix == null) ? ((HarmonyMethod)null) : new HarmonyMethod(typeof(Patches), postfix, (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				Log.LogInfo((object)("Player." + name + " hook: found (" + Describe(methodInfo) + ")"));
				return true;
			}
			catch (Exception arg)
			{
				Log.LogError((object)$"Could not hook Player.{name}, so {withoutIt}: {arg}");
				return false;
			}
		}

		private static string Describe(MethodInfo m)
		{
			return m.Name + "(" + string.Join(", ", (from p in m.GetParameters()
				select p.ParameterType.Name + " " + p.Name).ToArray()) + ")";
		}

		private static void SavePrefix(object __instance)
		{
			Game.Guard("saving Rested", delegate
			{
				IDictionary<string, string> dictionary = Game.CustomData(__instance);
				string text = Game.WorldId();
				if (dictionary == null || text == null)
				{
					if (!_warnedNoWorld)
					{
						Log.LogWarning((object)"Couldn't tell which world this is while saving, so Rested isn't saved");
					}
					_warnedNoWorld = true;
					return false;
				}
				RestState restState = null;
				object obj = (Game.IsDead(__instance) ? null : Game.Rested(__instance));
				if (obj != null)
				{
					float num = Game.Total(obj);
					float num2 = Game.Remaining(obj);
					if (!float.IsNaN(num) && !float.IsNaN(num2))
					{
						restState = new RestState
						{
							Remaining = num2,
							Total = num,
							Comfort = Game.Comfort(__instance),
							SavedTicks = DateTime.UtcNow.Ticks
						};
					}
				}
				RestStore.Save(dictionary, text, restState, FollowWorlds);
				if (restState != null && restState.Remaining >= 5f)
				{
					Log.LogInfo((object)$"Saved Rested for world {text}: {restState.Remaining:0} s left of {restState.Total:0} s, comfort {restState.Comfort}");
				}
				return true;
			}, fallback: false);
		}

		private static void DeathPrefix(object __instance)
		{
			Game.Guard("forgetting Rested on death", delegate
			{
				IDictionary<string, string> dictionary = Game.CustomData(__instance);
				string text = Game.WorldId();
				if (dictionary == null || text == null)
				{
					return false;
				}
				RestStore.Forget(dictionary, text, FollowWorlds);
				return true;
			}, fallback: false);
		}

		private static void SpawnedPostfix(object __instance)
		{
			Game.Guard("restoring Rested", delegate
			{
				IDictionary<string, string> dictionary = Game.CustomData(__instance);
				string text = Game.WorldId();
				if (dictionary == null || text == null)
				{
					Log.LogWarning((object)"Couldn't tell which world this is, so Rested isn't restored");
					return false;
				}
				RestState restState = RestStore.Find(dictionary, text, FollowWorlds);
				if (restState == null)
				{
					Log.LogInfo((object)("No saved Rested for world " + text));
					return false;
				}
				if (Game.IsDead(__instance))
				{
					return false;
				}
				object obj = Game.Rested(__instance);
				if (obj != null)
				{
					float num = Game.Remaining(obj);
					if (!float.IsNaN(num) && num >= restState.Remaining)
					{
						Log.LogInfo((object)$"Already Rested with {num:0} s left, which beats the saved {restState.Remaining:0} s, so it's kept");
						return false;
					}
				}
				else
				{
					obj = Game.AddRested(__instance);
				}
				if (obj == null || !Game.SetTimer(obj, restState.Remaining, restState.Total))
				{
					Log.LogWarning((object)"Couldn't give Rested back");
					return false;
				}
				Game.SetComfort(__instance, restState.Comfort);
				Log.LogInfo((object)$"Restored Rested in world {text}: {restState.Remaining:0} s left of {restState.Total:0} s, comfort {restState.Comfort}");
				return true;
			}, fallback: false);
		}
	}
	[BepInPlugin("austin.valheim.preserverest", "Keep Rested", "1.0.0")]
	public class PreserveRestPlugin : BaseUnityPlugin
	{
		public const string Guid = "austin.valheim.preserverest";

		public const string ModName = "Keep Rested";

		public const string Version = "1.0.0";

		internal static ConfigEntry<bool> RestoreInOtherWorlds;

		private Harmony _harmony;

		private void Awake()
		{
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Expected O, but got Unknown
			RestoreInOtherWorlds = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "RestoreInOtherWorlds", false, "Off: Rested only comes back when you rejoin the world or server you left it in. On: Rested follows you into any world or server, and only the latest Rested counts.");
			_harmony = new Harmony("austin.valheim.preserverest");
			try
			{
				Game.Init(((BaseUnityPlugin)this).Logger);
				Patches.Apply(_harmony, ((BaseUnityPlugin)this).Logger);
			}
			catch (Exception arg)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)$"Could not set up, so Rested won't be kept: {arg}");
			}
			((BaseUnityPlugin)this).Logger.LogInfo((object)("Keep Rested 1.0.0 loaded. Rested follows you to other worlds: " + (RestoreInOtherWorlds.Value ? "on" : "off") + "."));
		}

		private void OnDestroy()
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
	}
	internal sealed class RestState
	{
		public float Remaining;

		public float Total;

		public int Comfort;

		public long SavedTicks;

		private const string Version = "v1";

		public string Format()
		{
			return string.Format(CultureInfo.InvariantCulture, "{0};remaining={1:R};ttl={2:R};comfort={3};saved={4}", "v1", Remaining, Total, Comfort, SavedTicks);
		}

		public static RestState Parse(string value)
		{
			if (string.IsNullOrEmpty(value))
			{
				return null;
			}
			string[] array = value.Split(new char[1] { ';' });
			if (array[0] != "v1")
			{
				return null;
			}
			Dictionary<string, string> dictionary = new Dictionary<string, string>();
			for (int i = 1; i < array.Length; i++)
			{
				int num = array[i].IndexOf('=');
				if (num > 0)
				{
					dictionary[array[i].Substring(0, num)] = array[i].Substring(num + 1);
				}
			}
			RestState restState = new RestState();
			if (!TryFloat(dictionary, "remaining", out restState.Remaining) || !TryFloat(dictionary, "ttl", out restState.Total))
			{
				return null;
			}
			if (dictionary.TryGetValue("comfort", out var value2))
			{
				int.TryParse(value2, NumberStyles.Integer, CultureInfo.InvariantCulture, out restState.Comfort);
			}
			if (dictionary.TryGetValue("saved", out var value3))
			{
				long.TryParse(value3, NumberStyles.Integer, CultureInfo.InvariantCulture, out restState.SavedTicks);
			}
			return restState;
		}

		private static bool TryFloat(Dictionary<string, string> fields, string name, out float value)
		{
			value = 0f;
			if (fields.TryGetValue(name, out var value2) && float.TryParse(value2, NumberStyles.Float, CultureInfo.InvariantCulture, out value) && !float.IsNaN(value))
			{
				return !float.IsInfinity(value);
			}
			return false;
		}
	}
	internal static class RestStore
	{
		public const string KeyPrefix = "austin.valheim.preserverest.";

		public const float MinRemaining = 5f;

		public static string Key(string worldId)
		{
			return "austin.valheim.preserverest." + worldId;
		}

		public static void Save(IDictionary<string, string> data, string worldId, RestState state, bool followWorlds)
		{
			string text = Key(worldId);
			if (followWorlds)
			{
				RemoveAll(data, text);
			}
			if (state == null || state.Remaining < 5f || state.Total <= 0f)
			{
				data.Remove(text);
			}
			else
			{
				data[text] = state.Format();
			}
		}

		public static void Forget(IDictionary<string, string> data, string worldId, bool followWorlds)
		{
			data.Remove(Key(worldId));
			if (followWorlds)
			{
				RemoveAll(data, null);
			}
		}

		public static RestState Find(IDictionary<string, string> data, string worldId, bool followWorlds)
		{
			RestState restState = null;
			if (!followWorlds)
			{
				if (data.TryGetValue(Key(worldId), out var value))
				{
					restState = RestState.Parse(value);
				}
			}
			else
			{
				foreach (KeyValuePair<string, string> datum in data)
				{
					if (datum.Key.StartsWith("austin.valheim.preserverest.", StringComparison.Ordinal))
					{
						RestState restState2 = RestState.Parse(datum.Value);
						if (restState2 != null && (restState == null || restState2.SavedTicks > restState.SavedTicks))
						{
							restState = restState2;
						}
					}
				}
			}
			if (restState == null || !(restState.Remaining >= 5f) || !(restState.Total > 0f))
			{
				return null;
			}
			return restState;
		}

		private static void RemoveAll(IDictionary<string, string> data, string except)
		{
			List<string> list = new List<string>();
			foreach (string key in data.Keys)
			{
				if (key.StartsWith("austin.valheim.preserverest.", StringComparison.Ordinal) && key != except)
				{
					list.Add(key);
				}
			}
			foreach (string item in list)
			{
				data.Remove(item);
			}
		}
	}
}