using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using Jotunn; using Jotunn.Utils; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using TMPro; using UnityEngine; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("SkillFloors")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("SkillFloors")] [assembly: AssemblyCopyright("Copyright © 2025")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("e3243d22-4307-4008-ba36-9f326008cde5")] [assembly: AssemblyFileVersion("1.1.6")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.1.6.0")] [module: UnverifiableCode] namespace SkillFloors; [BepInPlugin("Armikur.mod.Valheim.SkillFloors", "SkillFloors", "1.1.6")] [BepInDependency(/*Could not decode attribute arguments.*/)] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] internal class SkillFloors : BaseUnityPlugin { public const string PluginName = "SkillFloors"; public const string PluginGUID = "Armikur.mod.Valheim.SkillFloors"; public const string PluginVersion = "1.1.6"; private readonly Harmony HarmonyInstance = new Harmony("Armikur.mod.Valheim.SkillFloors"); internal static ConfigEntry Config_Rate; internal static ConfigEntry Config_Debug; internal static bool BookIsLoaded = false; public static Dictionary Floors_Book = new Dictionary(); private void Awake() { CreateConfigValues(); SFLog.Info("Armikur's SkillFloors mod 1.1.6 has loaded!"); Assembly executingAssembly = Assembly.GetExecutingAssembly(); HarmonyInstance.PatchAll(executingAssembly); } private void CreateConfigValues() { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Expected O, but got Unknown //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Expected O, but got Unknown Config_Rate = ((BaseUnityPlugin)this).Config.Bind("Server Config", "Floor XP Gain Rate", 0.15f, new ConfigDescription("Multiplier for how much XP floor skills gain relative to regular skills (e.g., 0.15 = 15%). Values over 1 will give more XP than normal; helpful for catching up.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1.5f), new object[1] { (object)new ConfigurationManagerAttributes { IsAdminOnly = true } })); Config_Debug = ((BaseUnityPlugin)this).Config.Bind("Client Config", "Enable Debug Logging", false, new ConfigDescription("Shows each skill floor increase in the BepInEx window.", (AcceptableValueBase)null, Array.Empty())); } public static void FreshFloorsBook() { Floors_Book = new Dictionary(); if (Config_Debug.Value) { SFLog.Warn("Fresh Floors Book created"); } } public static void UpdateFloor(Skill skill, float skillXPGain) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) SkillType skill2 = skill.m_info.m_skill; if (!Floors_Book.TryGetValue(skill2, out var value)) { value = new FloorValues(); Floors_Book[skill2] = value; } float num = CalcFloorReqXP(value.Level); float nextLevelRequirement = skill.GetNextLevelRequirement(); value.XP += skillXPGain * Config_Rate.Value; if (value.XP >= num) { value.Level += 1f; value.XP = 0f; FloorGainNotify(skill2, value.Level); SFLog.Info($"{skill2} floor increased to {value.Level}!! | Skill: {skill.m_level} ({skill.m_accumulator}/{nextLevelRequirement} )"); } if (Config_Debug.Value) { SFLog.Info($"{skill2} Floor: {value.Level} ({value.XP}/{num}) | Skill: {skill.m_level} ({skill.m_accumulator}/{nextLevelRequirement})"); } } public static float GetFloorLevel(SkillType type) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) if (!Floors_Book.TryGetValue(type, out var value)) { return 0f; } return value.Level; } public static float CalcFloorReqXP(float curFloorLevel) { return Mathf.Pow(Mathf.Floor(curFloorLevel + 1f), 1.5f) * 0.5f + 0.5f; } private unsafe static void FloorGainNotify(SkillType skillType, float floorLevel) { if (!((Object)(object)MessageHud.instance == (Object)null)) { string arg = Localization.instance.Localize("$skill_" + ((object)(*(SkillType*)(&skillType))/*cast due to .constrained prefix*/).ToString().ToLower()); string text = $"{arg} floor increased to {floorLevel}"; ((Character)Player.m_localPlayer).Message((MessageType)2, text, 0, (Sprite)null); } } } internal static class SFLog { private const string prefix = "[SkillFloors] "; public static void Info(string msg) { Logger.LogInfo((object)("[SkillFloors] " + msg)); } public static void Warn(string msg) { Logger.LogWarning((object)("[SkillFloors] " + msg)); } public static void Err(string msg) { Logger.LogError((object)("[SkillFloors] " + msg)); } } public class FloorValues { public float Level; public float XP; } public class JSONFloorValuesReference { public float SkillFloors_Floor_Level; public float SkillFloors_Floor_XPProgress; } [HarmonyPatch(typeof(Skill), "Raise")] public class Patch_SkillFloor_Raise { private static void Postfix(Skill __instance, float factor) { float increseStep = __instance.m_info.m_increseStep; float skillGainRate = Game.m_skillGainRate; float skillXPGain = increseStep * factor * skillGainRate; SkillFloors.UpdateFloor(__instance, skillXPGain); } } [HarmonyPatch(typeof(Player), "OnDeath")] public class Patch_Player_OnDeath { private static void Postfix(Player __instance) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) foreach (Skill skill2 in ((Character)__instance).GetSkills().GetSkillList()) { SkillType skill = skill2.m_info.m_skill; float floorLevel = SkillFloors.GetFloorLevel(skill); if (skill2.m_level < floorLevel) { skill2.m_level = floorLevel; SFLog.Info($"{skill} hit its SkillFloor. Holding the line (I mean floor!) at level {floorLevel}!."); } } } } [HarmonyPatch(typeof(SkillsDialog), "Setup")] public class Patch_SkillsDialog { private unsafe static void Postfix(SkillsDialog __instance, Player player) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Unknown result type (might be due to invalid IL or missing references) //IL_0154: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Unknown result type (might be due to invalid IL or missing references) //IL_01ac: Unknown result type (might be due to invalid IL or missing references) List skillList = ((Character)player).GetSkills().GetSkillList(); for (int i = 0; i < skillList.Count && i < __instance.m_elements.Count; i++) { SkillType skill = skillList[i].m_info.m_skill; float floorLevel = SkillFloors.GetFloorLevel(skill); GameObject val = __instance.m_elements[i]; TMP_Text component = ((Component)Utils.FindChild(val.transform, "name", (IterativeSearchType)0)).GetComponent(); if ((Object)(object)component != (Object)null && floorLevel > 0f) { string arg = Localization.instance.Localize("$skill_" + ((object)(*(SkillType*)(&skill))/*cast due to .constrained prefix*/).ToString().ToLower()); component.text = $"{arg} {Mathf.FloorToInt(floorLevel)}"; } if (!SkillFloors.Floors_Book.TryGetValue(skill, out var value)) { continue; } GuiBar component2 = ((Component)Utils.FindChild(val.transform, "currentlevel", (IterativeSearchType)0)).GetComponent(); if ((Object)(object)component2 != (Object)null) { Transform obj = Utils.FindChild(val.transform, "floorlevel", (IterativeSearchType)0); GuiBar val2 = ((obj != null) ? ((Component)obj).GetComponent() : null); GuiBar val3; if ((Object)(object)val2 != (Object)null) { val3 = val2; } else { GameObject obj2 = Object.Instantiate(((Component)component2).gameObject, ((Component)component2).transform.parent); ((Object)obj2).name = "floorlevel"; val3 = obj2.GetComponent(); RectTransform component3 = obj2.GetComponent(); component3.anchoredPosition -= new Vector2(0f, 2f); component3.SetSizeWithCurrentAnchors((Axis)1, 2f); } float num = SkillFloors.CalcFloorReqXP(value.Level); float value2 = Mathf.Clamp01(value.XP / num); val3.SetValue(value2); val3.SetColor(new Color(0.66f, 0.76f, 0.84f, 1f)); } } } } public static class SaveData { private const string SaveDataKey = "SkillFloors_Data"; private const string SaveDataKey_OLDJSON = "SkillFloors_SkillFloorData_JSON"; private const int SaveVersion = 1; public static bool IsMainScene() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); return ((Scene)(ref activeScene)).name.Equals("main"); } public static void Save_Floors(Player player) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected I4, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(SkillFloors.Floors_Book.Count); foreach (KeyValuePair item in SkillFloors.Floors_Book) { val.Write((int)item.Key); val.Write(item.Value.Level); val.Write(item.Value.XP); } player.m_customData["SkillFloors_Data"] = val.GetBase64(); if (!SkillFloors.Config_Debug.Value) { SFLog.Info("Floors data saved"); } Log_Book("Floors data saved"); } public static void Load_Floors(Player player) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected O, but got Unknown //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) if (player.m_customData.TryGetValue("SkillFloors_Data", out var value)) { ZPackage val = new ZPackage(value); int num = val.ReadInt(); if (num != 1) { SFLog.Warn($"ZPackage found, unsupported save version {num}, aborting"); return; } int num2 = val.ReadInt(); for (int i = 0; i < num2; i++) { SkillType key = (SkillType)val.ReadInt(); float level = val.ReadSingle(); float xP = val.ReadSingle(); SkillFloors.Floors_Book[key] = new FloorValues { Level = level, XP = xP }; } if (!SkillFloors.Config_Debug.Value) { SFLog.Info("Floors data loaded"); } Log_Book("ZPackage found, Floors data loaded"); } else { SFLog.Warn("No ZPackage found, Trying JSON (old) system instead"); if (!Load_Floors_OLDJSON(player)) { SFLog.Warn("No Floors data found (normal for first use / new characters)."); } } } private static bool Load_Floors_OLDJSON(Player player) { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0032: 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_0043: Expected O, but got Unknown //IL_0049: Expected O, but got Unknown //IL_00c3: Unknown result type (might be due to invalid IL or missing references) if (!player.m_customData.TryGetValue("SkillFloors_SkillFloorData_JSON", out var value)) { if (SkillFloors.Config_Debug.Value) { SFLog.Warn("Tried to load from JSON data but found none."); } return false; } try { JsonSerializerSettings val = new JsonSerializerSettings { Converters = new List { (JsonConverter)new StringEnumConverter() } }; Dictionary dictionary = JsonConvert.DeserializeObject>(value, val); if (dictionary == null || dictionary.Count == 0) { SFLog.Warn("Found JSON data, but it was empty. Aborting JSON load."); return false; } if (SkillFloors.Config_Debug.Value) { SFLog.Warn($"JSON save has entries: {dictionary?.Count ?? (-1)}"); SFLog.Warn("JSON (raw): \n " + value); } foreach (KeyValuePair item in dictionary) { SkillFloors.Floors_Book[item.Key] = new FloorValues { Level = item.Value.SkillFloors_Floor_Level, XP = item.Value.SkillFloors_Floor_XPProgress }; } Save_Floors(player); player.m_customData.Remove("SkillFloors_SkillFloorData_JSON"); SFLog.Warn("JSON migrated to ZPackage. Old JSON removed."); return true; } catch (Exception arg) { SFLog.Err($"JSON migration failed:\n{arg}"); return false; } } private static void Log_Book(string headerMsg = null) { //IL_0056: Unknown result type (might be due to invalid IL or missing references) if (!SkillFloors.Config_Debug.Value) { return; } if (!string.IsNullOrWhiteSpace(headerMsg)) { SFLog.Warn(headerMsg); } Dictionary floors_Book = SkillFloors.Floors_Book; if (floors_Book.Count == 0) { SFLog.Warn("xxxxxx Floors Book Is Empty xxxxxx"); return; } SFLog.Warn("****** Current Floors Book ******"); foreach (KeyValuePair item in floors_Book) { SFLog.Info($"** {item.Key}: Floor: {item.Value.Level}, Floor XP: {item.Value.XP}"); } } } [HarmonyPatch(typeof(Player), "Save")] public class Patch_Player_Save { private static void Prefix(Player __instance) { if ((Object)(object)__instance == (Object)null || (Object)(object)Player.m_localPlayer != (Object)(object)__instance) { if (SkillFloors.Config_Debug.Value) { SFLog.Warn("skip save: non-local or null player"); } } else if (!SaveData.IsMainScene()) { if (SkillFloors.Config_Debug.Value) { SFLog.Warn("skip save: not in-world"); } } else { SaveData.Save_Floors(__instance); } } } [HarmonyPatch(typeof(Player), "OnSpawned")] public static class Patch_Player_OnSpawned { private static void Postfix(Player __instance) { if ((Object)(object)__instance == (Object)null || (Object)(object)Player.m_localPlayer != (Object)(object)__instance) { if (SkillFloors.Config_Debug.Value) { SFLog.Warn("skip load: non-local or null player"); } return; } if (!SaveData.IsMainScene()) { if (SkillFloors.Config_Debug.Value) { SFLog.Warn("skip load: not in-world"); } return; } if (SkillFloors.BookIsLoaded) { if (SkillFloors.Config_Debug.Value) { SFLog.Warn("skip load: only load once per login"); } return; } if (SkillFloors.Config_Debug.Value) { SFLog.Warn("Refreshing Book & loading Floors data"); } SkillFloors.FreshFloorsBook(); SaveData.Load_Floors(__instance); SkillFloors.BookIsLoaded = true; } } [HarmonyPatch(typeof(Game), "Logout")] public static class Patch_Game_Logout { private static void Prefix() { SkillFloors.BookIsLoaded = false; if (SkillFloors.Config_Debug.Value) { SFLog.Warn("\"Loaded\" state reset"); } } }