tModLoader Official tModLoader Help Thread

Is it possible to change your mod directory? I'd like to keep mine stored on ðe same external drive Terraria is installed to to save space.
 
I'm trying to add a sword that spawns a projectile on hit, but I can't seem to figure out how to get it to work.
Here's the code I have so far:
I would appreciate the help.
C#:
using Microsoft.Xna.Framework;
using EpicnessReloaded.Content.Projectiles;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.Audio;
using Terraria.GameContent.Bestiary;
using Terraria.GameContent.ItemDropRules;
using Terraria.Graphics.CameraModifiers;
using Terraria.ID;
using Terraria.ModLoader;

namespace EpicnessReloaded.Content.Items
{
    public class DarkWoodSword : ModItem
    {
        public override void SetDefaults()
        {
            Item.damage = 15;
            Item.DamageType = DamageClass.Melee;
            Item.width = 32;
            Item.height = 32;
            Item.useTime = 32;
            Item.useAnimation = 32;
            Item.useStyle = ItemUseStyleID.Swing;
            Item.knockBack = 5;
            Item.value = Item.buyPrice(silver: 1);
            Item.rare = ItemRarityID.Blue;
            Item.UseSound = SoundID.Item1;
            Item.autoReuse = true;
        }
        
        public override void OnHitNPC(Player player, NPC target, NPC.HitInfo hit, int damageDone) {
            var entitySource = player.GetSource_ItemUse(Item);
            Projectile.NewProjectile(entitySource, new Vector2(12, 0), 0, ModContent.ProjectileType<DarkBlast>(), 8, 0, Main.myPlayer);
            SoundEngine.PlaySound(SoundID.Item62.WithVolumeScale(0.5f), player.center);
        }

        public override void AddRecipes()
        {
            Recipe recipe = CreateRecipe();
            recipe.AddIngredient<Items.DarkWood>(15);
            recipe.AddTile(TileID.WorkBenches);
            recipe.Register();
        }
    }
}
 
I'm trying to add a sword that spawns a projectile on hit, but I can't seem to figure out how to get it to work.
Here's the code I have so far:
I would appreciate the help.
C#:
using Microsoft.Xna.Framework;
using EpicnessReloaded.Content.Projectiles;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.Audio;
using Terraria.GameContent.Bestiary;
using Terraria.GameContent.ItemDropRules;
using Terraria.Graphics.CameraModifiers;
using Terraria.ID;
using Terraria.ModLoader;

namespace EpicnessReloaded.Content.Items
{
    public class DarkWoodSword : ModItem
    {
        public override void SetDefaults()
        {
            Item.damage = 15;
            Item.DamageType = DamageClass.Melee;
            Item.width = 32;
            Item.height = 32;
            Item.useTime = 32;
            Item.useAnimation = 32;
            Item.useStyle = ItemUseStyleID.Swing;
            Item.knockBack = 5;
            Item.value = Item.buyPrice(silver: 1);
            Item.rare = ItemRarityID.Blue;
            Item.UseSound = SoundID.Item1;
            Item.autoReuse = true;
        }
       
        public override void OnHitNPC(Player player, NPC target, NPC.HitInfo hit, int damageDone) {
            var entitySource = player.GetSource_ItemUse(Item);
            Projectile.NewProjectile(entitySource, new Vector2(12, 0), 0, ModContent.ProjectileType<DarkBlast>(), 8, 0, Main.myPlayer);
            SoundEngine.PlaySound(SoundID.Item62.WithVolumeScale(0.5f), player.center);
        }

        public override void AddRecipes()
        {
            Recipe recipe = CreateRecipe();
            recipe.AddIngredient<Items.DarkWood>(15);
            recipe.AddTile(TileID.WorkBenches);
            recipe.Register();
        }
    }
}
Looks like you're spawning the projectile in the wrong spot. In the following line,
Projectile.NewProjectile(entitySource, new Vector2(12, 0), 0, ModContent.ProjectileType<DarkBlast>(), 8, 0, Main.myPlayer);
the 2nd argument is the position that the projectile will be spawned and you have 12, 0 which is near the upper left corner of the world. Try replacing 'new Vector2(12, 0)' with 'player.position'.
 
Looks like you're spawning the projectile in the wrong spot. In the following line,
Projectile.NewProjectile(entitySource, new Vector2(12, 0), 0, ModContent.ProjectileType<DarkBlast>(), 8, 0, Main.myPlayer);
the 2nd argument is the position that the projectile will be spawned and you have 12, 0 which is near the upper left corner of the world. Try replacing 'new Vector2(12, 0)' with 'player.position'.
Ok that seems to have fixed it, thank you!
 
Tmodloader says it's not on the newest version, despite the fact that IT LITERALLY IS. My mods will not work. Any help?
1729747379132.png
 
I Need Help!! I was playing T-Mod Loader when I migrated a journey Character with the same name as my Casual Character. It replaced my Casual Character, and I can't get it back. I have not backed up my game files. Can I find my character in my files or get it back?
 
