Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Keep Rested v1.0.1
PreserveRest.dll
Decompiled 2 weeks agousing 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); } } } }