Modules:Setup
minjaesong edited this page 2025-11-24 21:24:45 +09:00

Module Setup

Audience: Module developers creating new game content or total conversion mods.

Modules (mods) extend Terrarum with new blocks, items, actors, and systems. This guide covers the complete module structure, loading process, and best practises for creating modules.

Overview

Modules provide:

  • Content registration — Blocks, items, fluids, weather, crafting
  • Code execution — Custom actors, fixtures, game logic
  • Asset bundling — Textures, sounds, translations
  • Dependency management — Load order and version requirements
  • Configuration — User-customisable settings

Module Structure

Directory Layout

mods/
└── mymod/
    ├── metadata.properties       # Required: Module information
    ├── default.json              # Optional: Default configuration
    ├── mymod.jar                 # Optional: Compiled code
    ├── icon.png                  # Optional: Icon for the module
    ├── blocks/
    │   └── blocks.csv           # Block definitions
    ├── items/
    │   ├── itemid.csv           # Item class registrations
    │   └── items.tga            # Item spritesheet
    ├── materials/
    │   └── materials.csv        # Material properties
    ├── fluids/
    │   └── fluids.csv           # Fluid definitions
    ├── ores/
    │   └── ores.csv             # Ore generation params
    ├── crafting/
    │   ├── tools.json           # Crafting recipes for tools
    │   └── smelting.json        # Smelting recipes
    ├── weathers/
    │   ├── clear_day.json       # Weather definitions
    │   └── sky_clear_day.tga    # Weather assets
    ├── locales/
    │   ├── en/                  # English translations
    │   └── koKR/                # Korean translations
    ├── audio/
    │   ├── music/               # Music tracks
    │   └── sfx/                 # Sound effects
    └── sprites/
        ├── actors/              # Actor sprites
        └── fixtures/            # Fixture sprites

Required Files

  1. metadata.properties — Module metadata
  2. At least one Codex file — Content to load (blocks, items, etc.)

metadata.properties

Defines module information and loading behaviour.

Format

# Module identity
propername=My Awesome Mod
author=YourName
version=1.0.0
description=A description of what this mod does
description_ko=Non-latin character must be encoded with unicode literals like: \uACDF

# Module loading
order=10
entrypoint=net.torvald.mymod.EntryPoint
jar=mymod.jar
jarhash=<sha256sum of mymod.jar>
dependency=basegame

# Release info
releasedate=2025-01-15
package=net.torvald.mymod

Fields

Identity:

  • propername — Human-readable module name (required)
  • author — Module author (required)
  • version — Semantic version string (required)
  • description — English description (required)
  • description_<lang> — Translated descriptions (optional)
  • package — Java package name (required if using code)

Loading:

  • entrypoint — Fully-qualified class name of ModuleEntryPoint (required if using code)
  • jar — JAR filename containing code (required if using code)
  • jarhash — SHA256 hash of the JAR file
  • dependency — Comma-separated list of required modules

Release:

  • releasedate — ISO date (YYYY-MM-DD)

Dependency Format

The dependency string has the following syntax:

  • module_name version
  • module1 version;module2 version
  • module1 version;module2 version;module3 version; ...

The version string has the following syntax:

  • a.b.c — the exact version a.b.c
  • a.b.c+ — Any version between a.b.c and a.b.65535
  • a.b.* — Any version between a.b.0 and a.b.65535
  • a.b+ — Any version between a.b.0 and a.255.65535
  • a.* — Any version between a.0.0 and a.255.65535
  • * — Any version (for test only!)

For example, if your module requires basegame 0.3 or higher, the dependency string will be basegame 0.3+.

  • Why can't I specify the + sign on the major version number?
    • The change on the major version number denotes incompatible API changes, and therefore you're expected to investigate the changes, test if your module still works, and then manually update your module to ensure the end user that your module is still operational with the new version of the dependency.

icon.png

A module can have an icon about themselves. The image must be sized 48x48 and in 32-bit colour PNG format.

default.json

Provides default configuration values.

Format

{
  "enableFeatureX": true,
  "debugMode": false,
  "difficultyScale": 1.0,
  "customSettings": {
    "spawnRate": 0.5,
    "maxEnemies": 100
  }
}

Access in code:

val enabled = App.getConfigBoolean("mymod.enableFeatureX")
val difficulty = App.getConfigDouble("mymod.difficultyScale")

Configuration is automatically namespaced with module name.

Module Entry Point

