/// <summary> /// Wraps the current method with begin/end callbacks. /// </summary> /// <remarks> /// This will rename the current method and replace it with a new method that will take its place. /// In the new method it will call the callbacks and perform canceling on the begin callback if required.</remarks> /// <param name="current">The current method to be wrapped</param> /// <param name="beginCallback">The callback to be executed at the start of the method</param> /// <param name="endCallback">The optional end callback that will be executed at the end of the method</param> /// <param name="beginIsCancellable">Indicates that the begin callback can cancel the method execution</param> /// <param name="noEndHandling">Indicates to only return from the method and not do any special popping and so on</param> /// <param name="allowCallbackInstance">Indicates that the callbacks expect an instance parameter at the first parameter index</param> /// <returns></returns> public static MethodDefinition Wrap ( this MethodDefinition current, MethodReference beginCallback, MethodReference endCallback, bool beginIsCancellable, bool noEndHandling, bool allowCallbackInstance, TypeReference overrideReturnType = null ) { if (!current.HasBody) throw new InvalidOperationException("Method must have a body."); //This method has only yet been tested on void & string return type methods. if (new[] { current.Module.TypeSystem.Void, current.Module.TypeSystem.String, current.Module.TypeSystem.Double, }.Contains(current.ReturnType) || (overrideReturnType != null && current.ReturnType.Name == overrideReturnType.Name)) { //Create the new replacement method that will take place of the current method. //So we must ensure we clone to meet the signatures. var wrapped = new MethodDefinition(current.Name, current.Attributes, current.ReturnType); var instanceMethod = (current.Attributes & MethodAttributes.Static) == 0; //Rename the existing method, and replace all references to it so that the new //method receives the calls instead. current.Name = current.Name + WrappedMethodNameSuffix; //If we are renaming a virtual method, it does not need to be virtual anymore //as we want exclusive control over it. current.IsVirtual = false; //Finally replace all instances of the current method with the wrapped method //that is about to be generated current.ReplaceWith(wrapped); //Clone the parameters for the new method if (current.HasParameters) { foreach (var prm in current.Parameters) { wrapped.Parameters.Add(prm); } } //Place the new method in the declaring type of the method we are cloning current.DeclaringType.Methods.Add(wrapped); //Generate the il that will call and handle the begin callback. var beginResult = wrapped.EmitBeginCallback(beginCallback, instanceMethod, allowCallbackInstance, beginIsCancellable); //Emit the il that will execute the actual method that was renamed earlier var insFirstForMethod = wrapped.EmitMethodCallback(current, instanceMethod, current.ReturnType.Name != current.Module.TypeSystem.String.Name); //If the begin callback is cancelable, the EmitBeginCallback method will have left a Nop //instruction so we can direct where to continue on to, rather than exiting the method. if (beginIsCancellable && beginResult != null && beginResult.OpCode == OpCodes.Nop) { if (current.ReturnType.Name == current.Module.TypeSystem.Void.Name) { beginResult.OpCode = OpCodes.Brtrue_S; beginResult.Operand = insFirstForMethod; } else if (current.ReturnType.Name == current.Module.TypeSystem.String.Name) { beginResult.OpCode = OpCodes.Brtrue; beginResult.Operand = insFirstForMethod; } else if (current.ReturnType.Name == current.Module.TypeSystem.Double.Name) { beginResult.OpCode = OpCodes.Brtrue; beginResult.Operand = insFirstForMethod; } else if (overrideReturnType != null && current.ReturnType.Name == overrideReturnType.Name) { beginResult.OpCode = OpCodes.Brtrue; beginResult.Operand = insFirstForMethod; } } //If a end callback is specified then we can also emit the callback to it as well if (endCallback != null) { wrapped.EmitEndCallback(endCallback, instanceMethod, allowCallbackInstance); } //The custom method is now fully generated, so we can now complete the il for exiting. wrapped.EmitMethodEnding(noEndHandling); return wrapped; } else throw new NotSupportedException("Non Void methods not yet supported"); }
public override void Run() { foreach (var newItem in SourceDefinition.Type("Terraria.Item").Methods.Where( x => x.Name == "NewItem" )) { //In this patch we create a custom DropLoot method that will be the receiver //of all Item.NewItem calls in NPCLoot. var typeNpc = this.Type<Terraria.NPC>(); //Create the new DropLoot call in the Terraria.NPC class var dropLoot = new MethodDefinition("DropLoot", MethodAttributes.Public | MethodAttributes.Static, newItem.ReturnType); typeNpc.Methods.Add(dropLoot); dropLoot.Body.InitLocals = true; var il = dropLoot.Body.GetILProcessor(); //Clone the parameters from the Item.NewItem method (with no byreference) foreach (var prm in newItem.Parameters) dropLoot.Parameters.Add(prm); dropLoot.Parameters.Add(new ParameterDefinition(typeNpc) { Name = "npc", IsOptional = true, HasDefault = true, Constant = null }); //Collect the hooks var cbkBegin = ModificationDefinition.Type("OTAPI.Callbacks.Terraria.Npc").Method( "DropLootBegin", parameters: dropLoot.Parameters, skipMethodParameters: 1, substituteByRefs: true ); var cbkEnd = ModificationDefinition.Type("OTAPI.Callbacks.Terraria.Npc").Method( "DropLootEnd", parameters: dropLoot.Parameters, skipMethodParameters: 0, substituteByRefs: true ); //Create the value to hold the new item id var vrbItemId = new VariableDefinition("otaItem", (cbkBegin.Parameters[0].ParameterType as ByReferenceType).ElementType); dropLoot.Body.Variables.Add(vrbItemId); il.Emit(OpCodes.Ldloca_S, vrbItemId); //Loads our variable by reference so our callback and alter it. var beginResult = dropLoot.EmitBeginCallback(cbkBegin, false, false, false, parameterOffset: 1); //Inject the begin call var insFirstForMethod = dropLoot.EmitMethodCallback(newItem, false, false); //Store the result into our new variable il.Emit(OpCodes.Stloc, vrbItemId); //Set the vanilla instruction to be resumed upon continuation of the begin hook. if (beginResult != null && beginResult.OpCode == OpCodes.Pop) { beginResult.OpCode = OpCodes.Brtrue_S; beginResult.Operand = insFirstForMethod; //Emit the cancellation return IL il.InsertAfter(beginResult, il.Create(OpCodes.Ret)); il.InsertAfter(beginResult, il.Create(OpCodes.Ldloc, vrbItemId)); } //Inject the end callback dropLoot.EmitEndCallback(cbkEnd, false, false); //Emit the return value using the result variable we injected il.Emit(OpCodes.Ldloc, vrbItemId); il.Emit(OpCodes.Ret); } }