Developer Documentation

Modding Guide

Build native shared-library mods that extend VoxelGame with new blocks, crafting recipes, terminal commands, and event hooks — all via a clean C ABI. Supports Linux .so and Windows .dll.

⚡ Native .so / .dll 🔵 C ABI ✅ C++17 📦 Auto-loaded 🪟 Cross-platform

🗺️ Overview


VoxelGame's modloader scans two directories on startup and loads every .so (Linux) or .dll (Windows) it finds. Each mod exports a single C function — mod_init — which receives a pointer to a ModAPI struct pre-filled with engine callbacks.

🧱

New Block Types

Register blocks with custom names, solidity, and a texture PNG. They appear in the creative inventory automatically. IDs start at 342 (BlockType::COUNT).

⚗️

Crafting Recipes

Add shaped 3×3 recipes using any block IDs — vanilla or your own. Recipes are checked at craft-time.

💻

Terminal Commands

Register new commands that players can run on any in-game Computer block.

🔔

Event Hooks

React to blocks being placed or broken anywhere in the world, or intercept player damage to modify HP.

🛡️

Permissions System

Six role tiers (Owner → Banned) enforced server-side. Persisted to saves/roles.json. Accessible via the F3 admin console.

🌍

Portals & Gateways

WarpPortal A/B, Portal A/B, and Gateway blocks. Gateways open entirely separate procedurally-generated pocket worlds with return shrines.

ℹ️
Platform

Mods are native shared libraries — .so on Linux, .dll on Windows. The C ABI makes them compiler-agnostic. Your only required export is extern "C" void mod_init(ModAPI*). See Cross-Compiling for Windows build instructions.

🚀 Quick Start


The fastest way to get a mod running — a block that logs when placed.

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

extern "C" void mod_init(ModAPI* api) {

    // ── Identity ──────────────────────────────────────────
    api->modName    = "My First Mod";
    api->modVersion = "1.0";

    // ── Register a block ─────────────────────────────────
    ModBlockDef stone {
        "Neon Stone",   // display name
        true,           // isSolid
        ""              // no custom texture (uses fallback)
    };
    uint16_t stoneId = api->registerBlock(&stone);

    // ── Event hook: log every block placement ────────────
    api->onBlockPlaced = [](const ModBlockEvent* ev) {
        // handler must be a plain function pointer, not a capturing lambda
    };
    api->onBlockBroken  = nullptr;
    api->onPlayerDamage = nullptr;

    api->logMsg("My First Mod loaded!");
}
Shell Build & Install
# Compile as a shared library
g++ -shared -fPIC -std=c++17 \
    -o my_first_mod.so my_first_mod.cpp \
    -Iinclude

# Install to the primary mod directory
mkdir -p ~/.config/VoxelGame/mods
cp my_first_mod.so ~/.config/VoxelGame/mods/

# Launch the game — you should see in stderr:
# [ModLoader] Loaded: My First Mod v1.0

📁 Mod Structure


Every mod is a single .so file. It must export exactly one symbol:

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

Inside mod_init you must do three things:

  • Set api->modName and api->modVersion
  • Set each callback pointer (onBlockPlaced, onBlockBroken, onPlayerDamage) to a function or nullptr
  • Call any api->register* functions you need
⚠️
No capturing lambdas for callbacks

The callback fields in ModAPI are raw C function pointers — they cannot hold capturing lambdas. Use static functions or non-capturing lambdas only. To share state, use a global or static variable inside your mod.

🔨 Building Your Mod


Mods are compiled as position-independent shared libraries. You only need ModLoader.h from the VoxelGame source tree — no linking against the game binary.

ShellMinimal build command
g++ -shared -fPIC -std=c++17 \
    -o mymod.so mymod.cpp \
    -I/path/to/VoxelGame/include

CMakeLists.txt template

CMakeCMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyMod)

add_library(MyMod SHARED mymod.cpp)
target_compile_features(MyMod PRIVATE cxx_std_17)
target_include_directories(MyMod PRIVATE
    /path/to/VoxelGame/include
)
set_target_properties(MyMod PROPERTIES PREFIX "") # removes "lib" prefix

🪟 Cross-Compiling (Linux → Windows & native Windows)


VoxelGame supports mods as .dll on Windows. The same ModLoader.h header works on both platforms — the C ABI is identical.

