Full Tutorial Series

Modding Surviving Day! From Zero to Shipped Mod

A single-page course that takes you from an empty .cpp file to a working native mod — blocks, items, recipes, commands, GUI panels, NPCs, and every one of the engine's mod hooks explained with what fires them and what you can do in response. No prior engine knowledge assumed.

⚡ Native .so / .dll 🔵 C ABI, no game linkage ✅ C++17 🪟 Linux + Windows
Lesson 1.1

🗺️ How Mods Work


A mod is a native shared library — .so on Linux, .dll on Windows — that the engine loads at startup with dlopen (Linux) or LoadLibrary (Windows). It looks for one exported C function:

C++required export
extern "C" void mod_init(ModAPI* api);

The engine allocates a ModAPI struct, fills in about fifty engine-provided function pointers (things like getBlock, spawnNPC, giveItem), and passes it to your mod_init. Your job inside mod_init is two-fold:

  1. Call the engine functions you need — e.g. api->registerBlock(&def) — to register content.
  2. Fill in the mod-provided function pointers on the same struct — e.g. api->onBlockPlaced = myHandler; — so the engine can call back into your mod when something happens.

Because it's a flat C struct of function pointers, the ABI is compiler-agnostic: you can build with GCC, Clang, or MSVC and it'll still load correctly, as long as the struct layout matches field-for-field what the engine expects (more on this later — it's the single most common way mods break).

🧱

Content registration

Blocks, items, and crafting recipes — called once, during mod_init.

🔔

Event hooks

~30 callbacks covering blocks, players, NPCs, chat, weather, chunks, crafting, and multiplayer — the bulk of this tutorial.

🌍

World & entity queries

Read/write blocks, move NPCs, adjust player HP/mana/XP, manage inventory — call these any time from a hook.

💻

Terminal commands & GUI

Register commands players type into in-game Computer blocks, or draw a custom panel with onDraw.

Lesson 1.2

🛠️ Toolchain Setup


You need a C++17 compiler and one header file — include/ModLoader.h from the game's source tree. You do not link against the game binary or any other engine source file.

PlatformCompilerOutput
Linuxg++ or clang++.so
Windows (cross-compile from Linux)x86_64-w64-mingw32-g++.dll
Windows (native)MinGW/MSYS2 g++ or MSVC cl.exe.dll
💡
Two ways to depend on ModLoader.h

Either #include "ModLoader.h" and point -I at the game's include/ directory (pulls in the real struct — always correct), or vendor a copy of the structs into your own file if you want a zero-dependency single-file mod. If you vendor it, the field order, types, and count must match the engine's header exactly — see Gotchas for what happens when they drift.

Lesson 1.3

🚀 Your First Mod


A minimal mod that registers one block and logs a message whenever it's placed:

C++my_first_mod.cpp
#include "ModLoader.h"

static uint16_t gGlowStoneId = 0;
static ModAPI* gApi = nullptr;

static void onPlaced(const ModBlockEvent* ev) {
    if (ev->blockType == gGlowStoneId) {
        char buf[64];
        snprintf(buf, sizeof(buf), "Glowstone placed at %d,%d,%d",
                 ev->x, ev->y, ev->z);
        gApi->logMsg(buf);
    }
}

extern "C" void mod_init(ModAPI* api) {
    gApi = api;

    api->modName        = "My First Mod";
    api->modVersion     = "1.0";
    api->modDescription = "Adds a glowing block";

    ModBlockDef def{};
    def.name        = "Glowstone";
    def.isSolid     = true;
    def.texturePath = "";  // "" = engine picks a placeholder texture
    gGlowStoneId = api->registerBlock(&def);

    api->onBlockPlaced = onPlaced;
}

Compile it as a shared library:

ShellLinux build
g++ -shared -fPIC -std=c++17 \
    -o MyFirstMod.so my_first_mod.cpp \
    -I/path/to/VoxelGame/include
ℹ️
Registration IDs

registerBlock/registerItem return the runtime-assigned ID for your content — vanilla block IDs occupy the low range, so mod IDs start after BlockType::COUNT. Always store the returned ID (as this example does with gGlowStoneId) rather than hard-coding a number, since it can shift depending on load order of other installed mods.

Lesson 1.4

📦 Installing & Testing


PlatformPrimary mod pathFallback
Linux~/.config/VoxelGame/mods/*.so./mods/*.so
Windows%APPDATA%\VoxelGame\mods\*.dll.\mods\*.dll
  1. Copy the built library

    Drop MyFirstMod.so (or .dll) into the mods directory for your platform.

  2. Launch the game

    The modloader scans the directory on startup and calls mod_init for every library it finds. Check the game log for your mod's name to confirm it loaded.

  3. Find your block in creative

    Registered blocks appear automatically in the creative inventory — no extra registration step needed.

  4. Place it and watch the log

    Your logMsg calls show up in the game's console/log output in real time — this is your primary debugging tool since there's no attached debugger for a loaded .so by default.

⚠️
One mod, one crash, whole game down

Mods run in-process with no sandboxing. A null-pointer deref or out-of-bounds write in your onUpdate takes down the entire game the same as a bug in the engine itself. Guard every pointer argument, and treat this like writing engine code — because it is.

Lesson 2.1

🧱 Custom Blocks


FieldTypeMeaning
nameconst char*Display name shown in the creative inventory and tooltips.
isSolidboolWhether the block is a full solid cube for collision and culling purposes.
texturePathconst char*Path to a 48×16 PNG (top/side/bottom columns), or "" for a placeholder texture.
C++registering a block
ModBlockDef def{};
def.name        = "Reinforced Stone";
def.isSolid     = true;
def.texturePath = "textures/reinforced_stone.png";
uint16_t id = api->registerBlock(&def);
Lesson 2.2

⚔️ Custom Items


FieldTypeMeaning
nameconst char*Display name.
texturePathconst char*Atlas sprite path, or "" for placeholder.
maxStackfloatMax stack size — use 1 for tools/weapons that shouldn't stack.
damageintMelee damage if the item is wielded as a weapon; 0 means it isn't one.
C++registering a weapon item
ModItemDef sword{};
sword.name        = "Obsidian Blade";
sword.texturePath = "textures/obsidian_blade.png";
sword.maxStack    = 1;
sword.damage      = 14;
uint16_t swordId = api->registerItem(&sword);
Lesson 2.3

⚗️ Crafting Recipes


registerRecipe takes a shaped 3×3 grid pattern (9 block/item IDs, row-major, 0 = empty slot), an output type, an output count, and a display name.

C++shaped 3×3 recipe
uint16_t pattern[9] = {
    swordId, 0, 0,
    0, (uint16_t)BlockType::Stick, 0,
    0, 0, 0
};
api->registerRecipe(pattern, swordId, 1, "Obsidian Blade");
ℹ️
Vanilla + mod IDs mix freely

Pattern slots accept any registered block or item ID — vanilla BlockType values and your own mod-returned IDs are interchangeable in the same recipe.

Lesson 2.4

💻 Terminal Commands


registerCommand adds a command players can type into any in-game Computer block. Your handler receives the raw argument string and returns a heap or static string to print back to the terminal.

C++a simple command
static char* cmdHeal(const char* args) {
    gApi->setPlayerHP(100.f);
    static char buf[32] = "Healed to full.";
    return buf;
}

// In mod_init:
api->registerCommand("heal", cmdHeal);
⚠️
Return a stable pointer

Don't return a pointer to a stack-local buffer — use static storage, a heap allocation, or a string literal. The terminal reads the returned pointer after your function returns.

Lesson 2.5

🖼️ Custom GUI Panels


ModGuiPanel lets your mod draw a custom on-screen panel. The engine calls your onDraw every frame the panel is open (using the engine's own 2D draw calls), and onClose once when the player dismisses it.

C++registering and opening a panel
static void drawPanel(int w, int h) {
    // Issue engine draw calls here using w/h as the panel size in pixels
}
static void closePanel() { /* cleanup */ }

