Modules:Codex Systems
minjaesong edited this page 2025-11-24 21:24:45 +09:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 key
  • shdr/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 mined
  • spawn — 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:

  1. Blocks — Automatically registered from BlockCodex
  2. Static items — Defined in items/items.csv
  3. Dynamic items — Runtime-created (customised tools, signed books)
  4. Actor items — Items created from actors (drops)
  5. 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 items
  • dynamicToStaticTable — 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 code
  • tens — Tensile strength
  • impf — Impact force resistance
  • dsty — Density (kg/m³)
  • fmod — Flexibility modulus
  • endurance — Durability/endurance
  • tcond — Thermal conductivity
  • reach — Reach/range factor
  • rcs — 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 ID
  • name — Translation key
  • shdr/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 ID
  • item — Item ID that represents this ore
  • tags — 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 ID
  • itemid — Item ID for the canister item
  • tags — 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 tick
  • 60 — 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

  1. Use consistent ID namingmodulename:itemname for all IDs
  2. Tag extensively — Tags enable flexible queries
  3. Document CSV columns — Comment headers with field descriptions
  4. Version your content — Use versionsince field for compatibility
  5. Validate on load — Check for missing dependencies
  6. Use null objects — Return sensible defaults for missing IDs
  7. Prefix virtual tiles — Use virtualtile: prefix
  8. Don't hardcode IDs — Use string constants or enums

Common Pitfalls

  • Wrong module name — ID won't resolve (baseagme: vs basegame:)
  • Missing prefix — Fluids need fluid@, ores need ores@
  • 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