Option A — Cross-compile on Linux (MinGW-w64)

ShellLinux → Windows .dll
# Install MinGW-w64 cross-compiler (Debian/Ubuntu)
sudo apt install mingw-w64

# Build a Windows .dll from Linux
x86_64-w64-mingw32-g++ -shared -fPIC -std=c++17 \
    -o mymod.dll mymod.cpp \
    -I/path/to/VoxelGame/include \
    -Wl,--out-implib,mymod.lib

# The .dll is ready to copy to the Windows mod directory

Option B — Native Windows (MSVC / Visual Studio)

ShellMSVC Developer Command Prompt
rem Compile as a DLL using MSVC cl.exe
cl.exe /std:c++17 /LD /EHsc \
    /I"C:\VoxelGame\include" \
    mymod.cpp \
    /Fe:mymod.dll

Option C — Native Windows (MinGW / MSYS2)

ShellMSYS2 / MinGW shell
g++ -shared -std=c++17 \
    -o mymod.dll mymod.cpp \
    -I/c/VoxelGame/include

CMakeLists.txt — cross-platform template

CMakeCMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyMod)

add_library(MyMod SHARED mymod.cpp)
target_compile_features(MyMod PRIVATE cxx_std_17)
target_include_directories(MyMod PRIVATE
    /path/to/VoxelGame/include
)
# Remove "lib" prefix on Linux, keep clean name on Windows
set_target_properties(MyMod PROPERTIES PREFIX "")

# Cross-compile to Windows from Linux:
# cmake -DCMAKE_TOOLCHAIN_FILE=mingw-w64-x86_64.cmake ..

MinGW-w64 toolchain file

CMakemingw-w64-x86_64.cmake
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_C_COMPILER   x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
set(CMAKE_RC_COMPILER  x86_64-w64-mingw32-windres)
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

Windows installation path