static ModGuiPanel gPanel { "Shop", 320, 240, drawPanel, closePanel };

// In mod_init:
api->registerGuiPanel("shop_panel", &gPanel);

// Later, e.g. from a command handler or NPC interact hook:
api->openGuiPanel("shop_panel");
api->closeGuiPanel("shop_panel");
Lesson 3.1

🌍 Reading & Writing Blocks


FunctionSignatureUse
getBlockuint16_t(int x,int y,int z)Read the block type at a world position. Safe to call any time.
setBlockvoid(int x,int y,int z,uint16_t type)Write a block. Safe from the main thread only.
getBlockGenuint16_t(int x,int y,int z)Gen-thread-safe read — use only inside onChunkGenerate.
writeBlockGenvoid(int x,int y,int z,uint16_t type)Gen-thread-safe write — use only inside onChunkGenerate.
spawnHologramvoid(float x,y,z,const char* text)Spawns a floating text hologram at a world position.
explodeAtvoid(float x,y,z,float radius,bool griefing)Triggers an explosion: damages nearby NPCs, fires onExplosion to all mods. Set griefing to also destroy terrain.
🧵
getBlockGen/writeBlockGen are not interchangeable with getBlock/setBlock

onChunkGenerate runs on a background world-generation thread. Calling regular getBlock/setBlock from there races with the main thread and can corrupt world state. Always use the *Gen variants inside that one hook, and never call them anywhere else.

