This guide will help you create a simple Slime Rancher Mod for UMF that lets you modify your max health.
It will guide you through all the different methods you could use to modify the players max health, along with the method best suited for this particular task.
It is up to you to determine which method is better suited for anything else you might attempt to mod.
maxHealth
.MyFirstSRModConfig.cs
with the following code.//Add your config vars here.
public static int MaxHealth;
//Add your settings here
MaxHealth = cfg.Read("MaxHealth", new UMFConfigInt(999, 1, 9999), "This is the player's max health.");
using System; using UModFramework.API; namespace MyFirstSRMod { public class MyFirstSRModConfig { private static readonly string configVersion = "1.0"; //Add your config vars here. public static int MaxHealth; internal static void Load() { MyFirstSRMod.Log("Loading settings."); try { using (UMFConfig cfg = new UMFConfig()) { string cfgVer = cfg.Read("ConfigVersion", new UMFConfigString()); if (cfgVer != string.Empty && cfgVer != configVersion) { cfg.DeleteConfig(false); MyFirstSRMod.Log("The config file was outdated and has been deleted. A new config will be generated."); } //cfg.Write("SupportsHotLoading", new UMFConfigBool(false)); //Uncomment if your mod can't be loaded once the game has started. cfg.Read("LoadPriority", new UMFConfigString("Normal")); cfg.Write("MinVersion", new UMFConfigString("0.52.1")); //cfg.Write("MaxVersion", new UMFConfigString("0.54.99999.99999")); //Uncomment if you think your mod may break with the next major UMF release. cfg.Write("UpdateURL", new UMFConfigString("")); cfg.Write("ConfigVersion", new UMFConfigString(configVersion)); MyFirstSRMod.Log("Finished UMF Settings."); //Add your settings here MaxHealth = cfg.Read("MaxHealth", new UMFConfigInt(999, 1, 9999), "This is the player's max health."); MyFirstSRMod.Log("Finished loading settings."); } } catch (Exception e) { MyFirstSRMod.Log("Error loading mod settings: " + e.Message + "(" + e.InnerException?.Message + ")"); } } } }
This method uses only Unity Scripting and the game's own code to modify the max health value.
You can find this method by looking through the game code which will take some experience and analyzing.
For this particular scenario this method is not the best to use.
In Slime Rancher the PlayerModel class instance can be accessed through the GameModel instance class which in turn can be retrieved from the SceneContext using the SRSingleton class.
We can set this value directly using the following line:
SRSingleton<SceneContext>.Instance.GameModel.GetPlayerModel().maxHealth = MyFirstSRModConfig.MaxHealth;This would set maxHealth to whatever the config value is set to, in the default case 999.
MyFirstSRMod.cs
add the following at the top: using MonomiPark.SlimeRancher.DataModel;
//[UMFHarmony(1)]
private static PlayerModel playerModel;
MyFirstSRModConfig.Load();
if (!Levels.isSpecial() && SRSingleton<SceneContext>.Instance?.GameModel != null) //Makes sure we are in game and that the GameModel exists. { playerModel = SRSingleton<SceneContext>.Instance.GameModel.GetPlayerModel(); } if (playerModel != null) //Make sure that the PlayerModel has been retrieved. { playerModel.maxHealth = MyFirstSRModConfig.MaxHealth; //Set the max health to our config value. }
MyFirstSRMod.cs
should now look something like this:using UnityEngine; using UModFramework.API; using System; using System.Linq; using System.Collections.Generic; using MonomiPark.SlimeRancher.DataModel; namespace MyFirstSRMod { //[UMFHarmony(1)] //Set this to the number of harmony patches in your mod. [UMFScript] class MyFirstSRMod : MonoBehaviour { private static PlayerModel playerModel; internal static void Log(string text, bool clean = false) { using (UMFLog log = new UMFLog()) log.Log(text, clean); } [UMFConfig] public static void LoadConfig() { MyFirstSRModConfig.Load(); } void Awake() { Log("MyFirstSRMod v" + UMFMod.GetModVersion().ToString(), true); UMFGUI.RegisterPauseHandler(Pause); MyFirstSRModConfig.Load(); } public static void Pause(bool pause) { TimeDirector timeDirector = null; try { timeDirector = SRSingleton<SceneContext>.Instance.TimeDirector; } catch { } if (!timeDirector) return; if (pause) { if (!timeDirector.HasPauser()) timeDirector.Pause(); } else timeDirector.Unpause(); } void Update() { if (!Levels.isSpecial() && SRSingleton<SceneContext>.Instance?.GameModel != null) //Makes sure we are in game and that the GameModel exists. { playerModel = SRSingleton<SceneContext>.Instance.GameModel.GetPlayerModel(); } if (playerModel != null) //Make sure that the PlayerModel has been retrieved. { playerModel.maxHealth = MyFirstSRModConfig.MaxHealth; //Set the max health to our config value. } } } }
Patch_PURPOSEOFPATCH.cs
before building.
This method is the easiest way to do it, but it's still not entirely optimal.
MyFirstSRMod.cs
. [UMFHarmony(1)]
//MyFirstSRModConfig.Load();
Patch_PURPOSEOFPATCH.cs
.MyFirstSRMod.cs
should now look something like this:using UnityEngine; using UModFramework.API; using System; using System.Linq; using System.Collections.Generic; using MonomiPark.SlimeRancher.DataModel; namespace MyFirstSRMod { [UMFHarmony(1)] //Set this to the number of harmony patches in your mod. [UMFScript] class MyFirstSRMod : MonoBehaviour { internal static void Log(string text, bool clean = false) { using (UMFLog log = new UMFLog()) log.Log(text, clean); } [UMFConfig] public static void LoadConfig() { MyFirstSRModConfig.Load(); } void Awake() { Log("MyFirstSRMod v" + UMFMod.GetModVersion().ToString(), true); UMFGUI.RegisterPauseHandler(Pause); } public static void Pause(bool pause) { TimeDirector timeDirector = null; try { timeDirector = SRSingleton<SceneContext>.Instance.TimeDirector; } catch { } if (!timeDirector) return; if (pause) { if (!timeDirector.HasPauser()) timeDirector.Pause(); } else timeDirector.Unpause(); } } }
Patch_PURPOSEOFPATCH.cs
to Patch_MaxHealth.cs
along with the class name for it.Patch_MaxHealth.cs
: using MonomiPark.SlimeRancher.DataModel;
[HarmonyPatch(typeof(PlayerModel))]
[HarmonyPatch("ApplyUpgrade")]
public static void Postfix(PlayerModel __instance) { }
__instance.maxHealth = MyFirstSRModConfig.MaxHealth;
Patch_MaxHealth.cs
should now look something like this:using UnityEngine; using HarmonyLib; using MonomiPark.SlimeRancher.DataModel; namespace MyFirstSRMod.Patches { [HarmonyPatch(typeof(PlayerModel))] [HarmonyPatch("ApplyUpgrade")] static class Patch_MaxHealth { public static void Postfix(PlayerModel __instance) { __instance.maxHealth = MyFirstSRModConfig.MaxHealth; } } }
Sometimes a field/variable can be private or readonly and you may not be able to directly modify it through the game code or a harmony postfix/prefix patch.
In these cases Harmony's Traverse comes in handy.
This Method assumes you have successfully completed Method 2 first.
Patch_MaxHealth.cs
. //__instance.maxHealth = MyFirstSRModConfig.MaxHealth;
Traverse.Create(__instance).Field<int>("maxHealth").Value = MyFirstSRModConfig.MaxHealth;
Patch_MaxHealth.cs
should now look something like this: using UnityEngine; using HarmonyLib; using MonomiPark.SlimeRancher.DataModel; namespace MyFirstSRMod.Patches { [HarmonyPatch(typeof(PlayerModel))] [HarmonyPatch("ApplyUpgrade")] static class Patch_MaxHealth { public static void Postfix(PlayerModel __instance) { //__instance.maxHealth = MyFirstSRModConfig.MaxHealth; Traverse.Create(__instance).Field<int>("maxHealth").Value = MyFirstSRModConfig.MaxHealth; } } }
This method will show you how you can use a Transpiler to overwrite code in memory rather than inject new code into an existing function.
This method is really solid for when you really need to modify something that can't be otherwise modified with Method 1 or 2.
Patch_MaxHealth.cs
.MyFirstSRModConfig.cs
: public static float MaxHealthFloat;
MaxHealthFloat = MaxHealth;
MyFirstSRModConfig.cs
should now look something like this: using System; using UModFramework.API; namespace MyFirstSRMod { public class MyFirstSRModConfig { private static readonly string configVersion = "1.0"; //Add your config vars here. public static int MaxHealth; public static float MaxHealthFloat; internal static void Load() { MyFirstSRMod.Log("Loading settings."); try { using (UMFConfig cfg = new UMFConfig()) { string cfgVer = cfg.Read("ConfigVersion", new UMFConfigString()); if (cfgVer != string.Empty && cfgVer != configVersion) { cfg.DeleteConfig(false); MyFirstSRMod.Log("The config file was outdated and has been deleted. A new config will be generated."); } //cfg.Write("SupportsHotLoading", new UMFConfigBool(false)); //Uncomment if your mod can't be loaded once the game has started. cfg.Read("LoadPriority", new UMFConfigString("Normal")); cfg.Write("MinVersion", new UMFConfigString("0.52.1")); //cfg.Write("MaxVersion", new UMFConfigString("0.54.99999.99999")); //Uncomment if you think your mod may break with the next major UMF release. cfg.Write("UpdateURL", new UMFConfigString("")); cfg.Write("ConfigVersion", new UMFConfigString(configVersion)); MyFirstSRMod.Log("Finished UMF Settings."); //Add your settings here MaxHealth = cfg.Read("MaxHealth", new UMFConfigInt(999, 1, 9999), "This is the player's max health."); MaxHealthFloat = MaxHealth; MyFirstSRMod.Log("Finished loading settings."); } } catch (Exception e) { MyFirstSRMod.Log("Error loading mod settings: " + e.Message + "(" + e.InnerException?.Message + ")"); } } } }
Patch_MaxHealth.cs
: using System.Reflection; using System.Reflection.Emit; using System.Collections.Generic;
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { foreach (var instruction in instructions) { if (instruction.opcode.Equals(OpCodes.Ldc_R4) && instruction.operand.Equals(350f)) { //yield return new CodeInstruction(OpCodes.Ldc_R4, MyFirstSRModConfig.MaxHealthFloat); yield return new CodeInstruction(OpCodes.Ldsfld, typeof(MyFirstSRModConfig).GetField(nameof(MyFirstSRModConfig.MaxHealthFloat), BindingFlags.Public | BindingFlags.Static)); continue; } yield return instruction; } }
Patch_MaxHealth.cs
should now look something like this: using UnityEngine; using HarmonyLib; using MonomiPark.SlimeRancher.DataModel; using System.Reflection; using System.Reflection.Emit; using System.Collections.Generic; namespace MyFirstSRMod.Patches { [HarmonyPatch(typeof(PlayerModel))] [HarmonyPatch("ApplyUpgrade")] static class Patch_MaxHealth { public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { foreach (var instruction in instructions) { if (instruction.opcode.Equals(OpCodes.Ldc_R4) && instruction.operand.Equals(350f)) { //yield return new CodeInstruction(OpCodes.Ldc_R4, MyFirstSRModConfig.MaxHealthFloat); yield return new CodeInstruction(OpCodes.Ldsfld, typeof(MyFirstSRModConfig).GetField(nameof(MyFirstSRModConfig.MaxHealthFloat), BindingFlags.Public | BindingFlags.Static)); continue; } yield return instruction; } } } }
This method overwrites code in in the dll file on disk and is not reversible.
There are some cases where code may be inlined or simply inaccessible without overwriting something in the dll file.
This method should only ever be used for these rare cases, and is certainly the worst one you could use for this max health scenario.
WARNING: This Method is currently still in development and still missing a lot of functionality, as well as has several bugs.
MyFirstSRMod.cs
. //[UMFHarmony(1)]
Patch_MaxHealth.cs
or Patch_PURPOSEOFPATCH.cs
.MyFirstSRMod.cs
: private const string UMFPatchMaxHealth = "FILE: Assembly-CSharp.dll\n" + "TYPE: MonomiPark.SlimeRancher.DataModel.PlayerModel\n" + "METHOD: ApplyUpgrade\n" + "FIND: ldc.r4 350\n" + "REPLACE: ldc.r4 350\n" + "WITH: ldc.r4 999" ;
MyFirstSRModConfig.Load(); UMFPatch.ApplyPatch("MaxHealthPatch", UMFPatchMaxHealth);
MyFirstSRMod.cs
should now look something like this: using UnityEngine; using UModFramework.API; using System; using System.Linq; using System.Collections.Generic; using MonomiPark.SlimeRancher.DataModel; namespace MyFirstSRMod { //[UMFHarmony(1)] //Set this to the number of harmony patches in your mod. [UMFScript] class MyFirstSRMod : MonoBehaviour { private const string UMFPatchMaxHealth = "FILE: Assembly-CSharp.dll\n" + "TYPE: MonomiPark.SlimeRancher.DataModel.PlayerModel\n" + "METHOD: ApplyUpgrade\n" + "FIND: ldc.r4 350\n" + "REPLACE: ldc.r4 350\n" + "WITH: ldc.r4 999" ; internal static void Log(string text, bool clean = false) { using (UMFLog log = new UMFLog()) log.Log(text, clean); } [UMFConfig] public static void LoadConfig() { MyFirstSRModConfig.Load(); } void Awake() { Log("MyFirstSRMod v" + UMFMod.GetModVersion().ToString(), true); UMFGUI.RegisterPauseHandler(Pause); MyFirstSRModConfig.Load(); UMFPatch.ApplyPatch("MaxHealthPatch", UMFPatchMaxHealth); } public static void Pause(bool pause) { TimeDirector timeDirector = null; try { timeDirector = SRSingleton<SceneContext>.Instance.TimeDirector; } catch { } if (!timeDirector) return; if (pause) { if (!timeDirector.HasPauser()) timeDirector.Pause(); } else timeDirector.Unpause(); } } }
This shows you the most optimal way to mod Max Health in Slime Rancher.
While this is the best way to do it for this scenario, it is not necessarily that for modding other things.
You will have to determine your self which method is better for modding something.
Often times you will have to use more than one either in combination to patch one thing, or for several different things in a mod.
MyFirstSRModConfig.cs
. //public static int MaxHealth;
//MaxHealth = cfg.Read("MaxHealth", new UMFConfigInt(999, 1, 9999), "This is the player's max health.");
MyFirstSRModConfig.cs
: public static int MaxHealth0; public static int MaxHealth1; public static int MaxHealth2; public static int MaxHealth3; public static int MaxHealth4;
MaxHealth0 = cfg.Read("MaxHealth0", new UMFConfigInt(999, 1, 9999), "The player's max health without any health upgrades."); MaxHealth1 = cfg.Read("MaxHealth1", new UMFConfigInt(999, 1, 9999), "The player's max health with the first health upgrade"); MaxHealth2 = cfg.Read("MaxHealth2", new UMFConfigInt(999, 1, 9999), "The player's max health with the second health upgrade"); MaxHealth3 = cfg.Read("MaxHealth3", new UMFConfigInt(999, 1, 9999), "The player's max health with the third health upgrade"); MaxHealth4 = cfg.Read("MaxHealth4", new UMFConfigInt(999, 1, 9999), "The player's max health with the fourth health upgrade");
MyFirstSRModConfig.cs
should now look something like this: using System; using UModFramework.API; namespace MyFirstSRMod { public class MyFirstSRModConfig { private static readonly string configVersion = "1.1"; //Add your config vars here. //public static int MaxHealth; //public static float MaxHealthFloat; public static int MaxHealth0; public static int MaxHealth1; public static int MaxHealth2; public static int MaxHealth3; public static int MaxHealth4; internal static void Load() { MyFirstSRMod.Log("Loading settings."); try { using (UMFConfig cfg = new UMFConfig()) { string cfgVer = cfg.Read("ConfigVersion", new UMFConfigString()); if (cfgVer != string.Empty && cfgVer != configVersion) { cfg.DeleteConfig(false); MyFirstSRMod.Log("The config file was outdated and has been deleted. A new config will be generated."); } //cfg.Write("SupportsHotLoading", new UMFConfigBool(false)); //Uncomment if your mod can't be loaded once the game has started. cfg.Read("LoadPriority", new UMFConfigString("Normal")); cfg.Write("MinVersion", new UMFConfigString("0.52.1")); //cfg.Write("MaxVersion", new UMFConfigString("0.54.99999.99999")); //Uncomment if you think your mod may break with the next major UMF release. cfg.Write("UpdateURL", new UMFConfigString("")); cfg.Write("ConfigVersion", new UMFConfigString(configVersion)); MyFirstSRMod.Log("Finished UMF Settings."); //Add your settings here //MaxHealth = cfg.Read("MaxHealth", new UMFConfigInt(999, 1, 9999), "This is the player's max health."); //MaxHealthFloat = MaxHealth; MaxHealth0 = cfg.Read("MaxHealth0", new UMFConfigInt(999, 1, 9999), "The player's max health without any health upgrades."); MaxHealth1 = cfg.Read("MaxHealth1", new UMFConfigInt(999, 1, 9999), "The player's max health with the first health upgrade"); MaxHealth2 = cfg.Read("MaxHealth2", new UMFConfigInt(999, 1, 9999), "The player's max health with the second health upgrade"); MaxHealth3 = cfg.Read("MaxHealth3", new UMFConfigInt(999, 1, 9999), "The player's max health with the third health upgrade"); MaxHealth4 = cfg.Read("MaxHealth4", new UMFConfigInt(999, 1, 9999), "The player's max health with the fourth health upgrade"); MyFirstSRMod.Log("Finished loading settings."); } } catch (Exception e) { MyFirstSRMod.Log("Error loading mod settings: " + e.Message + "(" + e.InnerException?.Message + ")"); } } } }
public static void Postfix(PlayerModel __instance, ref PlayerState.Upgrade upgrade) { switch (upgrade) { case PlayerState.Upgrade.HEALTH_1: __instance.maxHealth = MyFirstSRModConfig.MaxHealth1; break; case PlayerState.Upgrade.HEALTH_2: __instance.maxHealth = MyFirstSRModConfig.MaxHealth2; break; case PlayerState.Upgrade.HEALTH_3: __instance.maxHealth = MyFirstSRModConfig.MaxHealth3; break; case PlayerState.Upgrade.HEALTH_4: __instance.maxHealth = MyFirstSRModConfig.MaxHealth4; break; } __instance.currHealth = __instance.maxHealth; }
Patch_MaxHealth.cs
: [HarmonyPatch(typeof(PlayerModel))] [HarmonyPatch("Reset")] static class Patch_MaxHealth2 { public static void Postfix(PlayerModel __instance) { __instance.maxHealth = MyFirstSRModConfig.MaxHealth0; __instance.currHealth = __instance.maxHealth; } }
Patch_MaxHealth.cs
should now look something like this: using UnityEngine; using HarmonyLib; using MonomiPark.SlimeRancher.DataModel; namespace MyFirstSRMod.Patches { [HarmonyPatch(typeof(PlayerModel))] [HarmonyPatch("ApplyUpgrade")] static class Patch_MaxHealth { public static void Postfix(PlayerModel __instance, ref PlayerState.Upgrade upgrade) { switch (upgrade) { case PlayerState.Upgrade.HEALTH_1: __instance.maxHealth = MyFirstSRModConfig.MaxHealth1; break; case PlayerState.Upgrade.HEALTH_2: __instance.maxHealth = MyFirstSRModConfig.MaxHealth2; break; case PlayerState.Upgrade.HEALTH_3: __instance.maxHealth = MyFirstSRModConfig.MaxHealth3; break; case PlayerState.Upgrade.HEALTH_4: __instance.maxHealth = MyFirstSRModConfig.MaxHealth4; break; } __instance.currHealth = __instance.maxHealth; } } [HarmonyPatch(typeof(PlayerModel))] [HarmonyPatch("Reset")] static class Patch_MaxHealth2 { public static void Postfix(PlayerModel __instance) { __instance.maxHealth = MyFirstSRModConfig.MaxHealth0; __instance.currHealth = __instance.maxHealth; } } }
MyFirstSRMod.cs
. [UMFHarmony(2)]
MyFirstSRMod.cs
should now look something like this: using UnityEngine; using UModFramework.API; namespace MyFirstSRMod { [UMFHarmony(2)] //Set this to the number of harmony patches in your mod. [UMFScript] class MyFirstSRMod : MonoBehaviour { internal static void Log(string text, bool clean = false) { using (UMFLog log = new UMFLog()) log.Log(text, clean); } [UMFConfig] public static void LoadConfig() { MyFirstSRModConfig.Load(); } void Awake() { Log("MyFirstSRMod v" + UMFMod.GetModVersion().ToString(), true); UMFGUI.RegisterPauseHandler(Pause); } public static void Pause(bool pause) { TimeDirector timeDirector = null; try { timeDirector = SRSingleton<SceneContext>.Instance.TimeDirector; } catch { } if (!timeDirector) return; if (pause) { if (!timeDirector.HasPauser()) timeDirector.Pause(); } else timeDirector.Unpause(); } } }
Mods
folder, and ready to play.
Since this is just an example mod you should not publicize it anywhere since anyone can easily do this.
However for the purpose of completion these steps should be taken before releasing any mod.
Properties\AssemblyInfo.cs
and fill in all the details of your mod.ModInfo.txt
to whatever you want to show users who install or update the mod.configVersion
in MyFirstSRModConfig.cs
to match the version in AssemblyInfo.cs
.