PlatformPrimary mod pathFallback
Linux ~/.config/VoxelGame/mods/*.so ./mods/*.so
Windows %APPDATA%\VoxelGame\mods\*.dll .\mods\*.dll
⚠️
No runtime dependencies

Your mod only needs ModLoader.h at build time — you do not link against the game binary. On Windows, ensure the MinGW runtime DLLs (libstdc++-6.dll, libgcc_s_seh-1.dll) are either statically linked (-static-libgcc -static-libstdc++) or present alongside your .dll.

📋 ModAPI Reference


The ModAPI struct is the complete interface between the engine and your mod. The engine fills its function pointers before calling mod_init; you fill the rest.

Engine-provided — call these

FieldSignatureDescription
registerBlock uint16_t (*)(const ModBlockDef*) Register a new block. Returns the assigned ID starting at 342 (BlockType::COUNT). The block immediately appears in the creative inventory.
registerRecipe void (*)(const uint16_t[9], uint16_t out, int count, const char* name) Add a shaped 3×3 crafting recipe. The pattern array is row-major (index 0 = top-left, 8 = bottom-right). Use 0 for Air/empty slots. Use your registered block ID for mod blocks.
registerCommand void (*)(const char* name, char* (*handler)(const char*)) Register a Computer terminal command. handler receives the remainder of the typed line as args. Return a malloc-allocated char* response string — the engine will free() it.
logMsg void (*)(const char* msg) Writes [Mod] <msg> to stderr. Use for startup confirmation and debugging.
getBlock uint16_t (*)(int x, int y, int z) Returns the block type ID at world position (x, y, z). Returns 0 (Air) for out-of-range coordinates or unloaded chunks.
setBlock void (*)(int x, int y, int z, uint16_t type) Places a block of the given type at world position (x, y, z). Triggers a chunk mesh rebuild. Has no effect on unloaded chunks.
spawnHologram void (*)(float x, float y, float z, const char* text) Spawns a floating text hologram at the given world position. Holograms fade after a few seconds. Useful for debug overlays or in-world labels.

Mod-provided — set these

FieldTypeDescription
modName const char* Human-readable mod name. Shown in the engine startup log.
modVersion const char* Version string, e.g. "1.0.0".
onBlockPlaced void (*)(const ModBlockEvent*) Called whenever any block is placed in the world. Set to nullptr to skip.
onBlockBroken void (*)(const ModBlockEvent*) Called whenever any block is broken. Set to nullptr to skip.
onPlayerDamage void (*)(float* hp, float dmg) Called when the player takes damage. You may read or write *hp directly to modify the final HP. Set to nullptr to skip.
onUpdate void (*)(float dt) Called every game tick with the elapsed time in seconds. Use for per-frame logic such as spawning particles, updating mod state, or animating holograms. Set to nullptr to skip.
onItemUsed void (*)(int itemTypeId) Called whenever the player uses (right-clicks with) an item. itemTypeId is the raw ItemType integer value. Set to nullptr to skip.
onChatMessage void (*)(const char* msg) Called when the player sends a chat or terminal message before it is processed by the engine. You may inspect or log msg. Set to nullptr to skip.

🧱 ModBlockDef


Passed to api->registerBlock() to define a new block type.

FieldTypeDescription
name const char* Display name shown in the creative inventory and search. E.g. "Neon Cobble".
isSolid bool Whether the block is a full opaque cube. Affects face-culling (solid blocks cull adjacent faces), collision, and pathfinding.
texturePath const char* Path to a 48 × 16 px PNG. The three 16×16 columns map to: top face, side faces, bottom face — left to right. Pass "" to use a placeholder checkerboard texture.

Texture format

Diagram48×16 block texture layout
┌────────────┬────────────┬────────────┐
│  Top face  │ Side faces │Bottom face │
│  (16×16)   │  (16×16)   │  (16×16)   │
└────────────┴────────────┴────────────┘
  col 0–15     col 16–31    col 32–47

🔔 ModBlockEvent


Passed to onBlockPlaced and onBlockBroken callbacks.

FieldTypeDescription
x int World X coordinate of the affected block.
y int World Y coordinate (vertical axis). Sea level ≈ 96.
z int World Z coordinate of the affected block.
blockType uint16_t Numeric block type ID. IDs 0–341 are vanilla blocks. IDs ≥ 342 are mod-registered blocks.

🧱 Register Blocks


Call api->registerBlock() for each block type you want to add. Block IDs are assigned sequentially starting at 342 (BlockType::COUNT). Save the returned ID — you'll need it for recipes and to compare against ModBlockEvent.blockType.

C++Registering multiple blocks
// Globals so callbacks can use them
static uint16_t gNeonStoneId = 0;
static uint16_t gVoidGlassId = 0;

extern "C" void mod_init(ModAPI* api) {
    api->modName    = "Neon Blocks";
    api->modVersion = "2.1";

    // Solid opaque block with a custom texture
    ModBlockDef neonStone {
        "Neon Stone",
        true,
        "/home/user/mods/neon_stone.png"
    };
    gNeonStoneId = api->registerBlock(&neonStone);  // → 342

    // Transparent / non-solid block
    ModBlockDef voidGlass {
        "Void Glass",
        false,   // not solid — won't cull adjacent faces
        ""
    };
    gVoidGlassId = api->registerBlock(&voidGlass);   // → 343

    api->onBlockPlaced  = nullptr;
    api->onBlockBroken  = nullptr;
    api->onPlayerDamage = nullptr;
}
💡
Block IDs are stable per session

IDs are assigned in load order starting at 342 (BlockType::COUNT). Multiple mods adding blocks will get sequential IDs. The creative inventory search automatically includes all registered mod blocks.

⚗️ Register Recipes


Recipes use a flat 9-element array of uint16_t block IDs representing a 3×3 grid in row-major order (index 0 = top-left, index 8 = bottom-right). Use 0 (Air) for empty slots.

C++Shaped recipe: 2×2 Neon Stone → 1 Neon Bricks
// Pattern visualised:
//   [ NS ][ NS ][ -- ]
//   [ NS ][ NS ][ -- ]
//   [ -- ][ -- ][ -- ]
uint16_t pattern[9] = {
    gNeonStoneId, gNeonStoneId, 0,
    gNeonStoneId, gNeonStoneId, 0,
    0,            0,            0
};

api->registerRecipe(
    pattern,
    gNeonBricksId,   // output block ID
    4,               // output count
    "NEON BRICKS"    // display name
);
ℹ️
Shape normalisation

Recipes are shape-normalised — the filled bounding box of the crafting grid is extracted and compared to the recipe bounding box. A 2×2 recipe placed in the top-left matches a 2×2 placed in the bottom-right. First registered match wins.

Using vanilla block IDs in patterns

You can reference vanilla blocks by casting their BlockType enum value to uint16_t. Key IDs to know:

BlockIDBlockID
Air (empty)0Stone1
Wood (Oak Log)6Coal Ore30
Iron Ore31Diamond Ore37
Iron Block129Gold Block133
Cobblestone64Glass85

See the Blocks & Recipes reference for the complete ID table.

💻 Terminal Commands


Register commands that players can type into any Computer block terminal. The command name is the first word typed; the remainder of the line is passed as args.

C++Registering a 'ping' command
// Handler must be a plain function (not capturing lambda)
static char* handlePing(const char* args) {
    // malloc the response — engine will free() it
    const char* msg = "PONG! Mod is alive.\n";
    char* buf = (char*)malloc(strlen(msg) + 1);
    strcpy(buf, msg);
    return buf;
}

// In mod_init:
api->registerCommand("ping", handlePing);

// Player types: ping
// Terminal shows: PONG! Mod is alive.
⚠️
Always malloc your response

The engine calls free() on the pointer returned by your command handler. Return a malloc-allocated string, or nullptr for no output (treated as empty/not-handled and falls through to "Unknown command").

Parsing arguments

C++Command with arguments
static char* handleSpawn(const char* args) {
    // args = "dragon 3"  when player typed: spawn dragon 3
    std::string s(args);
    std::ostringstream oss;
    oss << "Spawning: " << s << "\n";
    std::string result = oss.str();
    char* buf = (char*)malloc(result.size() + 1);
    memcpy(buf, result.c_str(), result.size() + 1);
    return buf;
}

api->registerCommand("spawn", handleSpawn);

🔔 Event Hooks


Three event hooks let your mod react to world and player state changes in real time.

onBlockPlaced
void (*)(const ModBlockEvent*)
Fired after any block is placed. The event contains the block's world position and type ID. You cannot cancel placement.
onBlockBroken
void (*)(const ModBlockEvent*)
Fired after any block is broken. blockType is the type that was there — the position is already Air when this fires.
onPlayerDamage
void (*)(float* hp, float dmg)
Fired when the player takes damage. You may write to *hp to set a custom final HP value — useful for god-mode, shields, or resistance effects.

Example: detect when your custom block is broken

C++Block-specific break detection
static uint16_t gMyBlockId = 0;

static void onBroken(const ModBlockEvent* ev) {
    if (ev->blockType == gMyBlockId) {
        // React to our specific block being broken
    }
}

extern "C" void mod_init(ModAPI* api) {
    api->modName    = "Block Detector";
    api->modVersion = "1.0";

    ModBlockDef def{ "Detector Block", true, "" };
    gMyBlockId = api->registerBlock(&def);

    api->onBlockPlaced  = nullptr;
    api->onBlockBroken  = onBroken;
    api->onPlayerDamage = nullptr;
}

Example: damage shield

C++50% damage reduction
static void shieldDamage(float* hp, float dmg) {
    // Halve the damage by adding back half of it to HP
    *hp += dmg * 0.5f;
}

// In mod_init:
api->onPlayerDamage = shieldDamage;

📂 Installation & Paths


The modloader scans two directories in order. A mod found in the primary path will not be loaded again from the fallback, even if both directories contain the same filename.

PriorityPathNotes
Primary ~/.config/VoxelGame/mods/ User-level mods. Created automatically on first launch. Recommended for end users.
Fallback ./mods/ Working-directory-relative. Useful for development and testing alongside the game binary.
💡
Load order

Within each directory, mods are loaded in filesystem iteration order (typically alphabetical). Block IDs are assigned sequentially across all mods in load order, so if ordering matters for cross-mod recipes, name your mod files accordingly (e.g. 00_core.so, 01_extension.so).

ShellInstall commands
# Primary location (recommended)
mkdir -p ~/.config/VoxelGame/mods
cp mymod.so ~/.config/VoxelGame/mods/

# Development / testing alongside game binary
mkdir -p mods
cp mymod.so mods/

📖 Full Examples


Complete mod: Neon Block Pack

C++neon_blocks.cpp
#include "ModLoader.h"
#include <cstdlib>
#include <cstring>
#include <cstdio>

static uint16_t gNeonStoneId = 0;
static uint16_t gNeonBricksId = 0;
static int gPlacedCount = 0;

static void onPlaced(const ModBlockEvent* ev) {
    if (ev->blockType == gNeonStoneId) gPlacedCount++;
}

static char* neonStats(const char*) {
    char* buf = (char*)malloc(128);
    snprintf(buf, 128, "Neon Stone placed: %d times\n", gPlacedCount);
    return buf;
}

extern "C" void mod_init(ModAPI* api) {
    api->modName    = "Neon Blocks";
    api->modVersion = "1.2";

    ModBlockDef stone { "Neon Stone",  true,  "" };
    ModBlockDef bricks{ "Neon Bricks", true,  "" };
    gNeonStoneId  = api->registerBlock(&stone);
    gNeonBricksId = api->registerBlock(&bricks);

    // 2×2 Neon Stone → 4 Neon Bricks
    uint16_t pat[9] = {
        gNeonStoneId, gNeonStoneId, 0,
        gNeonStoneId, gNeonStoneId, 0,
        0,            0,            0
    };
    api->registerRecipe(pat, gNeonBricksId, 4, "NEON BRICKS");

    api->registerCommand("neonstats", neonStats);

    api->onBlockPlaced  = onPlaced;
    api->onBlockBroken  = nullptr;
    api->onPlayerDamage = nullptr;

    api->logMsg("Neon Blocks loaded!");
}

🛡️ Permissions & Roles


VoxelGame includes a built-in six-tier permission system enforced server-side. Roles are persisted to saves/roles.json and loaded on server start.

Role hierarchy

RoleValueCapabilities
Owner5All permissions; cannot be demoted by Admins
Admin4Can promote/demote up to Moderator; all Mod permissions
Moderator3Kick, ban, unban, tp, broadcast; access to F3 console
Member2Can place and break blocks (trusted builder)
Player1Default role — can interact but not build
Banned0Connection refused on join

roles.json format

JSONsaves/roles.json
{
    "Alice":   "OWNER",
    "Bob":     "ADMIN",
    "Charlie": "MODERATOR",
    "Dave":    "MEMBER",
    "Eve":     "BANNED"
}

Role strings are case-insensitive. Unrecognised strings default to PLAYER. The file is written automatically whenever a role changes via the F3 console.

Server enforcement

  • Block place/break packets are rejected server-side if the sender's role is below Member.
  • A Banned player's connection is refused immediately at the JoinInfo packet stage.
  • Kick/ban/promote commands only work when the issuer's role is high enough (Moderator+ for kick/ban; Admin+ for promote).
ℹ️
Singleplayer

In singleplayer the permissions system is inactive — the local player is treated as Owner for all checks. Staff tools like the NPC Edit Wand are always accessible.

💻 F3 Admin Console


Press F3 while hosting or connected to a server (Moderator role or above) to open a terminal-style overlay console. It slides over the centre of the screen and accepts commands while the game continues running.

Opening condition

  • Must be in an active server session (hosting or connected).
  • Local player role must be Moderator or higher.
  • Pressing F3 again closes the console.

Available commands

CommandSyntaxMin RoleDescription
listlistModeratorShow all connected players and their roles.
kickkick <name> [reason]ModeratorDisconnect a player with an optional message.
banban <name>ModeratorSet role to Banned and kick immediately. Persisted.
unbanunban <name>ModeratorReset role to Player. Persisted.
promotepromote <name> <role>AdminSet any role up to Admin. Role name is case-insensitive.
demotedemote <name> <role>AdminAlias for promote — sets role to the given value.
tptp <name> [x y z]ModeratorTeleport a player. Omit coords to tp them to yourself.
broadcastbroadcast <message>ModeratorSend a chat message visible to all connected players.
helphelpModeratorList all available commands.
ConsoleExample session
> list
  Alice   [OWNER]   connected
  Bob     [MEMBER]  connected
  Grieger [PLAYER]  connected

> ban Grieger
  Grieger banned and kicked.

> promote Bob MODERATOR
  Bob promoted to MODERATOR.

> broadcast Server restarting in 5 minutes!
  Broadcast sent.

🪄 NPC Edit Wand


The NPC Edit Wand (items: NpcWand and NpcWand2) lets staff right-click any NPC or mob to open an NBT-style editor panel, similar to Minecraft's NPCEdit. Available in singleplayer always; in multiplayer requires Moderator role or above.

How to use

  1. 1
    Obtain the wand

    Find NpcWand or NpcWand2 in the Items tab of the creative inventory.

  2. 2
    Right-click an NPC

    Aim at any NPC or mob within 8 blocks and right-click. The editor panel opens.

  3. 3
    Navigate fields

    Use / or Tab to move between fields.

  4. 4
    Edit a field

    Press Enter to begin editing the selected field. Type the new value, then Enter again to confirm or Esc to cancel.

  5. 5
    Close

    Press Esc or right-click away from any NPC to close the panel.

Editable fields

FieldTypeDescription
namestringNPC display name shown above its head.
kindintSpecies / mob type index.
hpfloatCurrent hit points.
maxhpfloatMaximum hit points.
levelintCombat level (affects scaling).
tint R/G/Bfloat 0–1Body colour tint. A colour swatch previews the result live.
aggressionfloat 0–1Genome: chance to attack on sight.
lungefloat 0–1Genome: lunge attack probability.
strafefloat 0–1Genome: lateral strafing weight.
defensefloat 0–1Genome: damage reduction factor.
limbSpeedfloatAnimation limb swing speed multiplier.

🌀 Portals & Gateways


Four teleportation block types allow dimension-like travel. All are placed from the creative inventory and interact on player contact or right-click.

Block types

BlockPairingTriggerVelocity
WarpPortalA / WarpPortalB Auto-pairs: last placed A pairs with last placed B Walk into or stand in the block Preserved — player keeps momentum through the portal
PortalA / PortalB Auto-pairs: same as warp portals Walk into the block Zeroed — player arrives stationary
HoverBlock N/A Stand on top Launches player upward at 14 m/s²
GatewayBlock Requires gold frame (see below) Right-click (two-step charge → enter) Player arrives at pocket world spawn

Gateway activation

A GatewayBlock must be surrounded by 6 adjacent GoldBlocks (one on each face: top, bottom, north, south, east, west) to become active. Once the frame is complete:

  1. First right-click: charges the gateway (status: Charged).
  2. Second right-click: transports the player to the pocket world.

Pocket worlds

Each Gateway generates a unique procedural world when first entered. The terrain config is randomised per biome theme associated with the gateway's location:

Biome ThemeCharacteristics
Deep JungleHigh humidity, tall dense canopy, heavy erosion
Arctic TundraLow terrain, snow cover, sparse fir/spruce forests
Scorched DesertExtreme height variance, red sand, dead trees
Mystic ForestMagic log trees, bioluminescent flora, rolling foothills
Sky ArchipelagoFloating islands, low water level, wide terrain scale
Deep OceanVery low terrain, broad coral reefs, shallow islands
Volcanic MesaHigh ridges, heavy terracing, marble/alabaster layers
Willow SwampFlat, near-water, dense willow canopy, dark oak patches

Return shrine

A return shrine is automatically built at the pocket world's spawn point on first entry. It consists of a stone platform, four corner columns capped with GoldBlocks, and a central GatewayBlock surrounded by its own gold frame — right-clicking it returns the player to the main world.

💡
Cooldowns

WarpPortal and Portal have a 2-second cooldown per activation to prevent rapid oscillation. Gateway has a 2.5-second cooldown. These are defined as PORTAL_COOLDOWN and GATEWAY_COOLDOWN constants in Circuitry.h.

🌲 World Generation


VoxelGame procedurally generates terrain using a multi-pass noise pipeline. Understanding the biome and generation system is useful if your mod places blocks via event hooks.

Biomes

BiomeSurface blockTreesFlora
JungleForestGrass5 forest sub-types (see below)Ferns, mushrooms, flowers, moss
PlainsGrassBirch, Maple, Cherry, Apple, Willow (near water)Red/Yellow/Blue flowers, Rose Bushes, Ferns, Mushrooms
DesertRedSandDead (rare)Cacti
TundraTundraGrass / SnowFir, Spruce, Redwood, DeadFerns, Moss, Brown mushrooms, Blue flowers (sparse)
BeachTropicalSandPalmFerns, Yellow flowers, Tall grass (near water)
MesaMarbleRed / MarbleWhiteDead (ridges), Willow (valleys)Rose Bushes, Yellow flowers (valleys)
OceanOceanMudCoral reefs (Blue, Orange, Pink) in shallow water

JungleForest sub-types

The JungleForest biome is divided into regional sub-types based on a per-16-block hash. Each sub-type has a distinct tree mix:

Sub-typePrimary treesNotes
Deep JungleJungle, Dark Oak, Redwood giants, MapleDensest canopy
Dark Oak ForestDark Oak (dominant), occasional MapleWide canopy, dark floor
Birch ForestBirch (dominant), CherryBright, open feel
Willow SwampWillow (dominant), sparse Dark OakSlightly less dense, drooping canopy
Magic GroveMagic, Birch, Dark OakRare bioluminescent trees

WorldConfig terrain sliders

When creating a world, the following parameters are exposed as menu sliders:

ParameterRangeEffect
Terrain Scale0.003 – 0.15Noise frequency — low = giant continents, high = chaotic hills
Height20 – 200Terrain height multiplier above sea level
Water LeveladjustableSea level Y coordinate (default 96)
Ridge Peaks0 – 1Sharpness of mountain ridges
Terracing0 – 1Stepped plateau effect
Erosion0 – 1Cave frequency and cliff undercutting. 0 = no caves, 1 = heavy carving
Foothills0 – 1Width of rolling transition between plains and mountains
Biome SizeadjustableScale of biome noise — large = wider biome regions

Sea level

The default sea level is Y = 96. Beach biomes span Y = 96–102 (shallow). Ocean biomes sit below Y = 88. When a mod places blocks via event hooks and needs to know if a position is underwater, compare against WorldConfig::waterLevel.

Ore Generation (v1.3+)

As of v1.3, ores are distributed through solid rock using 3D scatter veins rather than clustering exclusively on cavern walls. Veins are placed at biome-appropriate depth ranges and embedded throughout the rock mass. Mod blocks registered via registerBlock are not placed by world generation automatically — use onUpdate or a custom world pass to scatter your mod's ore blocks.

New Blocks in v1.3 (IDs 328–341)

Fourteen new vanilla blocks were added in v1.3, occupying IDs 328–341. Mod-registered block IDs now start at 342. The new blocks include the farming system (FarmLand, crop growth stages), decoration (Campfire, GraveMarker), and the magic crafting station (SpellForge):

IDEnum NameDescription
328FarmLandTilled soil created by using a hoe on Dirt or Grass. Crops grow on FarmLand near water.
329WheatStage0Wheat crop — just planted.
330WheatStage1Wheat crop — mid growth.
331WheatStage2Wheat crop — nearly mature.
332WheatMatureWheat crop — ready to harvest.
333CarrotStage0Carrot crop — just planted.
334CarrotStage1Carrot crop — mid growth.
335CarrotMatureCarrot crop — ready to harvest. Drops Carrot (food/companion tameness).
336PotatoStage0Potato crop — just planted.
337PotatoStage1Potato crop — mid growth.
338PotatoMaturePotato crop — ready to harvest.
339CampfireDecorative fire block. Emits light.
340GraveMarkerPlaced automatically at player death location.
341SpellForgeCrafts spells from wands + gems. Spell strength scales with wand tier.

Tips & Gotchas


  • Save returned block IDs. Store them in static globals so callbacks and recipe patterns can reference them.
  • No capturing lambdas. The callback slots are raw function pointers. Use static functions instead.
  • malloc your command responses. The engine calls free() on the pointer. Returning a string literal will crash.
  • Returning nullptr from a command handler is treated as "this command doesn't handle this" and falls through to the engine's "Unknown command" message.
  • Block IDs ≥ 342 are dynamic. Don't hardcode them — always use the value returned by registerBlock.
  • Load order matters for block IDs. If your mod B needs mod A's block IDs, name the files so A loads first (00_a.so, 01_b.so).
  • Texture PNG must be 48×16 pixels exactly. Passing a wrong-size image may result in incorrect display or a crash on some drivers.
  • All event callbacks execute on the main thread — keep them fast. Do not do heavy I/O or blocking calls inside event handlers.
🔬
Debugging tip

Run the game from a terminal to see all stderr output including [ModLoader] load messages and [Mod] log output. This is the fastest way to confirm your mod loaded and diagnose issues.