Lesson 3.2

🐾 Spawning & Controlling NPCs


FunctionSignatureUse
spawnNPCint(int kind,float x,y,z)Spawns a mob by kind ID at a position; returns its npcIndex.
killNPCvoid(int npcIndex)Sets HP to 0 and marks it dead.
getNPCCountint()Total live NPC count — useful for bounding a loop over indices.
getNPCPos / setNPCPosvoid(int,float*,*,*) / void(int,float,float,float)Read or teleport an NPC's position.
getNPCHP / setNPCHPfloat(int) / void(int,float)Read or directly set an NPC's current HP.
setNPCCompanionvoid(int,bool)Marks an NPC as a player companion (follows, fights alongside).
C++spawn a tamed companion
int idx = api->spawnNPC(KIND_WOLF, px, py, pz);
if (idx >= 0) api->setNPCCompanion(idx, true);
⚠️
Indices can be reused

npcIndex refers to a live slot, not a stable entity ID. If you hold onto an index across frames, re-validate it against getNPCCount/getNPCHP before acting on it — a dead NPC's slot can be recycled by a later spawn.

Lesson 3.3

🧍 Player State API


FunctionSignatureUse
getPlayerPosvoid(float* x,y,z)Local player's eye position.
setPlayerPosvoid(float x,y,z)Teleports the local player and zeroes fall/impulse velocity so they don't immediately keep sliding.
getPlayerHP / setPlayerHPfloat() / void(float)Clamped to [0, maxHp] on write.
getPlayerMana / setPlayerManafloat() / void(float)Clamped to [0, maxMana] on write.
getSkillLevelint(int skill)Current level for a skill ID.
addXPvoid(int skill,float xp)Grants XP; triggers normal level-up logic (and onLevelUp) if it crosses a threshold.
getTotalLevelint()Sum of all skill levels.
💡
Position is camera-based

The engine has one first-person camera that doubles as the player's world position. getPlayerPos/setPlayerPos read and write that camera directly, including on teleport.

Lesson 3.4

🎒 Inventory API


FunctionSignatureUse
giveItemvoid(uint16_t itemType,int count)Adds items/blocks to the hotbar.
removeItemvoid(uint16_t itemType,int count)Removes up to count across hotbar then bag slots.
getItemCountint(uint16_t itemType)Total count across hotbar + bag.
ℹ️
Watch it fire onInventoryChange

Adding items via giveItem (or any in-game pickup) fires onInventoryChange to every loaded mod with the item's new total count — see Player Hooks. If your own mod reacts to that hook, make sure your handler doesn't call giveItem again for the same item, or you'll get infinite recursion.