The ModuleEntryPoint is invoked when the module loads.

Structure

package net.torvald.mymod

import net.torvald.terrarum.*

class EntryPoint : ModuleEntryPoint() {

    private val moduleName = "mymod"

    override fun getTitleScreen(batch: FlippingSpriteBatch): IngameInstance? {
        // Return custom title screen (optional)
        // Only the first module's title screen is used
        return MyTitleScreen(batch)
    }

    override fun invoke() {
        // Called when module loads
        // Register content here
        loadContent()
        registerActors()
        setupCustomSystems()
    }

    override fun dispose() {
        // Called when game shuts down
        // Clean up resources
    }

    private fun loadContent() {
        // Load assets
        CommonResourcePool.addToLoadingList("$moduleName.items") {
            ItemSheet(ModMgr.getGdxFile(moduleName, "items/items.tga"))
        }
        CommonResourcePool.loadAll()

        // Load Codices in dependency order
        ModMgr.GameMaterialLoader.invoke(moduleName)
        ModMgr.GameFluidLoader.invoke(moduleName)
        ModMgr.GameItemLoader.invoke(moduleName)
        ModMgr.GameBlockLoader.invoke(moduleName)
        ModMgr.GameOreLoader.invoke(moduleName)
        ModMgr.GameCraftingRecipeLoader.invoke(moduleName)
        ModMgr.GameLanguageLoader.invoke(moduleName)
        ModMgr.GameAudioLoader.invoke(moduleName)
        ModMgr.GameWeatherLoader.invoke(moduleName)
        ModMgr.GameCanistersLoader.invoke(moduleName)
    }
}

Codex Loading

Codices are loaded via ModMgr.Game*Loader objects.

Loading Order

Critical: Load in dependency order!

// GROUP 0: No dependencies
ModMgr.GameMaterialLoader.invoke(moduleName)
ModMgr.GameFluidLoader.invoke(moduleName)

// GROUP 1: Depends on materials
ModMgr.GameItemLoader.invoke(moduleName)

// GROUP 2: Depends on items and materials
ModMgr.GameBlockLoader.invoke(moduleName)
ModMgr.GameOreLoader.invoke(moduleName)

// GROUP 3: Depends on items and blocks
ModMgr.GameCraftingRecipeLoader.invoke(moduleName)
ModMgr.GameLanguageLoader.invoke(moduleName)
ModMgr.GameAudioLoader.invoke(moduleName)
ModMgr.GameWeatherLoader.invoke(moduleName)
ModMgr.GameCanistersLoader.invoke(moduleName)

Loader Details

Each loader expects specific files:

