using Microsoft.Xna.Framework; using MonoMod.Cil; using System; using System.Linq; using Terraria; using Terraria.ID; using Terraria.ModLoader; namespace ExampleMod.NPCs { /// /// This file shows off a critter npc. The unique thing about critters is how you can catch them with a bug net. /// The important bits are: Main.npcCatchable, npc.catchItem, and item.makeNPC /// We will also show off adding an item to an existing RecipeGroup (see ExampleMod.AddRecipeGroups) /// internal class ExampleCritterNPC : ModNPC { public override bool Autoload(ref string name) { IL.Terraria.Wiring.HitWireSingle += HookStatue; return base.Autoload(ref name); } /// /// Change the following code sequence in Wiring.HitWireSingle /// num145 = Utils.SelectRandom(Main.rand, new short[5] /// { /// 359, /// 359, /// 359, /// 359, /// 360, /// }); /// /// to /// /// var arr = new short[5] /// { /// 359, /// 359, /// 359, /// 359, /// 360, /// } /// arr = arr.ToList().Add(id).ToArray(); /// num145 = Utils.SelectRandom(Main.rand, arr); /// /// /// private void HookStatue(ILContext il) { // obtain a cursor positioned before the first instruction of the method // the cursor is used for navigating and modifying the il var c = new ILCursor(il); // the exact location for this hook is very complex to search for due to the hook instructions not being unique, and buried deep in control flow // switch statements are sometimes compiled to if-else chains, and debug builds litter the code with no-ops and redundant locals // in general you want to search using structure and function rather than numerical constants which may change across different versions or compile settings // using local variable indices is almost always a bad idea // we can search for // switch (*) // case 56: // Utils.SelectRandom * // in general you'd want to look for a specific switch variable, or perhaps the containing switch (type) { case 105: // but the generated IL is really variable and hard to match in this case // we'll just use the fact that there are no other switch statements with case 56, followed by a SelectRandom ILLabel[] targets = null; while (c.TryGotoNext(i => i.MatchSwitch(out targets))) { // some optimising compilers generate a sub so that all the switch cases start at 0 // ldc.i4.s 51 // sub // switch int offset = 0; if (c.Prev.MatchSub() && c.Prev.Previous.MatchLdcI4(out offset)) { ; } // not enough switch instructions if (targets.Length < 56 - offset) { continue; } var target = targets[56 - offset]; if (target == null) { continue; } // move the cursor to case 56: c.GotoLabel(target); // there's lots of extra checks we could add here to make sure we're at the right spot, such as not encountering any branching instructions c.GotoNext(i => i.MatchCall(typeof(Utils), nameof(Utils.SelectRandom))); // goto next positions us before the instruction we searched for, so we can insert our array modifying code right here c.EmitDelegate>(arr => { // resize the array and add our custom snail Array.Resize(ref arr, arr.Length+1); arr[arr.Length-1] = (short)npc.type; return arr; }); // hook applied successfully return; } // couldn't find the right place to insert throw new Exception("Hook location not found, switch(*) { case 56: ..."); } public override void SetStaticDefaults() { DisplayName.SetDefault("Lava Snail"); Main.npcFrameCount[npc.type] = Main.npcFrameCount[NPCID.GlowingSnail]; Main.npcCatchable[npc.type] = true; } public override void SetDefaults() { //npc.width = 14; //npc.height = 14; //npc.aiStyle = 67; //npc.damage = 0; //npc.defense = 0; //npc.lifeMax = 5; //npc.HitSound = SoundID.NPCHit1; //npc.DeathSound = SoundID.NPCDeath1; //npc.npcSlots = 0.5f; //npc.noGravity = true; //npc.catchItem = 2007; npc.CloneDefaults(NPCID.GlowingSnail); npc.catchItem = (short)mod.ItemType(); npc.lavaImmune = true; //npc.aiStyle = 0; npc.friendly = true; // We have to add this and CanBeHitByItem/CanBeHitByProjectile because of reasons. aiType = NPCID.GlowingSnail; animationType = NPCID.GlowingSnail; } public override bool? CanBeHitByItem(Player player, Item item) { return true; } public override bool? CanBeHitByProjectile(Projectile projectile) { return true; } public override float SpawnChance(NPCSpawnInfo spawnInfo) { return SpawnCondition.Underworld.Chance * 0.1f; } public override void HitEffect(int hitDirection, double damage) { if (npc.life <= 0) { for (int i = 0; i < 6; i++) { int dust = Dust.NewDust(npc.position, npc.width, npc.height, 200, 2 * hitDirection, -2f); if (Main.rand.NextBool(2)) { Main.dust[dust].noGravity = true; Main.dust[dust].scale = 1.2f * npc.scale; } else { Main.dust[dust].scale = 0.7f * npc.scale; } } Gore.NewGore(npc.position, npc.velocity, mod.GetGoreSlot("Gores/LavaSnailHead"), npc.scale); Gore.NewGore(npc.position, npc.velocity, mod.GetGoreSlot("Gores/LavaSnailShell"), npc.scale); } } public override Color? GetAlpha(Color drawColor) { // GetAlpha gives our Lava Snail a red glow. drawColor.R = 255; // both these do the same in this situation, using these methods is useful. drawColor.G = Utils.Clamp(drawColor.G, 175, 255); drawColor.B = Math.Min(drawColor.B, (byte)75); drawColor.A = 255; return drawColor; } public override bool PreAI() { // Usually we can use npc.wet, but aiStyle 67 prevents wet from being set. if (Collision.WetCollision(npc.position, npc.width, npc.height)) //if (npc.wet) { // These 3 lines instantly kill the npc without showing damage numbers, dropping loot, or playing DeathSound. Use this for instant deaths npc.life = 0; npc.HitEffect(); npc.active = false; Main.PlaySound(SoundID.NPCDeath16, npc.position); // plays a fizzle sound } return base.PreAI(); } // TODO: Hooks for Collision_MoveSnailOnSlopes and npc.aiStyle = 67 problem } internal class ExampleCritterItem : ModItem { public override void SetStaticDefaults() { DisplayName.SetDefault("Lava Snail"); } public override void SetDefaults() { //item.useStyle = 1; //item.autoReuse = true; //item.useTurn = true; //item.useAnimation = 15; //item.useTime = 10; //item.maxStack = 999; //item.consumable = true; //item.width = 12; //item.height = 12; //item.makeNPC = 360; //item.noUseGraphic = true; //item.bait = 15; item.CloneDefaults(ItemID.GlowingSnail); item.bait = 17; item.makeNPC = (short)mod.NPCType(); } } }