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.
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.
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.
#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!");
}
# 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:
extern "C" void mod_init(ModAPI* api);
Inside mod_init you must do three things:
- Set
api->modNameandapi->modVersion - Set each callback pointer (
onBlockPlaced,onBlockBroken,onPlayerDamage) to a function ornullptr - Call any
api->register*functions you need
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.
g++ -shared -fPIC -std=c++17 \
-o mymod.so mymod.cpp \
-I/path/to/VoxelGame/include
CMakeLists.txt template
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)
# 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)
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)
g++ -shared -std=c++17 \
-o mymod.dll mymod.cpp \
-I/c/VoxelGame/include
CMakeLists.txt — cross-platform template
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
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
| Platform | Primary mod path | Fallback |
|---|---|---|
| Linux | ~/.config/VoxelGame/mods/*.so |
./mods/*.so |
| Windows | %APPDATA%\VoxelGame\mods\*.dll |
.\mods\*.dll |
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
| Field | Signature | Description |
|---|---|---|
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
| Field | Type | Description |
|---|---|---|
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.
| Field | Type | Description |
|---|---|---|
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
┌────────────┬────────────┬────────────┐
│ 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.
| Field | Type | Description |
|---|---|---|
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.
// 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;
}
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.
// 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
);
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:
| Block | ID | Block | ID |
|---|---|---|---|
| Air (empty) | 0 | Stone | 1 |
| Wood (Oak Log) | 6 | Coal Ore | 30 |
| Iron Ore | 31 | Diamond Ore | 37 |
| Iron Block | 129 | Gold Block | 133 |
| Cobblestone | 64 | Glass | 85 |
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.
// 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.
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
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.
blockType is the type that was there — the position is already Air when this fires.*hp to set a custom final HP value — useful for god-mode, shields, or resistance effects.Example: detect when your custom block is broken
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
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.
| Priority | Path | Notes |
|---|---|---|
| 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. |
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).
# 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
#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
| Role | Value | Capabilities |
|---|---|---|
Owner | 5 | All permissions; cannot be demoted by Admins |
Admin | 4 | Can promote/demote up to Moderator; all Mod permissions |
Moderator | 3 | Kick, ban, unban, tp, broadcast; access to F3 console |
Member | 2 | Can place and break blocks (trusted builder) |
Player | 1 | Default role — can interact but not build |
Banned | 0 | Connection refused on join |
roles.json format
{
"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
JoinInfopacket stage. - Kick/ban/promote commands only work when the issuer's role is high enough (Moderator+ for kick/ban; Admin+ for promote).
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
| Command | Syntax | Min Role | Description |
|---|---|---|---|
list | list | Moderator | Show all connected players and their roles. |
kick | kick <name> [reason] | Moderator | Disconnect a player with an optional message. |
ban | ban <name> | Moderator | Set role to Banned and kick immediately. Persisted. |
unban | unban <name> | Moderator | Reset role to Player. Persisted. |
promote | promote <name> <role> | Admin | Set any role up to Admin. Role name is case-insensitive. |
demote | demote <name> <role> | Admin | Alias for promote — sets role to the given value. |
tp | tp <name> [x y z] | Moderator | Teleport a player. Omit coords to tp them to yourself. |
broadcast | broadcast <message> | Moderator | Send a chat message visible to all connected players. |
help | help | Moderator | List all available commands. |
> 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
- 1Obtain the wand
Find NpcWand or NpcWand2 in the Items tab of the creative inventory.
- 2Right-click an NPC
Aim at any NPC or mob within 8 blocks and right-click. The editor panel opens.
- 3Navigate fields
Use ↑/↓ or Tab to move between fields.
- 4Edit a field
Press Enter to begin editing the selected field. Type the new value, then Enter again to confirm or Esc to cancel.
- 5Close
Press Esc or right-click away from any NPC to close the panel.
Editable fields
| Field | Type | Description |
|---|---|---|
name | string | NPC display name shown above its head. |
kind | int | Species / mob type index. |
hp | float | Current hit points. |
maxhp | float | Maximum hit points. |
level | int | Combat level (affects scaling). |
tint R/G/B | float 0–1 | Body colour tint. A colour swatch previews the result live. |
aggression | float 0–1 | Genome: chance to attack on sight. |
lunge | float 0–1 | Genome: lunge attack probability. |
strafe | float 0–1 | Genome: lateral strafing weight. |
defense | float 0–1 | Genome: damage reduction factor. |
limbSpeed | float | Animation 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
| Block | Pairing | Trigger | Velocity |
|---|---|---|---|
| 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:
- First right-click: charges the gateway (status: Charged).
- 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 Theme | Characteristics |
|---|---|
| Deep Jungle | High humidity, tall dense canopy, heavy erosion |
| Arctic Tundra | Low terrain, snow cover, sparse fir/spruce forests |
| Scorched Desert | Extreme height variance, red sand, dead trees |
| Mystic Forest | Magic log trees, bioluminescent flora, rolling foothills |
| Sky Archipelago | Floating islands, low water level, wide terrain scale |
| Deep Ocean | Very low terrain, broad coral reefs, shallow islands |
| Volcanic Mesa | High ridges, heavy terracing, marble/alabaster layers |
| Willow Swamp | Flat, 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.
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
| Biome | Surface block | Trees | Flora |
|---|---|---|---|
| JungleForest | Grass | 5 forest sub-types (see below) | Ferns, mushrooms, flowers, moss |
| Plains | Grass | Birch, Maple, Cherry, Apple, Willow (near water) | Red/Yellow/Blue flowers, Rose Bushes, Ferns, Mushrooms |
| Desert | RedSand | Dead (rare) | Cacti |
| Tundra | TundraGrass / Snow | Fir, Spruce, Redwood, Dead | Ferns, Moss, Brown mushrooms, Blue flowers (sparse) |
| Beach | TropicalSand | Palm | Ferns, Yellow flowers, Tall grass (near water) |
| Mesa | MarbleRed / MarbleWhite | Dead (ridges), Willow (valleys) | Rose Bushes, Yellow flowers (valleys) |
| Ocean | OceanMud | — | Coral 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-type | Primary trees | Notes |
|---|---|---|
| Deep Jungle | Jungle, Dark Oak, Redwood giants, Maple | Densest canopy |
| Dark Oak Forest | Dark Oak (dominant), occasional Maple | Wide canopy, dark floor |
| Birch Forest | Birch (dominant), Cherry | Bright, open feel |
| Willow Swamp | Willow (dominant), sparse Dark Oak | Slightly less dense, drooping canopy |
| Magic Grove | Magic, Birch, Dark Oak | Rare bioluminescent trees |
WorldConfig terrain sliders
When creating a world, the following parameters are exposed as menu sliders:
| Parameter | Range | Effect |
|---|---|---|
| Terrain Scale | 0.003 – 0.15 | Noise frequency — low = giant continents, high = chaotic hills |
| Height | 20 – 200 | Terrain height multiplier above sea level |
| Water Level | adjustable | Sea level Y coordinate (default 96) |
| Ridge Peaks | 0 – 1 | Sharpness of mountain ridges |
| Terracing | 0 – 1 | Stepped plateau effect |
| Erosion | 0 – 1 | Cave frequency and cliff undercutting. 0 = no caves, 1 = heavy carving |
| Foothills | 0 – 1 | Width of rolling transition between plains and mountains |
| Biome Size | adjustable | Scale 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):
| ID | Enum Name | Description |
|---|---|---|
| 328 | FarmLand | Tilled soil created by using a hoe on Dirt or Grass. Crops grow on FarmLand near water. |
| 329 | WheatStage0 | Wheat crop — just planted. |
| 330 | WheatStage1 | Wheat crop — mid growth. |
| 331 | WheatStage2 | Wheat crop — nearly mature. |
| 332 | WheatMature | Wheat crop — ready to harvest. |
| 333 | CarrotStage0 | Carrot crop — just planted. |
| 334 | CarrotStage1 | Carrot crop — mid growth. |
| 335 | CarrotMature | Carrot crop — ready to harvest. Drops Carrot (food/companion tameness). |
| 336 | PotatoStage0 | Potato crop — just planted. |
| 337 | PotatoStage1 | Potato crop — mid growth. |
| 338 | PotatoMature | Potato crop — ready to harvest. |
| 339 | Campfire | Decorative fire block. Emits light. |
| 340 | GraveMarker | Placed automatically at player death location. |
| 341 | SpellForge | Crafts 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
nullptrfrom 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.
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.