Vexxvididu Posted November 9, 2025 Report Posted November 9, 2025 I did my best to follow the tutorials on the wiki to setup visual studio to connect with vintage story. I am able to view the game code but can't change anything in it. I'm trying to create a mod that sets the greenhouse temperature to a constant. I was able to find at least some of the code that computes this, which I attached. The tutorials talk about how to change JSON files and create new objects but I think what I'm trying to do might require changing the game code. I suspect I'm missing some obvious way to change this code but I've not figured it out yet. Any help would be appreciated. I also realize the code snippet I attached specifically says Fruit Trees but haven't yet found similar definitions for crops or berry bushes etc.
The Insanity God Posted November 9, 2025 Report Posted November 9, 2025 @Vexxvididu You probably missed the documentation due to the way it's named https://wiki.vintagestory.at/Modding:Monkey_patching If you need examples there are some other mods out there that change greenhouse buff like: https://github.com/Kaktur/VS-Mod-GreenhouseBuff/tree/master/GreenhouseBuff/GreenhouseBuff/ModPatches (makes the buff +20 C instead of +5 C) 1
Vexxvididu Posted November 9, 2025 Author Report Posted November 9, 2025 55 minutes ago, The Insanity God said: @Vexxvididu You probably missed the documentation due to the way it's named https://wiki.vintagestory.at/Modding:Monkey_patching If you need examples there are some other mods out there that change greenhouse buff like: https://github.com/Kaktur/VS-Mod-GreenhouseBuff/tree/master/GreenhouseBuff/GreenhouseBuff/ModPatches (makes the buff +20 C instead of +5 C) THANKS!! and yeah... I missed that specific document. And I saw that mod, but also saw it was dated and I want to do something slightly different. ....and just to learn it! thanks!!
Vexxvididu Posted November 10, 2025 Author Report Posted November 10, 2025 I spent hours fighting with the mod templates in visual studio. It was very helpful to use that greenhouse mod as an example but I still can't quite get my changes to work. I suspect my mod isn't quite calling my .cs scripts in my ModPatches folder. Not entirely sure what I'm still doing wrong. My folder structure looks a lot like that greenhousebuff mod except that I'm not reading from a config .json. I am obviously missing something that calls all my scripts but am including the: var harmony = new Harmony(Mod.Info.ModID); statement ...if it's even seeing that... I'm obviously missing something. Any ideas on what I could be doing wrong? I also noticed that when I compile my mod, it shows up twice in the game mod screen as shown in the screen shot below:
The Insanity God Posted November 10, 2025 Report Posted November 10, 2025 (edited) I can't say much about the actual code without seeing it but when you say "compile" do you mean: Build and manually create mod zip Use CakeBuild and grab the mod zip from the "Release" folder Just pressing the play button in visual studio (All of those are viable though I wouldn't recommend the first) In regards to the mod showing up twice, this is something that commonly happens if you install the mod (by putting it in mod folder) and then run from visual studio or if you just have multiple versions of the same mod installed. Usually not an issue as it will just load the one with the latest version number. The folder structure of the code is irrelevant btw, it's compiled into a single .DLL file. Game searches for `ModSystem` classes and uses those as entry point (assuming "type": "code" in modinfo, but that should be standard with the template) in your case the mod system would look somewhat like this: public class MyModSystem : ModSystem { public override void Start(ICoreAPI api) { base.Start(api); //Prevent duplicate patching in single player (because it creates 2 instances of the ModSystem, 1 for server side and 1 for client side) if (!Harmony.HasAnyPatches(Mod.Info.ModID)) { var harmony = new Harmony(Mod.Info.ModID); harmony.PatchAllUncategorized(); //and possibly some category patches based on config } } public override void Dispose() { new Harmony(Mod.Info.ModID).UnpatchAll(Mod.Info.ModID); } } Edited November 10, 2025 by The Insanity God 1
Vexxvididu Posted November 11, 2025 Author Report Posted November 11, 2025 (edited) 9 hours ago, The Insanity God said: I can't say much about the actual code without seeing it but when you say "compile" do you mean: Build and manually create mod zip Use CakeBuild and grab the mod zip from the "Release" folder Just pressing the play button in visual studio (All of those are viable though I wouldn't recommend the first) In regards to the mod showing up twice, this is something that commonly happens if you install the mod (by putting it in mod folder) and then run from visual studio or if you just have multiple versions of the same mod installed. Usually not an issue as it will just load the one with the latest version number. The folder structure of the code is irrelevant btw, it's compiled into a single .DLL file. Game searches for `ModSystem` classes and uses those as entry point (assuming "type": "code" in modinfo, but that should be standard with the template) in your case the mod system would look somewhat like this: public class MyModSystem : ModSystem { public override void Start(ICoreAPI api) { base.Start(api); //Prevent duplicate patching in single player (because it creates 2 instances of the ModSystem, 1 for server side and 1 for client side) if (!Harmony.HasAnyPatches(Mod.Info.ModID)) { var harmony = new Harmony(Mod.Info.ModID); harmony.PatchAllUncategorized(); //and possibly some category patches based on config } } public override void Dispose() { new Harmony(Mod.Info.ModID).UnpatchAll(Mod.Info.ModID); } } I mean just hitting the play button in visual studio. And I'll gladly post the code I have so far in case you or any other kind soul has the time to tell me what I am doing wrong. The mod system page was recently changed from your suggestion. The other scripts are modified from the GreenHouseBuff mod. I am NOT super confident that I'm using the opcodes properly since I've never used those before. ...google AI was giving very inconsistent feedback on what to do with those, haha. The farmland and berry bush scripts are supposed to set the temperature in the greenhouse to a steady 20 degrees. The fruit tree script likely isn't right for that so I just set the bonus to a steady 10 degrees. I can come back to that one later. SteadyGreenHousesModSystem.cs BerryBush.cs Farmland.cs FruitTree.cs Edited November 11, 2025 by Vexxvididu Clarity.
The Insanity God Posted November 11, 2025 Report Posted November 11, 2025 The `[HarmonyPatch]` attribute does not need to be on the `SteadyGreenHouses` class (it doesn't contain any patches) The class containing patches (for instance `BerryBush`) does not need to inherit `ModSystem` and might as well be a static classes (since it only contains static methods) I'd recommend using the `CodeMatcher`, makes patches a lot more readable You replaced the `Add` operation with a `Nop` this will result in invalid code as you never removed the variable that was loaded so it's still on the stack (should be putting a big error about this in your log file). Berry patch would end up looking like this using CodeMatcher and after fixing number 4: [HarmonyTranspiler] [HarmonyPatch(typeof(BlockEntityBerryBush), "CheckGrow")] internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { var matcher = new CodeMatcher(instructions); matcher.MatchStartForward( CodeMatch.LoadsLocal(), //match the loading of a local variable CodeMatch.LoadsConstant(5f), //match the static 5 new CodeMatch(OpCodes.Add), //match the addition CodeMatch.StoresLocal() // match the storing of a local variable ); if (matcher.IsInvalid) { throw new Exception("BerryBush transpiler could not find a match, either base game code has changed or another mod has changed this logic"); } matcher.RemoveInstruction(); //We don't need the original temperature (so don't push the value onto the stack) matcher.Instruction.operand = 20f; matcher.Advance(1); matcher.RemoveInstruction(); //No addition is needed anymore return matcher.InstructionEnumeration(); } 1
Vexxvididu Posted November 11, 2025 Author Report Posted November 11, 2025 6 hours ago, The Insanity God said: The `[HarmonyPatch]` attribute does not need to be on the `SteadyGreenHouses` class (it doesn't contain any patches) The class containing patches (for instance `BerryBush`) does not need to inherit `ModSystem` and might as well be a static classes (since it only contains static methods) I'd recommend using the `CodeMatcher`, makes patches a lot more readable You replaced the `Add` operation with a `Nop` this will result in invalid code as you never removed the variable that was loaded so it's still on the stack (should be putting a big error about this in your log file). Berry patch would end up looking like this using CodeMatcher and after fixing number 4: [HarmonyTranspiler] [HarmonyPatch(typeof(BlockEntityBerryBush), "CheckGrow")] internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { var matcher = new CodeMatcher(instructions); matcher.MatchStartForward( CodeMatch.LoadsLocal(), //match the loading of a local variable CodeMatch.LoadsConstant(5f), //match the static 5 new CodeMatch(OpCodes.Add), //match the addition CodeMatch.StoresLocal() // match the storing of a local variable ); if (matcher.IsInvalid) { throw new Exception("BerryBush transpiler could not find a match, either base game code has changed or another mod has changed this logic"); } matcher.RemoveInstruction(); //We don't need the original temperature (so don't push the value onto the stack) matcher.Instruction.operand = 20f; matcher.Advance(1); matcher.RemoveInstruction(); //No addition is needed anymore return matcher.InstructionEnumeration(); } Thank you so much! I kind of figured that use of Nop wasn't right but wasn't sure what else to do. Your approach here makes a lot more sense. So now I'm pretty sure it's still just not running my scripts and I think (based on the logs) it might not like my modinfo.json. It keeps saying it's not found. I have it in the defaulted location created by the project. modinfo.json
The Insanity God Posted November 11, 2025 Report Posted November 11, 2025 ModInfo file looks just fine, if it's not found you might want to check the `csproj` file to ensure it's actually including your ModInfo file Might want to also manually delete the content of the `bin` folder (folder is normally hidden inside visual studio) to do a fresh rebuild. 1
Vexxvididu Posted November 11, 2025 Author Report Posted November 11, 2025 THANKS! I made more progress thanks to you. What worked for me was to delete the main project out of the mods folder and only move the released bin contents into the mods folder. This actually appears to be functional and is working for berry bushes! WOOHOO! Now I just need to fix it for farmland. I THINK the new problem is this CodeMatch.LoadsLocal() below. This works for berry bushes, since that is a local variable in that code, but in the farmland script it directly loads climateCondition.Temperature. ...which is maybe is a "Field"? I tried using LoadsField() but that seems to expect an argument. I assume the StoresLocal() also needs to change. [HarmonyTranspiler] [HarmonyPatch(typeof(BlockEntityFarmland), "Update")] internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { var matcher = new CodeMatcher(instructions); matcher.MatchStartForward( CodeMatch.LoadsLocal(), //match the loading of a local variable CodeMatch.LoadsConstant(5f), //match the static 5 new CodeMatch(OpCodes.Add), //match the addition CodeMatch.StoresLocal() // match the storing of a local variable ); if (matcher.IsInvalid) { throw new Exception("Farmland transpiler could not find a match, either base game code has changed or another mod has changed this logic"); } matcher.RemoveInstruction(); //We don't need the original temperature (so don't push the value onto the stack) matcher.Instruction.operand = 20f; matcher.Advance(1); matcher.RemoveInstruction(); //No addition is needed anymore return matcher.InstructionEnumeration(); } //Change tooltip description. [HarmonyPostfix] [HarmonyPatch(typeof(BlockEntityFarmland), "GetBlockInfo")] static void Postfix(StringBuilder dsc) { // Replace the '5' in the greenhouse temp bonus string string originalString = Lang.Get("greenhousetempbonus"); string modifiedString = originalString.Replace("5", "X"); // Modify the description if (dsc.ToString().Contains(originalString)) { dsc.Replace(originalString, modifiedString); } }
The Insanity God Posted November 11, 2025 Report Posted November 11, 2025 (edited) Wait you had the main project inside of the mods folder? well that does explain why it was showing up twice, since pressing play normally launches the game with your bin folder as an additional mod path. Meaning it will attempt to load both your actual compiled mod and the non compiled project because it's in the mods folder (which fails because folder structure doesn't match) But yeah in the case of farmland the IL code looks a bit different: So would end up looking more like this: [HarmonyTranspiler] [HarmonyPatch(typeof(BlockEntityFarmland), "Update")] internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { var matcher = new CodeMatcher(instructions); var temperatureField = AccessTools.Field(typeof(ClimateCondition), nameof(ClimateCondition.Temperature)); matcher.MatchStartForward( CodeMatch.LoadsLocal(), //match the loading of a local variable (ClimateCondition) new CodeMatch(OpCodes.Dup), //duplicate climate condition (cause it needs to both retrieve and store field CodeMatch.LoadsField(temperatureField), //match the retrieval of the field CodeMatch.LoadsConstant(5f), //match the static 5 new CodeMatch(OpCodes.Add), //match the addition CodeMatch.StoresField(temperatureField) // match the storing of the field ); if (matcher.IsInvalid) { throw new Exception("Farmland transpiler could not find a match, either base game code has changed or another mod has changed this logic"); } matcher.Advance(1); //Still need to load the ClimateCondition matcher.RemoveInstruction(); //But don't duplicate (we only need it once) matcher.RemoveInstruction(); //Also don't try to load the field matcher.Instruction.operand = 20f; matcher.Advance(1); matcher.RemoveInstruction(); //No addition is needed anymore return matcher.InstructionEnumeration(); } Edited November 11, 2025 by The Insanity God 1
Vexxvididu Posted November 11, 2025 Author Report Posted November 11, 2025 22 minutes ago, The Insanity God said: Wait you had the main project inside of the mods folder? well that does explain why it was showing up twice, since pressing play normally launches the game with your bin folder as an additional mod path. Meaning it will attempt to load both your actual compiled mod and the non compiled project because it's in the mods folder (which fails because folder structure doesn't match) But yeah in the case of farmland the IL code looks a bit different: So would end up looking more like this: [HarmonyTranspiler] [HarmonyPatch(typeof(BlockEntityFarmland), "Update")] internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { var matcher = new CodeMatcher(instructions); var temperatureField = AccessTools.Field(typeof(ClimateCondition), nameof(ClimateCondition.Temperature)); matcher.MatchStartForward( CodeMatch.LoadsLocal(), //match the loading of a local variable (ClimateCondition) new CodeMatch(OpCodes.Dup), //duplicate climate condition (cause it needs to both retrieve and store field CodeMatch.LoadsField(temperatureField), //match the retrieval of the field CodeMatch.LoadsConstant(5f), //match the static 5 new CodeMatch(OpCodes.Add), //match the addition CodeMatch.StoresField(temperatureField) // match the storing of the field ); if (matcher.IsInvalid) { throw new Exception("Farmland transpiler could not find a match, either base game code has changed or another mod has changed this logic"); } matcher.Advance(1); //Still need to load the ClimateCondition matcher.RemoveInstruction(); //But don't duplicate (we only need it once) matcher.RemoveInstruction(); //Also don't try to load the field matcher.Instruction.operand = 20f; matcher.Advance(1); matcher.RemoveInstruction(); //No addition is needed anymore return matcher.InstructionEnumeration(); } I am indebted to you! My mod works! THANKS! And yeah.. I had the project in the mods folder due to me misinterpreting one of the tutorials. THANKS SO MUCH! This is what I wanted. You did most of the work, but I learned a lot from you!
Recommended Posts