From 932a295b6bc21db0a969f9edeca628bd1b3b114a Mon Sep 17 00:00:00 2001 From: Arthri <41360489+a@users.noreply.github.com> Date: Wed, 28 Jan 2026 03:29:40 +0000 Subject: [PATCH 1/3] Implement Quick Stack hook for --- .../Patches/HookChestQuickStack.Server.cs | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/OTAPI.Scripts/Patches/HookChestQuickStack.Server.cs b/OTAPI.Scripts/Patches/HookChestQuickStack.Server.cs index 58550c2b..7bf9a075 100644 --- a/OTAPI.Scripts/Patches/HookChestQuickStack.Server.cs +++ b/OTAPI.Scripts/Patches/HookChestQuickStack.Server.cs @@ -24,7 +24,11 @@ You should have received a copy of the GNU General Public License using MonoMod; using MonoMod.Cil; using System; +using System.Collections.Generic; using System.Linq; +using Terraria; +using Terraria.GameContent; +using Terraria.ID; /// /// @doc Creates Hooks.Chest.QuickStack. @@ -39,7 +43,67 @@ partial class ChestHooks static void HookChestQuickStack(ModFwModder modder) { #if TerrariaServer_1450_OrAbove || Terraria__1450_OrAbove || tModLoader_1450_OrAbove - Console.WriteLine("[TODO] reimplement HookChestQuickStack for 1.4.5+"); + // used for the out parameter only + List _blockedChests; + + // DO NOT use GetILCursor(). The instruction body does not have jumps transformed into labels, + // a process which is required to happen for the edits below to work. + var ctx = new ILContext(modder.GetMethodDefinition(() => QuickStacking.Transfer(default, default, out _blockedChests, false))); + ctx.Invoke((ctx) => + { + var csr = new ILCursor(ctx); + int count = 0; + + while (csr.TryGotoNext( + MoveType.Before, + i => i.MatchLdarg(0), + i => i.MatchLdloc(out _), + i => i.MatchCall(typeof(QuickStacking), nameof(QuickStacking.Consolidate)) + )) + { + if (++count > 2) + { + throw new Exception($"More than two matches of {nameof(QuickStacking.Consolidate)}."); + } + + // Both targets are preceded by a jump instruction exiting the loop + var endInstruction = csr.Prev.Operand; + + csr.MoveAfterLabels(); + + // Load player ID (source.slots[0].Player.whoAmI) + // source + csr.Emit(OpCodes.Ldarg_0); + // ^.slots + csr.Emit(OpCodes.Ldfld, modder.GetFieldDefinition(() => default(QuickStacking.SourceInventory).slots)); + // ^[0] + csr.Emit(OpCodes.Ldc_I4_0); + csr.Emit(OpCodes.Ldelem_Any, typeof(PlayerItemSlotID.SlotReference)); + // ^.Player + csr.Emit(OpCodes.Ldfld, modder.GetFieldDefinition(() => default(PlayerItemSlotID.SlotReference).Player)); + // ^.whoAmI + csr.Emit(OpCodes.Ldfld, modder.GetFieldDefinition(() => default(Player)!.whoAmI)); + + // Load item + // NOTE: this is very fragile, but I can't think of a way to write it better without significantly bloating the code + csr.Emit(OpCodes.Ldloc_S, (byte)(count == 1 ? 6 : 9)); + + // Load chest index + // NOTE: this is very fragile, but I can't think of a way to write it better without significantly bloating the code + csr.Emit(OpCodes.Ldloc_S, (byte)(count == 1 ? 7 : 10)); + // ^.ChestIndex (property get) + csr.Emit(OpCodes.Callvirt, typeof(QuickStacking.DestinationHelper).GetProperty("ChestIndex")!.GetGetMethod()); + + // Call hook + csr.Emit(OpCodes.Call, modder.GetMethodDefinition(() => OTAPI.Hooks.Chest.InvokeQuickStack(default, default!, default))); + + // Continue if handled + csr.Emit(OpCodes.Brtrue, endInstruction); + + // Move after Consolidate call + csr.Goto(csr.Index + 3); + } + }); #else var csr = modder.GetILCursor(() => Terraria.Chest.PutItemInNearbyChest(null, default)); PutItemInNearbyChest = csr.Method; From 215bc346ce001d586603f822cf6514a09771a78f Mon Sep 17 00:00:00 2001 From: Arthri <41360489+a@users.noreply.github.com> Date: Wed, 28 Jan 2026 04:39:40 +0000 Subject: [PATCH 2/3] Fix references --- OTAPI.Scripts/Patches/HookChestQuickStack.Server.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OTAPI.Scripts/Patches/HookChestQuickStack.Server.cs b/OTAPI.Scripts/Patches/HookChestQuickStack.Server.cs index 7bf9a075..a047a533 100644 --- a/OTAPI.Scripts/Patches/HookChestQuickStack.Server.cs +++ b/OTAPI.Scripts/Patches/HookChestQuickStack.Server.cs @@ -78,7 +78,7 @@ static void HookChestQuickStack(ModFwModder modder) csr.Emit(OpCodes.Ldfld, modder.GetFieldDefinition(() => default(QuickStacking.SourceInventory).slots)); // ^[0] csr.Emit(OpCodes.Ldc_I4_0); - csr.Emit(OpCodes.Ldelem_Any, typeof(PlayerItemSlotID.SlotReference)); + csr.Emit(OpCodes.Ldelem_Any, modder.GetDefinition()); // ^.Player csr.Emit(OpCodes.Ldfld, modder.GetFieldDefinition(() => default(PlayerItemSlotID.SlotReference).Player)); // ^.whoAmI @@ -92,7 +92,7 @@ static void HookChestQuickStack(ModFwModder modder) // NOTE: this is very fragile, but I can't think of a way to write it better without significantly bloating the code csr.Emit(OpCodes.Ldloc_S, (byte)(count == 1 ? 7 : 10)); // ^.ChestIndex (property get) - csr.Emit(OpCodes.Callvirt, typeof(QuickStacking.DestinationHelper).GetProperty("ChestIndex")!.GetGetMethod()); + csr.Emit(OpCodes.Callvirt, modder.GetDefinition().Properties.Single(p => p.Name == "ChestIndex")!.GetMethod); // Call hook csr.Emit(OpCodes.Call, modder.GetMethodDefinition(() => OTAPI.Hooks.Chest.InvokeQuickStack(default, default!, default))); From 234a9a0492faf4d4b1e50de56035a640c28832d0 Mon Sep 17 00:00:00 2001 From: Arthri <41360489+a@users.noreply.github.com> Date: Wed, 28 Jan 2026 05:28:16 +0000 Subject: [PATCH 3/3] Fix hook --- .../Patches/HookChestQuickStack.Server.cs | 116 +++++++++++++----- 1 file changed, 85 insertions(+), 31 deletions(-) diff --git a/OTAPI.Scripts/Patches/HookChestQuickStack.Server.cs b/OTAPI.Scripts/Patches/HookChestQuickStack.Server.cs index a047a533..33809e3d 100644 --- a/OTAPI.Scripts/Patches/HookChestQuickStack.Server.cs +++ b/OTAPI.Scripts/Patches/HookChestQuickStack.Server.cs @@ -43,32 +43,25 @@ partial class ChestHooks static void HookChestQuickStack(ModFwModder modder) { #if TerrariaServer_1450_OrAbove || Terraria__1450_OrAbove || tModLoader_1450_OrAbove - // used for the out parameter only - List _blockedChests; - - // DO NOT use GetILCursor(). The instruction body does not have jumps transformed into labels, - // a process which is required to happen for the edits below to work. - var ctx = new ILContext(modder.GetMethodDefinition(() => QuickStacking.Transfer(default, default, out _blockedChests, false))); - ctx.Invoke((ctx) => { - var csr = new ILCursor(ctx); - int count = 0; - - while (csr.TryGotoNext( - MoveType.Before, - i => i.MatchLdarg(0), - i => i.MatchLdloc(out _), - i => i.MatchCall(typeof(QuickStacking), nameof(QuickStacking.Consolidate)) - )) - { - if (++count > 2) - { - throw new Exception($"More than two matches of {nameof(QuickStacking.Consolidate)}."); - } + // DO NOT use GetILCursor(). The instruction body does not have jumps transformed into labels, + // a process which is required to happen for the edits below to work. + var ctx = new ILContext(modder.GetMethodDefinition(() => QuickStacking.BuildDestinationMetricsAndStackItems(default, default, default))); - // Both targets are preceded by a jump instruction exiting the loop - var endInstruction = csr.Prev.Operand; + // Hooks onto quick stacking into existing slot + ctx.Invoke((ctx) => + { + var csr = new ILCursor(ctx); + ILLabel endLabel = null!; + csr.GotoNext( + MoveType.After, + i => i.MatchLdarg(1), + i => i.MatchLdcI4(1), + i => i.MatchStfld(typeof(QuickStacking.DestinationHelper), nameof(QuickStacking.DestinationHelper.transferBlocked)), + i => i.MatchBr(out endLabel) + ); + // AfterLabel is not the same as After + MoveAfterLabels for some reason csr.MoveAfterLabels(); // Load player ID (source.slots[0].Player.whoAmI) @@ -86,11 +79,10 @@ static void HookChestQuickStack(ModFwModder modder) // Load item // NOTE: this is very fragile, but I can't think of a way to write it better without significantly bloating the code - csr.Emit(OpCodes.Ldloc_S, (byte)(count == 1 ? 6 : 9)); + csr.Emit(OpCodes.Ldloc_S, (byte)3); // Load chest index - // NOTE: this is very fragile, but I can't think of a way to write it better without significantly bloating the code - csr.Emit(OpCodes.Ldloc_S, (byte)(count == 1 ? 7 : 10)); + csr.Emit(OpCodes.Ldarg_S, (byte)1); // ^.ChestIndex (property get) csr.Emit(OpCodes.Callvirt, modder.GetDefinition().Properties.Single(p => p.Name == "ChestIndex")!.GetMethod); @@ -98,12 +90,74 @@ static void HookChestQuickStack(ModFwModder modder) csr.Emit(OpCodes.Call, modder.GetMethodDefinition(() => OTAPI.Hooks.Chest.InvokeQuickStack(default, default!, default))); // Continue if handled - csr.Emit(OpCodes.Brtrue, endInstruction); + csr.Emit(OpCodes.Brfalse, endLabel); + }); + } + { + // used for the out parameter only + List _blockedChests; - // Move after Consolidate call - csr.Goto(csr.Index + 3); - } - }); + // DO NOT use GetILCursor(). The instruction body does not have jumps transformed into labels, + // a process which is required to happen for the edits below to work. + var ctx = new ILContext(modder.GetMethodDefinition(() => QuickStacking.Transfer(default, default, out _blockedChests, default))); + + // Hooks onto quick stacking overflowing into a new stack + ctx.Invoke((ctx) => + { + var csr = new ILCursor(ctx); + int count = 0; + + while (csr.TryGotoNext( + MoveType.Before, + i => i.MatchLdarg(0), + i => i.MatchLdloc(out _), + i => i.MatchCall(typeof(QuickStacking), nameof(QuickStacking.Consolidate)) + )) + { + if (++count > 2) + { + throw new Exception($"More than two matches of {nameof(QuickStacking.Consolidate)}."); + } + + // Both targets are preceded by a jump instruction exiting the loop + var endInstruction = csr.Prev.Operand; + + csr.MoveAfterLabels(); + + // Load player ID (source.slots[0].Player.whoAmI) + // source + csr.Emit(OpCodes.Ldarg_0); + // ^.slots + csr.Emit(OpCodes.Ldfld, modder.GetFieldDefinition(() => default(QuickStacking.SourceInventory).slots)); + // ^[0] + csr.Emit(OpCodes.Ldc_I4_0); + csr.Emit(OpCodes.Ldelem_Any, modder.GetDefinition()); + // ^.Player + csr.Emit(OpCodes.Ldfld, modder.GetFieldDefinition(() => default(PlayerItemSlotID.SlotReference).Player)); + // ^.whoAmI + csr.Emit(OpCodes.Ldfld, modder.GetFieldDefinition(() => default(Player)!.whoAmI)); + + // Load item + // NOTE: this is very fragile, but I can't think of a way to write it better without significantly bloating the code + csr.Emit(OpCodes.Ldloc_S, (byte)(count == 1 ? 6 : 9)); + + // Load chest index + // NOTE: this is very fragile, but I can't think of a way to write it better without significantly bloating the code + csr.Emit(OpCodes.Ldloc_S, (byte)(count == 1 ? 7 : 10)); + // ^.ChestIndex (property get) + csr.Emit(OpCodes.Callvirt, modder.GetDefinition().Properties.Single(p => p.Name == "ChestIndex")!.GetMethod); + + // Call hook + csr.Emit(OpCodes.Call, modder.GetMethodDefinition(() => OTAPI.Hooks.Chest.InvokeQuickStack(default, default!, default))); + + // Continue if handled + csr.Emit(OpCodes.Brfalse, endInstruction); + + // Move after Consolidate call + csr.Goto(csr.Index + 3); + } + }); + } #else var csr = modder.GetILCursor(() => Terraria.Chest.PutItemInNearbyChest(null, default)); PutItemInNearbyChest = csr.Method;