Tested Patterns¶
These patterns are extracted from the framework's bundled mods and are proven to work with Terraria 1.4.5.
For comprehensive Harmony documentation, see Harmony Basics.
Harmony Patching¶
Basic Postfix Patch¶
Run code after a method executes:
using HarmonyLib;
[HarmonyPatch(typeof(Terraria.Main), "DoUpdate")]
public static class DoUpdatePatch
{
[HarmonyPostfix]
public static void Postfix()
{
// Runs after every Main.DoUpdate() call
MyMod.OnUpdate();
}
}
Basic Prefix Patch¶
Run code before a method, optionally skip the original:
[HarmonyPatch(typeof(Terraria.Player), "Update")]
public static class PlayerUpdatePatch
{
[HarmonyPrefix]
public static bool Prefix(Player __instance)
{
// __instance is the Player being updated
// Return true to run original method
// Return false to skip it
return true;
}
}
Applying Patches¶
All patches must be applied manually using _harmony.Patch(). Attribute-based patches ([HarmonyPatch] attributes) are not auto-applied — they are only used in the Core framework itself. Mod patches must use the manual approach shown below.
Manual patches use the OnGameReady lifecycle hook. The injector calls this when Main.Initialize() completes:
private static Harmony _harmony;
private static ILogger _log;
public void Initialize(ModContext context)
{
_log = context.Logger;
}
public static void OnGameReady()
{
_harmony = new Harmony("com.yourname.yourmod");
try
{
var method = typeof(Terraria.Main).GetMethod("DoUpdate",
BindingFlags.NonPublic | BindingFlags.Instance);
_harmony.Patch(method, postfix: new HarmonyMethod(typeof(Mod), "MyPostfix"));
_log?.Info("Patches applied");
}
catch (Exception ex)
{
_log?.Error($"Patch failed: {ex.Message}");
}
}
public void Unload()
{
_harmony?.UnpatchAll("com.yourname.yourmod");
}
Input Handling¶
Detecting Key Press (Not Held)¶
Track previous state to detect the moment a key is pressed:
Note: This pattern uses XNA's
Keysenum directly viaMain.keyState. For keybind registration, use string key names instead (see Core API Reference).
private bool _wasKeyDown = false;
public void Update()
{
bool isKeyDown = Main.keyState.IsKeyDown(Keys.F);
if (isKeyDown && !_wasKeyDown)
{
// Key was just pressed this frame
OnKeyPressed();
}
_wasKeyDown = isKeyDown;
}
Checking Modifier Keys¶
bool ctrl = Main.keyState.IsKeyDown(Keys.LeftControl) ||
Main.keyState.IsKeyDown(Keys.RightControl);
bool shift = Main.keyState.IsKeyDown(Keys.LeftShift) ||
Main.keyState.IsKeyDown(Keys.RightShift);
bool alt = Main.keyState.IsKeyDown(Keys.LeftAlt) ||
Main.keyState.IsKeyDown(Keys.RightAlt);
if (ctrl && isKeyDown && !_wasKeyDown)
{
// Ctrl+Key pressed
}
Keybind Persistence¶
User keybinds are saved to TerrariaModder/core/keybinds.json:
Format: "modId.keybindId": "KeyCombo"
Keybinds persist automatically - no manual save needed.
Config/Keybind Baseline Checking¶
Check if settings changed since startup (used by ModMenu for "Restart Required"):
// Check if config values changed from what they were at startup
bool configChanged = _context.Config.HasChangesFromBaseline();
// Check if keybinds changed for a specific mod
bool keybindsChanged = KeybindManager.HasKeybindChangesFromBaseline(modId);
This allows detecting whether a restart is needed for mods that don't support hot reload.
Player and Inventory¶
Getting Local Player¶
Searching Inventory¶
Search all 59 slots (indices 0-58), not just hotbar (10 slots):
private int FindItemInInventory(Player player, int[] validTypes)
{
for (int i = 0; i < player.inventory.Length; i++)
{
Item item = player.inventory[i];
if (item != null && item.type != 0 &&
Array.IndexOf(validTypes, item.type) >= 0)
{
return i;
}
}
return -1;
}
// Usage: find recall potions, magic mirrors, etc.
int[] recallItems = { 50, 3124, 3199, 5437, 2350 };
int slot = FindItemInInventory(player, recallItems);
Using an Item Programmatically¶
When selectedItem is read-only, swap inventory slots:
private int _originalSlot = -1;
private int _itemSlot = -1;
private bool _needsRestore = false;
public void UseItemInSlot(Player player, int slot)
{
_originalSlot = player.selectedItem;
_itemSlot = slot;
// Swap items
Item temp = player.inventory[_originalSlot];
player.inventory[_originalSlot] = player.inventory[slot];
player.inventory[slot] = temp;
// Trigger use
player.controlUseItem = true;
_needsRestore = true;
}
// Call every frame to restore when done
public void UpdateRestore(Player player)
{
if (!_needsRestore) return;
if (player.itemAnimation > 0) return; // Still animating
// Swap back
Item temp = player.inventory[_originalSlot];
player.inventory[_originalSlot] = player.inventory[_itemSlot];
player.inventory[_itemSlot] = temp;
_needsRestore = false;
}
World and Tiles¶
World Position from Mouse¶
// Preferred: use Main.MouseWorld (handles zoom/coordinate transforms correctly)
int tileX = (int)(Main.MouseWorld.X / 16f);
int tileY = (int)(Main.MouseWorld.Y / 16f);
// Manual approach (only correct during Draw phase):
// float worldX = Main.screenPosition.X + Main.mouseX;
// float worldY = Main.screenPosition.Y + Main.mouseY;
// Note: Main.mouseX may be world-space during Update (after SetZoom_World).
// Use Main.MouseWorld or PlayerInput.MouseX for reliable results.
Checking Tile State¶
Tile tile = Main.tile[tileX, tileY];
// Check if solid
bool isSolid = tile.active() && Main.tileSolid[tile.type];
// Check if empty air
bool isEmpty = !tile.active();
// Check tile type (tile.type is ushort)
if (tile.active() && tile.type == TileID.Torches)
{
// It's a torch
}
Placing Tiles¶
// For items from inventory, use item.createTile and item.placeStyle
int tileType = item.createTile; // e.g., TileID.Torches (4)
int style = item.placeStyle; // e.g., 0=orange, 1=blue, 8=bone, etc.
bool placed = WorldGen.PlaceTile(tileX, tileY, tileType,
mute: false, // Play sound
forced: false, // Don't force placement
plr: Main.myPlayer,
style: style); // Item's place style for variants
if (placed)
{
// Consume item properly
item.stack--;
if (item.stack <= 0)
item.TurnToAir(); // Remove empty slot
// Sync in multiplayer
if (Main.netMode == NetmodeID.MultiplayerClient)
NetMessage.SendTileSquare(-1, tileX, tileY);
}
Scanning Nearby Tiles¶
private List<Point> FindNearbyFurniture(Player player, int tileType, int range)
{
var found = new List<Point>();
int centerX = (int)(player.Center.X / 16f);
int centerY = (int)(player.Center.Y / 16f);
for (int x = centerX - range; x <= centerX + range; x++)
{
for (int y = centerY - range; y <= centerY + range; y++)
{
if (x < 0 || x >= Main.maxTilesX ||
y < 0 || y >= Main.maxTilesY)
continue;
Tile tile = Main.tile[x, y];
if (tile.active() && tile.type == tileType)
{
found.Add(new Point(x, y));
}
}
}
return found;
}
Buffs¶
Applying a Buff¶
// Apply buff to player
// buffType: the buff ID
// duration: in frames (60 = 1 second)
player.AddBuff(buffType, duration);
Checking for Buff¶
bool hasBuff = false;
for (int i = 0; i < Player.MaxBuffs; i++)
{
if (player.buffType[i] == buffType && player.buffTime[i] > 0)
{
hasBuff = true;
break;
}
}
Chat Messages¶
Show Message in Chat¶
// Basic white text
Main.NewText("Hello!");
// Colored text (RGB)
Main.NewText("Warning!", 255, 200, 100); // Orange
// Common colors
Main.NewText("Success", 100, 255, 100); // Green
Main.NewText("Error", 255, 100, 100); // Red
Main.NewText("Info", 100, 200, 255); // Blue
UI Rendering¶
Using the Widget Library (Recommended)¶
The Widget Library provides pre-built UI components. Use DraggablePanel + StackLayout for most UIs:
using TerrariaModder.Core.UI;
using TerrariaModder.Core.UI.Widgets;
private DraggablePanel _panel;
private bool _featureEnabled;
public void Initialize(ModContext context)
{
_panel = new DraggablePanel("my-mod-panel", "Settings", 350, 250);
_panel.RegisterDrawCallback(DrawPanel);
context.RegisterKeybind("toggle", "Toggle UI", "Open settings", "F5", () => _panel.Toggle());
}
private void DrawPanel()
{
if (!_panel.BeginDraw()) return;
var layout = new StackLayout(_panel.ContentX, _panel.ContentY, _panel.ContentWidth, spacing: 6);
layout.SectionHeader("Options");
if (layout.Toggle("Feature", _featureEnabled)) _featureEnabled = !_featureEnabled;
if (layout.Button("Do Something")) { /* action */ }
_panel.EndDraw();
}
public void Unload()
{
_panel.UnregisterDrawCallback();
}
See Core API Reference - Widget Library for all available widgets.
Drawing with UIRenderer (Low-Level)¶
For custom drawing beyond what widgets provide:
using TerrariaModder.Core.UI;
void OnDraw()
{
// Filled rectangle (Color4 or RGBA)
UIRenderer.DrawRect(x, y, width, height, UIColors.PanelBg);
UIRenderer.DrawRect(x, y, width, height, r, g, b, alpha);
// Text
UIRenderer.DrawText("Hello", x, y, UIColors.Text);
// Mouse state
bool hovering = UIRenderer.IsMouseOver(x, y, width, height);
bool clicked = UIRenderer.MouseLeftClick;
int scroll = UIRenderer.ScrollWheel;
}
Subscribing to Draw Events¶
// Option 1: Widget Library (recommended) - register panel draw callback
_panel.RegisterDrawCallback(DrawPanel);
// Option 2: Manual event subscription (for HUD overlays, etc.)
public void Initialize(ModContext context)
{
FrameEvents.OnUIOverlay += OnDraw;
}
public void Unload()
{
FrameEvents.OnUIOverlay -= OnDraw;
}
Debug Logging¶
Throttled Logging¶
Avoid spamming logs every frame:
private int _logCounter = 0;
void Update()
{
_logCounter++;
if (_logCounter >= 300) // Every 5 seconds at 60fps
{
_log.Debug($"Status: {_processedCount} items processed");
_logCounter = 0;
}
}
Dump Inventory for Discovery¶
_log.Debug("=== Inventory Dump ===");
for (int i = 0; i < player.inventory.Length; i++)
{
Item item = player.inventory[i];
if (item != null && item.type != 0)
{
_log.Debug($"[{i}] type={item.type} name={item.Name} stack={item.stack}");
}
}
Reflection Patterns¶
Mods reference Terraria.exe and XNA directly — use types without reflection for all public members. Only use BindingFlags-based reflection for private members. See Core API Reference for the Game class helpers.
Direct Type Access¶
Mods have compile-time references to Terraria and XNA:
using Terraria;
using Terraria.ID;
using Microsoft.Xna.Framework;
// Use typeof() directly — no Type.GetType() needed
var updateMethod = typeof(Player).GetMethod("Update", new Type[] { typeof(int) });
var privateField = typeof(Main).GetField("_isAsyncLoadComplete",
BindingFlags.NonPublic | BindingFlags.Static);
// Public fields and methods: access directly
Player local = Main.LocalPlayer;
int health = local.statLife;
local.AddBuff(BuffID.Ironskin, 3600);
Vector2 mouseWorld = Main.MouseWorld;
Reflection for Private Members¶
Only use reflection when a member is private or internal:
// Private instance field
var ownerField = typeof(TagEffectState).GetField("_owner",
BindingFlags.NonPublic | BindingFlags.Instance);
int owner = (int)ownerField.GetValue(instance);
// Private static field
var loadCompleteField = typeof(Main).GetField("_isAsyncLoadComplete",
BindingFlags.NonPublic | BindingFlags.Static);
bool ready = (bool)loadCompleteField.GetValue(null);
Using the Game Class¶
TerrariaModder.Core.Reflection.Game provides convenience accessors for common state:
using TerrariaModder.Core.Reflection;
// State checks
if (Game.InWorld && !Game.InMenu)
{
Player player = Game.LocalPlayer;
Vector2 mouse = Game.MouseWorld;
bool dayTime = Game.IsDayTime;
}
// Tile access (bounds-checked)
Tile tile = Game.GetTile(tileX, tileY);
Vector2 worldPos = Game.TileToWorld(tileX, tileY);
// UI helpers
Game.BlockMouse();
Game.ShowMessage("Hello!", 255, 200, 100);
Input Blocking (Widget Library vs Manual)¶
Recommended: Use DraggablePanel from the Widget Library. It handles all input blocking, z-order, and click-through prevention automatically:
var panel = new DraggablePanel("my-panel", "Settings", 400, 300);
panel.RegisterDrawCallback(DrawPanel);
// That's it - input blocking, z-order, click-through prevention all handled.
Manual approach (for custom UIs without widgets):
private bool _modalOpen = false;
private const string PanelId = "my-mod-panel";
void OnDraw()
{
if (!_modalOpen) return;
int x = 100, y = 100, width = 400, height = 300;
// Register bounds every frame (handles position changes)
UIRenderer.RegisterPanelBounds(PanelId, x, y, width, height);
// Draw your panel...
UIRenderer.DrawPanel(x, y, width, height, UIColors.PanelBg);
// Check priority for multi-panel scenarios
if (UIRenderer.ShouldBlockForHigherPriorityPanel(PanelId))
return;
// Handle scroll - read then consume
int scroll = UIRenderer.ScrollWheel;
if (scroll != 0)
{
UIRenderer.ConsumeScroll(); // Prevent hotbar scrolling
}
// Handle clicks - consume to prevent propagation
if (UIRenderer.MouseLeftClick)
{
UIRenderer.ConsumeClick();
}
}
void CloseModal()
{
_modalOpen = false;
UIRenderer.UnregisterPanelBounds(PanelId);
}
The panel registration system:
- Automatically prevents inventory click-through via ItemSlot.Handle patch
- Blocks HUD buttons (Quick Stack, Bestiary, Sort, Smart Stack) via PlayerInput.IgnoreMouseInterface patch
- Sets Main.blockMouse and Player.mouseInterface
- Tracks multiple panels with dynamic z-order (click-to-focus)
- Clears player controls when modal is open
For private-member reflection patterns, see the SkipIntro Walkthrough (private static fields) or WhipStacking Walkthrough (private instance fields on internal types).