Lesson 3.5

🔔 UI, Sound & Waypoints


FunctionSignatureUse
broadcastChatvoid(const char* message)Sends a chat/game-event message to all connected clients.
showTitlevoid(const char* l1,l2,float dur)Shows a two-line centered title card for dur seconds.
playSoundvoid(const char* soundId,float x,y,z)Plays one of the engine's built-in one-shot sounds — see the ID table below.
openGuiPanel / closeGuiPanelvoid(const char* panelId)Opens/closes a panel registered with registerGuiPanel.
createWaypointvoid(name,x,y,z,r,g,b)Adds a named, colored waypoint marker.
removeWaypointvoid(const char* name)Removes a waypoint by name.

playSound ID reference

playSound maps a small set of documented string IDs onto the engine's procedural audio system — it does not accept arbitrary sample files:

IDSound
"block_break"Generic block-break crunch
"block_place"Generic block-place thud
"pickup"Item pickup chime
"eat"Eating/consuming sound
"jump"Jump grunt
"splash"Water splash
"lava_sizzle"Lava sizzle
"melee_hit"Melee weapon connecting
"melee_crit"Melee critical hit
"victory"Short victory fanfare
"thunder"Lightning crack + rumble

Unrecognized IDs are silently ignored, so it's safe to call speculatively.

Lesson 3.6

🌤️ Time, Season & Weather


FunctionSignatureUse
getDayTimefloat()0.0–1.0, where 0 = midnight, 0.5 = noon.
getSeasonint()0=spring, 1=summer, 2=autumn, 3=winter.
getWeatherint()0=clear, 1=rain, 2=storm, 3=snow.
setWeathervoid(int weatherType)Forces the world's weather state.
Lesson 3.7

💾 Saving Mod Data


FunctionSignatureUse
saveDatavoid(const char* key,const char* value)Persists a string value under a key, scoped to your mod and the current save.
loadDataconst char*(const char* key)Reads back a previously saved value, or nullptr if unset.

This is intentionally a flat string key/value store — for structured data, serialize to JSON (or your own simple format) into a single string yourself.

C++persisting a counter across sessions
const char* raw = api->loadData("kills");
int kills = raw ? atoi(raw) : 0;
kills++;
char buf[16];
snprintf(buf, sizeof(buf), "%d", kills);
api->saveData("kills", buf);
Lesson 3.8

🌐 Multiplayer API


FunctionSignatureUse
isMultiplayerbool()True if connected to or hosting a multiplayer session.
isServerbool()True if this instance is the authoritative server (or singleplayer host).
sendModPacketvoid(const char* channel,const void* data,int len)Sends an arbitrary byte payload on a named channel to the other side; received via onModPacket.
getLocalUsernameconst char*()The local player's username.

See Multiplayer Hooks for the receiving side and lifecycle events (join/leave/server tick).

Lesson 4.1

🔁 Lifecycle Hooks


Every hook below is a field on the same ModAPI struct you fill in during mod_init. Leave any you don't need as nullptr — the engine checks before calling.

onUpdate
void (*)(float dt)
Called every frame on the main thread with delta-time in seconds. This is your general-purpose "tick" — timers, polling, animation.
Fires: once per rendered frame, client-side
onUnload
void (*)()
Called once when the mod is being unloaded (game shutdown or mod hot-reload). Release any resources you allocated.
Fires: once, at shutdown
Lesson 4.2

🧱 Block Hooks


onBlockPlaced
void (*)(const ModBlockEvent*)
Fired after any block is placed anywhere in the world. ev->x/y/z is the position, ev->blockType the new type. Placement cannot be cancelled from this hook.
Fires: on any block placement, including by other mods
onBlockBroken
void (*)(const ModBlockEvent*)
ev->blockType is the type that was there — the world position is already Air by the time this fires.
Fires: on any block break
onBlockInteract
void (*)(const ModBlockInteractEvent*)
Fired on right-click or E-key interaction with any block. ev->button: 0=left, 1=right, 2=E-key.
Fires: on player interact input against a block