I Need Help!! I was playing T-Mod Loader when I migrated a journey Character with the same name as my Casual Character. It replaced my Casual Character, and I can't get it back. I have not backed up my game files. Can I find my character in my files or get it back?
search your recycle bin, try to look at the backup file in documents > my games > terraria > modloader if 1.3, tmodloader in 1.4, Tmodloader for 1.4.4 (i might've switched those, you should find out which one it is through the worlds/players you created though) > players > backups, try seeing if the character is still in the "players" file, if yes remove it from there, change the journey character's name, then try putting it back and rebooting the game.
 
search your recycle bin, try to look at the backup file in documents > my games > terraria > modloader if 1.3, tmodloader in 1.4, Tmodloader for 1.4.4 (i might've switched those, you should find out which one it is through the worlds/players you created though) > players > backups, try seeing if the character is still in the "players" file, if yes remove it from there, change the journey character's name, then try putting it back and rebooting the game.
Thank You so much for your help, but I could not get my character back, unfortunately. It's a shame that some character that I didn't even play just destroyed my first real playthrough that I was doing. Thank You Though!
 
Last edited:
C#:
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using Terraria;
using Terraria.Audio;
using Terraria.DataStructures;
using Terraria.GameContent;
using Terraria.GameContent.Bestiary;
using Terraria.GameContent.ItemDropRules;
using Terraria.GameContent.Personalities;
using Terraria.GameContent.UI;
using Terraria.ID;
using Terraria.Localization;
using Terraria.ModLoader;
using Terraria.ModLoader.IO;
using Terraria.Utilities;

namespace ZephyrusNPCs.Content.NPCs
{
    // [AutoloadHead] and NPC.townNPC are extremely important and absolutely both necessary for any Town NPC to work at all.
    [AutoloadHead]
    public class Gunseller : ModNPC
    {
        public const string ShopName = "GunShop";

        private static int ShimmerHeadIndex;
        private static Profiles.StackedNPCProfile NPCProfile;

        public override void Load() {
            // Adds our Shimmer Head to the NPCHeadLoader.
            ShimmerHeadIndex = Mod.AddNPCHeadTexture(Type, Texture + "_Shimmer_Head");
        }

        public override void SetStaticDefaults() {
            Main.npcFrameCount[Type] = 25; // The total amount of frames the NPC has

            NPCID.Sets.ExtraFramesCount[Type] = 9; // Generally for Town NPCs, but this is how the NPC does extra things such as sitting in a chair and talking to other NPCs. This is the remaining frames after the walking frames.
            NPCID.Sets.AttackFrameCount[Type] = 4; // The amount of frames in the attacking animation.
            NPCID.Sets.DangerDetectRange[Type] = 500; // The amount of pixels away from the center of the NPC that it tries to attack enemies.
            NPCID.Sets.AttackType[Type] = 1; // The type of attack the Town NPC performs. 0 = throwing, 1 = shooting, 2 = magic, 3 = melee
            NPCID.Sets.AttackTime[Type] = 45; // The amount of time it takes for the NPC's attack animation to be over once it starts.
            NPCID.Sets.AttackAverageChance[Type] = 30; // The denominator for the chance for a Town NPC to attack. Lower numbers make the Town NPC appear more aggressive.
            NPCID.Sets.HatOffsetY[Type] = 4; // For when a party is active, the party hat spawns at a Y offset.
            NPCID.Sets.ShimmerTownTransform[NPC.type] = true; // This set says that the Town NPC has a Shimmered form. Otherwise, the Town NPC will become transparent when touching Shimmer like other enemies.

            NPCID.Sets.ShimmerTownTransform[Type] = true; // Allows for this NPC to have a different texture after touching the Shimmer liquid.

            // Influences how the NPC looks in the Bestiary
            NPCID.Sets.NPCBestiaryDrawModifiers drawModifiers = new NPCID.Sets.NPCBestiaryDrawModifiers() {
                Velocity = 1f, // Draws the NPC in the bestiary as if its walking +1 tiles in the x direction
                Direction = 1 // -1 is left and 1 is right. NPCs are drawn facing the left by default but ExamplePerson will be drawn facing the right
                // Rotation = MathHelper.ToRadians(180) // You can also change the rotation of an NPC. Rotation is measured in radians
                // If you want to see an example of manually modifying these when the NPC is drawn, see PreDraw
            };

            NPCID.Sets.NPCBestiaryDrawOffset.Add(Type, drawModifiers);

            // Set Example Person's biome and neighbor preferences with the NPCHappiness hook. You can add happiness text and remarks with localization (See an example in ExampleMod/Localization/en-US.lang).
            // NOTE: The following code uses chaining - a style that works due to the fact that the SetXAffection methods return the same NPCHappiness instance they're called on.
            NPC.Happiness
                .SetBiomeAffection<OceanBiome>(AffectionLevel.Like) // Example Person prefers the forest.
                .SetBiomeAffection<HallowBiome>(AffectionLevel.Dislike) // Example Person dislikes the snow.
                .SetBiomeAffection<JungleBiome>(AffectionLevel.Love) // Example Person likes the Example Surface Biome
                .SetNPCAffection(NPCID.Stylist, AffectionLevel.Love) // Loves living near the dryad.
                .SetNPCAffection(NPCID.ArmsDealer, AffectionLevel.Like) // Likes living near the guide.
                .SetNPCAffection(NPCID.Nurse, AffectionLevel.Dislike) // Dislikes living near the merchant.
                .SetNPCAffection(NPCID.GoblinTinkerer, AffectionLevel.Hate) // Hates living near the demolitionist.
            ; // < Mind the semicolon!

            // This creates a "profile" for ExamplePerson, which allows for different textures during a party and/or while the NPC is shimmered.
            NPCProfile = new Profiles.StackedNPCProfile(
                new Profiles.DefaultNPCProfile(Texture, NPCHeadLoader.GetHeadSlot(HeadTexture), Texture + "_Party"),
                new Profiles.DefaultNPCProfile(Texture + "_Shimmer", ShimmerHeadIndex, Texture + "_Shimmer_Party")
            );
        }

        public override void SetDefaults() {
            NPC.townNPC = true; // Sets NPC to be a Town NPC
            NPC.friendly = true; // NPC Will not attack player
            NPC.width = 18;
            NPC.height = 40;
            NPC.aiStyle = 7;
            NPC.damage = 10;
            NPC.defense = 25;
            NPC.lifeMax = 250;
            NPC.HitSound = SoundID.NPCHit1;
            NPC.DeathSound = SoundID.NPCDeath1;
            NPC.knockBackResist = 0.5f;

            AnimationType = NPCID.ArmsDealer;
        }

        public override void SetBestiary(BestiaryDatabase database, BestiaryEntry bestiaryEntry) {
            // We can use AddRange instead of calling Add multiple times in order to add multiple items at once
            bestiaryEntry.Info.AddRange(new IBestiaryInfoElement[] {
                // Sets the preferred biomes of this town NPC listed in the bestiary.
                // With Town NPCs, you usually set this to what biome it likes the most in regards to NPC happiness.
                BestiaryDatabaseNPCsPopulator.CommonTags.SpawnConditions.Biomes.Surface,

                // You can add multiple elements if you really wanted to
                // You can also use localization keys (see Localization/en-US.lang)
                new FlavorTextBestiaryInfoElement("Mods.ZephyrusNPCs.Bestiary.Gunseller")
            });
        }

        // The PreDraw hook is useful for drawing things before our sprite is drawn or running code before the sprite is drawn
        // Returning false will allow you to manually draw your NPC
        public override bool PreDraw(SpriteBatch spriteBatch, Vector2 screenPos, Color drawColor) {
            // This code slowly rotates the NPC in the bestiary
            // (simply checking NPC.IsABestiaryIconDummy and incrementing NPC.Rotation won't work here as it gets overridden by drawModifiers.Rotation each tick)
            if (NPCID.Sets.NPCBestiaryDrawOffset.TryGetValue(Type, out NPCID.Sets.NPCBestiaryDrawModifiers drawModifiers)) {
                drawModifiers.Rotation += 0.001f;

                // Replace the existing NPCBestiaryDrawModifiers with our new one with an adjusted rotation
                NPCID.Sets.NPCBestiaryDrawOffset.Remove(Type);
                NPCID.Sets.NPCBestiaryDrawOffset.Add(Type, drawModifiers);
            }

            return true;
        }

        public override void HitEffect(NPC.HitInfo hit) {
            int num = NPC.life > 0 ? 1 : 5;

            // Create gore when the NPC is killed.
            if (Main.netMode != NetmodeID.Server && NPC.life <= 0) {
                // Retrieve the gore types. This NPC has shimmer and party variants for head, arm, and leg gore. (12 total gores)
                string variant = "";
                if (NPC.IsShimmerVariant) variant += "_Shimmer";
                if (NPC.altTexture == 1) variant += "_Party";
                int hatGore = NPC.GetPartyHatGore();
                int headGore = Mod.Find<ModGore>($"{Name}_Gore{variant}_Head").Type;
                int armGore = Mod.Find<ModGore>($"{Name}_Gore{variant}_Arm").Type;
                int legGore = Mod.Find<ModGore>($"{Name}_Gore{variant}_Leg").Type;

                // Spawn the gores. The positions of the arms and legs are lowered for a more natural look.
                if (hatGore > 0) {
                    Gore.NewGore(NPC.GetSource_Death(), NPC.position, NPC.velocity, hatGore);
                }
                Gore.NewGore(NPC.GetSource_Death(), NPC.position, NPC.velocity, headGore, 1f);
                Gore.NewGore(NPC.GetSource_Death(), NPC.position + new Vector2(0, 20), NPC.velocity, armGore);
                Gore.NewGore(NPC.GetSource_Death(), NPC.position + new Vector2(0, 20), NPC.velocity, armGore);
                Gore.NewGore(NPC.GetSource_Death(), NPC.position + new Vector2(0, 34), NPC.velocity, legGore);
                Gore.NewGore(NPC.GetSource_Death(), NPC.position + new Vector2(0, 34), NPC.velocity, legGore);
            }
        }

        public override bool CanTownNPCSpawn(int numTownNPCs) { // Requirements for the town NPC to spawn.
            if (NPC.downedBoss2) {
                // If Example Person has spawned in this world before, we don't require the user satisfying the ExampleItem/ExampleBlock inventory conditions for a respawn.
                return true;
            }
            return false;
        }

        public override ITownNPCProfile TownNPCProfile() {
            return NPCProfile;
        }

        public override List<string> SetNPCNameList() {
            return new List<string>() {
                "Zayne",
                "Zippy",
                "Zeph",
                "Zak",
                "Emmett",
                "Sean",
                "Carl",
                "Ceasar",
                "Jason"
            };
        }

        public override void FindFrame(int frameHeight) {
            /*npc.frame.Width = 40;
            if (((int)Main.time / 10) % 2 == 0)
            {
                npc.frame.X = 40;
            }
            else
            {
                npc.frame.X = 0;
            }*/
        }

        public override string GetChat() {
            WeightedRandom<string> chat = new WeightedRandom<string>();

            int partyGirl = NPC.FindFirstNPC(NPCID.PartyGirl);
            if (partyGirl >= 0 && Main.rand.NextBool(4)) {
                chat.Add(Language.GetTextValue("Mods.ZephyrusNPCs.GunsellerPartyGirlDialogue", Main.npc[partyGirl].GivenName));
            }
            // These are things that the NPC has a chance of telling you when you talk to it.
            chat.Add(Language.GetTextValue("Mods.ZephyrusNPCs.GunsellerStandardDialogue1"));
            chat.Add(Language.GetTextValue("Mods.ZephyrusNPCs.GunsellerStandardDialogue2"));
            chat.Add(Language.GetTextValue("Mods.ZephyrusNPCs.GunsellerStandardDialogue3"));
            chat.Add(Language.GetTextValue("Mods.ZephyrusNPCs.GunsellerStandardDialogue4"));
            chat.Add(Language.GetTextValue("Mods.ZephyrusNPCs.GunsellerRareDialogue"), 0.1);

            string chosenChat = chat; // chat is implicitly cast to a string. This is where the random choice is made.

            return chosenChat;
        }

        public override void SetChatButtons(ref string button, ref string button2)
        {
            button = Language.GetTextValue("LegacyInterface.28");
        }

        public override void OnChatButtonClicked(bool firstButton, ref string shopName)
        {
            if (firstButton)
            {
                shopName = "GunShop";
            }
        }

        // Not completely finished, but below is what the NPC will sell
        public override void AddShops() {
            var npcShop = new NPCShop(Type, ShopName)
                .Add(new Item(ItemID.Revolver) { shopCustomPrice = 60000 })
                .Add(new Item(ItemID.Boomstick) { shopCustomPrice = 30000 })
                .Add(new Item(ItemID.Handgun) { shopCustomPrice = 50000 }, Condition.DownedSkeletron)
                .Add(new Item(ItemID.Shotgun) { shopCustomPrice = 100000 }, new Condition("", () => Main.hardMode))
                .Add(new Item(ItemID.Uzi) { shopCustomPrice = 150000 }, new Condition("", () => Main.hardMode))
                .Add(new Item(ItemID.TacticalShotgun) { shopCustomPrice = 500000 }, new Condition("", () => NPC.downedPlantBoss))
                .Add(new Item(ItemID.SniperRifle) { shopCustomPrice = 1000000 }, new Condition("", () => NPC.downedPlantBoss));

            npcShop.Register(); // Name of this shop tab
        }

        public override void ModifyActiveShop(string shopName, Item[] items) {
            foreach (Item item in items) {
                // Skip 'air' items and null items.
                if (item == null || item.type == ItemID.None) {
                    continue;
                }

                // If NPC is shimmered then reduce all prices by 50%.
                if (NPC.IsShimmerVariant) {
                    int value = item.shopCustomPrice ?? item.value;
                    item.shopCustomPrice = value / 2;
                }
            }
        }

        // Make this Town NPC teleport to the King and/or Queen statue when triggered. Return toKingStatue for only King Statues. Return !toKingStatue for only Queen Statues. Return true for both.
        public override bool CanGoToStatue(bool toKingStatue) => true;

        // Make something happen when the npc teleports to a statue. Since this method only runs server side, any visual effects like dusts or gores have to be synced across all clients manually.
        public override void OnGoToStatue(bool toKingStatue) {
            StatueTeleport();
        }

        // Create a square of pixels around the NPC on teleport.
        public void StatueTeleport() {
            for (int i = 0; i < 30; i++) {
                Vector2 position = Main.rand.NextVector2Square(-20, 21);
                if (Math.Abs(position.X) > Math.Abs(position.Y)) {
                    position.X = Math.Sign(position.X) * 20;
                }
                else {
                    position.Y = Math.Sign(position.Y) * 20;
                }
            }
        }

        public override void TownNPCAttackStrength(ref int damage, ref float knockback) {
            if (!Main.hardMode)
            {
                damage = 26;
            }
            if (Main.hardMode)
            {
                damage = 30;
            }
            knockback = 4f;
        }

        public override void TownNPCAttackCooldown(ref int cooldown, ref int randExtraCooldown) {
            cooldown = 10;
            randExtraCooldown = 5;
        }

        public override void TownNPCAttackProj(ref int projType, ref int attackDelay) {
            attackDelay = 14;
            if (!Main.hardMode)
            {
                projType = 14;
            }
            if (Main.hardMode)
            {
                attackDelay = 9;
                projType = 242;
            }
        }

        public override void DrawTownAttackGun(ref Texture2D item, ref Rectangle itemFrame, ref float scale, ref int horizontalHoldoutOffset)/* tModPorter Note: closeness is now horizontalHoldoutOffset, use 'horizontalHoldoutOffset = Main.DrawPlayerItemPos(1f, itemtype) - originalClosenessValue' to adjust to the change. See docs for how to use hook with an item type. */ //Allows you to customize how this town NPC's weapon is drawn when this NPC is shooting (this NPC must have an attack type of 1). Scale is a multiplier for the item's drawing size, item is the ID of the item to be drawn, and closeness is how close the item should be drawn to the NPC.
        {
            scale = 1f;
            horizontalHoldoutOffset = 20;
            if (!Main.hardMode)
            {
                item = TextureAssets.Item[ItemID.Handgun].Value;
            }
            if (Main.hardMode)
            {
                item = TextureAssets.Item[ItemID.Uzi].Value;
            }
        }

        public override void TownNPCAttackProjSpeed(ref float multiplier, ref float gravityCorrection, ref float randomOffset) {
            multiplier = 12f;
        }
    }
}
ShareX_1kyobaYLQs.png


Something is wrong with my code... I tried to draw NPC's gun, but it won't showing up. I need some help.
 
I think at some point I pressed deny instead of allow to a pop up and I don’t know how to revert it. please help!
 

Attachments

  • image.jpg
    image.jpg
    1.8 MB · Views: 20
I think at some point I pressed deny instead of allow to a pop up and I don’t know how to revert it. please help!
you need to go into your firewall settings and allow TML through

also, the print key exists, don't use your phone to take pictures of your monitor, it's 2025 :guidesad:
 
you need to go into your firewall settings and allow TML through

also, the print key exists, don't use your phone to take pictures of your monitor, it's 2025 :guidesad:
Win + shift + s works if you don’t want to take a picture of your whole monitor or don’t have a prtscrn key too
 
I clearly can see the error, try to disable One drive, so that Terraria stores your TML files on your PC.

No need to reprimand only because the photo of the monitor was taken with a phone, as long as the error is clearly visible.
 
C#:
using DestroyerTest.SwordLineage;
using System;
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.DataStructures;
using Terraria.Enums;
using Terraria.GameContent.ObjectInteractions;
using Terraria.ID;
using Terraria.Localization;
using Terraria.ModLoader;
using Terraria.ObjectData;

namespace DestroyerTest.Tiles
{
    public class DestroyerTestPlayer : ModPlayer
    {
        public int DashResourceCurrent { get; internal set; }

        public void OnKill(Player player, double damage, int hitDirection, bool pvp, PlayerDeathReason damageSource)
        {
            // 1/4 chance to replace gravestone with custom tile
            if (Main.rand.NextFloat() < 1.25f)
            {
                // Find the gravestone tile
                int gravestoneType = GetGravestoneType();
                if (gravestoneType != -1)
                {
                    // Replace the gravestone with the custom tile
                    ReplaceGravestoneWithCustomTile(gravestoneType);
                    CheckTilePlacement((int)(Player.position.X / 16f), (int)(Player.position.Y / 16f));
                }
            }
        }

        private int GetGravestoneType()
        {
            // List of gravestone types
            int[] gravestoneTypes = new int[]
            {
                TileID.Tombstones
            };

            // Find the gravestone type at the player's death position
            int x = (int)(Player.position.X / 16f);
            int y = (int)(Player.position.Y / 16f);
            Tile tile = Main.tile[x, y];
            if (tile != null && Array.Exists(gravestoneTypes, type => type == tile.TileType))
            {
                return tile.TileType;
            }

            return -1;
        }

        private void ReplaceGravestoneWithCustomTile(int gravestoneType)
        {
            int x = (int)(Player.position.X / 16f);
            int y = (int)(Player.position.Y / 16f);

            // Replace the gravestone with the custom tile
            WorldGen.PlaceTile(x, y, ModContent.TileType<Memoriam_Grave_Placed>(), true, true);

            // Check if the tile was placed correctly
            CheckTilePlacement(x, y);
        }

        private void CheckTilePlacement(int x, int y)
        {
            // Check if the tile was placed correctly
            if (Main.tile[x, y].TileType != ModContent.TileType<Memoriam_Grave_Placed>())
            {
                // Log a message if the tile was not placed correctly
                ModContent.GetInstance<DestroyerTest>().Logger.Warn("Failed to place Memoriam_Grave_Placed tile at (" + x + ", " + y + ").");
            }
        }
    }

    public class Memoriam_Grave_Placed : ModTile
    {
        public override void SetStaticDefaults()
        {
            Main.tileFrameImportant[Type] = true;
            Main.tileSolid[Type] = false;
            Main.tileLavaDeath[Type] = true;
            Main.tileNoSunLight[Type] = true;
            TileID.Sets.DisableSmartCursor[Type] = true;

            RegisterItemDrop(ModContent.ItemType<Memoriam>(), 0);

            AddMapEntry(new Color(200, 200, 200), Language.GetText("MapObject.Grave"));

            TileObjectData.newTile.Width = 4;
            TileObjectData.newTile.Height = 4;
            TileObjectData.newTile.Origin = new Point16(0, 0);
            TileObjectData.newTile.AnchorTop = new AnchorData(AnchorType.SolidTile, 0, 0);
            TileObjectData.newTile.AnchorBottom = new AnchorData(AnchorType.SolidTile, 4, 0);
            TileObjectData.newTile.UsesCustomCanPlace = true;
            TileObjectData.newTile.LavaDeath = true;
            TileObjectData.newTile.CoordinateHeights = new[] { 16, 16, 16, 16 };
            TileObjectData.newTile.CoordinateWidth = 16;
            TileObjectData.newTile.CoordinatePadding = 2;
            TileObjectData.newTile.StyleHorizontal = true;
            TileObjectData.newTile.StyleMultiplier = 2;
            TileObjectData.newTile.StyleWrapLimit = 2;
            TileObjectData.newTile.Direction = TileObjectDirection.PlaceRight;
            TileObjectData.newAlternate.CopyFrom(TileObjectData.newTile);
            TileObjectData.newAlternate.Origin = new Point16(0, 1);
            TileObjectData.addAlternate(0);
            TileObjectData.newAlternate.CopyFrom(TileObjectData.newTile);
            TileObjectData.newAlternate.Origin = new Point16(0, 2);
            TileObjectData.addAlternate(0);
            TileObjectData.newAlternate.CopyFrom(TileObjectData.newTile);
            TileObjectData.newAlternate.Origin = new Point16(1, 0);
            TileObjectData.newAlternate.AnchorTop = new AnchorData(AnchorType.SolidTile, 1, 1);
            TileObjectData.newAlternate.AnchorBottom = new AnchorData(AnchorType.SolidTile, 1, 1);
            TileObjectData.newAlternate.Direction = TileObjectDirection.PlaceLeft;
            TileObjectData.addAlternate(1);
            TileObjectData.newAlternate.CopyFrom(TileObjectData.newTile);
            TileObjectData.newAlternate.Origin = new Point16(1, 1);
            TileObjectData.newAlternate.AnchorTop = new AnchorData(AnchorType.SolidTile, 1, 1);
            TileObjectData.newAlternate.AnchorBottom = new AnchorData(AnchorType.SolidTile, 1, 1);
            TileObjectData.newAlternate.Direction = TileObjectDirection.PlaceLeft;
            TileObjectData.addAlternate(1);
            TileObjectData.newAlternate.CopyFrom(TileObjectData.newTile);
            TileObjectData.newAlternate.Origin = new Point16(1, 2);
            TileObjectData.newAlternate.AnchorTop = new AnchorData(AnchorType.SolidTile, 1, 1);
            TileObjectData.newAlternate.AnchorBottom = new AnchorData(AnchorType.SolidTile, 1, 1);
            TileObjectData.newAlternate.Direction = TileObjectDirection.PlaceLeft;
            TileObjectData.addAlternate(1);
            TileObjectData.addTile(Type);
        }

        public override bool HasSmartInteract(int i, int j, SmartInteractScanSettings settings)
        {
            return true;
        }

        public override void NumDust(int i, int j, bool fail, ref int num)
        {
            num = 1;
        }

        public override void MouseOver(int i, int j)
        {
            Player player = Main.LocalPlayer;
            player.noThrow = 2;
            player.cursorItemIconEnabled = true;
            player.cursorItemIconID = ModContent.ItemType<Memoriam>();
        }
    }
}

I have this custom tile that is supposed to have a 1/4 chance to replace a gravestone when the player is killed. Except it doesn't spawn. I have a debug line that reports a failure to spawn the tile on player death, and it never is reported, so I'm unsure of where it's going wrong.
 
C#:
using DestroyerTest.SwordLineage;
using System;
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.DataStructures;
using Terraria.Enums;
using Terraria.GameContent.ObjectInteractions;
using Terraria.ID;
using Terraria.Localization;
using Terraria.ModLoader;
using Terraria.ObjectData;

namespace DestroyerTest.Tiles
{
    public class DestroyerTestPlayer : ModPlayer
    {
        public int DashResourceCurrent { get; internal set; }

        public void OnKill(Player player, double damage, int hitDirection, bool pvp, PlayerDeathReason damageSource)
        {
            // 1/4 chance to replace gravestone with custom tile
            if (Main.rand.NextFloat() < 1.25f)
            {
                // Find the gravestone tile
                int gravestoneType = GetGravestoneType();
                if (gravestoneType != -1)
                {
                    // Replace the gravestone with the custom tile
                    ReplaceGravestoneWithCustomTile(gravestoneType);
                    CheckTilePlacement((int)(Player.position.X / 16f), (int)(Player.position.Y / 16f));
                }
            }
        }

        private int GetGravestoneType()
        {
            // List of gravestone types
            int[] gravestoneTypes = new int[]
            {
                TileID.Tombstones
            };

            // Find the gravestone type at the player's death position
            int x = (int)(Player.position.X / 16f);
            int y = (int)(Player.position.Y / 16f);
            Tile tile = Main.tile[x, y];
            if (tile != null && Array.Exists(gravestoneTypes, type => type == tile.TileType))
            {
                return tile.TileType;
            }

            return -1;
        }

        private void ReplaceGravestoneWithCustomTile(int gravestoneType)
        {
            int x = (int)(Player.position.X / 16f);
            int y = (int)(Player.position.Y / 16f);

            // Replace the gravestone with the custom tile
            WorldGen.PlaceTile(x, y, ModContent.TileType<Memoriam_Grave_Placed>(), true, true);

            // Check if the tile was placed correctly
            CheckTilePlacement(x, y);
        }

        private void CheckTilePlacement(int x, int y)
        {
            // Check if the tile was placed correctly
            if (Main.tile[x, y].TileType != ModContent.TileType<Memoriam_Grave_Placed>())
            {
                // Log a message if the tile was not placed correctly
                ModContent.GetInstance<DestroyerTest>().Logger.Warn("Failed to place Memoriam_Grave_Placed tile at (" + x + ", " + y + ").");
            }
        }
    }

    public class Memoriam_Grave_Placed : ModTile
    {
        public override void SetStaticDefaults()
        {
            Main.tileFrameImportant[Type] = true;
            Main.tileSolid[Type] = false;
            Main.tileLavaDeath[Type] = true;
            Main.tileNoSunLight[Type] = true;
            TileID.Sets.DisableSmartCursor[Type] = true;

            RegisterItemDrop(ModContent.ItemType<Memoriam>(), 0);

            AddMapEntry(new Color(200, 200, 200), Language.GetText("MapObject.Grave"));

            TileObjectData.newTile.Width = 4;
            TileObjectData.newTile.Height = 4;
            TileObjectData.newTile.Origin = new Point16(0, 0);
            TileObjectData.newTile.AnchorTop = new AnchorData(AnchorType.SolidTile, 0, 0);
            TileObjectData.newTile.AnchorBottom = new AnchorData(AnchorType.SolidTile, 4, 0);
            TileObjectData.newTile.UsesCustomCanPlace = true;
            TileObjectData.newTile.LavaDeath = true;
            TileObjectData.newTile.CoordinateHeights = new[] { 16, 16, 16, 16 };
            TileObjectData.newTile.CoordinateWidth = 16;
            TileObjectData.newTile.CoordinatePadding = 2;
            TileObjectData.newTile.StyleHorizontal = true;
            TileObjectData.newTile.StyleMultiplier = 2;
            TileObjectData.newTile.StyleWrapLimit = 2;
            TileObjectData.newTile.Direction = TileObjectDirection.PlaceRight;
            TileObjectData.newAlternate.CopyFrom(TileObjectData.newTile);
            TileObjectData.newAlternate.Origin = new Point16(0, 1);
            TileObjectData.addAlternate(0);
            TileObjectData.newAlternate.CopyFrom(TileObjectData.newTile);
            TileObjectData.newAlternate.Origin = new Point16(0, 2);
            TileObjectData.addAlternate(0);
            TileObjectData.newAlternate.CopyFrom(TileObjectData.newTile);
            TileObjectData.newAlternate.Origin = new Point16(1, 0);
            TileObjectData.newAlternate.AnchorTop = new AnchorData(AnchorType.SolidTile, 1, 1);
            TileObjectData.newAlternate.AnchorBottom = new AnchorData(AnchorType.SolidTile, 1, 1);
            TileObjectData.newAlternate.Direction = TileObjectDirection.PlaceLeft;
            TileObjectData.addAlternate(1);
            TileObjectData.newAlternate.CopyFrom(TileObjectData.newTile);
            TileObjectData.newAlternate.Origin = new Point16(1, 1);
            TileObjectData.newAlternate.AnchorTop = new AnchorData(AnchorType.SolidTile, 1, 1);
            TileObjectData.newAlternate.AnchorBottom = new AnchorData(AnchorType.SolidTile, 1, 1);
            TileObjectData.newAlternate.Direction = TileObjectDirection.PlaceLeft;
            TileObjectData.addAlternate(1);
            TileObjectData.newAlternate.CopyFrom(TileObjectData.newTile);
            TileObjectData.newAlternate.Origin = new Point16(1, 2);
            TileObjectData.newAlternate.AnchorTop = new AnchorData(AnchorType.SolidTile, 1, 1);
            TileObjectData.newAlternate.AnchorBottom = new AnchorData(AnchorType.SolidTile, 1, 1);
            TileObjectData.newAlternate.Direction = TileObjectDirection.PlaceLeft;
            TileObjectData.addAlternate(1);
            TileObjectData.addTile(Type);
        }

        public override bool HasSmartInteract(int i, int j, SmartInteractScanSettings settings)
        {
            return true;
        }

        public override void NumDust(int i, int j, bool fail, ref int num)
        {
            num = 1;
        }

        public override void MouseOver(int i, int j)
        {
            Player player = Main.LocalPlayer;
            player.noThrow = 2;
            player.cursorItemIconEnabled = true;
            player.cursorItemIconID = ModContent.ItemType<Memoriam>();
        }
    }
}

I have this custom tile that is supposed to have a 1/4 chance to replace a gravestone when the player is killed. Except it doesn't spawn. I have a debug line that reports a failure to spawn the tile on player death, and it never is reported, so I'm unsure of where it's going wrong.
A good way to find where problems lie is to put Main.NewText("descriptive text"); as the first line inside a if statement or function. Replace "descriptive text" with something that describes what is supposed to happen there. This will output text to chat and is a good way to check if your code is being reached. Also, I'm fairly sure that Main.rand.NextFloat() generates a number between 0 and 1, so it will always be below 1.25f.
 
C#:
public void OnKill(Player player, double damage, int hitDirection, bool pvp, PlayerDeathReason damageSource)
        {
            // Original chance is meant to be 1/1000, but as per suggestion, it has been set to 100% chance while still being a random check.
            if (Main.rand.Next() == 0)
            {
                Main.NewText("Gracestone Replaced");
                // Find the gravestone tile
                int gravestoneType = GetGravestoneType();
                if (gravestoneType != -1)
                {
                    // Replace the gravestone with the custom tile
                    ReplaceGravestoneWithCustomTile(gravestoneType);
                    CheckTilePlacement((int)(Player.position.X / 16f), (int)(Player.position.Y / 16f));
                }
            }
        }
(Note: This may not have been the correct implementation of the logging suggestion. My goal with this mod was to learn C# as I went along, and I'm starting from basically nothing in terms of C# knowledge. I could ramble about why the heck I didn't take the time for fundamental things before jumping into mod making, but that would veer far off topic. Short answer: ADHD)
- - -
After implementing those and not receiving any messages in chat, I have one remaining hypothesis. I may need to create a projectile for the custom gravestone, since the vanilla gravestones spawn as projectiles before becoming tiles when proper conditions are met.
 
C#:
public void OnKill(Player player, double damage, int hitDirection, bool pvp, PlayerDeathReason damageSource)
        {
            // Original chance is meant to be 1/1000, but as per suggestion, it has been set to 100% chance while still being a random check.
            if (Main.rand.Next() == 0)
            {
                Main.NewText("Gracestone Replaced");
                // Find the gravestone tile
                int gravestoneType = GetGravestoneType();
                if (gravestoneType != -1)
                {
                    // Replace the gravestone with the custom tile
                    ReplaceGravestoneWithCustomTile(gravestoneType);
                    CheckTilePlacement((int)(Player.position.X / 16f), (int)(Player.position.Y / 16f));
                }
            }
        }
(Note: This may not have been the correct implementation of the logging suggestion. My goal with this mod was to learn C# as I went along, and I'm starting from basically nothing in terms of C# knowledge. I could ramble about why the heck I didn't take the time for fundamental things before jumping into mod making, but that would veer far off topic. Short answer: ADHD)
- - -
After implementing those and not receiving any messages in chat, I have one remaining hypothesis. I may need to create a projectile for the custom gravestone, since the vanilla gravestones spawn as projectiles before becoming tiles when proper conditions are met.
Looking at this again, you make a good point. On kill happens on the moment of death, but the gravestone projectile takes a few seconds to appear so modifying the OnKill hook won't ever replace the gravestone. You'd either need to either block the gravestone projectile from spawning and instead create your own that always turns into your custom gravestone OR do nothing in OnKill and instead modify the gravestone projectile to have a chance to become your custom gravestone. I'm rusty with tModLoader and can't suggest which would be easier.

Also, Main.rand.Next() generates a 32 bit number from 0 to 1 which will look something like this: 0.037317983. The odds of it generating exactly zero are exceedingly slim. A better way to guarantee that something inside an if statement will always happen is to comment it out entirely. In other words, put // before the 'if'.
 
Back
Top Bottom