guide_firstsrmod
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
guide_firstsrmod [2019/07/04 08:38] – [Method 5] umfdev | guide_firstsrmod [2019/07/04 12:27] (current) – umfdev | ||
---|---|---|---|
Line 1: | Line 1: | ||
~~NOTOC~~ | ~~NOTOC~~ | ||
~~Title: My First Slime Rancher Mod (Modding Guide) ~~ | ~~Title: My First Slime Rancher Mod (Modding Guide) ~~ | ||
- | {{tag> | + | {{tag> |
===== My First Slime Rancher Mod (Modding Guide) ===== | ===== My First Slime Rancher Mod (Modding Guide) ===== | ||
<WRAP center round info 100%> | <WRAP center round info 100%> | ||
Line 181: | Line 181: | ||
* This will make sure that your max health is always 999 every single frame regardless of which health upgrade the player has. This is obviously not normally recommended. | * This will make sure that your max health is always 999 every single frame regardless of which health upgrade the player has. This is obviously not normally recommended. | ||
* This code also ensures the config value takes effect immediately upon applying it in the UMF Menu. | * This code also ensures the config value takes effect immediately upon applying it in the UMF Menu. | ||
+ | * For more information on the **Awake** function, see [[https:// | ||
+ | * For more information on the **Update** function, see [[https:// | ||
- See [[# | - See [[# | ||
* WARNING: If this is the first method you try, you will also need to comment out '' | * WARNING: If this is the first method you try, you will also need to comment out '' | ||
Line 209: | Line 211: | ||
class MyFirstSRMod : MonoBehaviour | class MyFirstSRMod : MonoBehaviour | ||
{ | { | ||
- | //private static PlayerModel playerModel; | ||
- | |||
internal static void Log(string text, bool clean = false) | internal static void Log(string text, bool clean = false) | ||
{ | { | ||
Line 222: | Line 222: | ||
} | } | ||
- | void Awake() | + | |
- | { | + | { |
- | Log(" | + | Log(" |
UMFGUI.RegisterPauseHandler(Pause); | UMFGUI.RegisterPauseHandler(Pause); | ||
- | // | ||
} | } | ||
Line 244: | Line 243: | ||
else timeDirector.Unpause(); | else timeDirector.Unpause(); | ||
} | } | ||
- | + | | |
- | /*void Update() | + | |
- | { | + | |
- | if (!Levels.isSpecial() && SRSingleton< | + | |
- | { | + | |
- | playerModel = SRSingleton< | + | |
- | } | + | |
- | if (playerModel != null) //Make sure that the PlayerModel has been retrieved. | + | |
- | { | + | |
- | playerModel.maxHealth = MyFirstSRModConfig.MaxHealth; | + | |
- | } | + | |
- | }*/ | + | |
- | } | + | |
}</ | }</ | ||
- Rename '' | - Rename '' | ||
- Add the following to the top of '' | - Add the following to the top of '' | ||
+ | * This lets us target the **PlayerModel** class which is inside the **MonomiPark.SlimeRancher.DataModel** namespace without having to type the full namespace everytime. | ||
- Set **typeof** in the first HarmonyPatch attribute to use the **PlayerModel** class we discovered with dnSpy. <sxh csharp> | - Set **typeof** in the first HarmonyPatch attribute to use the **PlayerModel** class we discovered with dnSpy. <sxh csharp> | ||
+ | * You could also use **MonomiPark.SlimeRancher.DataModel.PlayerModel** instead for the type. | ||
- Set the second **HarmonyPatch** attribute to the **ApplyUpgrade** function we discovered with dnSpy. <sxh csharp> | - Set the second **HarmonyPatch** attribute to the **ApplyUpgrade** function we discovered with dnSpy. <sxh csharp> | ||
- Inside the patch class create the following postfix function: <sxh csharp> | - Inside the patch class create the following postfix function: <sxh csharp> | ||
Line 287: | Line 276: | ||
* A Harmony **Postfix** patch will make the code in it execute at the end of the function when all other code in the original function has executed. | * A Harmony **Postfix** patch will make the code in it execute at the end of the function when all other code in the original function has executed. | ||
* You could change it to a **Prefix** by simply renaming the function to **Prefix**. This would cause the code to be run before the other code in the original function. However that would not work for this scenario since the other code then overwrites our max health again. | * You could change it to a **Prefix** by simply renaming the function to **Prefix**. This would cause the code to be run before the other code in the original function. However that would not work for this scenario since the other code then overwrites our max health again. | ||
+ | * It would also be a good idea to set the current health to be equal to your max health here, however we show you this in Method 6. | ||
* You can also alternatively apply this patch to the **Reset** function instead if you do not have any upgrades at all. | * You can also alternatively apply this patch to the **Reset** function instead if you do not have any upgrades at all. | ||
- See [[# | - See [[# | ||
Line 294: | Line 284: | ||
\\ | \\ | ||
- | ====== Method 3 (Harmony Transpiler) ====== | + | ====== Method 3 (Harmony Traverse) ====== |
+ | Sometimes a field/ | ||
+ | In these cases Harmony' | ||
+ | This Method assumes you have successfully completed Method 2 first. | ||
+ | - Comment out the only line in the **Postfix** function of '' | ||
+ | - Add the following line to the **Postfix** function: <sxh csharp> | ||
+ | - Your '' | ||
+ | using HarmonyLib; | ||
+ | using MonomiPark.SlimeRancher.DataModel; | ||
+ | |||
+ | namespace MyFirstSRMod.Patches | ||
+ | { | ||
+ | [HarmonyPatch(typeof(PlayerModel))] | ||
+ | [HarmonyPatch(" | ||
+ | static class Patch_MaxHealth | ||
+ | { | ||
+ | public static void Postfix(PlayerModel __instance) | ||
+ | { | ||
+ | // | ||
+ | Traverse.Create(__instance).Field< | ||
+ | } | ||
+ | } | ||
+ | }</ | ||
+ | * The Traverse will bypass any private or readonly variable and let you read/write to it. | ||
+ | * Traverse does not need to be used inside a Harmony patch, it can be used anywhere anytime in any code, however you have to make sure it doesn' | ||
+ | * This will function exactly like Method 2, and is wholly unnecessary since **maxHealth** is public and not readonly. | ||
+ | - See [[# | ||
+ | \\ | ||
+ | ---- | ||
+ | |||
+ | \\ | ||
+ | |||
+ | ====== Method 4 (Harmony Transpiler) ====== | ||
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 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. | This method is really solid for when you really need to modify something that can't be otherwise modified with Method 1 or 2. | ||
Line 371: | Line 393: | ||
} | } | ||
} | } | ||
+ | </ | ||
+ | - Your '' | ||
+ | using HarmonyLib; | ||
+ | using MonomiPark.SlimeRancher.DataModel; | ||
+ | using System.Reflection; | ||
+ | using System.Reflection.Emit; | ||
+ | using System.Collections.Generic; | ||
+ | |||
+ | namespace MyFirstSRMod.Patches | ||
+ | { | ||
+ | [HarmonyPatch(typeof(PlayerModel))] | ||
+ | [HarmonyPatch(" | ||
+ | static class Patch_MaxHealth | ||
+ | { | ||
+ | public static IEnumerable< | ||
+ | { | ||
+ | foreach (var instruction in instructions) | ||
+ | { | ||
+ | if (instruction.opcode.Equals(OpCodes.Ldc_R4) && instruction.operand.Equals(350f)) | ||
+ | { | ||
+ | //yield return new CodeInstruction(OpCodes.Ldc_R4, | ||
+ | yield return new CodeInstruction(OpCodes.Ldsfld, | ||
+ | continue; | ||
+ | } | ||
+ | yield return instruction; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | } | ||
</ | </ | ||
* This example assumes you have all health upgrades and only affects the final health upgrade of 350 max health. | * This example assumes you have all health upgrades and only affects the final health upgrade of 350 max health. | ||
Line 385: | Line 436: | ||
\\ | \\ | ||
- | ====== Method | + | ====== Method |
This method overwrites code in in the dll file on disk and is not reversible.\\ | 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.\\ | 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. | 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. | ||
+ | <WRAP center round important 100%> | ||
+ | WARNING: This Method is currently still in development and still missing a lot of functionality, | ||
+ | </ | ||
+ | - If you completed steps from any other method do the following steps: | ||
+ | - Comment out the UMFHarmony attribute in '' | ||
+ | - Comment out everything in '' | ||
+ | - Make a backup of Assembly-CSharp.dll. | ||
+ | - Add the following variable to '' | ||
+ | "FILE: Assembly-CSharp.dll\n" | ||
+ | "TYPE: MonomiPark.SlimeRancher.DataModel.PlayerModel\n" | ||
+ | " | ||
+ | "FIND: ldc.r4 350\n" + | ||
+ | " | ||
+ | "WITH: ldc.r4 999" | ||
+ | ; | ||
+ | </ | ||
+ | - Add the following lines to the bottom of the **Awake** function: <sxh csharp> | ||
+ | UMFPatch.ApplyPatch(" | ||
+ | </ | ||
+ | - Your '' | ||
+ | using UModFramework.API; | ||
+ | using System; | ||
+ | using System.Linq; | ||
+ | using System.Collections.Generic; | ||
+ | using MonomiPark.SlimeRancher.DataModel; | ||
+ | |||
+ | namespace MyFirstSRMod | ||
+ | { | ||
+ | // | ||
+ | [UMFScript] | ||
+ | class MyFirstSRMod : MonoBehaviour | ||
+ | { | ||
+ | private const string UMFPatchMaxHealth = | ||
+ | "FILE: Assembly-CSharp.dll\n" | ||
+ | "TYPE: MonomiPark.SlimeRancher.DataModel.PlayerModel\n" | ||
+ | " | ||
+ | "FIND: 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, | ||
+ | } | ||
+ | |||
+ | [UMFConfig] | ||
+ | public static void LoadConfig() | ||
+ | { | ||
+ | MyFirstSRModConfig.Load(); | ||
+ | } | ||
+ | |||
+ | void Awake() | ||
+ | { | ||
+ | Log(" | ||
+ | UMFGUI.RegisterPauseHandler(Pause); | ||
+ | MyFirstSRModConfig.Load(); | ||
+ | UMFPatch.ApplyPatch(" | ||
+ | } | ||
+ | |||
+ | public static void Pause(bool pause) | ||
+ | { | ||
+ | TimeDirector timeDirector = null; | ||
+ | try | ||
+ | { | ||
+ | timeDirector = SRSingleton< | ||
+ | } | ||
+ | catch { } | ||
+ | if (!timeDirector) return; | ||
+ | if (pause) | ||
+ | { | ||
+ | if (!timeDirector.HasPauser()) timeDirector.Pause(); | ||
+ | } | ||
+ | else timeDirector.Unpause(); | ||
+ | } | ||
+ | } | ||
+ | }</ | ||
+ | * This same umf patch could be made by creating a file called MaxHealth.umfpatch and putting the contents of the **UMFPatchMaxHealth** variable in there instead. This patch would need to always be included/ | ||
+ | * See [[api: | ||
+ | - See [[# | ||
+ | * Don't forget to restore your Assembly-CSharp.dll backup after you are done with this test. | ||
\\ | \\ | ||
---- | ---- | ||
Line 394: | Line 526: | ||
\\ | \\ | ||
- | ====== Method | + | ====== Method |
This shows you the most optimal way to mod Max Health in Slime Rancher.\\ | 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.\\ | 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.\\ | 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. | 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. | ||
+ | |||
+ | - Comment out the **MaxHealth** variable in '' | ||
+ | - Comment out the loading of the **MaxHealth** variable as well. <sxh csharp>// | ||
+ | - Add the following variables to '' | ||
+ | public static int MaxHealth1; | ||
+ | public static int MaxHealth2; | ||
+ | public static int MaxHealth3; | ||
+ | public static int MaxHealth4; | ||
+ | </ | ||
+ | - Add the follow lines below the **Add your settings here** line: <sxh csharp> | ||
+ | MaxHealth1 = cfg.Read(" | ||
+ | MaxHealth2 = cfg.Read(" | ||
+ | MaxHealth3 = cfg.Read(" | ||
+ | MaxHealth4 = cfg.Read(" | ||
+ | </ | ||
+ | - Change the **configVersion** variable to " | ||
+ | * This is done so that the old MaxHealth config variable which is no longer used is also removed from existing older config files. | ||
+ | - Your '' | ||
+ | using UModFramework.API; | ||
+ | |||
+ | namespace MyFirstSRMod | ||
+ | { | ||
+ | public class MyFirstSRModConfig | ||
+ | { | ||
+ | private static readonly string configVersion = " | ||
+ | |||
+ | //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(" | ||
+ | try | ||
+ | { | ||
+ | using (UMFConfig cfg = new UMFConfig()) | ||
+ | { | ||
+ | string cfgVer = cfg.Read(" | ||
+ | if (cfgVer != string.Empty && cfgVer != configVersion) | ||
+ | { | ||
+ | cfg.DeleteConfig(false); | ||
+ | MyFirstSRMod.Log(" | ||
+ | } | ||
+ | |||
+ | // | ||
+ | cfg.Read(" | ||
+ | cfg.Write(" | ||
+ | // | ||
+ | cfg.Write(" | ||
+ | cfg.Write(" | ||
+ | |||
+ | MyFirstSRMod.Log(" | ||
+ | |||
+ | //Add your settings here | ||
+ | //MaxHealth = cfg.Read(" | ||
+ | // | ||
+ | MaxHealth0 = cfg.Read(" | ||
+ | MaxHealth1 = cfg.Read(" | ||
+ | MaxHealth2 = cfg.Read(" | ||
+ | MaxHealth3 = cfg.Read(" | ||
+ | MaxHealth4 = cfg.Read(" | ||
+ | |||
+ | MyFirstSRMod.Log(" | ||
+ | } | ||
+ | } | ||
+ | catch (Exception e) | ||
+ | { | ||
+ | MyFirstSRMod.Log(" | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | }</ | ||
+ | * We have now created a setting for each health upgrade. | ||
+ | - Add the following function to the **Patch_MaxHealth** class: <sxh csharp> | ||
+ | { | ||
+ | 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; | ||
+ | } | ||
+ | </ | ||
+ | * This ensures we are patching all of the different health upgrades only when they are applied. | ||
+ | * We are still missing the health you have without any health upgrades though. | ||
+ | - Add the following code to '' | ||
+ | [HarmonyPatch(" | ||
+ | static class Patch_MaxHealth2 | ||
+ | { | ||
+ | public static void Postfix(PlayerModel __instance) | ||
+ | { | ||
+ | __instance.maxHealth = MyFirstSRModConfig.MaxHealth0; | ||
+ | __instance.currHealth = __instance.maxHealth; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | * Here we also patch in the health you have with no health upgrades as seen in the **Reset** function with dnSpy. | ||
+ | - Your '' | ||
+ | using HarmonyLib; | ||
+ | using MonomiPark.SlimeRancher.DataModel; | ||
+ | |||
+ | namespace MyFirstSRMod.Patches | ||
+ | { | ||
+ | [HarmonyPatch(typeof(PlayerModel))] | ||
+ | [HarmonyPatch(" | ||
+ | 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(" | ||
+ | static class Patch_MaxHealth2 | ||
+ | { | ||
+ | public static void Postfix(PlayerModel __instance) | ||
+ | { | ||
+ | __instance.maxHealth = MyFirstSRModConfig.MaxHealth0; | ||
+ | __instance.currHealth = __instance.maxHealth; | ||
+ | } | ||
+ | } | ||
+ | }</ | ||
+ | * We have also made sure that the current player health is set to the new max health when applied, just like the normal game code does. | ||
+ | - Change the **UMFHarmony** attribute to 2 in '' | ||
+ | * This tells UMF the number of patches that is expected to be successfully applied, and will show a warning to the user should any of them fail. | ||
+ | * Since you now have two patches, one targeting the **ApplyUpgrade** function, and one targeting the **Reset** function, this number should be 2. | ||
+ | - Your '' | ||
+ | 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, | ||
+ | } | ||
+ | |||
+ | [UMFConfig] | ||
+ | public static void LoadConfig() | ||
+ | { | ||
+ | MyFirstSRModConfig.Load(); | ||
+ | } | ||
+ | |||
+ | void Awake() | ||
+ | { | ||
+ | Log(" | ||
+ | UMFGUI.RegisterPauseHandler(Pause); | ||
+ | } | ||
+ | |||
+ | public static void Pause(bool pause) | ||
+ | { | ||
+ | TimeDirector timeDirector = null; | ||
+ | try | ||
+ | { | ||
+ | timeDirector = SRSingleton< | ||
+ | } | ||
+ | catch { } | ||
+ | if (!timeDirector) return; | ||
+ | if (pause) | ||
+ | { | ||
+ | if (!timeDirector.HasPauser()) timeDirector.Pause(); | ||
+ | } | ||
+ | else timeDirector.Unpause(); | ||
+ | } | ||
+ | } | ||
+ | }</ | ||
+ | - See [[# | ||
+ | * You should now have a fully working Max Health Mod. | ||
\\ | \\ | ||
---- | ---- | ||
Line 406: | Line 741: | ||
====== Building ====== | ====== Building ====== | ||
- In Visual Studio in the top bar menu click **Build > Rebuild solution**. | - In Visual Studio in the top bar menu click **Build > Rebuild solution**. | ||
+ | * Your mod is automatically packed, placed into the game's '' | ||
- Start the game and test if your mod works. | - Start the game and test if your mod works. | ||
+ | * You can verify that the mod works by seeing if your Max Health bar is higher than your current health. | ||
- Proceed to test out the other methods or see [[# | - Proceed to test out the other methods or see [[# | ||
* Don't forget to comment out the code from each method so it doesn' | * Don't forget to comment out the code from each method so it doesn' | ||
Line 417: | Line 754: | ||
Since this is just an example mod you should not publicize it anywhere since anyone can easily do this.\\ | 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. | However for the purpose of completion these steps should be taken before releasing any mod. | ||
- | | + | |
- | | + | - Edit '' |
- | * Edit '' | + | |
- | * Clean and remove or comment out any unused code. | + | * After your first release you should only need to edit this variable when you remove or rename a config option. |
+ | - Clean and remove or comment out any unused code. | ||
\\ | \\ | ||
---- | ---- |
guide_firstsrmod.1562225921.txt.gz · Last modified: 2019/07/04 08:38 by umfdev