Example — react only to your own block

C++block-specific break detection
static void onBroken(const ModBlockEvent* ev) {
    if (ev->blockType == gGlowStoneId) {
        // spawn particles, drop a custom item, etc.
    }
}
// api->onBlockBroken = onBroken;
Lesson 4.3

🧍 Player Hooks


onPlayerDamage
void (*)(ModDamageEvent*)
ev->hp is a pointer to the player's live HP float, ev->dmg is the incoming damage amount before it's subtracted. Write to *ev->hp or mutate the semantics you need — e.g. add back HP to simulate a shield, or leave it untouched to observe damage without changing it.
Fires: from every player-damage source — melee, fall, fire, lava, drowning, pressure, projectiles, and boss attacks — right before damage is applied
onPlayerDeath
void (*)()
Fired the instant player HP reaches 0.
Fires: once per death
onPlayerRespawn
void (*)()
Fired when the player respawns after death.
Fires: once per respawn
onPlayerLogin
void (*)(const char* username)
Fired when the local player first loads into the world (world load / join).
Fires: once per session start
onPlayerMove
void (*)(const ModPlayerPos*)
Fired on player position changes. Keep this handler cheap — it can fire very frequently.
Fires: up to once per frame while moving
onItemUsed
void (*)(const ModItemEvent*)
Fired when the player uses/consumes/activates an item. ev->itemType plus the target position.
Fires: on item-use input
onInventoryChange
void (*)(uint16_t itemType, int newCount)
Fired with the item's new total count (hotbar + bag) whenever it changes via the inventory add path.
Fires: on pickups and calls to giveItem
onLevelUp
void (*)(const ModLevelEvent*)
ev->skill and ev->newLevel. Fired for any skill, including via addXP calls from other mods.
Fires: when a skill crosses a level threshold
onFishCaught
void (*)(const ModFishEvent*)
Position plus itemCaught type ID.
Fires: on a successful fishing catch
onCropHarvest
void (*)(int x,int y,int z,uint16_t cropType)
Fired at the harvested crop's position with its block type.
Fires: on crop harvest

Example — 50% damage shield

C++onPlayerDamage handler
static void shieldDamage(ModDamageEvent* ev) {
    *ev->hp += ev->dmg * 0.5f;  // refund half the incoming damage
}
// api->onPlayerDamage = shieldDamage;
Lesson 4.4

💬 Chat Hook


