using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; 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 BepInEx.Logging; using HarmonyLib; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("SkillGainModifier")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("SkillGainModifier")] [assembly: AssemblyCopyright("Copyright © Juuso Piippo 2026-")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("b56f4534-679e-43b6-b28f-3e68f83d562f")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] namespace SkillGainModifier; internal class SkillData { public float level; public float progress; public override string ToString() { return $"level: {level}, progress: {progress}"; } } [BepInPlugin("jujuz1.mods.skillgainmodifier", "SkillGainModifier", "0.1.1")] public class SkillGainModifier : BaseUnityPlugin { [HarmonyPatch(typeof(Skills), "RaiseSkill")] public static class Patch_Skills_RaiseSkill { private static void Prefix(SkillType skillType, ref float factor) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) if (!skillGainModifiers.TryGetValue(skillType, out var value)) { LogError($"Couldn't find value by key {skillType}. Raising skill by default (1.0f)"); return; } LogDebug($"{skillType} gain before: {factor}"); if (value.Value == 0f) { float value2 = skillGainModifiers[(SkillType)999].Value; LogDebug($"Using global value: {value2}"); factor *= value2; } else { LogDebug($"Using overriden value: {value.Value}"); if (value.Value < 0f) { LogWarning($"{skillType} has a negative modifier of {value.Value}. This will result in negative xp gain, which will not be shown in the UI beyond the progress bar. Are you sure this is correct? Applying negative xp gain..."); } factor *= value.Value; } LogDebug($"After: {factor}\n"); } } [HarmonyPatch(typeof(Skills), "LowerAllSkills")] public static class Patch_Skills_LowerAllSkills { private static void Prefix(Skills __instance, ref float factor) { //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) LogDebug($"Game m_skillReductionRate: {Game.m_skillReductionRate}"); LogDebug($"Skills m_skillReductionRate: {__instance.m_DeathLowerFactor}\n"); if (skillReductionModifier.Value == 1f) { LogDebug("Default skill reduction!"); return; } LogDebug($"Factor before modifying: {factor}"); if (skillReductionModifier.Value == 0f) { LogDebug("No skill reduction!"); LogInfo("Reduction modifier is 0. Saving skill progress...\n"); { foreach (KeyValuePair skillDatum in __instance.m_skillData) { SkillData skillData = new SkillData { level = skillDatum.Value.m_level, progress = skillDatum.Value.m_accumulator }; SkillGainModifier.skillData.Add(skillDatum.Key, skillData); LogDebug($"Added {skillDatum.Key}, {skillData}"); } return; } } if (skillReductionModifier.Value < 0f) { LogWarning($"Any reduction modifier below zero results in the level increasing when dying! Current modifier: {skillReductionModifier.Value}. Recommended values are 0-0.2! Applying positive xp gain..."); } factor = skillReductionModifier.Value; LogDebug($"Factor after modifying: {factor}\n"); } private static void Postfix(Skills __instance) { //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0160: Unknown result type (might be due to invalid IL or missing references) //IL_0192: Unknown result type (might be due to invalid IL or missing references) //IL_0175: Unknown result type (might be due to invalid IL or missing references) //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_01ce: Unknown result type (might be due to invalid IL or missing references) LogDebug("Skill progress before applying saved:"); bool flag = false; foreach (KeyValuePair skillDatum in __instance.m_skillData) { float level = skillDatum.Value.m_level; string text = ""; if (skillDatum.Value.m_level < 0f) { if (!flag) { WarnAboutReductionModifier(); flag = true; } LogWarning($"Skill {skillDatum.Key} would have had a level of {level} if it wasn't corrected to 0!"); skillDatum.Value.m_level = 0f; text = $"Corrected level: {skillDatum.Value.m_level}"; } LogDebug($"{skillDatum.Key}: level: {level} progress: {skillDatum.Value.m_accumulator}. {text}"); } if (skillData.Count <= 0) { return; } LogInfo("Applying saved skill progress...\n"); foreach (KeyValuePair skillDatum2 in __instance.m_skillData) { SkillData value = new SkillData { level = 0f, progress = 0f }; if (!skillData.TryGetValue(skillDatum2.Key, out value)) { LogError($"Couldn't find value by key {skillDatum2.Key}. Setting value to default (0.0f)"); } else { LogDebug($"Found {skillDatum2.Key}, setting data to {value}"); } __instance.m_skillData[skillDatum2.Key].m_level = value.level; __instance.m_skillData[skillDatum2.Key].m_accumulator = value.progress; } skillData.Clear(); } } [HarmonyPatch(typeof(Player), "UpdateStats", new Type[] { typeof(float) })] public static class Patch_Player_UpdateStats { private static void Postfix(Player __instance) { WarnAboutReductionModifier(); if (!noSkillDrainEnabled.Value && __instance.m_timeSinceDeath <= 10000f) { LogDebug("No skill drain disabled, timeSinceDeath set to 999999.0f"); __instance.m_timeSinceDeath = 999999f; } } } [HarmonyPatch(typeof(TombStone), "Awake")] public static class Patch_TombStone_Awake { private static void Postfix(TombStone __instance) { StatusEffect lootStatusEffect = __instance.m_lootStatusEffect; lootStatusEffect.m_ttl = corpseRunDuration.Value; LogDebug($"Set {lootStatusEffect} ttl (duration) to: {lootStatusEffect.m_ttl}"); } } public const string pluginGUID = "jujuz1.mods.skillgainmodifier"; public const string pluginName = "SkillGainModifier"; public const string pluginVersion = "0.1.1"; private readonly Harmony harmonyInstance = new Harmony("jujuz1.mods.skillgainmodifier"); private static readonly ManualLogSource logger = Logger.CreateLogSource("SkillGainModifier"); private static Dictionary skillData = new Dictionary(); private static string configFileName = "jujuz1.mods.skillgainmodifier.cfg"; private static string configFileFullPath; private static ConfigEntry loggingEnabled; private static ConfigEntry corpseRunDuration; private static ConfigEntry noSkillDrainEnabled; private static readonly Dictionary> skillGainModifiers; private static ConfigEntry skillReductionModifier; private DateTime lastReloadTime; private const long ONE_SECOND_IN_TICKS = 10000000L; private const long RELOAD_DELAY = 10000000L; private static DateTime lastReductionWarningTime; private const long WARNING_INTERVAL = 100000000L; public void Awake() { //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Invalid comparison between Unknown and I4 //IL_00d1: Unknown result type (might be due to invalid IL or missing references) ((BaseUnityPlugin)this).Config.SaveOnConfigSet = false; loggingEnabled = ((BaseUnityPlugin)this).Config.Bind("General", "Logging Enabled", true, "Enable logging"); noSkillDrainEnabled = ((BaseUnityPlugin)this).Config.Bind("General", "No skill drain enabled", true, "Enable no skill drain. The default length for it is 10 minutes, and its too big of a hassle to modify to work correctly. So here is a feature to enable or disable it :)"); corpseRunDuration = ((BaseUnityPlugin)this).Config.Bind("General", "Duration", 60f, "Corpse run duration. The default for it is 50 seconds, here we set a default of 60 seconds"); skillGainModifiers[(SkillType)999] = ((BaseUnityPlugin)this).Config.Bind("Skill Gain", "Global", 2.5f, "Multiplier for all XP gain. A factor of 2.5x is a solid default value. I mean who in the hell is reaching level 100 in any playthrough with the default of 1x?"); foreach (SkillType value in Enum.GetValues(typeof(SkillType))) { SkillType val = value; if ((int)val != 0 && (int)val != 999) { skillGainModifiers[val] = ((BaseUnityPlugin)this).Config.Bind("Skill Gain", ((object)(SkillType)(ref val)).ToString(), 0f, ""); } } skillReductionModifier = ((BaseUnityPlugin)this).Config.Bind("Skill reduction", "Modifier", 0f, "A multiplier for skill reduction when dying. RECOMMENDED VALUES: 0-0.2, see later for explanation. The default value of 0 means the skill level AND progress is not affected at all. A value of 1 signals to use the game's default world modifiers. Any value other than 0 resets skill progress! The game does the following when calculating the new level: new level = modifier * current level. Using a value above one will always set the level 0!!! So be careful with too big modifiers. The code checks that the minimum level is capped to 0 for safety"); ((BaseUnityPlugin)this).Config.Save(); ((BaseUnityPlugin)this).Config.SaveOnConfigSet = true; SetupWatcher(); WarnAboutReductionModifier(atStartup: true); Assembly executingAssembly = Assembly.GetExecutingAssembly(); harmonyInstance.PatchAll(executingAssembly); } private void OnDestroy() { ((BaseUnityPlugin)this).Config.Save(); } private void SetupWatcher() { FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(Paths.ConfigPath, configFileName); fileSystemWatcher.Changed += ReadConfigValues; fileSystemWatcher.Created += ReadConfigValues; fileSystemWatcher.Renamed += ReadConfigValues; fileSystemWatcher.IncludeSubdirectories = true; fileSystemWatcher.SynchronizingObject = ThreadingHelper.SynchronizingObject; fileSystemWatcher.EnableRaisingEvents = true; } private void ReadConfigValues(object sender, FileSystemEventArgs e) { DateTime now = DateTime.Now; long num = now.Ticks - lastReloadTime.Ticks; if (!File.Exists(configFileFullPath)) { LogError("Config doesn't exist, check " + configFileFullPath); return; } if (num < 10000000) { LogDebug($"Attempting new reload in {(float)(10000000 - num) / 10000000f}s"); return; } lastReloadTime = now; try { LogInfo("Attempting to reload configuration..."); ((BaseUnityPlugin)this).Config.Reload(); } catch { LogError("There was an issue loading " + configFileName); } LogInfo("Reloaded configuration!\n"); } private static void LogInfo(string message) { if (loggingEnabled.Value) { logger.LogInfo((object)message); } } private static void LogDebug(string message) { if (loggingEnabled.Value) { logger.LogDebug((object)message); } } private static void LogWarning(string message) { if (loggingEnabled.Value) { logger.LogWarning((object)message); } } private static void LogError(string message) { if (loggingEnabled.Value) { logger.LogError((object)message); } } private static void WarnAboutReductionModifier(bool atStartup = false) { bool flag = skillReductionModifier.Value > 1f; bool flag2 = skillReductionModifier.Value < 0f; if (!(flag || flag2)) { return; } if (atStartup) { if (flag) { LogWarning($"Any reduction modifier above one results in the level being 0 when dying! Current modifier: {skillReductionModifier.Value}. Recommended values are 0-0.2! The mod will warn you about this every {10L}s"); } else if (flag2) { LogWarning($"Any reduction modifier below zero results in the level increasing when dying! Current modifier: {skillReductionModifier.Value}. Recommended values are 0-0.2! The mod will warn you about this every {10L}s"); } return; } DateTime now = DateTime.Now; if (now.Ticks - lastReductionWarningTime.Ticks >= 100000000) { lastReductionWarningTime = now; if (flag) { LogWarning($"Any reduction modifier above one results in the level being 0 when dying! Current modifier: {skillReductionModifier.Value}. Recommended values are 0-0.2!"); } else if (flag2) { LogWarning($"Any reduction modifier below zero results in the level increasing when dying! Current modifier: {skillReductionModifier.Value}. Recommended values are 0-0.2!"); } } } static SkillGainModifier() { string configPath = Paths.ConfigPath; char directorySeparatorChar = Path.DirectorySeparatorChar; configFileFullPath = configPath + directorySeparatorChar + configFileName; skillGainModifiers = new Dictionary>(); } }