Table of Contents
- Module Setup
- Overview
- Module Structure
- metadata.properties
- default.json
- Module Entry Point
- Codex Loading
- Custom Code
- Asset Loading
- Module Dependencies
- Load Order File
- Module Override
- Configuration System
- Debugging Modules
- Module Distribution
- Example: Complete Module
- metadata.properties
- EntryPoint.kt
- materials/materials.csv
- items/itemid.csv
- crafting/tools.json
- locales/en/items.txt
- Best Practises
- Common Pitfalls
- See Also
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
metadata.properties— Module metadata- 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 filedependency— Comma-separated list of required modules
Release:
releasedate— ISO date (YYYY-MM-DD)
Dependency Format
The dependency string has the following syntax:
module_name versionmodule1 version;module2 versionmodule1 version;module2 version;module3 version; ...
The version string has the following syntax:
a.b.c— the exact version a.b.ca.b.c+— Any version between a.b.c and a.b.65535a.b.*— Any version between a.b.0 and a.b.65535a.b+— Any version between a.b.0 and a.255.65535a.*— 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:
- Load order file specifies which modules to load
- Dependencies are checked before loading
- Missing dependencies cause module to fail loading
- 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 namespaced — enableNewFeature 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
- Compile code →
mymod.jar - Organise assets → directory structure
- Write
metadata.properties - Create
default.json(if using config) - Test with load order
- Archive →
mymod.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
- Use unique module names — Avoid conflicts with other mods
- Namespace all IDs —
modulename:itemname - Document your config — Add comments to
default.json - Test load order — Test as first module, middle module, last module
- Version your content — Track compatibility
- Provide translations — At least English
- Handle errors gracefully — Log, don't crash
- Clean up resources — Implement
dispose() - Follow conventions — Match basegame patterns
- 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
- Modules:Codex Systems — Detailed Codex documentation
- Blocks — Creating custom blocks
- Items — Creating custom items
- Actors — Creating custom actors
- Fixtures — Creating custom fixtures