/// <summary>When adding an HUD message that stacks with a previous one, use the newest message to update messageSubject.</summary> internal static void addHUDMessage_Postfix(HUDMessage message) { try { if (message.type != null || message.whatType != 0) { for (int index = 0; index < Game1.hudMessages.Count; ++index) { if (message.type != null && Game1.hudMessages[index].type != null && (Game1.hudMessages[index].type.Equals(message.type) && Game1.hudMessages[index].add == message.add)) { // Altered code to affect and keep current message in the place of existing one // Keep the updated stack number message.number = Game1.hudMessages[index].number; // Replace the old HUDMessage with the new one, instead of keeping the old. Game1.hudMessages.RemoveAt(index); Game1.hudMessages.Insert(index, message); ModMonitor.LogOnce($"Ran patch for Game1.addHUDMessage method in game code: {nameof(addHUDMessage_Postfix)}", LogLevel.Trace); return; } } } } catch (Exception ex) { ModMonitor.Log($"Failed in {nameof(addHUDMessage_Postfix)}:\n{ex}", LogLevel.Error); } }
/// <summary>Changes an argument in this.messageSubject.drawInMenu() to use StackDrawType.Draw instead of StackDrawType.Hide)</summary> internal static IEnumerable<CodeInstruction> HUDMessageDraw_Transpiler(IEnumerable<CodeInstruction> instructions) { try { var codes = new List<CodeInstruction>(instructions); for (int i = 0; i < codes.Count - 1; i++) { // find Enum value of 0 (StackDrawType.Hide) loaded for the last argument of this.messageSubject.drawInMenu() if (codes[i].opcode == OpCodes.Ldc_I4_0 && codes[i + 1].opcode == OpCodes.Callvirt && codes[i + 1].operand.ToString() == "Void drawInMenu(Microsoft.Xna.Framework.Graphics.SpriteBatch, Microsoft.Xna.Framework.Vector2, Single, Single, Single, StardewValley.StackDrawType)") { // change to Enum value 1 (StackDrawType.Draw) codes[i].opcode = OpCodes.Ldc_I4_1; ModMonitor.LogOnce($"Changed StackDrawType Enum in HUDMessage.draw method: {nameof(HUDMessageDraw_Transpiler)}", LogLevel.Trace); break; } } return codes.AsEnumerable(); } catch (Exception ex) { ModMonitor.Log($"Failed in {nameof(HUDMessageDraw_Transpiler)}:\n{ex}", LogLevel.Error); return instructions; // use original code } }
/// <summary>Changes the spouse.Contains check to use spouse.Equals instead.</summary> public static IEnumerable <CIL> spouseContainsEquals_Transpiler(IEnumerable <CIL> instructions, MethodBase original) { try { var codes = new List <CodeInstruction>(instructions); int patchCount = 0; for (int i = 0; i < codes.Count - 4; i++) { // This is the snippet of code we want to find and change: // OLD Game1.player.spouse.Contains(this.Name) or who.spouse.Contains(this.Name) (who is Farmer) // NEW Game1.player.spouse.Equals(this.Name) or who.spouse.Equals(this.Name) // It can be done with a single opcode call change from string.Contains to string.Equals if (//callvirt instance string StardewValley.Farmer::get_spouse() codes[i].opcode == OpCodes.Callvirt && (MethodInfo)codes[i].operand == typeof(Farmer).GetProperty("spouse").GetGetMethod() && //ldarg.0 codes[i + 1].opcode == OpCodes.Ldarg_0 && //call instance string StardewValley.Character::get_Name() codes[i + 2].opcode == OpCodes.Call && (MethodInfo)codes[i + 2].operand == typeof(Character).GetProperty("Name").GetGetMethod() && //callvirt instance bool[mscorlib] System.String::Contains(string) codes[i + 3].opcode == OpCodes.Callvirt && (MethodInfo)codes[i + 3].operand == typeof(string).GetMethod("Contains", new Type[] { typeof(string) })) { // Insert the new replacement instruction codes[i + 3] = new CIL(OpCodes.Callvirt, typeof(string).GetMethod("Equals", new Type[] { typeof(string) })); patchCount += 1; } } // Log results of the patch attempt if (patchCount == 0) { ModMonitor.LogOnce($"Couldn't find a code location to apply {nameof(spouseContainsEquals_Transpiler)} patch to {original.DeclaringType.Name}.{original.Name}\n" + $"This is probably fine. Maybe a game update or another harmony mod already patched it?", LogLevel.Info); } else //patched at least once { //do stuff ModMonitor.LogOnce($"Applied bugfix patch to {original.DeclaringType.Name}.{original.Name}: {nameof(spouseContainsEquals_Transpiler)}", LogLevel.Trace); if (patchCount > 1) { ModMonitor.LogOnce($"Found an unusual number of patch locations for {nameof(spouseContainsEquals_Transpiler)} in {original.DeclaringType.Name}.{original.Name}\n" + $"This might cause unexpected behaviour. Please share your SMAPI log with the creator of this mod.", LogLevel.Warn); } } return(codes.AsEnumerable()); } catch (Exception ex) { ModMonitor.Log($"Failed in {nameof(spouseContainsEquals_Transpiler)}:\n{ex}", LogLevel.Error); return(instructions); // use original code } }
/// <summary>Adds a hook that calls FixedHUDMessage_Hook inside the game's addItemToInventoryBool method.</summary> public static IEnumerable<CIL> addItemToInventoryBool_Transpiler(IEnumerable<CIL> instructions) { try { var codes = new List<CodeInstruction>(instructions); for (int i = 0; i < codes.Count - 9; i++) { // This is the line of code we want to find and change: // Game1.addHUDMessage(new HUDMessage(displayName, Math.Max(1, item.Stack), true, color, item)); // Game1.addHUDMessage(new HUDMessage(displayName, 1, true, color, item)); // We can most easily do this by replacing several instructions with nop if (//ldloc.3 codes[i].opcode == OpCodes.Ldloc_3 && //ldc.i4.1 // Replace with nop codes[i + 1].opcode == OpCodes.Ldc_I4_1 && //ldarg.1 // Replace with nop codes[i + 2].opcode == OpCodes.Ldarg_1 && //callvirt instance int32 StardewValley.Item::get_Stack() // Replace with nop codes[i + 3].opcode == OpCodes.Callvirt && (MethodInfo)codes[i + 3].operand == typeof(Item).GetMethod("get_Stack") && //call int32 [mscorlib]System.Math::Max(int32, int32) // Replace with Ldc_I4_1 codes[i + 4].opcode == OpCodes.Call && (MethodInfo)codes[i + 4].operand == typeof(Math).GetMethod("Max", types: new Type[] { typeof(int), typeof(int) }) && //ldc.i4.1 codes[i + 5].opcode == OpCodes.Ldc_I4_1 && //ldloc.2 codes[i + 6].opcode == OpCodes.Ldloc_2 && //ldarg.1 codes[i + 7].opcode == OpCodes.Ldarg_1 && //newobj instance void StardewValley.HUDMessage::.ctor(string, int32, bool, valuetype [Microsoft.Xna.Framework]Microsoft.Xna.Framework.Color, class StardewValley.Item) codes[i + 8].opcode == OpCodes.Newobj && (ConstructorInfo)codes[i + 8].operand == typeof(HUDMessage).GetConstructor( types: new Type[] { typeof(string), typeof(int), typeof(bool), typeof(Color), typeof(Item) }) && //call void StardewValley.Game1::addHUDMessage(class StardewValley.HUDMessage) codes[i + 9].opcode == OpCodes.Call && (MethodInfo)codes[i + 9].operand == typeof(Game1).GetMethod("addHUDMessage")) { // Compose the new instructions to use as replacements codes[i + 8] = new CIL(OpCodes.Call, Instance.Helper.Reflection.GetMethod( typeof(FarmerPatch), nameof(FixedHUDMessage_Hook)).MethodInfo); // Call my function that returns a fixed HUDMessage ModMonitor.LogOnce($"Added hook to Farmer.addItemToInventoryBool method: {nameof(addItemToInventoryBool_Transpiler)}", LogLevel.Trace); } } return codes.AsEnumerable(); } catch (Exception ex) { ModMonitor.Log($"Failed in {nameof(addItemToInventoryBool_Transpiler)}:\n{ex}", LogLevel.Error); return instructions; // use original code } }
/// <summary>Insertable, altered HUDMessage constructor that uses a single-stack clone of the original object.</summary> public static HUDMessage FixedHUDMessage_Hook(string displayName, int number, bool add, Color color, Item item) { try { Item hudItem = item.getOne(); // Grabs a clone of the item to display in HUDMessage ModMonitor.LogOnce($"Ran patch for HUDMessage creation in Farmer.addItemToInventoryBool method: {nameof(FixedHUDMessage_Hook)}", LogLevel.Trace); return new HUDMessage(displayName, number, add, color, hudItem); } catch (Exception ex) { ModMonitor.Log($"Failed in {nameof(FixedHUDMessage_Hook)}:\n{ex}", LogLevel.Error); return new HUDMessage(displayName, number, add, color, item); } }