Codex Systems
Audience: Module developers and engine maintainers working with game content registration.
Terrarum's content is registered through Codex systems — centralised registries for game subsystems. Each Codex stores definitions loaded from CSV or JSON files in modules, accessible via unique identifiers.
Overview
Codex systems provide:
- Centralised registration — Single source of truth for all content
- Module isolation — Each module's content is prefixed with module name
- Hot-reloading support — Later modules override earlier definitions
- Type-safe access — Compile-time checking via ItemID type aliases
- Tag-based queries — Search by tags for similar content
Codex Architecture
Common Pattern
All Codices follow this structure:
class SomeCodex {
@Transient val registry = HashMap<ItemID, SomeProperty>()
@Transient private val nullProp = SomeProperty()
// Access by ID
operator fun get(id: ItemID?): SomeProperty {
return registry[id] ?: nullProp
}
// Load from module
fun fromModule(module: String, path: String) {
register(module, CSVFetcher.readFromModule(module, path))
}
private fun register(module: String, records: List<CSVRecord>) {
records.forEach { setProp(module, it) }
}
}
ID Naming Convention
All IDs follow the pattern: [prefix]@<modulename>:<id>
Examples:
- Block:
basegame:1(stone) - Fluid:
fluid@basegame:1(water) - Ore:
ores@basegame:3(iron ore) - Item:
basegame:pickaxe_copper
Global Singletons
Codices are stored as global singletons:
object Terrarum {
lateinit var blockCodex: BlockCodex
lateinit var fluidCodex: FluidCodex
}
object ItemCodex {
val itemCodex = ItemTable()
}
object WeatherCodex {
internal val weatherById = HashMap<String, BaseModularWeather>()
}
BlockCodex
Defines properties for all terrain and wall tiles.
CSV Format
File: blocks/blocks.csv
Note: CSVs use semicolons (;) as delimiters, not commas.
"id";"drop";"spawn";"name";"shdr";"shdg";"shdb";"shduv";"str";"dsty";"mate";"solid";"wall";"grav";"dlfn";"fv";"fr";"lumr";"lumg";"lumb";"lumuv";"refl";"tags"
"0";"N/A";"N/A";"BLOCK_AIR";"0.0312";"0.0312";"0.0312";"0.0312";"1";"1";"AIIR";"0";"1";"N/A";"0";"0";"4";"0.0000";"0.0000";"0.0000";"0.0000";"0.0";"INCONSEQUENTIAL,AIR,NORANDTILE"
"2";"basegame:2";"basegame:2";"BLOCK_STONE";"0.6290";"0.6290";"0.6290";"0.6290";"120";"2600";"ROCK";"1";"0";"N/A";"0";"0";"16";"0.0000";"0.0000";"0.0000";"0.0000";"0.0";"STONE,NATURAL,MINERAL"
Key Fields:
id— Numeric tile ID (unique within module)name— Translation keyshdr/shdg/shdb/shduv— Shade colour (RGB + UV channels, 0.0-1.0)str— Strength/HP (mining difficulty)dsty— Density (kg/m³)mate— Material ID (4-letter code)solid— Is solid (1) or passable (0)wall— Is wall tile (1) or terrain (0)fr— Horizontal friction coefficient (16 = normal)lumr/lumg/lumb/lumuv— Luminosity (RGB + UV channels, 0.0-1.0)tags— Comma-separated tags (STONE, SOIL, etc.)drop— Item ID dropped when minedspawn— Item ID used to place this block
Usage
// Access block properties
val stone = BlockCodex["basegame:1"]
println("HP: ${stone.strength}")
println("Solid: ${stone.isSolid}")
println("Friction: ${stone.frictionCoeff}")
// Check tags
if (stone.hasTag("STONE")) {
println("This is stone!")
}
// Get all blocks with a tag
val soilBlocks = BlockCodex.blockProps.values.filter { it.hasTag("SOIL") }
Loading
object GameBlockLoader {
init {
Terrarum.blockCodex = BlockCodex()
}
operator fun invoke(module: String) {
Terrarum.blockCodex.fromModule(module, "blocks/blocks.csv") { tile ->
// Register block as item
ItemCodex[tile.id] = makeNewItemObj(tile, isWall = false)
}
}
}
ItemCodex
Defines all items, including blocks, tools, and fixtures.
Item Types
Items come from multiple sources:
- Blocks — Automatically registered from BlockCodex
- Static items — Defined in
items/items.csv - Dynamic items — Runtime-created (customised tools, signed books)
- Actor items — Items created from actors (drops)
- Fixture items — Items that spawn fixtures
CSV Format
File: items/itemid.csv
Note: Items are registered by fully-qualified class names, not inline properties.
id;classname;tags
1;net.torvald.terrarum.modulebasegame.gameitems.PickaxeCopper;TOOL,PICK
2;net.torvald.terrarum.modulebasegame.gameitems.PickaxeIron;TOOL,PICK
14;net.torvald.terrarum.modulebasegame.gameitems.PickaxeWood;TOOL,PICK
Each item is a Kotlin class extending GameItem:
class PickaxeCopper(originalID: ItemID) : GameItem(originalID) {
override var baseMass = 2.5
override var baseToolSize: Double? = 1.0
override val canBeDynamic = true
override var inventoryCategory = "tool"
override val materialId = "COPR"
init {
originalName = "ITEM_PICKAXE_COPPER"
tags.add("TOOL")
tags.add("PICK")
}
}
Usage
// Get item
val pickaxe = ItemCodex["basegame:pickaxe_copper"]
println("Mass: ${pickaxe.mass}")
println("Stackable: ${pickaxe.maxStackSize}")
// Check if item exists
if (ItemCodex.itemCodex.containsKey("basegame:sword_iron")) {
// Item exists
}
// Create dynamic item
val customSword = ItemCodex["basegame:sword_iron"]?.copy()
customSword.tags.add("LEGENDARY")
val dynamicID = "dyn:${UUID.randomUUID()}"
ItemCodex.registerNewDynamicItem(dynamicID, customSword)
Dynamic Items
Dynamic items are runtime-created items that can be modified:
// Original static item
val baseSword = ItemCodex["basegame:sword_iron"]
// Create customised version
val legendarySwor =d baseSword.copy()
legendarySword.nameStr = "§o§Excalibur§.§"
legendarySword.tags.add("LEGENDARY")
legendarySword.actorValue.set(AVKey.DAMAGE, 999.0)
// Register with dynamic ID
val dynamicID = "dyn:${System.nanoTime()}"
ItemCodex.registerNewDynamicItem(dynamicID, legendarySword)
Dynamic item serialisation:
dynamicItemInventory— Stores all dynamic itemsdynamicToStaticTable— Maps dynamic ID → original static ID- Both saved to disk with player/world data
MaterialCodex
Defines physical materials and their properties.
CSV Format
File: materials/materials.csv
Note: Uses semicolons (;) as delimiters.
idst;tens;impf;dsty;fmod;endurance;tcond;reach;rcs;sondrefl;comments
WOOD;10;10;800;0.3;0.23;0.17;5;18;0.5;just a generic wood
ROCK;15;210;3000;0.55;0.64;2.9;5;48;1.0;data is that of marble
COPR;30;120;8900;0.35;0.35;400;5;82;0.8;copper
IRON;50;210;7800;0.3;0.48;80;5;115;0.9;wrought iron
Key Fields:
idst— 4-letter material ID codetens— Tensile strengthimpf— Impact force resistancedsty— Density (kg/m³)fmod— Flexibility modulusendurance— Durability/endurancetcond— Thermal conductivityreach— Reach/range factorrcs— Radar cross-section (for detection)sondrefl— Sound reflectance (0.0-1.0)comments— Human-readable notes
Usage
val iron = MaterialCodex["IRON"]
println("Density: ${iron.density} kg/m³")
println("Melting point: ${iron.meltingPoint}°C")
println("Conductive: ${iron.hasTag("CONDUCTIVE")}")
FluidCodex
Defines liquid properties for fluid simulation.
CSV Format
File: fluid/fluids.csv
Note: Uses semicolons (;) as delimiters.
"id";"name";"shdr";"shdg";"shdb";"shduv";"str";"dsty";"mate";"lumr";"lumg";"lumb";"lumuv";"colour";"vscs";"refl";"tags";"therm"
"1";"BLOCK_WATER";"0.1016";"0.0744";"0.0508";"0.0826";"100";"1000";"WATR";"0.0000";"0.0000";"0.0000";"0.0000";"005599A6";"5";"0.0";"NATURAL";0
"2";"BLOCK_LAVA";"0.1252";"0.1252";"0.1252";"0.1252";"100";"2600";"ROCK";"0.7664";"0.2032";"0.0000";"0.0000";"FF4600E6";"16";"0.0";"NATURAL,MOLTEN";2
Key Fields:
id— Numeric fluid IDname— Translation keyshdr/shdg/shdb/shduv— Shade colour (RGB + UV, 0.0-1.0)lumr/lumg/lumb/lumuv— Luminosity (RGB + UV, 0.0-1.0)str— Fluid strength (flow pressure)dsty— Density (kg/m³)mate— Material ID (4-letter code)vscs— Viscosity (resistance to flow)colour— Display colour (RRGGBBAA hex, no 0x prefix)refl— Reflectance (0.0-1.0)therm— Thermal state (-1=cryogenic, 0=normal, 1=hot, 2=molten)tags— Comma-separated tags
Usage
val water = FluidCodex["fluid@basegame:1"]
println("Density: ${water.density}")
println("Viscosity: ${water.viscosity}")
println("Colour: ${water.colour.toString(16)}")
// Flow simulation uses these properties
val flowSpeed = fluid.strength / fluid.viscosity
OreCodex
Defines ore generation parameters for world generation.
CSV Format
File: ores/ores.csv
Note: Uses semicolons (;) as delimiters.
"id";"item";"tags";"versionsince"
"1";"item@basegame:128";"COPPER,MALACHITE";0
"2";"item@basegame:129";"IRON,HAEMATITE";0
"3";"item@basegame:130";"COAL,CARBON";0
"6";"item@basegame:133";"GOLD,NATURAL_GOLD";0
Key Fields:
id— Numeric ore IDitem— Item ID that represents this oretags— Comma-separated tags (ore type, mineral name)versionsince— Version when ore was introduced
Usage
val ironOre = OreCodex["ores@basegame:1"]
println("Item drop: ${ironOre.item}")
println("Rare: ${ironOre.hasTag("RARE")}")
// Used by world generator
val allOres = OreCodex.getAll()
allOres.filter { it.hasTag("METAL") }.forEach { ore ->
worldgen.generateOreVein(ore)
}
WeatherCodex
Defines weather systems with skybox, clouds, and lighting.
JSON Format
File: weathers/clear_day.json
{
"identifier": "clear_day",
"tags": "CLEAR,DAY",
"skyboxGradColourMap": "lut:sky_clear_day.tga",
"daylightClut": "lut:daylight_clear.tga",
"cloudChance": 0.3,
"windSpeed": 2.5,
"windSpeedVariance": 1.0,
"windSpeedDamping": 0.95,
"cloudGamma": [1.0, 1.0],
"cloudGammaVariance": [0.1, 0.1],
"shaderVibrancy": [1.0, 0.9, 0.8],
"clouds": {
"cumulus": {
"filename": "cloud_cumulus.tga",
"tw": 64,
"th": 32,
"probability": 0.7,
"baseScale": 1.0,
"scaleVariance": 0.3,
"altLow": 0.6,
"altHigh": 0.8
}
}
}
Usage
// Get weather by ID
val clearDay = WeatherCodex.getById("clear_day")
// Query by tags
val clearWeathers = WeatherCodex.getByTag("CLEAR")
val stormyWeathers = WeatherCodex.getByAllTagsOf("STORM", "RAIN")
// Random weather
val randomWeather = WeatherCodex.getRandom(tag = "DAY")
// Apply weather to world
world.weather = clearDay
CraftingCodex
Defines crafting recipes for workbenches.
JSON Format
File: crafting/tools.json (or other recipe files)
Note: JSON format supports C-style comments (/* */).
{
"item@basegame:14": { /* wooden pick */
"workbench": "basiccrafting",
"ingredients": [[1, 5, "$WOOD", 2, "item@basegame:18"]] /* 5 woods, 2 sticks */
},
"item@basegame:1": { /* copper pick */
"workbench": "basiccrafting,metalworking",
"ingredients": [[1, 5, "item@basegame:112", 2, "item@basegame:18"]] /* 5 bars, 2 sticks */
},
"item@basegame:2": { /* iron pick */
"workbench": "basiccrafting,metalworking",
"ingredients": [[1, 5, "item@basegame:113", 2, "item@basegame:18"]] /* 5 bars, 2 sticks */
}
}
Format:
workbench— Required workbench ID (comma-separated for multiple workbenches)ingredients— Array of recipe variations (multiple ways to craft same item)- First element: MOQ (minimum output quantity)
- Remaining elements: alternating quantity and item ID
$TAG— Tag-based ingredient (any item with tag, e.g.,$WOOD,$ROCK)
Usage
// Get recipes for item
val recipes = CraftingCodex.props["basegame:pickaxe_iron"]
recipes?.forEach { recipe ->
println("Workbench: ${recipe.workbench}")
println("Makes: ${recipe.moq} items")
recipe.ingredients.forEach { ingredient ->
println(" - ${ingredient.qty}× ${ingredient.item}")
}
}
// Check if craftable
fun canCraft(itemID: ItemID, inventory: Inventory): Boolean {
val recipes = CraftingCodex.props[itemID] ?: return false
return recipes.any { recipe ->
recipe.ingredients.all { ingredient ->
inventory.has(ingredient.item, ingredient.qty)
}
}
}
CanistersCodex
Defines canister properties for fluid containers.
CSV Format
File: canisters/canisters.csv
Note: Uses semicolons (;) as delimiters.
id;itemid;tags
1;item@basegame:59;FLUIDSTORAGE,OPENSTORAGE,NOEXTREMETHERM
2;item@basegame:60;FLUIDSTORAGE,OPENSTORAGE
Key Fields:
id— Numeric canister IDitemid— Item ID for the canister itemtags— Comma-separated tags (storage type, constraints)
Usage
val bucket = CanistersCodex["basegame_1"]
println("Item: ${bucket.itemID}")
println("Is bucket: ${bucket.hasTag("BUCKET")}")
// Get canister for item
val canister = CanistersCodex.CanisterProps.values.find {
it.itemID == "basegame:bucket_wooden"
}
TerrarumWorldWatchdog
Defines periodic world update functions.
Structure
abstract class TerrarumWorldWatchdog(val runIntervalByTick: Int) {
abstract operator fun invoke(world: GameWorld)
}
runIntervalByTick:
1— Every tick60— Every second (at 60 TPS)1200— Every 20 seconds
Usage
class MyModWatchdog : TerrarumWorldWatchdog(60) {
override fun invoke(world: GameWorld) {
// Runs every second
world.actors.forEach { actor ->
if (actor.hasTag("BURNING")) {
actor.takeDamage(1.0)
}
}
}
}
// Register in module entry point
override fun invoke() {
ModMgr.registerWatchdog(MyModWatchdog())
}
ExtraGUI and Retexturing
These are specialised systems for UI extensions and texture replacements.
ExtraGUI
Defines additional UI overlays:
// Register custom GUI
ModMgr.registerExtraGUI("mymod:custom_hud") {
MyCustomHUDCanvas()
}
Retexturing
Allows texture pack overrides:
// Register texture replacement
ModMgr.registerRetexture("basegame:stone") {
TextureRegionPack(getFile("blocks/stone_alternate.tga"), 16, 16)
}
Module Loading Order
Codices must be loaded in dependency order:
override fun invoke() {
// GROUP 0: Dependencies for everything
ModMgr.GameMaterialLoader.invoke(moduleName)
ModMgr.GameFluidLoader.invoke(moduleName)
// GROUP 1: Items depend on materials
ModMgr.GameItemLoader.invoke(moduleName)
// GROUP 2: Blocks/ores depend on items/materials
ModMgr.GameBlockLoader.invoke(moduleName)
ModMgr.GameOreLoader.invoke(moduleName)
// GROUP 3: Everything else
ModMgr.GameCraftingRecipeLoader.invoke(moduleName)
ModMgr.GameLanguageLoader.invoke(moduleName)
ModMgr.GameAudioLoader.invoke(moduleName)
ModMgr.GameWeatherLoader.invoke(moduleName)
ModMgr.GameCanistersLoader.invoke(moduleName)
}
Best Practises
- Use consistent ID naming —
modulename:itemnamefor all IDs - Tag extensively — Tags enable flexible queries
- Document CSV columns — Comment headers with field descriptions
- Version your content — Use
versionsincefield for compatibility - Validate on load — Check for missing dependencies
- Use null objects — Return sensible defaults for missing IDs
- Prefix virtual tiles — Use
virtualtile:prefix - Don't hardcode IDs — Use string constants or enums
Common Pitfalls
- Wrong module name — ID won't resolve (
baseagme:vsbasegame:) - Missing prefix — Fluids need
fluid@, ores needores@ - Load order violation — Loading blocks before materials
- Circular dependencies — Item A requires Item B which requires Item A
- Forgetting tags — Can't query content without tags
- Not calling reload() — Dynamic items need manual reconstruction
- Hardcoded numeric IDs — Use string IDs for portability
See Also
- Modules-Setup — Creating and loading modules
- Blocks — Block system details
- Items — Item system details
- World — World generation using Codices