onChatMessage
bool (*)(const char* user, const char* msg)
The only hook with a meaningful return value — return false to swallow the message (it won't be shown/broadcast), or true to let it through.
Fires: on every chat message, before delivery
C++simple profanity filter
static bool filterChat(const char* user, const char* msg) {
    if (strstr(msg, "badword")) return false;  // swallow it
    return true;
}
// api->onChatMessage = filterChat;
Lesson 4.5

🐾 NPC Hooks


onNPCDamage
void (*)(ModNPCEvent*)
Fired before damage is finalized on an NPC — mutate ev->hp for custom resistance, or read ev->npcIndex/kind to react per-species.
Fires: on any damage source hitting any NPC
onNPCDeath
void (*)(const ModNPCEvent*)
Fired once an NPC's HP reaches 0.
Fires: once per NPC death
onNPCSpawn
void (*)(const ModNPCEvent*)
Fired whenever an NPC spawns — including ones your own mod spawns via spawnNPC.
Fires: on any NPC spawn, from any source
onNPCInteract
void (*)(const ModNPCInteractEvent*)
Right-click or E-key on an NPC. ev->button: 1=right, 2=E-key.
Fires: on player interact input against an NPC
Lesson 4.6

🌦️ World Hooks


onWeatherChange
void (*)(const ModWeatherEvent*)
ev->weatherType: 0=clear, 1=rain, 2=storm, 3=snow.
Fires: whenever the world's weather transitions
onSeasonChange
void (*)(int newSeason)
0=spring, 1=summer, 2=autumn, 3=winter.
Fires: on season rollover
onExplosion
void (*)(const ModExplosionEvent*)
Position and radius of any explosion, including TNT and mod-triggered ones via explodeAt.
Fires: on TNT detonation and explodeAt calls
onChunkGenerate
void (*)(int chunkX,int chunkZ)
Runs on the world-generation thread, not the main thread. Use only getBlockGen/writeBlockGen inside this hook — regular block calls or any other engine API are unsafe here.
Fires: once per newly generated chunk, off the main thread
onChunkLoad
void (*)(int chunkX,int chunkZ)
Main-thread-safe notification that a chunk finished generating — queued from the gen thread and drained on the next frame, so it's safe to call any regular API here.
Fires: once per chunk, on the main thread, shortly after generation
onChunkUnload
void (*)(int chunkX,int chunkZ)
Fired right before a chunk's GPU resources are freed and it's removed from memory.
Fires: when a chunk is evicted (out of render distance)
🧵
onChunkGenerate is the one thread-unsafe hook

It's the only hook the engine calls from a background thread. Every other hook — including onChunkLoad — runs on the main thread and can safely call any engine API. Mixing this up is the single most common source of hard-to-reproduce crashes in world-gen mods.

Lesson 4.7

🏆 Progression Hooks


onAchievementUnlock
void (*)(int achIndex)
Fired with the unlocked achievement's index.
Fires: on any achievement unlock
onCraft
void (*)(const ModCraftEvent*)
ev->outputType and ev->count — the output is pulled from the recipe table, including mod-registered recipes.
Fires: when the player completes any craft
Lesson 4.8

🌐 Multiplayer Hooks


onPlayerJoin
void (*)(const char* username)
Fired on the server when a remote player joins.
Fires: server-side, per join
onPlayerLeave
void (*)(const char* username)
Fired on the server when a remote player disconnects.
Fires: server-side, per disconnect
onServerTick
void (*)(float dt)
Like onUpdate, but only fires on the server (or singleplayer host) — use this for authoritative game-logic that must not run duplicated on clients.
Fires: once per server tick, server-side only
onModPacket
void (*)(const char* channel,const void* data,int dataLen)
Receives payloads sent by the other side via sendModPacket. Use channel to route between multiple message types in the same mod.
Fires: on receipt of a mod packet on either side
⚠️
Every client must have the mod installed

There's no server-authoritative fallback for missing mods — if the host sends a mod packet or spawns mod-registered content and a connecting client doesn't have the same mod loaded, that client will desync or reject the unknown IDs. Version your mod and check compatibility in onPlayerJoin if this matters to you.

Lesson 5.1 · Project

🛡️ Project: Damage Shield Mod


Combines onPlayerDamage, getPlayerMana/setPlayerMana, and showTitle: while the player has mana, incoming damage is absorbed from mana instead of HP.

C++mana_shield.cpp
#include "ModLoader.h"

static ModAPI* gApi = nullptr;

static void onDamage(ModDamageEvent* ev) {
    float mana = gApi->getPlayerMana();
    if (mana <= 0.f) return;  // no mana left — full damage applies

    float absorbed = std::min(mana, ev->dmg);
    gApi->setPlayerMana(mana - absorbed);
    *ev->hp += absorbed;  // refund the absorbed portion

    if (absorbed > 0.f)
        gApi->showTitle("Shield Absorbed", "", 0.6f);
}

extern "C" void mod_init(ModAPI* api) {
    gApi = api;
    api->modName    = "Mana Shield";
    api->modVersion = "1.0";
    api->onPlayerDamage = onDamage;
}
Lesson 5.2 · Project

🐕 Project: Tameable Companion


Combines item registration, onItemUsed, spawnNPC/setNPCCompanion, and onNPCInteract — feed a wolf a custom treat item to tame it.

C++tame_wolf.cpp (excerpt)
static uint16_t gTreatId = 0;
static const int KIND_WOLF = 7;  // example MobKind value

static void onNPCInteract(const ModNPCInteractEvent* ev) {
    if (ev->kind != KIND_WOLF || ev->button != 2) return;  // only on E-key
    if (gApi->getItemCount(gTreatId) > 0) {
        gApi->removeItem(gTreatId, 1);
        gApi->setNPCCompanion(ev->npcIndex, true);
        gApi->showTitle("Wolf Tamed!", "", 1.5f);
    }
}
Lesson 5.3 · Project

💰 Project: Persistent Economy


Combines saveData/loadData, registerCommand, and onCraft — a per-save currency that's earned by crafting and spendable via a terminal command.

C++economy.cpp (excerpt)
static int getGold() {
    const char* raw = gApi->loadData("gold");
    return raw ? atoi(raw) : 0;
}
static void setGold(int g) {
    char buf[16]; snprintf(buf, sizeof(buf), "%d", g);
    gApi->saveData("gold", buf);
}

static void onCraft(const ModCraftEvent* ev) {
    setGold(getGold() + ev->count);  // 1 gold per crafted item
}

static char* cmdBalance(const char*) {
    static char buf[32];
    snprintf(buf, sizeof(buf), "Gold: %d", getGold());
    return buf;
}
Reference

⚠️ Threading & Gotchas


  • Struct layout drift is the #1 way mods break. ModAPI is a flat sequence of function pointers with no per-field versioning — only apiVersion at the top. If you vendor your own copy of the struct instead of including ModLoader.h directly, and your copy's field order, count, or types diverge even slightly from the engine's, every field after the divergence point silently reads as the wrong function pointer. This does not crash immediately — it corrupts behavior in ways that are hard to trace back to the cause. Always diff your vendored struct against the engine's include/ModLoader.h after any engine update, or just include the header directly.
  • onChunkGenerate runs off the main thread. It's the only hook that does. Use getBlockGen/writeBlockGen there, never regular getBlock/setBlock, and never touch any other engine API from that hook — including your own mod's shared state without a lock, if onUpdate also touches it.
  • onPlayerMove and onUpdate can fire very frequently. Keep these handlers allocation-free and branch-light; heavy work here is felt as frame stutter.
  • NPC indices are not stable IDs. Re-validate before acting on a stored npcIndex across frames.
  • Terminal command handlers must return stable pointers. Use static, heap, or literal storage — never a stack buffer.
  • Mods share one process with the engine and each other. There's no sandboxing — a crash in your mod is a crash of the whole game, and two mods registering conflicting global state (e.g. the same save-data key) can interfere with each other.
  • onChatMessage's return value matters. It's the only hook whose return value changes engine behavior — returning false swallows the message for every other mod and the player, not just yours.
Reference

🪟 Cross-Compiling for Windows


From Linux, using MinGW-w64

ShellLinux → Windows .dll
# Debian/Ubuntu
sudo apt install mingw-w64

x86_64-w64-mingw32-g++ -shared -fPIC -std=c++17 \
    -o mymod.dll mymod.cpp \
    -I/path/to/VoxelGame/include \
    -static-libgcc -static-libstdc++ \
    -Wl,--out-implib,mymod.lib

Native Windows — MSVC

ShellMSVC Developer Command Prompt
cl.exe /std:c++17 /LD /EHsc ^
    /I"C:\VoxelGame\include" ^
    mymod.cpp ^
    /Fe:mymod.dll

Native Windows — MSYS2/MinGW

ShellMSYS2 shell
g++ -shared -std=c++17 \
    -o mymod.dll mymod.cpp \
    -I/c/VoxelGame/include
⚠️
Static-link the MinGW runtime

If you cross-compile or use MinGW natively, either add -static-libgcc -static-libstdc++ or ship libstdc++-6.dll/libgcc_s_seh-1.dll alongside your .dll — players without MinGW installed will otherwise fail to load your mod with a missing-DLL error.

See the reference Modding Guide for the full ModAPI field table, permissions/roles, F3 admin console, portals, and world-gen documentation not covered in this tutorial.