Loader File Path Format
GameMaterialLoader materials/materials.csv CSV
GameFluidLoader fluids/fluids.csv CSV
GameItemLoader items/itemid.csv CSV
GameBlockLoader blocks/blocks.csv CSV
GameOreLoader ores/ores.csv CSV
GameCraftingRecipeLoader crafting/*.json JSON
GameLanguageLoader locales/*/*.txt Text
GameAudioLoader audio/* Audio files
GameWeatherLoader weathers/*.json JSON
GameCanistersLoader canisters/canisters.csv CSV

Custom Code

Modules can include custom Kotlin/Java code.

JAR Structure

mymod.jar
└── net/torvald/mymod/
    ├── EntryPoint.class
    ├── actors/
    │   └── MyCustomActor.class
    ├── items/
    │   └── MyCustomItem.class
    └── fixtures/
        └── MyCustomFixture.class

Registering Custom Actors

class EntryPoint : ModuleEntryPoint() {
    override fun invoke() {
        // Register custom actor
        ActorRegistry.register("mymod:custom_npc") {
            MyCustomNPC()
        }
    }
}

class MyCustomNPC : ActorWithBody(RenderOrder.MIDTOP, PhysProperties.HUMANOID_DEFAULT(), "mymod:custom_npc") {
    init {
        val sprite = ADProperties(ModMgr.getGdxFile("mymod", "sprites/custom_npc.properties"))
        this.sprite = AssembledSpriteAnimation(sprite, this, false, false)
    }

    override fun updateImpl(delta: Float) {
        super.updateImpl(delta)
        // Custom AI logic
    }
}

Registering Custom Items

class MyCustomSword(originalID: ItemID) : GameItem(originalID) {
    override var baseMass = 1.5

    init {
        itemImage = ItemCodex.getItemImage(this)
        tags.add("WEAPON")
        tags.add("LEGENDARY")
        originalName = "mymod:legendary_sword"
    }

    override fun effectWhileEquipped(actor: ActorWithBody, delta: Float) {
        if (actor.actorValue.getAsBoolean("mymod:legendary_sword.strengthGiven") != true) {
            // Give strength buff while equipped
            actor.actorValue[AVKey.STRENGTHBUFF] += 50.0
            // Only give the buff once
            actor.actorValue["mymod:legendary_sword.strengthGiven"] = true
        }
    }
    
    override fun effectOnUnequip(actor: ActorWithBody) {
        // Remove strength buff while unequipped
        actor.actorValue[AVKey.STRENGTHBUFF] += 50.0
        actor.actorValue.remove("mymod:legendary_sword.strengthGiven")
    }
}

// Register in EntryPoint
ItemCodex.itemCodex["mymod:legendary_sword"] = MyCustomSword()

Asset Loading

Textures

// Load spritesheet
CommonResourcePool.addToLoadingList("mymod.custom_tiles") {
    TextureRegionPack(ModMgr.getGdxFile("mymod", "sprites/custom_tiles.tga"), 16, 16)
}

// Load single image
val texture = ModMgr.getGdxFile("mymod", "sprites/logo.png")

Audio

// Load music
val music = MusicContainer("My Song", ModMgr.getFile("mymod", "audio/music/song.ogg"))

// Load sound effect
AudioCodex.addToLoadingList("mymod:explosion") {
    SoundContainer(ModMgr.getGdxFile("mymod", "audio/sfx/explosion.ogg"))
}

Translations

File: locales/en/items.txt (or other translation files)

ITEM_CUSTOM_SWORD=Legendary Sword
ITEM_CUSTOM_SWORD_DESC=A blade of immense power.
ACTOR_CUSTOM_NPC=Mysterious Stranger

Usage:

val name = Lang["ITEM_CUSTOM_SWORD"]
val description = Lang["ITEM_CUSTOM_SWORD_DESC"]

Module Dependencies

Specify required modules in metadata.properties:

dependencies=basegame,anothermod

Dependency resolution:

  1. Load order file specifies which modules to load
  2. Dependencies are checked before loading
  3. Missing dependencies cause module to fail loading
  4. Circular dependencies are not allowed

Load Order File

File: <appdata>/load_order.csv

# Module load order
basegame
mymod
anothermod

Rules:

  • One module per line
  • Comments start with #
  • First module must provide title screen
  • Modules are loaded in order
  • Later modules override earlier content

Module Override

Later modules can override content from earlier modules:

// In mymod's EntryPoint
override fun invoke() {
    // Load content normally
    ModMgr.GameBlockLoader.invoke("mymod")

    // Override basegame stone with custom properties
    val customStone = BlockCodex["basegame:1"].copy()
    customStone.strength = 200  // Make stone harder
    BlockCodex.blockProps["basegame:1"] = customStone
}

This allows:

  • Texture packs — Replace sprites
  • Balance mods — Tweak block/item properties
  • Total conversions — Replace all content

Configuration System

Defining Config

metadata.properties:

configplan=enableNewFeature,spawnRateMultiplier,debugLogging

default.json:

{
  "enableNewFeature": true,
  "spawnRateMultiplier": 1.0,
  "debugLogging": false
}

Accessing Config

// In module code
val enabled = App.getConfigBoolean("mymod.enableNewFeature")
val multiplier = App.getConfigDouble("mymod.spawnRateMultiplier")

if (App.getConfigBoolean("mymod.debugLogging")) {
    println("Debug: Spawning actor at $x, $y")
}

Config is automatically namespacedenableNewFeature becomes mymod.enableNewFeature.

Debugging Modules

Development Build

Enable development features in config.jse:

config.enableScriptMods = true;
config.developerMode = true;

Logging

App.printdbg(this, "Module loaded successfully")
App.printmsg(this, "Important message")

if (App.IS_DEVELOPMENT_BUILD) {
    println("[MyMod] Detailed debug info")
}

Error Handling

override fun invoke() {
    try {
        ModMgr.GameBlockLoader.invoke(moduleName)
    }
    catch (e: Exception) {
        App.printmsg(this, "Failed to load blocks: ${e.message}")
        e.printStackTrace()
        ModMgr.logError(ModMgr.LoadErrorType.MY_FAULT, moduleName, e)
    }
}

Module Distribution

Packaging

  1. Compile codemymod.jar
  2. Organise assets → directory structure
  3. Write metadata.properties
  4. Create default.json (if using config)
  5. Test with load order
  6. Archivemymod.zip

Distribution Formats

  • Directory — For development (mods/mymod/)
  • ZIP archive — For end users (extract to mods/)

Version Compatibility

Use semantic versioning: MAJOR.MINOR.PATCH

version=1.2.3
  • MAJOR — Breaking changes
  • MINOR — New features (backwards-compatible)
  • PATCH — Bug fixes

Example: Complete Module

metadata.properties

name=Better Tools
author=Alice
version=1.0.0
description=Adds diamond tools to the game
order=20
entrypoint=net.alice.bettertools.EntryPoint
jarfile=bettertools.jar
dependencies=basegame
releasedate=2025-01-15
packagename=net.alice.bettertools

EntryPoint.kt

package net.alice.bettertools

import net.torvald.terrarum.*

class EntryPoint : ModuleEntryPoint() {

    private val moduleName = "bettertools"

    override fun invoke() {
        // Load assets
        CommonResourcePool.addToLoadingList("$moduleName.items") {
            ItemSheet(ModMgr.getGdxFile(moduleName, "items/items.tga"))
        }
        CommonResourcePool.loadAll()

        // Load content
        ModMgr.GameMaterialLoader.invoke(moduleName)
        ModMgr.GameItemLoader.invoke(moduleName)
        ModMgr.GameCraftingRecipeLoader.invoke(moduleName)
        ModMgr.GameLanguageLoader.invoke(moduleName)

        println("[BetterTools] Loaded successfully!")
    }

    override fun dispose() {
        // Cleanup if needed
    }
}

materials/materials.csv

idst;tens;impf;dsty;fmod;endurance;tcond;reach;rcs;sondrefl;comments
DIAM;100;500;3500;0.2;0.95;2000;5;95;1.0;diamond - hardest material

items/itemid.csv

Note: Items are registered by fully-qualified class names.

id;classname;tags
pickaxe_diamond;net.alice.bettertools.PickaxeDiamond;TOOL,PICK
axe_diamond;net.alice.bettertools.AxeDiamond;TOOL,AXE

Each item needs a corresponding Kotlin class:

package net.alice.bettertools

class PickaxeDiamond(originalID: ItemID) : GameItem(originalID) {
    override var baseMass = 1.8
    override var baseToolSize: Double? = 2.5
    override val materialId = "DIAM"

    init {
        originalName = "ITEM_PICKAXE_DIAMOND"
        tags.add("TOOL")
        tags.add("PICK")
    }
}

crafting/tools.json

{
  "item@bettertools:pickaxe_diamond": { /* diamond pick */
    "workbench": "basiccrafting,metalworking",
    "ingredients": [[1, 5, "item@bettertools:diamond_gem", 2, "item@basegame:18"]] /* 5 gems, 2 sticks */
  },
  "item@bettertools:axe_diamond": { /* diamond axe */
    "workbench": "basiccrafting,metalworking",
    "ingredients": [[1, 5, "item@bettertools:diamond_gem", 2, "item@basegame:18"]]
  }
}

locales/en/items.txt

ITEM_PICKAXE_DIAMOND=Diamond Pickaxe
ITEM_PICKAXE_DIAMOND_DESC=The ultimate mining tool.
ITEM_AXE_DIAMOND=Diamond Axe
ITEM_AXE_DIAMOND_DESC=Chops trees with ease.

Best Practises

  1. Use unique module names — Avoid conflicts with other mods
  2. Namespace all IDsmodulename:itemname
  3. Document your config — Add comments to default.json
  4. Test load order — Test as first module, middle module, last module
  5. Version your content — Track compatibility
  6. Provide translations — At least English
  7. Handle errors gracefully — Log, don't crash
  8. Clean up resources — Implement dispose()
  9. Follow conventions — Match basegame patterns
  10. Test with other mods — Ensure compatibility

Common Pitfalls

  • Wrong entry point class name — Module won't load
  • Missing JAR file — Code not found
  • Load order violations — Dependencies loaded after dependents
  • Hardcoded paths — Use ModMgr.getGdxFile()
  • Forgotten asset loading — Textures not in CommonResourcePool
  • ID conflicts — Two modules use same IDs
  • Circular dependencies — A depends on B depends on A
  • Not testing clean install — Missing files in distribution

See Also