wiki update

minjaesong
2025-11-24 21:24:45 +09:00
parent 75a6cba056
commit 3019ace6dd
23 changed files with 12264 additions and 120 deletions

748
Actor-Values-Reference.md Normal file

@@ -0,0 +1,748 @@
# Actor Values Reference
**Audience:** Module developers working with actor properties and behaviour.
Actor values (`actorValue`) are key-value pairs that define an actor's properties, stats, and state. This reference documents all standard actor value keys defined in the engine.
## Overview
Actor values provide:
- **Stats** — Health, strength, speed, defence
- **Physical properties** — Mass, height, luminosity
- **Movement** — Jump power, flight, climbing
- **Combat** — Tool size, reach, action timing
- **Identity** — Name, race, species
- **State** — Timers, counters, temporary flags
- **Special** — Portal dictionary, ore dictionary, game mode
## Accessing Actor Values
```kotlin
// Get value
val speed = actor.actorValue.getAsDouble(AVKey.SPEED)
val name = actor.actorValue.getAsString(AVKey.NAME)
val health = actor.actorValue.getAsDouble(AVKey.HEALTH)
// Set value
actor.actorValue[AVKey.SPEED] = 5.0
actor.actorValue[AVKey.NAME] = "Hero"
actor.actorValue[AVKey.HEALTH] = 100.0
// Check if exists
if (actor.actorValue.containsKey(AVKey.MAGIC)) {
// Actor has magic
}
// Remove value
actor.actorValue.remove(AVKey.__ACTION_TIMER)
```
## Movement and Physics
### Speed
**Key:** `AVKey.SPEED` / `AVKey.SPEEDBUFF`
**Type:** Double
**Unit:** Pixels per frame
**Description:** Walking/running speed
**Typical values:**
- Slow: 2-3 px/frame
- Normal: 4-6 px/frame
- Fast: 7-10 px/frame
```kotlin
actor.actorValue[AVKey.SPEED] = 5.0 // Base speed
actor.actorValue[AVKey.SPEEDBUFF] = 2.0 // Speed buff from equipment
// Effective speed = 5.0 + 2.0 = 7.0 px/frame
```
### Acceleration
**Key:** `AVKey.ACCEL` / `AVKey.ACCELBUFF`
**Type:** Double
**Unit:** Pixels per frame squared
**Description:** Acceleration of movement (running, flying, driving)
```kotlin
actor.actorValue[AVKey.ACCEL] = 0.5 // Gradual acceleration
```
### Jump Power
**Key:** `AVKey.JUMPPOWER` / `AVKey.JUMPPOWERBUFF`
**Type:** Double
**Unit:** Pixels per frame
**Description:** Initial velocity when jumping
**Typical values:**
- Weak jump: 8-10 px/frame
- Normal jump: 12-15 px/frame
- High jump: 18-25 px/frame
```kotlin
actor.actorValue[AVKey.JUMPPOWER] = 15.0
```
### Vertical Stride
**Key:** `AVKey.VERTSTRIDE`
**Type:** Double
**Unit:** Pixels
**Description:** Maximum height of a stair the actor can climb
**Typical values:**
- Small creatures: 8-12 pixels
- Humanoids: 16-20 pixels
- Large creatures: 24-32 pixels
```kotlin
actor.actorValue[AVKey.VERTSTRIDE] = 18.0 // Can climb 18-pixel stairs
```
### Air Jumping
**Key:** `AVKey.AIRJUMPPOINT` (max) / `AVKey.AIRJUMPCOUNT` (current)
**Type:** Int
**Description:** Number of air jumps allowed (double-jump, triple-jump, etc.)
**Note:** 0 is treated as 1 (one air jump allowed)
```kotlin
actor.actorValue[AVKey.AIRJUMPPOINT] = 2 // Can double-jump
actor.actorValue[AVKey.AIRJUMPCOUNT] = 0 // No jumps used yet
// When air jumping:
val current = actor.actorValue.getAsInt(AVKey.AIRJUMPCOUNT) ?: 0
val max = actor.actorValue.getAsInt(AVKey.AIRJUMPPOINT) ?: 0
if (current < max) {
performAirJump()
actor.actorValue[AVKey.AIRJUMPCOUNT] = current + 1
}
// Reset on landing:
actor.actorValue[AVKey.AIRJUMPCOUNT] = 0
```
### Flight
**Key:** `AVKey.FLIGHTPOINT` (max) / `AVKey.FLIGHTTIMER` (current)
**Type:** Double
**Unit:** Seconds
**Description:** How long the actor can fly continuously
```kotlin
actor.actorValue[AVKey.FLIGHTPOINT] = 10.0 // Can fly for 10 seconds
actor.actorValue[AVKey.FLIGHTTIMER] = 0.0 // Not flying yet
// During flight:
val timer = actor.actorValue.getAsDouble(AVKey.FLIGHTTIMER) ?: 0.0
actor.actorValue[AVKey.FLIGHTTIMER] = timer + delta
// Check if out of flight time:
if (timer >= actor.actorValue.getAsDouble(AVKey.FLIGHTPOINT) ?: 0.0) {
stopFlying()
}
```
### Friction Multiplier
**Key:** `AVKey.FRICTIONMULT`
**Type:** Double
**Description:** Friction multiplier (only effective when noclip=true, e.g., camera actors)
**Not meant for living creatures**
```kotlin
actor.actorValue[AVKey.FRICTIONMULT] = 0.8 // Less friction
```
### Drag Coefficient
**Key:** `AVKey.DRAGCOEFF`
**Type:** Double
**Description:** Air/water drag resistance
```kotlin
actor.actorValue[AVKey.DRAGCOEFF] = 0.5
```
### Fall Dampen Multiplier
**Key:** `AVKey.FALLDAMPENMULT`
**Type:** Double
**Description:** Multiplier for fall damage reduction
```kotlin
actor.actorValue[AVKey.FALLDAMPENMULT] = 0.5 // 50% fall damage reduction
```
## Physical Properties
### Base Mass
**Key:** `AVKey.BASEMASS`
**Type:** Double
**Unit:** Kilograms
**Description:** Actor's body mass
**Typical values:**
- Small animals: 5-20 kg
- Humanoids: 50-100 kg
- Large creatures: 150-500 kg
```kotlin
actor.actorValue[AVKey.BASEMASS] = 70.0 // 70 kg human
```
### Base Height
**Key:** `AVKey.BASEHEIGHT`
**Type:** Double
**Unit:** Pixels
**Description:** Actor's standing height
```kotlin
actor.actorValue[AVKey.BASEHEIGHT] = 48.0 // 48 pixels tall
```
### Scale
**Key:** `AVKey.SCALE` / `AVKey.SCALEBUFF`
**Type:** Double
**Description:** Size scale multiplier (aka PHYSIQUE)
```kotlin
actor.actorValue[AVKey.SCALE] = 1.0 // Normal size
actor.actorValue[AVKey.SCALE] = 1.5 // 150% size (larger, heavier)
```
## Luminosity and Opacity
### Luminosity
**Keys:**
- `AVKey.LUMR` — Red channel
- `AVKey.LUMG` — Green channel
- `AVKey.LUMB` — Blue channel
- `AVKey.LUMA` — UV channel
**Type:** Double
**Unit:** 0.0-1.0
**Description:** Light emitted by the actor
```kotlin
// Glowing actor (orange light)
actor.actorValue[AVKey.LUMR] = 0.8
actor.actorValue[AVKey.LUMG] = 0.4
actor.actorValue[AVKey.LUMB] = 0.1
actor.actorValue[AVKey.LUMA] = 0.0
```
### Opacity
**Keys:**
- `AVKey.OPAR` — Red channel opacity
- `AVKey.OPAG` — Green channel opacity
- `AVKey.OPAB` — Blue channel opacity
- `AVKey.OPAA` — UV channel opacity
**Type:** Double
**Unit:** 0.0-1.0
**Description:** Light blocked by the actor
```kotlin
// Semi-transparent actor
actor.actorValue[AVKey.OPAR] = 0.5
actor.actorValue[AVKey.OPAG] = 0.5
actor.actorValue[AVKey.OPAB] = 0.5
actor.actorValue[AVKey.OPAA] = 1.0
```
## Combat and Actions
### Strength
**Key:** `AVKey.STRENGTH` / `AVKey.STRENGTHBUFF`
**Type:** Int
**Default:** 1000
**Description:** Physical strength
```kotlin
actor.actorValue[AVKey.STRENGTH] = 1000 // Base strength
actor.actorValue[AVKey.STRENGTHBUFF] = 200 // Buff from equipment
// Effective strength = 1200
```
### Defence
**Key:** `AVKey.DEFENCE` / `AVKey.DEFENCEBUFF`
**Type:** Double
**Unit:** TBA
**Description:** Base defence point of the species
```kotlin
actor.actorValue[AVKey.DEFENCE] = 50.0
actor.actorValue[AVKey.DEFENCEBUFF] = 10.0
```
### Armour Defence
**Key:** `AVKey.ARMOURDEFENCE` / `AVKey.ARMOURDEFENCEBUFF`
**Type:** Double
**Unit:** TBA
**Description:** Defence from equipped armour
```kotlin
actor.actorValue[AVKey.ARMOURDEFENCE] = 75.0 // From equipped armour
```
### Reach
**Key:** `AVKey.REACH` / `AVKey.REACHBUFF`
**Type:** Double
**Unit:** Pixels
**Description:** Hand reach distance (affects gameplay as player)
**Typical values:**
- Short reach: 32-48 pixels
- Normal reach: 64-80 pixels
- Long reach: 96-128 pixels
```kotlin
actor.actorValue[AVKey.REACH] = 72.0 // Can interact 72 pixels away
```
### Tool Size
**Key:** `AVKey.TOOLSIZE`
**Type:** Double
**Unit:** Kilograms
**Description:** Size/weight of equipped tool
Affects:
- Attack strength
- Mining speed
- Stamina drain
```kotlin
actor.actorValue[AVKey.TOOLSIZE] = 2.5 // 2.5 kg pickaxe
```
### Action Interval
**Key:** `AVKey.ACTION_INTERVAL`
**Type:** Double
**Unit:** Seconds
**Description:** Time between actions (attack speed, mining speed)
```kotlin
actor.actorValue[AVKey.ACTION_INTERVAL] = 0.5 // 2 actions per second
```
### Action Timer
**Key:** `AVKey.__ACTION_TIMER`
**Type:** Double
**Unit:** Milliseconds
**Description:** How long action button has been held, or NPC wait time
**Internal state variable (prefixed with `__`)**
```kotlin
// Track button hold time
val timer = actor.actorValue.getAsDouble(AVKey.__ACTION_TIMER) ?: 0.0
actor.actorValue[AVKey.__ACTION_TIMER] = timer + delta * 1000
// Reset on release
actor.actorValue.remove(AVKey.__ACTION_TIMER)
```
### Encumbrance
**Key:** `AVKey.ENCUMBRANCE`
**Type:** Double
**Description:** Weight penalty from inventory
```kotlin
val totalWeight = inventory.getTotalWeight()
actor.actorValue[AVKey.ENCUMBRANCE] = totalWeight
```
## Health and Magic
### Health
**Key:** `AVKey.HEALTH`
**Type:** Double
**Description:** Current health points
```kotlin
actor.actorValue[AVKey.HEALTH] = 100.0
```
### Magic
**Key:** `AVKey.MAGIC`
**Type:** Double
**Description:** Current magic/mana points
```kotlin
actor.actorValue[AVKey.MAGIC] = 50.0
```
### Magic Regeneration Rate
**Key:** `AVKey.MAGICREGENRATE` / `AVKey.MAGICREGENRATEBUFF`
**Type:** Double
**Unit:** Points per second
**Description:** Magic regeneration rate
```kotlin
actor.actorValue[AVKey.MAGICREGENRATE] = 5.0 // Regenerate 5 magic/sec
```
## Identity
### Name
**Key:** `AVKey.NAME`
**Type:** String
**Description:** Actor's individual name
```kotlin
actor.actorValue[AVKey.NAME] = "Jarppi"
```
### Race Name
**Key:** `AVKey.RACENAME` (singular) / `AVKey.RACENAMEPLURAL` (plural)
**Type:** String
**Description:** Species/race name
```kotlin
actor.actorValue[AVKey.RACENAME] = "Duudsoni"
actor.actorValue[AVKey.RACENAMEPLURAL] = "Duudsonit"
```
### Intelligent
**Key:** `AVKey.INTELLIGENT`
**Type:** Boolean
**Description:** Whether the player can talk with this actor
```kotlin
actor.actorValue[AVKey.INTELLIGENT] = true // Can be talked to
```
### UUID
**Key:** `AVKey.UUID`
**Type:** String
**Description:** Unique identifier (for fixtures and special actors)
```kotlin
actor.actorValue[AVKey.UUID] = "550e8400-e29b-41d4-a716-446655440000"
```
## Player-Specific
### Game Mode
**Key:** `AVKey.GAMEMODE`
**Type:** String
**Description:** Current game mode (only for IngamePlayers)
**Reserved values:**
- `"survival"` — Survival mode (future use)
- `""` (empty) — Creative mode
```kotlin
actor.actorValue[AVKey.GAMEMODE] = "survival"
```
### Quick Slot Selection
**Key:** `AVKey.__PLAYER_QUICKSLOTSEL`
**Type:** Int
**Description:** Currently selected quick slot index
**Internal state variable**
```kotlin
actor.actorValue[AVKey.__PLAYER_QUICKSLOTSEL] = 3 // Slot 3 selected
```
### Wire Cutter Selection
**Key:** `AVKey.__PLAYER_WIRECUTTERSEL`
**Type:** Int
**Description:** Wire cutter mode selection
**Internal state variable**
```kotlin
actor.actorValue[AVKey.__PLAYER_WIRECUTTERSEL] = 1
```
## Historical Data
### Born Time
**Key:** `AVKey.__HISTORICAL_BORNTIME`
**Type:** Long
**Unit:** TIME_T (world time seconds)
**Description:** When the actor was spawned
```kotlin
actor.actorValue[AVKey.__HISTORICAL_BORNTIME] = world.worldTime.TIME_T
```
### Dead Time
**Key:** `AVKey.__HISTORICAL_DEADTIME`
**Type:** Long
**Unit:** TIME_T (world time seconds)
**Description:** When the actor died (-1 if alive)
```kotlin
actor.actorValue[AVKey.__HISTORICAL_DEADTIME] = -1L // Alive
// On death:
actor.actorValue[AVKey.__HISTORICAL_DEADTIME] = world.worldTime.TIME_T
```
## Special Dictionaries
### World Portal Dictionary
**Key:** `AVKey.WORLD_PORTAL_DICT`
**Type:** String
**Format:** Comma-separated UUIDs (Ascii85-encoded, big endian)
**Description:** List of discovered world portals
```kotlin
// Example value:
"SIxM+kGlrjZgLx5Zeqz7,;:UIZ5Q=2WT35SgKpOp.,vvf'fNW3G<ROimy(Y;E<,-mdtr5|^RGOqr0x*T*lC,YABr1oQwErKG)pGC'gUG"
// Add portal:
val currentDict = actor.actorValue.getAsString(AVKey.WORLD_PORTAL_DICT) ?: ""
val newPortal = portalUUID.toAscii85()
actor.actorValue[AVKey.WORLD_PORTAL_DICT] = if (currentDict.isEmpty()) {
newPortal
} else {
"$currentDict,$newPortal"
}
```
### Ore Dictionary
**Key:** `AVKey.ORE_DICT`
**Type:** String
**Format:** Comma-separated ItemIDs
**Description:** Ores the player has discovered
**Note:** Uses item IDs (`item@basegame:128`) not ore IDs (`ores@basegame:1`)
```kotlin
// Add discovered ore:
val oreDict = actor.actorValue.getAsString(AVKey.ORE_DICT) ?: ""
val newOre = "item@basegame:128" // Copper ore item
if (!oreDict.contains(newOre)) {
actor.actorValue[AVKey.ORE_DICT] = if (oreDict.isEmpty()) {
newOre
} else {
"$oreDict,$newOre"
}
}
// Check if ore discovered:
fun hasDiscoveredOre(actor: ActorWithBody, oreItemID: ItemID): Boolean {
val oreDict = actor.actorValue.getAsString(AVKey.ORE_DICT) ?: ""
return oreDict.split(',').contains(oreItemID)
}
```
## Bare-Hand Actions
### Bare-Hand Minimum Height
**Key:** `AVKey.BAREHAND_MINHEIGHT`
**Type:** Double
**Description:** Minimum height to enable bare-hand actions
```kotlin
actor.actorValue[AVKey.BAREHAND_MINHEIGHT] = 16.0
```
### Bare-Hand Base Digging Size
**Key:** `AVKey.BAREHAND_BASE_DIGSIZE`
**Type:** Double
**Description:** Base digging size with bare hands
```kotlin
actor.actorValue[AVKey.BAREHAND_BASE_DIGSIZE] = 1.0 // 1x1 block
```
## Buff System
Many keys have corresponding `*BUFF` keys for temporary bonuses:
```kotlin
// Base value + buff = effective value
val baseSpeed = actor.actorValue.getAsDouble(AVKey.SPEED) ?: 0.0
val speedBuff = actor.actorValue.getAsDouble(AVKey.SPEEDBUFF) ?: 0.0
val effectiveSpeed = baseSpeed + speedBuff
// Apply temporary speed boost:
actor.actorValue[AVKey.SPEEDBUFF] = 3.0 // +3 px/frame
// Remove buff:
actor.actorValue.remove(AVKey.SPEEDBUFF)
```
**Buffable keys:**
- `SPEED` / `SPEEDBUFF`
- `ACCEL` / `ACCELBUFF`
- `JUMPPOWER` / `JUMPPOWERBUFF`
- `SCALE` / `SCALEBUFF`
- `STRENGTH` / `STRENGTHBUFF`
- `DEFENCE` / `DEFENCEBUFF`
- `REACH` / `REACHBUFF`
- `ARMOURDEFENCE` / `ARMOURDEFENCEBUFF`
- `MAGICREGENRATE` / `MAGICREGENRATEBUFF`
## Naming Conventions
- **Regular keys** — Lowercase, descriptive name
- **Buff keys** — Base key + `"buff"` suffix
- **Internal state** — Prefixed with `__` (double underscore)
- **Historical data** — Prefixed with `__HISTORICAL_`
- **Player-specific** — Prefixed with `__PLAYER_`
## Best Practises
1. **Use AVKey constants** — Never hardcode key strings
2. **Check type before casting** — Use `getAsDouble()`, `getAsString()`, etc.
3. **Handle null values** — Provide sensible defaults
4. **Apply buffs additively** — Base + Buff = Effective
5. **Clean up temporary values** — Remove timer/counter keys when done
6. **Document custom keys** — Add comments for module-specific keys
7. **Namespace custom keys** — Use `"mymod:customkey"` format
8. **Serialise carefully** — Not all keys should be saved
9. **Use internal prefix** — Mark transient state with `__`
10. **Validate ranges** — Clamp values to reasonable bounds
## Common Pitfalls
- **Forgetting to check null** — `getAsDouble()` returns null if missing
- **Not removing buffs** — Buffs persist forever unless removed
- **Hardcoding key strings** — Use AVKey constants
- **Wrong type casting** — Check types before accessing
- **Modifying base stats directly** — Use buffs for temporary changes
- **Not serialising important data** — Historical data may be lost
- **Conflicting custom keys** — Use unique namespaced keys
- **Assuming defaults** — Not all actors have all keys
- **Not updating timers** — Increment/decrement counters properly
- **Overwriting instead of adding** — Buffs should add, not replace
## See Also
- [[Actors]] — Actor system overview
- [[Items#Item-Effects]] — Item effects on actor values
- [[Modules-Setup]] — Creating custom actor values

453
Actors.md Normal file

@@ -0,0 +1,453 @@
# Actors
Actors are the fundamental interactive entities in the Terrarum engine. Everything that can be updated and rendered in the game world—players, NPCs, creatures, particles, projectiles, furniture, and more—is an Actor.
## Overview
The Actor system is built on a hierarchy of classes providing increasing levels of functionality:
1. **Actor** — Base class for all entities
2. **ActorWithBody** — Actors with position, physics, and sprites
3. **Specialised Actor Types** — Players, NPCs, fixtures, and more
## Actor Base Class
All actors inherit from `net.torvald.terrarum.gameactors.Actor`.
### Key Properties
#### Reference ID
Every actor has a unique **Reference ID** (ActorID), an integer between 16,777,216 and 0x7FFF_FFFF. This ID is used to track and retrieve actors throughout their lifecycle.
```kotlin
val referenceID: ActorID
```
The player actor always has a special reference ID: `Terrarum.PLAYER_REF_ID` (0x91A7E2).
#### Render Order
Determines which visual layer the actor is drawn on. From back to front:
- **FAR_BEHIND** — Wires and conduits
- **BEHIND** — Tapestries, some particles (obstructed by terrain)
- **MIDDLE** — Standard actors (players, NPCs, creatures)
- **MIDTOP** — Projectiles, thrown items
- **FRONT** — Front walls (blocks that obstruct actors)
- **OVERLAY** — Screen overlays, not affected by lightmap
```kotlin
var renderOrder: RenderOrder
```
### Actor Lifecycle
#### Update Cycle
Actors implement the `updateImpl(delta: Float)` method, which is called every frame:
```kotlin
abstract fun updateImpl(delta: Float)
```
The base `update()` method automatically handles despawn logic before calling your implementation.
#### Despawning
Actors can be flagged for removal from the game world:
```kotlin
var flagDespawn: Boolean
val canBeDespawned: Boolean // override to prevent despawning
```
When an actor is despawned, the `despawn()` method is called, which:
1. Stops any music tracks associated with the actor
2. Calls the despawn hook
3. Marks the actor as despawned
```kotlin
@Transient var despawnHook: (Actor) -> Unit = {}
```
#### Serialisation and Reload
When saving the game, actors are serialised. Fields marked with `@Transient` are not saved and must be reconstructed when loading.
The `reload()` method is called after deserialisation:
```kotlin
open fun reload() {
actorValue.actor = this
// Reconstruct transient fields here
}
```
### ActorValue System
Actors have a flexible property system called **ActorValue**, similar to Elder Scrolls' actor value system. It stores various attributes and stats:
```kotlin
var actorValue: ActorValue
```
ActorValue stores key-value pairs and notifies the actor when values change via the event handler:
```kotlin
abstract fun onActorValueChange(key: String, value: Any?)
```
Common ActorValue keys are defined in `AVKey` (e.g., `AVKey.SCALE`, `AVKey.BASEMASS`, `AVKey.NAME`).
### Audio
Actors can play music and sound effects with spatial positioning:
```kotlin
// Music tracks associated with this actor
val musicTracks: HashMap<MusicContainer, TerrarumAudioMixerTrack>
// Start playing music
fun startMusic(music: MusicContainer)
```
By default, music stops when the actor despawns. Override `stopMusicOnDespawn` to change this behaviour:
```kotlin
open val stopMusicOnDespawn: Boolean = true
```
## ActorWithBody
`ActorWithBody` extends `Actor` with physical presence, including position, collision, physics, and sprite rendering.
### Position and Hitbox
ActorWithBody has a precise pixel-based position system:
```kotlin
val hitbox: Hitbox // position and dimensions in pixels
```
The hitbox defines the actor's:
- **Position** (top-left corner in pixels)
- **Dimensions** (width and height in pixels)
- **Collision area**
#### Setting Position and Size
Use these methods to configure the actor's spatial properties:
```kotlin
fun setHitboxDimension(width: Int, height: Int, translateX: Int, translateY: Int)
fun setPosition(x: Double, y: Double)
fun translatePosition(deltaX: Double, deltaY: Double)
```
#### Tilewise Hitboxes
For tile-based calculations, ActorWithBody provides tilewise hitbox conversions:
```kotlin
// Half-integer tilewise hitbox (for physics)
val hIntTilewiseHitbox: Hitbox
// Integer tilewise hitbox (for block occupation checks)
val intTilewiseHitbox: Hitbox
```
When iterating over tiles occupied by an actor:
```kotlin
for (x in actor.intTilewiseHitbox.startX.toInt()..actor.intTilewiseHitbox.endX.toInt()) {
for (y in actor.intTilewiseHitbox.startY.toInt()..actor.intTilewiseHitbox.endY.toInt()) {
// Process tile at (x, y)
}
}
```
### Physics Properties
ActorWithBody contains a `PhysProperties` object defining physical characteristics:
```kotlin
var physProp: PhysProperties
```
PhysProperties includes:
- **immobile** — Whether the actor can move
- **usePhysics** — Whether physics simulation applies
- **movementType** — Determines physics behaviour (see PhysicalTypes)
The **physics simulation** applies acceleration, velocity, and collision. Use the following constants:
- **METER** — 1 metre = 25 pixels
- **PHYS_TIME_FRAME** — Time step for physics simulation
### Velocity
ActorWithBody has two velocity systems:
#### External Velocity
For physics-driven movement (gravity, collisions, forces):
```kotlin
internal val externalV: Vector2
```
#### Controller Velocity
For player/AI-controlled movement:
```kotlin
var controllerV: Vector2? // non-null only for Controllable actors
```
Velocity units are **pixels per frame** (at 60 FPS). The engine automatically scales velocity for different frame rates using:
```kotlin
val adjustedVelocity = velocity * (Terrarum.PHYS_REF_FPS * delta)
```
### Mass and Scale
ActorWithBody supports dynamic mass and size:
```kotlin
val mass: Double // Calculated from base mass × scale³
val scale: Double // Apparent scale (base scale × scale buffs)
```
Mass is stored in **kilograms**. Default mass is defined in the physics properties.
### Sprites and Animation
Actors can have multiple sprite layers:
```kotlin
@Transient var sprite: SpriteAnimation? // Main sprite
@Transient var spriteGlow: SpriteAnimation? // Glow layer
@Transient var spriteEmissive: SpriteAnimation? // Emissive layer
```
Sprites must be reconstructed in the `reload()` method as they are transient.
The `drawMode` property controls sprite blending:
```kotlin
var drawMode: BlendMode = BlendMode.NORMAL
```
### Lighting
ActorWithBody actors can emit and block light:
```kotlin
open var lightBoxList: ArrayList<Lightbox> // Light emission areas
open var shadeBoxList: ArrayList<Lightbox> // Light blocking areas
```
Lightboxes are relative to the actor's position and define RGB+UV light values.
**Important:** These lists must be marked `@Transient` and use `ArrayList` (which has a no-arg constructor for serialisation).
### Stationary vs. Mobile
Actors can be flagged as stationary to optimise rendering and physics:
```kotlin
open var isStationary: Boolean = true
```
Stationary actors don't move and can be culled more aggressively.
## Actor Interfaces
The engine provides several interfaces to add specific capabilities to actors:
### Controllable
For actors that can be controlled by players or AI:
```kotlin
interface Controllable {
var controllerV: Vector2?
var moveState: MoveState
// ... control methods
}
```
**AvailableControllers:**
- Player input
- AI controller
- Scripted movement
### Factionable
For actors belonging to factions:
```kotlin
interface Factionable {
var faction: String?
// Faction relationships affect AI behaviour
}
```
See also: [[Faction|Development:Faction]]
### AIControlled
For actors with AI behaviour:
```kotlin
interface AIControlled {
fun aiUpdate(delta: Float)
}
```
### Luminous
For actors that emit light (integrated into ActorWithBody via lightBoxList).
### NoSerialise
A marker interface for actors that should not be saved:
```kotlin
interface NoSerialise
```
## Specialised Actor Types
### BlockMarkerActor
A special actor that marks block positions without physical interaction. Used internally for debug visualisation and block tracking.
### WireActor
Represents wire/conduit connections in the game world. Rendered in the FAR_BEHIND layer.
### Fixtures
Fixtures are special ActorWithBody instances representing world objects like:
- Doors
- Chests
- Crafting stations
- Furniture
- Interactive machinery
Fixtures typically:
1. Implement `FixtureBase` or similar classes
2. Have associated UI interactions
3. May be immobile (`physProp.immobile = true`)
4. Often use `isStationary = true`
### Particles
Particles are lightweight actors for visual effects:
- Typically have short lifespans
- May not use full physics
- Often rendered in BEHIND or MIDTOP layers
- Usually set `canBeDespawned = true`
### Projectiles
Projectiles (arrows, bullets, thrown items):
- Extend ActorWithBody
- Typically use MIDTOP render order
- Implement collision detection
- Self-despawn on impact or timeout
## Creating Custom Actors
### Basic Actor Example
```kotlin
class MyActor : Actor(RenderOrder.MIDDLE, null) {
override fun updateImpl(delta: Float) {
// Update logic here
}
override fun onActorValueChange(key: String, value: Any?) {
// Respond to actor value changes
}
override fun run() {
// Thread execution if needed
}
}
```
### ActorWithBody Example
```kotlin
class MyPhysicalActor : ActorWithBody(
RenderOrder.MIDDLE,
PhysProperties.HUMANOID_DEFAULT(),
null // auto-generate ID
) {
init {
setHitboxDimension(32, 48, 0, 0) // 32×48 pixel hitbox
setPosition(100.0, 100.0)
}
override fun updateImpl(delta: Float) {
// Physics is handled automatically
// Add custom behaviour here
}
override fun onActorValueChange(key: String, value: Any?) {
when (key) {
AVKey.SCALE -> {
// Respond to scale changes
}
}
}
override fun reload() {
super.reload()
// Reconstruct sprite and other transient fields
sprite = loadMySprite()
}
}
```
## Actor Management in IngameInstance
Actors are managed by the `IngameInstance` which maintains actor lists and handles updates:
```kotlin
// Get an actor by ID
val actor = INGAME.getActorByID(actorID)
// Add an actor to the world
INGAME.addNewActor(myActor)
// Remove an actor
actor.flagDespawn = true
```
## Best Practises
1. **Always call `super.reload()`** when overriding reload()
2. **Mark sprites and UI as `@Transient`** — they cannot be serialised
3. **Use ArrayList for lightBoxList/shadeBoxList** — other collections won't serialise properly
4. **Set `canBeDespawned = false`** for actors that must persist (like the player)
5. **Use ActorValue for dynamic properties** rather than adding many fields
6. **Clean up resources in `despawn()`** — stop audio, dispose textures, etc.
7. **Keep actor updates efficient** — updateImpl is called every frame for every actor
8. **Use appropriate RenderOrder** — incorrect ordering causes visual glitches
## Common Pitfalls
- **Forgetting to reconstruct transient fields in reload()** leads to null sprite crashes
- **Modifying hitbox directly** instead of using setPosition/setHitboxDimension causes physics issues
- **Not accounting for delta time** makes behaviour frame-rate dependent
- **Creating circular actor references** can prevent serialisation
## See Also
- [[Glossary]] — Actor-related terminology
- [[Development:Actors]] — In-depth actor implementation details
- [[Modules:Actors]] — Creating actors in modules
- [[Faction|Development:Faction]] — Faction system for actors
- [[Creature RAW|Development:RAW]] — Data-driven creature definitions

@@ -0,0 +1,611 @@
# Animation Description Language (ADL)
**Audience:** Engine maintainers and module developers creating animated humanoid characters.
The Animation Description Language (ADL) is a declarative system for defining skeletal animations for humanoid actors. It uses `.properties` files to define body parts, skeletons, and frame-by-frame transformations, which are then assembled into animated spritesheets at runtime.
## Overview
ADL provides:
- **Skeletal animation** — Define joints and transform them per-frame
- **Modular body parts** — Separate images for head, torso, limbs, etc.
- **Animation sequences** — Run cycles, idle animations, etc.
- **Equipment layering** — Headgear, held items, armour overlays
- **Runtime assembly** — Build final spritesheet from components
## ADL File Format
ADL uses Java `.properties` format with special conventions.
### File Structure
```properties
SPRITESHEET=mods/basegame/sprites/fofu/fofu_
EXTENSION=.tga
CONFIG=SIZE 48,56;ORIGINX 29
BODYPARTS=HEADGEAR 11,11;\
HEAD 11,11;\
ARM_REST_RIGHT 4,2;\
TORSO 10,4
SKELETON_STAND=HEADGEAR 0,32;HEAD 0,32;\
ARM_REST_RIGHT -7,23;\
TORSO 0,22
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
ANIM_RUN_1=LEG_REST_RIGHT 1,1;LEG_REST_LEFT -1,0
ANIM_RUN_2=ALL 0,1;LEG_REST_RIGHT 0,-1;LEG_REST_LEFT 0,1
```
### Reserved Keywords
**Global Properties:**
- `SPRITESHEET` — Base path for body part images (required)
- `EXTENSION` — File extension for body part images (required)
- `CONFIG` — Frame size and origin point (required)
- `BODYPARTS` — List of body parts with joint positions (required)
**Skeleton Definitions:**
- `SKELETON_*` — Defines a skeleton (e.g., `SKELETON_STAND`, `SKELETON_CROUCH`)
**Animation Definitions:**
- `ANIM_*` — Defines an animation (e.g., `ANIM_RUN`, `ANIM_IDLE`)
- `ANIM_*_N` — Defines frame N of an animation (e.g., `ANIM_RUN_1`, `ANIM_RUN_2`)
**Special Body Parts:**
- `HEADGEAR` — Equipped helmet/hat
- `HELD_ITEM` — Item in hand
- `BOOT_L`, `BOOT_R` — Left/right boots
- `GAUNTLET_L`, `GAUNTLET_R` — Left/right gauntlets
- `ARMOUR_*` — Armour layers (e.g., `ARMOUR_0`, `ARMOUR_1`)
## Property Types
### SPRITESHEET and EXTENSION
```properties
SPRITESHEET=mods/basegame/sprites/fofu/fofu_
EXTENSION=.tga
```
Body part files are constructed as: `SPRITESHEET + bodypart_name + EXTENSION`
**Example:**
- `SPRITESHEET=mods/basegame/sprites/fofu/fofu_`
- Body part: `HEAD`
- Result: `mods/basegame/sprites/fofu/fofu_head.tga`
### CONFIG
```properties
CONFIG=SIZE 48,56;ORIGINX 29
```
**Parameters:**
- `SIZE w,h` — Frame dimensions (width, height) in pixels (required)
- `ORIGINX x` — X-coordinate of origin point (required)
Origin is always `(originX, 0)` — the top-centre anchor point for the character.
**Frame dimensions include extra headroom:**
```kotlin
frameWidth = configWidth + 32 // EXTRA_HEADROOM_X = 32
frameHeight = configHeight + 16 // EXTRA_HEADROOM_Y = 16
```
### BODYPARTS
```properties
BODYPARTS=HEAD 11,11;\
ARM_REST_RIGHT 4,2;\
LEG_REST_LEFT 4,7;\
TORSO 10,4;\
HELD_ITEM 0,0
```
Defines the list of body parts and their **joint positions** (anchor points within each sprite).
**Format:** `BODYPART_NAME jointX,jointY`
**Joint Position:**
- Coordinates are **relative to the body part sprite's top-left corner**
- Joint is where this body part connects to the skeleton
- Example: `HEAD 11,11` — Head sprite's joint is 11 pixels right, 11 pixels down from top-left
**Paint Order:**
Body parts are painted in **reverse order** — last in the list paints first (background), first in the list paints last (foreground).
```properties
# Paint order: TORSO (back) → LEG → ARM → HEAD (front)
BODYPARTS=HEAD 8,7;\
ARM_REST_RIGHT 3,8;\
LEG_REST_RIGHT 3,7;\
TORSO 9,4
```
### SKELETON Definitions
```properties
SKELETON_STAND=HEADGEAR 0,32;\
HEAD 0,32;\
ARM_REST_RIGHT -7,23;\
TORSO 0,22;\
LEG_REST_RIGHT -2,7
```
Defines joint positions for a skeleton pose.
**Format:** `SKELETON_NAME=JOINT_NAME offsetX,offsetY;...`
**Joint Offsets:**
- Coordinates are **relative to the character's origin** `(originX, 0)`
- Positive X → right, Positive Y → down
- Example: `HEAD 0,32` — Head joint is 0 pixels right, 32 pixels down from origin
- Example: `ARM_REST_RIGHT -7,23` — Right arm joint is 7 pixels left, 23 pixels down
**Multiple Skeletons:**
```properties
SKELETON_STAND=HEAD 0,32;TORSO 0,22;LEG_LEFT 2,7
SKELETON_CROUCH=HEAD 0,28;TORSO 0,20;LEG_LEFT 3,5
SKELETON_JUMP=HEAD 0,30;TORSO 0,21;LEG_LEFT 1,8
```
### ANIM Definitions
```properties
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
```
Defines an animation sequence.
**Parameters:**
- `DELAY seconds` — Time between frames (float; actors may override delays)
- `ROW row_number` — Row in the output spritesheet (starts at 1)
- `SKELETON skeleton_name` — Which skeleton this animation uses
**Frame Count:**
Frame count is determined by the highest `ANIM_*_N` suffix:
```properties
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
ANIM_RUN_1=...
ANIM_RUN_2=...
ANIM_RUN_3=...
ANIM_RUN_4=...
# This animation has 4 frames
```
### ANIM Frame Definitions
```properties
ANIM_RUN_1=LEG_REST_RIGHT 1,1;LEG_REST_LEFT -1,0
ANIM_RUN_2=ALL 0,1;LEG_REST_RIGHT 0,-1;LEG_REST_LEFT 0,1
```
Defines per-joint transformations for a specific frame.
**Format:** `ANIM_NAME_N=JOINT translateX,translateY;...`
**Transformations:**
- `JOINT offsetX,offsetY` — Translate joint by (offsetX, offsetY) pixels
- `ALL offsetX,offsetY` — Translate ALL joints by (offsetX, offsetY) pixels
**Empty Frame:**
```properties
ANIM_IDLE_1=
# Frame 1: No transformations (use skeleton as-is)
```
**Overlapping Transforms:**
```properties
ANIM_RUN_2=ALL 0,1;LEG_REST_RIGHT 0,-1
# 1. Move everything down 1 pixel
# 2. Then move right leg up 1 pixel (net: 0 offset for right leg)
```
## Complete Example
```properties
# File paths
SPRITESHEET=mods/basegame/sprites/fofu/fofu_
EXTENSION=.tga
# Frame configuration
CONFIG=SIZE 48,56;ORIGINX 29
# Body parts with joint positions
BODYPARTS=HEADGEAR 11,11;\
HEAD 11,11;\
ARM_REST_RIGHT 4,2;\
ARM_REST_LEFT 4,2;\
LEG_REST_RIGHT 4,7;\
LEG_REST_LEFT 4,7;\
TORSO 10,4;\
TAIL_0 20,1;\
HELD_ITEM 0,0
# Skeleton: standing pose (paint order: top to bottom)
SKELETON_STAND=HEADGEAR 0,32;\
HEAD 0,32;\
ARM_REST_RIGHT -7,23;\
ARM_REST_LEFT 5,24;\
TORSO 0,22;\
LEG_REST_RIGHT -2,7;\
LEG_REST_LEFT 2,7;\
TAIL_0 0,13;\
HELD_ITEM -6,11
# Animation: running (4 frames, 0.15s per frame)
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
ANIM_RUN_1=LEG_REST_RIGHT 1,1;LEG_REST_LEFT -1,0
ANIM_RUN_2=ALL 0,1;LEG_REST_RIGHT 0,-1;LEG_REST_LEFT 0,1
ANIM_RUN_3=LEG_REST_RIGHT -1,0;LEG_REST_LEFT 1,1
ANIM_RUN_4=ALL 0,1;LEG_REST_RIGHT 0,1;LEG_REST_LEFT 0,-1
# Animation: idle (2 frames, 2s per frame)
ANIM_IDLE=DELAY 2;ROW 1;SKELETON SKELETON_STAND
ANIM_IDLE_1=
ANIM_IDLE_2=TORSO 0,-1;HEAD 0,-1;HELD_ITEM 0,-1;\
ARM_REST_LEFT 0,-1;ARM_REST_RIGHT 0,-1;\
HEADGEAR 0,-1
```
## ADProperties Class
The Kotlin class that parses ADL files.
### Loading ADL
```kotlin
val adl = ADProperties(gdxFile)
```
**Constructors:**
```kotlin
constructor(gdxFile: FileHandle)
constructor(reader: Reader)
constructor(inputStream: InputStream)
```
### Properties
```kotlin
class ADProperties {
// File information
lateinit var baseFilename: String // From SPRITESHEET
lateinit var extension: String // From EXTENSION
// Frame configuration
var frameWidth: Int // From CONFIG SIZE + headroom
var frameHeight: Int // From CONFIG SIZE + headroom
var originX: Int // From CONFIG ORIGINX
// Spritesheet dimensions
var rows: Int // Max animation row
var cols: Int // Max frame count
// Body parts
lateinit var bodyparts: List<String>
lateinit var bodypartFiles: List<String>
val bodypartJoints: HashMap<String, ADPropertyObject.Vector2i>
// Animation data
internal lateinit var skeletons: HashMap<String, Skeleton>
internal lateinit var animations: HashMap<String, Animation>
internal lateinit var transforms: HashMap<String, List<Transform>>
}
```
### Data Classes
```kotlin
// Joint in a skeleton
internal data class Joint(
val name: String,
val position: ADPropertyObject.Vector2i
)
// Skeleton pose
internal data class Skeleton(
val name: String,
val joints: List<Joint>
)
// Animation sequence
internal data class Animation(
val name: String,
val delay: Float, // Seconds per frame
val row: Int, // Row in spritesheet
val frames: Int, // Frame count
val skeleton: Skeleton
)
// Per-frame joint transformation
internal data class Transform(
val joint: Joint,
val translate: ADPropertyObject.Vector2i
)
```
### Vector2i
```kotlin
data class Vector2i(var x: Int, var y: Int) {
operator fun plus(other: Vector2i) = Vector2i(x + other.x, y + other.y)
operator fun minus(other: Vector2i) = Vector2i(x - other.x, y - other.y)
fun invertY() = Vector2i(x, -y)
fun invertX() = Vector2i(-x, y)
fun invertXY() = Vector2i(-x, -y)
}
```
## Rendering Process
ADL animations are rendered **on-the-fly** each frame by `AssembledSpriteAnimation.renderThisAnimation()`.
### On-the-Fly Rendering
```kotlin
class AssembledSpriteAnimation(
val adp: ADProperties,
parentActor: ActorWithBody,
val disk: SimpleFileSystem?,
val isGlow: Boolean,
val isEmissive: Boolean
) : SpriteAnimation(parentActor) {
// Body part textures cached in memory
@Transient private val res = HashMap<String, TextureRegion?>()
var currentAnimation = "" // e.g., "ANIM_IDLE", "ANIM_RUN"
var currentFrame = 0 // Current frame index (zero-based)
fun renderThisAnimation(
batch: SpriteBatch,
posX: Float,
posY: Float,
scale: Float,
animName: String, // e.g., "ANIM_RUN_2"
mode: Int = 0
)
}
```
### Rendering Steps (per frame)
1. **Load body part textures** — Cache `TextureRegion` for each body part
2. **Get animation data** — Retrieve skeleton and transforms for current frame
3. **Calculate positions** — For each body part:
```kotlin
val skeleton = animation.skeleton.joints.reversed()
val transforms = adp.getTransform("ANIM_RUN_2")
val bodypartOrigins = adp.bodypartJoints
AssembleFrameBase.makeTransformList(skeleton, transforms).forEach { (name, bodypartPos) ->
// Calculate final position
val drawPos = adp.origin + bodypartPos - bodypartOrigins[name]
// Draw body part texture at calculated position
batch.draw(texture, drawPos.x * scale, drawPos.y * scale)
}
```
4. **Draw equipment** — Render held items and armour at joint positions
**No pre-assembly required** — Body parts are positioned and drawn directly each frame.
### Example Rendering: ANIM_RUN_2
**ADL Definition:**
```properties
SKELETON_STAND=HEAD 0,32;TORSO 0,22;LEG_RIGHT -2,7
ANIM_RUN_2=ALL 0,1;LEG_RIGHT 0,-1
BODYPARTS=LEG_REST_RIGHT 4,7
CONFIG=SIZE 48,56;ORIGINX 29
```
**Rendering LEG_RIGHT (per frame):**
1. **Origin:** `(29, 0)` (from `ORIGINX 29`)
2. **Skeleton pose:** `LEG_RIGHT -2,7` → joint offset from origin = `(-2, 7)`
3. **ALL transform:** `0,1` → shift all joints by `(0, 1)`
4. **LEG_RIGHT transform:** `0,-1` → shift this joint by `(0, -1)`
5. **Final joint offset:** `(-2, 7) + (0, 1) + (0, -1)` = `(-2, 7)`
6. **Joint in world:** `(29, 0) + (-2, 7)` = `(27, 7)`
7. **Body part anchor:** `LEG_REST_RIGHT 4,7` (from BODYPARTS)
8. **Draw position:** `(27, 7) - (4, 7)` = `(23, 0)`
Body part sprite is drawn at `(23, 0)` relative to character origin, with its anchor point at the skeleton joint `(27, 7)`.
## Practical Usage
### Creating a New Character
1. **Draw body parts** as separate images:
- `character_head.tga`
- `character_torso.tga`
- `character_arm_rest_left.tga`
- `character_arm_rest_right.tga`
- `character_leg_rest_left.tga`
- `character_leg_rest_right.tga`
2. **Mark joint positions** on each sprite (where it connects)
3. **Write ADL file:**
```properties
SPRITESHEET=mods/mymod/sprites/character/character_
EXTENSION=.tga
CONFIG=SIZE 48,56;ORIGINX 24
BODYPARTS=HEAD 10,8;\
ARM_REST_RIGHT 5,3;\
ARM_REST_LEFT 5,3;\
TORSO 12,6;\
LEG_REST_RIGHT 4,8;\
LEG_REST_LEFT 4,8
SKELETON_STAND=HEAD 0,30;\
ARM_REST_RIGHT -8,24;\
ARM_REST_LEFT 8,24;\
TORSO 0,22;\
LEG_REST_RIGHT -3,8;\
LEG_REST_LEFT 3,8
ANIM_IDLE=DELAY 1;ROW 1;SKELETON SKELETON_STAND
ANIM_IDLE_1=
```
4. **Load in code:**
```kotlin
val adl = ADProperties(ModMgr.getGdxFile("mymod", "sprites/character.properties"))
actor.sprite = AssembledSpriteAnimation(adl, actor, isGlow = false, isEmissive = false)
actor.sprite.currentAnimation = "ANIM_IDLE"
```
### Adding New Animations
```properties
# Walk cycle (8 frames)
ANIM_WALK=DELAY 0.1;ROW 3;SKELETON SKELETON_STAND
ANIM_WALK_1=LEG_REST_RIGHT 0,1;ARM_REST_LEFT 0,1
ANIM_WALK_2=LEG_REST_RIGHT 1,0;ARM_REST_LEFT 1,-1
ANIM_WALK_3=LEG_REST_RIGHT 1,-1;ARM_REST_LEFT 2,-2
ANIM_WALK_4=ALL 0,1;LEG_REST_RIGHT 0,-1;ARM_REST_LEFT 0,1
ANIM_WALK_5=LEG_REST_LEFT 0,1;ARM_REST_RIGHT 0,1
ANIM_WALK_6=LEG_REST_LEFT 1,0;ARM_REST_RIGHT 1,-1
ANIM_WALK_7=LEG_REST_LEFT 1,-1;ARM_REST_RIGHT 2,-2
ANIM_WALK_8=ALL 0,1;LEG_REST_LEFT 0,-1;ARM_REST_RIGHT 0,1
```
### Equipment Layering
Use special body part names for equipment:
```properties
BODYPARTS=HEADGEAR 11,11;\ # Helmet/hat slot
HELD_ITEM 0,0;\ # Item in hand
GAUNTLET_L 3,3;\ # Left glove
GAUNTLET_R 3,3;\ # Right glove
BOOT_L 4,2;\ # Left boot
BOOT_R 4,2;\ # Right boot
ARMOUR_0 10,4;\ # Armour layer 0
ARMOUR_1 10,4 # Armour layer 1
```
These slots are rendered dynamically from actor inventory:
```kotlin
// In AssembledSpriteAnimation.renderThisAnimation()
if (name in jointNameToEquipPos) {
val item = (parentActor as? Pocketed)?.inventory?.itemEquipped?.get(jointNameToEquipPos[name])
fetchItemImage(mode, item)?.let { image ->
// Draw equipped item at joint position
batch.draw(image, drawPos.x, drawPos.y)
}
}
```
Equipment is rendered **automatically** when actor has items equipped.
## Best Practises
1. **Use consistent joint positions** — Same body part across animations should have same joint
2. **Paint order matters** — List body parts background-to-foreground
3. **Use ALL for body movement** — Move entire character up/down with `ALL`
4. **Keep frame delays consistent** — Use same delay for similar animations (e.g., all walks 0.1s)
5. **Test with origin marker** — Verify origin is at character's centre-top
6. **Use skeletons for poses** — Define `SKELETON_CROUCH`, `SKELETON_JUMP` for clarity
7. **Name body parts clearly** — Use `_LEFT`/`_RIGHT`, `_REST`/`_ACTIVE` conventions
8. **Add headroom** — ADProperties adds 32×16 pixels automatically; don't pre-add
## Common Pitfalls
- **Wrong paint order** — Arm painting behind torso instead of in front
- **Inconsistent joint positions** — Head joint moves between body parts
- **Forgetting frame numbers** — `ANIM_RUN_1`, `ANIM_RUN_2`, not `ANIM_RUN_0`
- **Missing ALL transform** — Forgot to move entire body up/down
- **Wrong coordinate space** — Mixing up joint offsets vs. world positions
- **Overlapping transforms** — `ALL` and individual transforms don't add correctly
- **Origin misalignment** — Character appears to "slide" when moving
- **Negative delays** — Invalid delay value
## Advanced Techniques
### Layered Animations
```properties
# Base layer (body)
SKELETON_BASE=TORSO 0,22;LEG_LEFT 2,7;LEG_RIGHT -2,7
# Upper body layer (can animate separately)
SKELETON_UPPER=HEAD 0,32;ARM_LEFT 6,24;ARM_RIGHT -6,24
# Combine in animations
ANIM_WALK=DELAY 0.1;ROW 2;SKELETON SKELETON_BASE
ANIM_ATTACK=DELAY 0.05;ROW 3;SKELETON SKELETON_UPPER
```
### Dynamic Body Part Swapping
Body parts are loaded on construction and cached:
```kotlin
class AssembledSpriteAnimation {
@Transient private val res = HashMap<String, TextureRegion?>()
init {
// Load all body parts from ADL
adp.bodyparts.forEach {
res[it] = getPartTexture(fileGetter, it)
}
}
}
```
To swap body parts dynamically:
```kotlin
// Load base character
val sprite = AssembledSpriteAnimation(adl, actor, false, false)
// Replace body part texture (requires modifying internal res map)
val helmetTexture = loadHelmet("iron_helmet.tga")
// Note: res is private; body part swapping typically uses equipment slots instead
// For equipment, use HELD_ITEM/HEADGEAR slots which render from inventory
actor.inventory.itemEquipped[GameItem.EquipPosition.HEAD] = ItemCodex["item:helmet_iron"]
// Equipment renders automatically in renderThisAnimation()
```
### Mirror Animations
`AssembledSpriteAnimation` supports horizontal and vertical flipping:
```kotlin
class AssembledSpriteAnimation {
var flipHorizontal = false
var flipVertical = false
}
// Mirror character for right-facing
sprite.flipHorizontal = true
// In renderThisAnimation(), flipping is applied:
fun renderThisAnimation(...) {
if (flipHorizontal) bodypartPos = bodypartPos.invertX()
if (flipVertical) bodypartPos = bodypartPos.invertY()
// Draw with negative width/height for flipping
if (flipHorizontal && flipVertical)
batch.draw(image, x, y, -w, -h)
else if (flipHorizontal)
batch.draw(image, x, y, -w, h)
// ...
}
```
## See Also
- [[Actors]] — Actor system using ADL animations
- [[Fixtures]] — Fixture actors (non-animated)
- [[Glossary]] — Animation terminology

504
Audio-Engine-Internals.md Normal file

@@ -0,0 +1,504 @@
# Audio Engine Internals
**Audience:** Engine maintainers working on the audio system implementation.
This document describes the internal architecture of Terrarum's audio engine, including the mixer thread, DSP pipeline, spatial audio calculations, and low-level audio processing.
## Architecture Overview
The audio engine is built on a **threaded mixer architecture** with real-time DSP processing:
```
┌─────────────────┐
│ Game Thread │
│ (Main Loop) │
└────────┬────────┘
│ Control Commands
┌─────────────────┐
│ AudioMixer │ ← Singleton, runs on separate thread
│ (Central Hub) │
└────────┬────────┘
┌────┴────┬──────────┬──────────┐
↓ ↓ ↓ ↓
Track 0 Track 1 Track 2 Track N
│ │ │ │
↓ ↓ ↓ ↓
Filter Filter Filter Filter
Chain Chain Chain Chain
│ │ │ │
└────┬────┴──────────┴──────────┘
┌────────────┐
│ Mixer │ ← Combines all tracks
└────┬───────┘
┌────────────┐
│ OpenAL │ ← Hardware audio output
│ Output │
└────────────┘
```
## Threading Model
### AudioManagerRunnable
The audio system runs on a dedicated thread:
```kotlin
class AudioManagerRunnable(
val audioMixer: AudioMixer
) : Runnable {
@Volatile var running = true
private val updateInterval = 1000 / 60 // 60 Hz update rate
override fun run() {
while (running) {
val startTime = System.currentTimeMillis()
// Update all tracks
audioMixer.update()
// Sleep to maintain update rate
val elapsed = System.currentTimeMillis() - startTime
val sleep Time = maxOf(0, updateInterval - elapsed)
Thread.sleep(sleepTime)
}
}
}
```
### Thread Safety
All audio operations are thread-safe:
```kotlin
@Volatile var trackVolume: TrackVolume // Atomic updates
private val lock = ReentrantLock() // Protects critical sections
fun setMusic(music: MusicContainer) {
lock.withLock {
currentMusic?.stop()
currentMusic = music
music.play()
}
}
```
### Update Rate
Audio updates at **60 Hz** (every ~16.67ms) to match game frame rate:
- Position updates for spatial audio
- Volume envelope processing
- Filter parameter interpolation
- Crossfade calculations
## TerrarumAudioMixerTrack
### Track Structure
Each track is an independent audio channel:
```kotlin
class TerrarumAudioMixerTrack(val index: Int) {
var currentMusic: MusicContainer? = null
var trackVolume: TrackVolume = TrackVolume(1.0f, 1.0f)
var filters: Array<AudioFilter> = arrayOf(NullFilter, NullFilter)
var trackingTarget: ActorWithBody? = null
var state: TrackState = TrackState.STOPPED
val processor: StreamProcessor
}
```
### Track State Machine
```kotlin
enum class TrackState {
STOPPED, // Not playing
PLAYING, // Currently playing
PAUSED, // Paused, can resume
STOPPING, // Fading out before stop
}
```
State transitions:
```
STOPPED ─play()─→ PLAYING
↑ ↓
stop() pause()
↑ ↓
STOPPING ←─ PAUSED
↑ ↓
(fadeout) resume()
```
### Stream Processor
Handles low-level audio streaming:
```kotlin
class StreamProcessor(val track: TerrarumAudioMixerTrack) {
var streamBuf: AudioDevice? // OpenAL audio device
private val buffer = FloatArray(BUFFER_SIZE)
fun fillBuffer(): FloatArray {
val source = track.currentMusic ?: return emptyBuffer()
// Read raw PCM data from source
val samples = source.readSamples(BUFFER_SIZE)
// Apply volume
applyStereoVolume(samples, track.trackVolume)
// Process through filter chain
val filtered = applyFilters(samples, track.filters)
// Apply spatial positioning
if (track.trackingTarget != null) {
applySpatialAudio(filtered, track.trackingTarget!!)
}
return filtered
}
}
```
### Buffer Size
Buffer size is **user-configurable** (from `MixerTrackProcessor.kt` and `OpenALBufferedAudioDevice.kt`):
```kotlin
class MixerTrackProcessor(bufferSize: Int, val rate: Int, ...)
class OpenALBufferedAudioDevice(..., val bufferSize: Int, ...)
```
**Range:** 128 to 4096 samples per buffer
**Sample rate:** 48000 Hz (constant from `TerrarumAudioMixerTrack.SAMPLING_RATE`)
Example buffer duration calculations:
- 128 samples: 128 / 48000 ≈ **2.7ms latency**
- 4096 samples: 4096 / 48000 ≈ **85ms latency**
Trade-off:
- **Smaller buffers** → Lower latency, higher CPU usage, risk of audio dropouts
- **Larger buffers** → Higher latency, lower CPU usage, more stable playback
## Audio Streaming
### MusicContainer
Wraps audio files for streaming:
```kotlin
class MusicContainer(
val file: FileHandle,
val format: AudioFormat
) {
private var decoder: AudioDecoder? = null
private var position: Long = 0 // Sample position
fun readSamples(count: Int): FloatArray {
if (decoder == null) {
decoder = createDecoder(file, format)
}
val samples = FloatArray(count)
val read = decoder!!.read(samples, 0, count)
if (read < count) {
// Reached end of file
if (looping) {
seek(0)
decoder!!.read(samples, read, count - read)
} else {
// Pad with silence
samples.fill(0f, read, count)
}
}
position += read
return samples
}
}
```
### Audio Decoders
Support multiple formats:
```kotlin
interface AudioDecoder {
fun read(buffer: FloatArray, offset: Int, length: Int): Int
fun seek(samplePosition: Long)
fun close()
}
class OggVorbisDecoder : AudioDecoder { ... }
class WavDecoder : AudioDecoder { ... }
class Mp3Decoder : AudioDecoder { ... }
```
### Streaming vs. Loading
- **Streaming** — Large files (music): read chunks on-demand
- **Fully loaded** — Small files (SFX): load entire file into memory
```kotlin
val isTooLargeToLoad = file.length() > 5 * 1024 * 1024 // 5 MB threshold
val container = if (isTooLargeToLoad) {
StreamingMusicContainer(file)
} else {
LoadedMusicContainer(file)
}
```
## DSP Filter Pipeline
### Filter Interface
```kotlin
interface AudioFilter {
fun process(samples: FloatArray, sampleRate: Int)
fun reset()
}
```
### Filter Chain
Each track has two filter slots:
```kotlin
fun applyFilters(samples: FloatArray, filters: Array<AudioFilter>): FloatArray {
var current = samples.copyOf()
for (filter in filters) {
if (filter !is NullFilter) {
filter.process(current, SAMPLE_RATE)
}
}
return current
}
```
### Built-in Filters
#### LowPassFilter
Reduces high frequencies (muffled sound):
```kotlin
class LowPassFilter(var cutoffFreq: Float) : AudioFilter {
private var prevSample = 0f
override fun process(samples: FloatArray, sampleRate: Int) {
val rc = 1.0f / (cutoffFreq * 2 * PI)
val dt = 1.0f / sampleRate
val alpha = dt / (rc + dt)
for (i in samples.indices) {
prevSample = prevSample + alpha * (samples[i] - prevSample)
samples[i] = prevSample
}
}
}
```
#### HighPassFilter
Reduces low frequencies (tinny sound):
```kotlin
class HighPassFilter(var cutoffFreq: Float) : AudioFilter {
private var prevInput = 0f
private var prevOutput = 0f
override fun process(samples: FloatArray, sampleRate: Int) {
val rc = 1.0f / (cutoffFreq * 2 * PI)
val dt = 1.0f / sampleRate
val alpha = rc / (rc + dt)
for (i in samples.indices) {
prevOutput = alpha * (prevOutput + samples[i] - prevInput)
prevInput = samples[i]
samples[i] = prevOutput
}
}
}
```
#### Convolv
Convolutional reverb using impulse response (IR) files and FFT-based fast convolution:
```kotlin
class Convolv(
irModule: String, // Module containing IR file
irPath: String, // Path to IR file
val crossfeed: Float, // Stereo crossfeed amount (0.0-1.0)
gain: Float = 1f / 256f // Output gain
) : TerrarumAudioFilter() {
private val fftLen: Int
private val convFFT: Array<ComplexArray>
private val sumbuf: Array<ComplexArray>
init {
convFFT = AudioHelper.getIR(irModule, irPath)
fftLen = convFFT[0].size
sumbuf = Array(2) { ComplexArray(FloatArray(fftLen * 2)) }
}
override fun thru(inbuf: List<FloatArray>, outbuf: List<FloatArray>) {
// Fast convolution using overlap-add method
pushSum(gain, inbuf[0], inbuf[1], sumbuf)
convolve(sumbuf[0], convFFT[0], fftOutL)
convolve(sumbuf[1], convFFT[1], fftOutR)
// Extract final output from FFT results
for (i in 0 until App.audioBufferSize) {
outbuf[0][i] = fftOutL[fftLen - App.audioBufferSize + i]
outbuf[1][i] = fftOutR[fftLen - App.audioBufferSize + i]
}
}
private fun convolve(x: ComplexArray, h: ComplexArray, output: FloatArray) {
FFT.fftInto(x, fftIn)
fftIn.mult(h, fftMult)
FFT.ifftAndGetReal(fftMult, output)
}
}
```
**Impulse Response (IR) Files:**
- Binary files containing mono IR with two channels
- Loaded via `AudioHelper.getIR(module, path)`
- FFT length determined by IR file size
- Supports arbitrary room acoustics via recorded/synthesised IRs
#### BinoPan
Binaural panning for stereo positioning:
```kotlin
class BinoPan(var pan: Float) : AudioFilter {
// pan: -1.0 (full left) to 1.0 (full right)
override fun process(samples: FloatArray, sampleRate: Int) {
val leftGain = sqrt((1.0f - pan) / 2.0f)
val rightGain = sqrt((1.0f + pan) / 2.0f)
for (i in samples.indices step 2) {
val mono = (samples[i] + samples[i + 1]) / 2
samples[i] = mono * leftGain // Left channel
samples[i + 1] = mono * rightGain // Right channel
}
}
}
```
## Volume Envelope Processing
### Fade In/Out
```kotlin
class VolumeEnvelope {
var targetVolume: Float = 1.0f
var currentVolume: Float = 0.0f
var fadeSpeed: Float = 1.0f // Volume units per second
fun update(delta: Float) {
val change = fadeSpeed * delta
currentVolume = when {
currentVolume < targetVolume -> minOf(currentVolume + change, targetVolume)
currentVolume > targetVolume -> maxOf(currentVolume - change, targetVolume)
else -> currentVolume
}
}
}
```
## Track Allocation
### Dynamic Track Pool
```kotlin
class AudioMixer {
val dynamicTracks = Array(MAX_TRACKS) { TerrarumAudioMixerTrack(it) }
fun getAvailableTrack(): TerrarumAudioMixerTrack? {
// Find stopped track
dynamicTracks.firstOrNull { it.state == TrackState.STOPPED }?.let { return it }
// Find oldest playing track
return dynamicTracks.minByOrNull { it.startTime }
}
}
```
## Common Issues
### Buffer Underrun
Symptoms: Audio stuttering, crackling
Causes:
- Update thread lagging
- Heavy CPU load
- Inefficient filters
Solutions:
- Increase buffer size
- Optimise filter algorithms
- Reduce track count
### Clipping
Symptoms: Distorted, harsh sound
Causes:
- Excessive volume
- Too many overlapping sounds
- Improper mixing
Solutions:
- Normalise audio files
- Implement limiter/compressor
- Reduce master volume
### Latency
Symptoms: Delayed audio response
Causes:
- Large buffer size
- Slow update rate
Solutions:
- Reduce buffer size
- Increase update rate
- Use hardware audio acceleration
## Future Enhancements
1. **Hardware-accelerated DSP** — Use OpenAL EFX effects
2. **Audio compression** — Reduce memory usage with codecs
3. **Dynamic range compression** — Automatic volume normalisation
4. **3D HRTF** — Head-related transfer functions for realistic 3D
5. **Ambisonics** — Full spherical surround sound
6. **Audio streaming from network** — Multiplayer voice chat
7. **Procedural audio** — Generate sounds algorithmically
## See Also
- [[Audio System]] — User-facing audio API
- [[Actors]] — Actor audio integration
- [[Glossary]] — Audio terminology

449
Audio-System.md Normal file

@@ -0,0 +1,449 @@
# Audio System
Terrarum features a comprehensive audio engine with spatial sound, mixer buses, real-time effects processing, and advanced music playback capabilities.
## Overview
The audio system provides:
- **AudioMixer** — Central audio management with mixer buses
- **Spatial audio** — 3D positional sound
- **Dynamic tracks** — Multiple simultaneous music/sound channels
- **Effects processing** — Filters, reverb, panning, and more
- **Music streaming** — Efficient playback of large music files
- **Volume control** — Master, music, SFX, and UI volume
## Architecture
### AudioMixer
The `AudioMixer` singleton manages all audio output:
```kotlin
object App {
val audioMixer: AudioMixer
}
```
The mixer runs on a separate thread (`AudioManagerRunnable`) for low-latency audio processing.
### Audio Codex
All audio assets are registered in the `AudioCodex`:
```kotlin
object AudioCodex {
operator fun get(audioID: String): MusicContainer?
}
```
## Dynamic Tracks
The mixer provides multiple dynamic audio tracks:
```kotlin
val dynamicTracks: Array<TerrarumAudioMixerTrack>
```
Each track can play different audio with independent:
- Volume control
- Effects processing (filters)
- Panning
- Pitch shifting
### Track Count
The number of available tracks depends on configuration, typically:
- **Music tracks:** 4-8 simultaneous music streams
- **SFX tracks:** 16-32 sound effects
## Playing Audio
### Music Playback
```kotlin
fun startMusic(music: MusicContainer, track: Int = 0) {
val mixerTrack = App.audioMixer.dynamicTracks[track]
mixerTrack.setMusic(music)
mixerTrack.play()
}
```
### Sound Effects
Short sound effects use the same track system:
```kotlin
fun playSound(sound: MusicContainer, volume: Float = 1.0f) {
val track = App.audioMixer.getAvailableTrack()
track.setMusic(sound)
track.trackVolume = TrackVolume(volume, volume)
track.play()
}
```
## Track Management
### TerrarumAudioMixerTrack
```kotlin
class TerrarumAudioMixerTrack {
var trackVolume: TrackVolume
var filters: Array<AudioFilter>
var trackingTarget: ActorWithBody? // For spatial audio
fun play()
fun stop()
fun pause()
fun resume()
}
```
### Track States
- **Stopped** — Not playing
- **Playing** — Currently playing
- **Paused** — Paused, can resume
- **Stopping** — Fading out
## Volume Control
### TrackVolume
Stereo volume control:
```kotlin
class TrackVolume(
var left: Float, // 0.0-1.0
var right: Float // 0.0-1.0
)
```
### Master Volume
Global volume settings:
```kotlin
App.audioMixer.masterVolume // Overall volume
App.audioMixer.musicVolume // Music volume multiplier
App.audioMixer.sfxVolume // SFX volume multiplier
App.audioMixer.uiVolume // UI sounds volume multiplier
```
Final track volume = `trackVolume * categoryVolume * masterVolume`
## Spatial Audio
### Positional Sound
Attach sound to an actor for 3D positioning:
```kotlin
mixerTrack.trackingTarget = actor
```
The mixer automatically:
1. Calculates distance from listener
2. Applies distance attenuation
3. Calculates stereo panning
4. Updates in real-time as actor moves
### 3D Audio Calculation
```kotlin
val listener = playerActor // Usually the player
val source = trackingTarget
val distance = source.position.dst(listener.position)
val angle = atan2(
source.position.y - listener.position.y,
source.position.x - listener.position.x
)
// Distance attenuation
val attenuation = 1.0f / (1.0f + distance / referenceDistance)
// Stereo panning
val pan = sin(angle) // -1.0 (left) to 1.0 (right)
trackVolume.left = attenuation * (1.0f - max(0.0f, pan))
trackVolume.right = attenuation * (1.0f + min(0.0f, pan))
```
### Listener Position
The audio listener is typically the player's position:
```kotlin
App.audioMixer.listenerPosition = player.hitbox.center
```
## Audio Effects (DSP)
### Filter System
Each track has two filter slots:
```kotlin
val filters: Array<AudioFilter> = arrayOf(NullFilter, NullFilter)
```
### Built-in Filters
- **NullFilter** — No effect (bypass)
- **BinoPan** — Binaural panning
- **LowPassFilter** — Reduces high frequencies
- **HighPassFilter** — Reduces low frequencies
- **ReverbFilter** — Room ambience
- **EchoFilter** — Delay effect
### Applying Filters
```kotlin
import net.torvald.terrarum.audio.dsp.LowPassFilter
mixerTrack.filters[0] = LowPassFilter(cutoffFreq = 1000.0f)
mixerTrack.filters[1] = ReverbFilter(roomSize = 0.5f, damping = 0.7f)
```
### Filter Chaining
Filters process in order:
```
Audio Source → Filter[0] → Filter[1] → Output
```
### Removing Filters
```kotlin
mixerTrack.filters[0] = NullFilter
```
## Music Containers
### MusicContainer
Wraps audio assets for playback:
```kotlin
class MusicContainer(
val file: FileHandle,
val format: AudioFormat
)
enum class AudioFormat {
OGG_VORBIS,
WAV,
MP3
}
```
### Loading Music
```kotlin
val music = MusicContainer(
Gdx.files.internal("audio/music/theme.ogg"),
AudioFormat.OGG_VORBIS
)
AudioCodex.register("theme_music", music)
```
## Actor Audio Integration
Actors can manage their own audio:
```kotlin
abstract class Actor {
val musicTracks: HashMap<MusicContainer, TerrarumAudioMixerTrack>
fun startMusic(music: MusicContainer)
fun stopMusic(music: MusicContainer)
}
```
### Automatic Cleanup
When actors despawn, their audio stops automatically:
```kotlin
override fun despawn() {
if (stopMusicOnDespawn) {
musicTracks.forEach { (_, track) ->
track.stop()
}
}
}
```
## Music Streaming
Large music files are streamed rather than loaded entirely:
```kotlin
class MusicStreamer(
val file: FileHandle,
val bufferSize: Int = 4096
) {
fun readNextBuffer(): ByteArray
}
```
This allows playback of long music tracks without excessive memory usage.
## Audio Bank
The `AudioBank` manages audio asset loading and caching:
```kotlin
object AudioBank {
fun loadMusic(path: String): MusicContainer
fun unloadMusic(musicID: String)
}
```
## Pitch Shifting
Tracks support pitch adjustment:
```kotlin
mixerTrack.processor.streamBuf?.pitch = 1.5f // 1.5x speed (higher pitch)
```
Pitch range: 0.5 (half speed) to 2.0 (double speed).
## Music Playlists
```kotlin
class TerrarumMusicPlaylist {
val tracks: List<MusicContainer>
var currentIndex: Int
var shuffleMode: Boolean
fun next()
fun previous()
fun shuffle()
}
```
Playlists handle music progression and looping, and allows audio processor to fetch the next track in gapless manner.
## Common Patterns
### Background Music
```kotlin
fun playBackgroundMusic(musicID: String) {
val music = AudioCodex[musicID]
val track = App.audioMixer.dynamicTracks[0] // Reserve track 0 for BGM
track.setMusic(music)
track.trackVolume = TrackVolume(
App.audioMixer.musicVolume,
App.audioMixer.musicVolume
)
track.play()
}
```
### UI Click Sound
```kotlin
fun playUIClick() {
val sound = AudioCodex["ui_click"]
val track = App.audioMixer.getAvailableTrack()
track.setMusic(sound)
track.trackVolume = TrackVolume(
App.audioMixer.uiVolume,
App.audioMixer.uiVolume
)
track.play()
}
```
### Positional Ambient Sound
```kotlin
fun playAmbientSound(position: Vector2, sound: MusicContainer) {
val track = App.audioMixer.getAvailableTrack()
// Create temporary actor at position for spatial audio
val soundSource = object : ActorWithBody() {
init {
setPosition(position.x, position.y)
}
}
track.trackingTarget = soundSource
track.setMusic(sound)
track.play()
}
```
### Muffled Sound (Underwater Effect)
```kotlin
fun applyUnderwaterEffect(track: TerrarumAudioMixerTrack) {
track.filters[0] = LowPassFilter(cutoffFreq = 500.0f)
track.filters[1] = ReverbFilter(roomSize = 0.8f, damping = 0.9f)
}
```
## Audio Format Requirements
### Music Files
- **Format:** OGG Vorbis recommended
- **Quality:** `-q 10` (highest quality)
- **Sample rate:** 48000 Hz
- **Channels:** Stereo
### Sound Effects
- **Format:** OGG Vorbis or WAV
- **Duration:** < 5 seconds (use streaming for longer)
- **Sample rate:** 48000 Hz
- **Channels:** Mono (will be positioned) or Stereo (will also be positioned but poorly)
## Performance Considerations
1. **Limit simultaneous sounds** — Too many tracks cause CPU overhead
2. **Use streaming for music** — Don't load entire files
3. **Unload unused audio** — Free memory when not needed
4. **Pool sound effect tracks** — Reuse tracks instead of creating new ones
5. **Reduce filter complexity** — Heavy DSP effects impact performance
## Best Practises
1. **Reserve tracks for categories** — E.g., track 0 for BGM, 1-3 for ambient
2. **Stop music on despawn** — Prevent audio leaks
3. **Use spatial audio sparingly** — Not all sounds need positioning
4. **Normalise audio levels** — Ensure consistent volume across files
5. **Test with volume at 0** — Game should work without audio
6. **Provide audio toggles** — Let users disable categories
7. **Crossfade music transitions** — Avoid abrupt cuts
## Troubleshooting
### No Sound
- Check master volume settings
- Verify audio files are loaded
- Ensure OpenAL is initialised
- Check track availability
### Crackling/Popping
- Increase buffer size
- Reduce simultaneous tracks
- Check for audio file corruption
- Verify sample rate consistency
### Spatial Audio Not Working
- Ensure `trackingTarget` is set
- Verify listener position is updated
- Check distance attenuation settings
## See Also
- [[Glossary]] — Audio terminology
- [[Actors]] — Actor audio integration
- [[Modules]] — Adding audio to modules

539
Autotiling-In-Depth.md Normal file

@@ -0,0 +1,539 @@
# Autotiling In-Depth (Engine Internals)
**Audience:** Engine maintainers implementing or modifying the autotiling algorithm.
This document describes the internal implementation of Terrarum's autotiling system, including the connection algorithms, lookup tables, and subtiling patterns.
## Overview
Terrarum's autotiling system automatically selects appropriate tile sprites based on neighbouring tiles, creating seamless connections for blocks like terrain, ores, and platforms.
The system supports:
- **47-tile autotiling** — Full tileset with all connection variants
- **16-tile platforms** — Simplified platform connections
- **Subtile autotiling** — 8×8 pixel subdivisions for advanced patterns
- **Connection modes** — Self-connection and mutual connection
- **Brick patterns** — Alternative tiling modes for brick-like textures
## Connection Detection
### 8-Neighbour System
Autotiling examines the **8 surrounding tiles** (Moore neighbourhood):
```
┌───┬───┬───┐
│ 5 │ 6 │ 7 │
├───┼───┼───┤
│ 4 │ @ │ 0 │ @ = Current tile
├───┼───┼───┤
│ 3 │ 2 │ 1 │
└───┴───┴───┘
```
Each neighbour position maps to a bit in an 8-bit bitmask:
```kotlin
// Bit positions
val RIGHT = 0 // bit 0 (LSB)
val BOTTOM_RIGHT = 1
val BOTTOM = 2
val BOTTOM_LEFT = 3
val LEFT = 4
val TOP_LEFT = 5
val TOP = 6
val TOP_RIGHT = 7 // bit 7 (MSB)
```
### Bitmask Generation
```kotlin
// Get neighbour positions (8 surrounding tiles)
private fun getNearbyTilesPos(x: Int, y: Int): Array<Point2i> {
return arrayOf(
Point2i(x + 1, y), // 0: RIGHT
Point2i(x + 1, y + 1), // 1: BOTTOM_RIGHT
Point2i(x, y + 1), // 2: BOTTOM
Point2i(x - 1, y + 1), // 3: BOTTOM_LEFT
Point2i(x - 1, y), // 4: LEFT
Point2i(x - 1, y - 1), // 5: TOP_LEFT
Point2i(x, y - 1), // 6: TOP
Point2i(x + 1, y - 1) // 7: TOP_RIGHT
)
}
// Calculate bitmask for CONNECT_SELF tiles
private fun getNearbyTilesInfoConSelf(x: Int, y: Int, mode: Int, mark: ItemID?): Int {
val nearbyTiles = getNearbyTilesPos(x, y).map { world.getTileFrom(mode, it.x, it.y) }
var ret = 0
for (i in nearbyTiles.indices) {
if (nearbyTiles[i] == mark) {
ret += (1 shl i) // add 1, 2, 4, 8, etc. for i = 0, 1, 2, 3...
}
}
return ret
}
```
### Connection Rules
#### CONNECT_SELF
Tiles connect only to identical tiles. The actual implementation from `BlocksDrawer.kt`:
```kotlin
private fun getNearbyTilesInfoConSelf(x: Int, y: Int, mode: Int, mark: ItemID?): Int {
val nearbyTiles = getNearbyTilesPos(x, y).map { world.getTileFrom(mode, it.x, it.y) }
var ret = 0
for (i in nearbyTiles.indices) {
if (nearbyTiles[i] == mark) { // Only connect to identical tiles
ret += (1 shl i)
}
}
return ret
}
```
**Use cases:**
- Individual block types that don't blend with others
- Special decorative blocks
- Unique terrain features
#### CONNECT_MUTUAL
Tiles connect to any solid tile with the same connection tag. The actual implementation from `BlocksDrawer.kt`:
```kotlin
private fun getNearbyTilesInfoConMutual(x: Int, y: Int, mode: Int): Int {
val mode = if (mode == TERRAIN_WALLSTICKER) TERRAIN else mode
val nearbyTiles: List<ItemID> = getNearbyTilesPos(x, y).map { world.getTileFrom(mode, it.x, it.y) }
var ret = 0
for (i in nearbyTiles.indices) {
// Connect to solid tiles that are also marked CONNECT_MUTUAL
if (BlockCodex[nearbyTiles[i]].isSolidForTileCnx && isConnectMutual(nearbyTiles[i])) {
ret += (1 shl i)
}
}
return ret
}
```
**Use cases:**
- Dirt, stone, and similar terrain blocks
- All blocks tagged as "connect mutually" blend together
- Creates natural-looking transitions
## 47-Tile Autotiling
### Tile Positions
The 112×112 texture atlas stores 47 tile variants in a 7×7 grid:
```
Grid layout (each cell is 16×16 pixels):
0 1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31 32 33 34
35 36 37 38 39 40 BB
41 42 43 44 45 46 **
```
Position (6,5) is reserved for the barcode. Positions 47 is unused.
### Connection Lookup Table
The `connectLut47` maps 8-bit connection bitmasks to tile indices:
```kotlin
val connectLut47 = intArrayOf(
0, 1, 2, 3, 4, 5, 6, 7, // 0x00-0x07
8, 9, 10, 11, 12, 13, 14, 15, // 0x08-0x0F
// ... 256 entries total
)
```
**Example:**
```kotlin
val bitmask = 0b11010110 // Connected top, top-right, left, bottom-right, bottom
val tileIndex = connectLut47[bitmask] // Returns appropriate tile variant
```
### Pre-calculated LUT
The lookup table is **pre-calculated and hard-coded** in `BlocksDrawer.kt`. The actual 256-entry array (from `BlocksDrawer.kt:188`):
```kotlin
val connectLut47 = intArrayOf(
17,1,17,1,2,3,2,14,17,1,17,1,2,3,2,14,9,7,9,7,4,5,4,35,9,7,9,7,16,37,16,15,
17,1,17,1,2,3,2,14,17,1,17,1,2,3,2,14,9,7,9,7,4,5,4,35,9,7,9,7,16,37,16,15,
8,10,8,10,0,12,0,43,8,10,8,10,0,12,0,43,11,13,11,13,6,20,6,34,11,13,11,13,36,33,36,46,
8,10,8,10,0,12,0,43,8,10,8,10,0,12,0,43,30,42,30,42,38,26,38,18,30,42,30,42,23,45,23,31,
17,1,17,1,2,3,2,14,17,1,17,1,2,3,2,14,9,7,9,7,4,5,4,35,9,7,9,7,16,37,16,15,
17,1,17,1,2,3,2,14,17,1,17,1,2,3,2,14,9,7,9,7,4,5,4,35,9,7,9,7,16,37,16,15,
8,28,8,28,0,41,0,21,8,28,8,28,0,41,0,21,11,44,11,44,6,27,6,40,11,44,11,44,36,19,36,32,
8,28,8,28,0,41,0,21,8,28,8,28,0,41,0,21,30,29,30,29,38,39,38,25,30,29,30,29,23,24,23,22
)
```
Each index (0-255) represents a possible 8-bit connection bitmask, and the value at that index is the tile number (0-46) to use from the 47-tile atlas. The exact mapping was designed manually following the visual design in `work_files/dynamic_shape_2_0.psd`.
## 16-Tile Platform Autotiling
### Platform Connections
Platforms only connect horizontally:
```
Tile layout (128×16 = 8 tiles × 16 pixels):
0: Middle segment
1: Right end
2: Left end
3: Planted on left (middle)
4: Planted on left (end)
5: Planted on right (middle)
6: Planted on right (end)
7: Single piece (isolated)
```
### Platform Connection Algorithm
```kotlin
fun getPlatformTileIndex(x: Int, y: Int, blockID: ItemID): Int {
val hasLeft = isConnectedHorizontally(x - 1, y, blockID)
val hasRight = isConnectedHorizontally(x + 1, y, blockID)
val hasBlockBelow = isSolidBlock(x, y + 1)
val hasBlockBelowLeft = isSolidBlock(x - 1, y + 1)
val hasBlockBelowRight = isSolidBlock(x + 1, y + 1)
return when {
!hasLeft && !hasRight -> 7 // Single piece
hasLeft && !hasRight -> 1 // Right end
!hasLeft && hasRight -> 2 // Left end
hasBlockBelow -> when {
hasLeft && hasRight -> 0 // Middle
// ... planted variants
}
else -> 0 // Middle segment
}
}
```
### Platform LUT
```kotlin
val connectLut16 = intArrayOf(
7, 2, 1, 0, // Isolated, left-connected, right-connected, both
// ... 256 entries for all combinations
)
```
## Subtiling System
### Subtile Resolution
Subtiles are **8×8 pixels** (half of standard 16×16 tiles):
```
Standard tile subdivided:
┌────┬────┐
│ TL │ TR │ 8×8 each
├────┼────┤
│ BL │ BR │
└────┴────┘
```
Subtile indices: **0 = Top-Left, 1 = Top-Right, 2 = Bottom-Right, 3 = Bottom-Left**
### Subtile Variant LUTs
Four lookup tables determine subtile variants based on connection patterns:
```kotlin
val subtileVarBaseLuts = arrayOf(
// TL subtile variants (47 entries)
intArrayOf(10,2,2,2,1,1,3,1,10,1,10,3,10,3,2,1,1,2,0,3,3,10,0,0,0,0,0,3,10,0,0,0,3,3,3,1,3,1,0,0,3,10,0,10,3,0,3),
// TR subtile variants (47 entries)
intArrayOf(4,1,5,1,5,1,4,1,4,5,6,4,6,6,1,1,5,5,6,0,6,0,0,4,0,0,6,0,0,0,4,6,0,6,6,1,4,1,4,0,0,0,6,6,0,6,6),
// BR subtile variants (47 entries)
intArrayOf(4,7,4,9,4,9,4,7,8,8,7,8,9,7,0,0,4,8,0,9,9,0,0,4,9,0,9,9,7,7,8,0,0,9,0,0,4,9,4,9,0,9,7,0,7,9,0),
// BL subtile variants (47 entries)
intArrayOf(10,11,10,10,12,12,12,7,11,7,11,7,10,7,10,0,0,11,12,0,12,10,0,0,0,12,12,12,11,7,7,0,0,0,12,12,0,0,12,12,12,10,7,10,7,0,0),
)
```
Each array maps from the 47-tile connection index to a subtile variant (0-12).
### Subtile Reorientation
Subtiles can be flipped/rotated to create more variations:
```kotlin
val subtileReorientLUT = arrayOf(
// For each subtile position (TL, TR, BR, BL):
// Map variant 0-15 to (rotation, flip) commands
)
```
### Subtile Rendering
```kotlin
fun renderSubtiledTile(x: Int, y: Int, blockID: ItemID) {
val connectionIndex = get47TileIndex(x, y, blockID)
for (subtileIndex in 0 until 4) {
val variantIndex = subtileVarBaseLuts[subtileIndex][connectionIndex]
val (rotation, flip) = subtileReorientLUT[subtileIndex][variantIndex]
val subtileTexture = getSubtileTexture(variantIndex)
val (sx, sy) = subtileOffsets[subtileIndex]
renderSubtile(
subtileTexture,
x * TILE_SIZE + sx,
y * TILE_SIZE + sy,
rotation, flip
)
}
}
```
This composites four 8×8 subtiles into each 16×16 tile position.
## Brick Patterns
### Tiling Modes
```kotlin
const val TILING_FULL = 0 // Standard 47-tile autotiling
const val TILING_FULL_NOFLIP = 1 // No flipping (for asymmetric textures)
const val TILING_BRICK_SMALL = 2 // Small brick pattern
const val TILING_BRICK_SMALL_NOFLIP = 3
const val TILING_BRICK_LARGE = 4 // Large brick pattern
const val TILING_BRICK_LARGE_NOFLIP = 5
```
### Brick Reorientation
Brick patterns use different subtile remapping:
```kotlin
val tilingModeReorientLUTs = arrayOf(
// TILING_FULL
arrayOf(16 to 0, 16 to 0, 16 to 0, 16 to 0),
// TILING_BRICK_SMALL
arrayOf(8 to 0, 8 to 8, 8 to 8, 8 to 0), // (modulo, offset) per subtile
// TILING_BRICK_LARGE
arrayOf(8 to 8, 8 to 0, 8 to 8, 8 to 0),
)
```
#### Brick Formula
```kotlin
fun getBrickSubtileVariant(baseVariant: Int, subtilePos: Int, mode: Int): Int {
val (modulo, offset) = tilingModeReorientLUTs[mode][subtilePos]
return (baseVariant % modulo) + offset
}
```
This creates brick-like patterns by cycling through specific subtile ranges.
## Wall Stickers
### 4-Tile Format
Wall stickers (torches, paintings) use a simplified 64×16 atlas:
```
0: Free-floating
1: Planted on left wall
2: Planted on right wall
3: Planted on bottom (ceiling)
```
### Wall Sticker Algorithm
```kotlin
fun getWallStickerTile(x: Int, y: Int): Int {
val hasLeft = isSolidWall(x - 1, y)
val hasRight = isSolidWall(x + 1, y)
val hasBottom = isSolidWall(x, y + 1)
return when {
hasLeft -> 1
hasRight -> 2
hasBottom -> 3
else -> 0 // Free-floating
}
}
```
## Occlusion and Corner Darkening
**NOTE: Corner occlusion now uses shader-based approach. Following section is obsolete and preserved for historical reasons.**
### Occlusion Tiles
Corner occlusion uses a separate tile set stored at a reserved atlas position:
```kotlin
val OCCLUSION_TILE_NUM_BASE = 48 // Starting position in atlas
```
### Occlusion Bitmask
Similar to regular autotiling, but affects lighting:
```kotlin
fun calculateOcclusionMask(x: Int, y: Int): Int {
var mask = 0
for (i in 0 until 8) {
val (nx, ny) = getNeighbourPosition(x, y, i)
if (isOccluding(nx, ny)) {
mask = mask or (1 shl i)
}
}
return connectLut47[mask] // Reuse autotiling LUT
}
```
### Occlusion Rendering
Rendered as a dark overlay using multiplicative blending:
```kotlin
batch.setBlendFunction(GL_DST_COLOR, GL_ZERO) // Multiplicative
batch.setColor(0.5f, 0.5f, 0.5f, 1f) // Darken by 50%
batch.draw(occlusionTexture, x, y)
batch.setBlendFunction(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) // Reset
```
## Performance Optimisations
### Pre-Calculated Tile Lists
The engine pre-calculates lists of tile numbers by connection type:
```kotlin
lateinit var connectMutualTiles: Array<Int>
lateinit var connectSelfTiles: Array<Int>
lateinit var platformTiles: Array<Int>
lateinit var wallStickerTiles: Array<Int>
```
**Binary search** determines tile type in O(log n):
```kotlin
fun isConnectMutual(tileNum: Int): Boolean {
return connectMutualTiles.binarySearch(tileNum) >= 0
}
```
### Caching
Connection bitmasks are cached per-tile:
```kotlin
private val connectionCache = HashMap<Long, Int>() // BlockAddress -> Bitmask
fun getCachedConnection(x: Int, y: Int): Int? {
val address = (y.toLong() shl 32) or x.toLong()
return connectionCache[address]
}
```
Cache invalidates when blocks change.
### Shader-Based Autotiling
**Future enhancement:** Move autotiling to GPU shaders:
```glsl
// Vertex shader passes world coordinates
v_worldPos = vec2(x, y);
// Fragment shader samples neighbours and selects tile
int connections = sampleNeighbours(v_worldPos);
int tileIndex = connectLut[connections];
vec2 atlasUV = getTileUV(tileIndex);
fragColor = texture(u_atlas, atlasUV);
```
Benefits:
- Offload CPU work to GPU
- Dynamic retiling without rebuilding geometry
- Per-pixel precision
## Debugging
### Visualise Connection Bitmask
```kotlin
fun debugRenderConnections(x: Int, y: Int) {
val mask = calculateConnectionBitmask(x, y, blockID)
for (i in 0 until 8) {
if ((mask and (1 shl i)) != 0) {
val (nx, ny) = getNeighbourPosition(x, y, i)
shapeRenderer.line(
x * TILE_SIZE + 8, y * TILE_SIZE + 8,
nx * TILE_SIZE + 8, ny * TILE_SIZE + 8
)
}
}
}
```
### Verify LUT Completeness
All 256 bitmask values must have mappings:
```kotlin
fun verifyLUT(lut: IntArray) {
require(lut.size == 256) { "LUT must have 256 entries" }
for (i in lut.indices) {
require(lut[i] in 0 until 47) { "Invalid tile index ${lut[i]} at position $i" }
}
}
```
## Common Pitfalls
1. **Incorrect bit ordering** — Ensure neighbour positions match bit positions
2. **Self vs. Mutual confusion** — Check connection type before comparing
3. **Cache invalidation** — Update cache when blocks change
4. **Off-by-one in LUT** — 256 entries (0-255), not 255
5. **Subtile index ordering** — Remember: TL=0, TR=1, BR=2, BL=3
6. **Forgetting corners** — Diagonal neighbours affect corner tiles
7. **Platform direction** — Platforms only connect horizontally
## Future Enhancements
1. **3D autotiling** — Connect blocks in vertical layers
2. **Gradient transitions** — Smooth colour blending between terrain types
3. **Animated autotiles** — Time-varying tile selection
4. **Smart slopes** — Automatic slope generation at edges
5. **Isometric support** — Adapt algorithm for isometric views
6. **Custom connection rules** — Per-block connection predicates
## See Also
- [[Tile Atlas System]] — Atlas generation and seasonal blending
- [[Rendering Pipeline]] — How autotiled tiles are rendered
- [[Modules:Blocks]] — Creating autotiled block textures

655
Blocks.md Normal file

@@ -0,0 +1,655 @@
# Blocks
**Audience:** Module developers creating terrain, walls, and decorative blocks.
Blocks are the fundamental building units of Terrarum's voxel world. This guide covers the block system, properties, rendering, autotiling, and creating custom blocks.
## Overview
Blocks provide:
- **Terrain tiles** — Ground, stone, dirt, ores
- **Wall tiles** — Backgrounds, structures, buildings
- **Physics properties** — Collision, friction, density
- **Visual properties** — Colour, luminosity, reflectance
- **Material properties** — Strength, material composition
- **Autotiling** — Automatic visual connections between adjacent blocks
- **Dynamic properties** — Animated/flickering lights, seasonal variation
## Block System Architecture
### BlockProp Class
**Note:** You don't implement `BlockProp` yourself — it's automatically generated by `ModMgr.GameBlockLoader` at runtime from CSV data.
Each block is represented by a `BlockProp` instance containing all properties:
```kotlin
class BlockProp : TaggedProp {
var id: ItemID = ""
var numericID: Int = -1
var nameKey: String = ""
// Visual properties
var shadeColR = 0f // Red shade (0.0-1.0)
var shadeColG = 0f // Green shade
var shadeColB = 0f // Blue shade
var shadeColA = 0f // UV shade
var opacity = Cvec() // Opacity per channel
// Luminosity
internal var baseLumColR = 0f
internal var baseLumColG = 0f
internal var baseLumColB = 0f
internal var baseLumColA = 0f
// Physical properties
var strength: Int = 0 // Mining difficulty (HP)
var density: Int = 0 // Mass density (kg/m³)
var isSolid: Boolean = false
var isPlatform: Boolean = false
var friction: Int = 0 // Horizontal friction
// Material & drops
var material: String = "" // 4-letter material code
var drop: ItemID = "" // Item dropped when mined
var world: ItemID = "" // Item used to place block
// Advanced
var dynamicLuminosityFunction: Int = 0 // 0=static, >0=animated
var reflectance = 0f // Light reflectance (0.0-1.0)
var maxSupport: Int = -1 // Structural support limit
var tags = HashSet<String>()
val extra = Codex() // Extra module data
}
```
### BlockCodex Singleton
All blocks are registered in the global `BlockCodex`:
```kotlin
class BlockCodex {
val blockProps = HashMap<ItemID, BlockProp>()
val dynamicLights = SortedArrayList<ItemID>()
val tileToVirtual = HashMap<ItemID, List<ItemID>>()
val virtualToTile = HashMap<ItemID, ItemID>()
operator fun get(id: ItemID?): BlockProp
fun fromModule(module: String, path: String, blockRegisterHook: (BlockProp) -> Unit)
}
```
**Access pattern:**
```kotlin
val stone = BlockCodex["basegame:2"]
println("Strength: ${stone.strength}")
println("Density: ${stone.density} kg/m³")
println("Is solid: ${stone.isSolid}")
```
## Block Types
### Terrain Tiles
Solid blocks that make up the ground:
```kotlin
val dirt = BlockCodex["basegame:3"]
dirt.isSolid = true
dirt.isWall = false // Terrain, not wall
dirt.material = "SOIL"
```
**Properties:**
- `isSolid = true` — Blocks movement
- Wall flag = `0` — Terrain tile
- Friction affects horizontal movement
- Density determines weight
### Wall Tiles
Background blocks for structures:
```kotlin
val stoneWall = BlockCodex["basegame:131"]
stoneWall.isSolid = false // Walls don't block movement
stoneWall.isWall = true // Render behind terrain
stoneWall.material = "ROCK"
```
**Properties:**
- `isSolid = false` — Passable
- Wall flag = `1` — Background tile
- Lower opacity than terrain
- No collision physics
### Platform Blocks
Special blocks that allow one-way passage:
```kotlin
val woodPlatform = BlockCodex["basegame:45"]
woodPlatform.isSolid = true
woodPlatform.isPlatform = true // Can jump through from below
```
**Behaviour:**
- Players can jump up through platforms
- Players stand on top of platforms
- Can pass through from below/sides
### Actor Blocks
Blocks that are actually actors (trees, plants):
```kotlin
val tree = BlockCodex["basegame:80"]
tree.isActorBlock = true // Not a real tile, just looks like one
tree.isSolid = false // Actors handle collision
```
**Uses:**
- Trees with complex behaviour
- Plants that grow
- Animated decorations
- Blocks that need per-instance state
## Block Properties
### Visual Properties
#### Shade Colour
Tints the block texture:
```csv
"shdr";"shdg";"shdb";"shduv"
"0.6290";"0.6290";"0.6290";"0.6290"
```
- RGB channels for visible light
- UV channel for fluorescence
- Values 0.0 (black) to 1.0 (full brightness)
#### Luminosity
Blocks can emit light:
```csv
"lumr";"lumg";"lumb";"lumuv"
"0.7664";"0.2032";"0.0000";"0.0000"
```
**Example: Lava**
- Red: 0.7664 (bright orange-red)
- Green: 0.2032 (dim)
- Blue: 0.0 (none)
- UV: 0.0 (none)
**Dynamic luminosity:**
```kotlin
var dynamicLuminosityFunction: Int = 0
```
- `0` — Static light
- `1-7` — Various flicker/pulse patterns
- Used for torches, lava, glowstone
#### Reflectance
Surface reflectivity for lighting:
```csv
"refl"
"0.8"
```
- `0.0` — Fully absorbing (dark surfaces)
- `1.0` — Fully reflective (mirrors, metal)
- Affects how light bounces off surfaces
### Physical Properties
#### Strength (Mining Difficulty)
Hit points required to break block:
```csv
"str"
"120"
```
**Common values:**
- `1` — Air (instant)
- `50` — Dirt (easy)
- `120` — Stone (normal)
- `500` — Obsidian (hard)
- `10000` — Bedrock (unbreakable)
#### Density
Mass per cubic metre:
```csv
"dsty"
"2600"
```
**Common densities:**
- Air: 1 kg/m³
- Wood: 800 kg/m³
- Stone: 2600 kg/m³
- Iron ore: 7800 kg/m³
- Gold ore: 19300 kg/m³
#### Friction
Horizontal movement resistance:
```csv
"fr"
"16"
```
**Values:**
- `0` — Frictionless (ice)
- `4` — Low friction (polished)
- `16` — Normal friction (stone, dirt)
- `32` — High friction (rough)
### Material Assignment
Every block has a material ID:
```csv
"mate"
"ROCK"
```
Materials define:
- Physical properties (hardness, density)
- Sound effects (footsteps, impacts)
- Tool effectiveness
- Thermal properties
**Common materials:**
- `AIIR` — Air
- `DIRT` — Dirt, sand
- `ROCK` — Stone, ores
- `WOOD` — Wooden blocks
- `WATR` — Water
See [[Modules-Codex-Systems#MaterialCodex]] for full material properties.
## Block Tags
Tags enable flexible queries and categorisation:
```csv
"tags"
"STONE,NATURAL,MINERAL"
```
### Common Tags
**Terrain types:**
- `DIRT` — Dirt, sand, clay
- `STONE` — Rock, ores
- `WOOD` — Wooden planks
- `METAL` — Metal blocks
**Placement:**
- `NATURAL` — Generated naturally
- `ARTIFICIAL` — Player-created
- `INCONSEQUENTIAL` — Doesn't affect world generation
**Behaviour:**
- `TREE` — Tree blocks (special handling)
- `ORE` — Ore blocks
- `FLUID` — Liquid blocks
- `NORANDTILE` — Disable autotiling randomisation
**Usage:**
```kotlin
// Find all stone blocks
val stoneBlocks = BlockCodex.blockProps.values.filter { it.hasTag("STONE") }
// Check if block is ore
if (block.hasTag("ORE")) {
println("This is an ore block!")
}
// Check multiple tags
if (block.hasAllTagsOf("TREE", "NATURAL")) {
println("Natural tree")
}
```
## Creating Custom Blocks
**Important:** You don't write Kotlin/Java code to create blocks. You simply define block properties in CSV files, and the system automatically generates everything at runtime.
### CSV Format
Blocks are defined in `blocks/blocks.csv`:
```csv
"id";"drop";"spawn";"name";"shdr";"shdg";"shdb";"shduv";"str";"dsty";"mate";"solid";"wall";"grav";"dlfn";"fv";"fr";"lumr";"lumg";"lumb";"lumuv";"refl";"tags"
"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 columns:**
- `id` — Numeric tile ID (unique within module)
- `drop` — Item ID dropped when mined
- `spawn` — Item ID used to place block
- `name` — Translation key
- `shdr/shdg/shdb/shduv` — Shade colour (RGBA)
- `str` — Strength/HP
- `dsty` — Density (kg/m³)
- `mate` — Material ID (4-letter code)
- `solid` — Is solid (1) or passable (0)
- `wall` — Is wall (1) or terrain (0)
- `fr` — Friction coefficient
- `lumr/lumg/lumb/lumuv` — Luminosity (RGBA)
- `dlfn` — Dynamic luminosity function (0-7)
- `refl` — Reflectance (0.0-1.0)
- `tags` — Comma-separated tags
### Example: Custom Glowing Ore
```csv
"id";"drop";"spawn";"name";"shdr";"shdg";"shdb";"shduv";"str";"dsty";"mate";"solid";"wall";"grav";"dlfn";"fv";"fr";"lumr";"lumg";"lumb";"lumuv";"refl";"tags"
"200";"mymod:crystal";"mymod:200";"BLOCK_CRYSTAL_ORE";"0.7";"0.8";"0.9";"0.2";"150";"3200";"ROCK";"1";"0";"N/A";"0";"0";"16";"0.2";"0.5";"0.8";"0.0";"0.3";"STONE,ORE,GLOWING"
```
This creates:
- Blue-tinted glowing crystal ore
- Strength 150 (harder than normal stone)
- Emits cyan light (0.2 red, 0.5 green, 0.8 blue)
- Drops `mymod:crystal` item when mined
### Loading Custom Blocks
In your module's `EntryPoint`:
```kotlin
override fun invoke() {
// Load blocks
ModMgr.GameBlockLoader.invoke("mymod")
}
```
The loader automatically:
1. Reads `blocks/blocks.csv`
2. Parses CSV records
3. Creates `BlockProp` instances from CSV data
4. Registers blocks in `BlockCodex`
5. Creates corresponding block items via `makeNewItemObj()` and registers them in `ItemCodex`
**You never manually create `BlockProp` instances** — the system handles everything. You only define CSV data.
## Block Rendering
### Tile Atlas System
Blocks are rendered from texture atlases with autotiling.
**Atlas structure:**
- 16×16 pixel tiles
- Organised in sprite sheets
- Multiple tiles per block (autotiling variants)
- Six seasonal variations per tile
See [[Tile Atlas System]] for details on atlas generation and seasonal mixing.
### Autotiling
Blocks automatically connect to adjacent similar blocks.
**Tile connections:**
- 4-directional (cardinal)
- 8-directional (cardinal + diagonal)
- Corner pieces
- Edge pieces
**Subtiling:**
Some blocks have internal variation using subtiles:
- Random patterns (ore veins)
- Gradients (stone texture)
- Detail variations
See [[Autotiling in Depth]] for complete autotiling documentation.
### Rendering Order
Blocks render in layers:
1. **Wall layer** — Background walls
2. **Terrain layer** — Solid terrain
3. **Overlay layer** — Liquids, effects
**Depth sorting:**
- Walls always behind terrain
- Transparent blocks blend correctly
- Liquids render with transparency
## Advanced Features
### Dynamic Luminosity
Blocks with `dlfn > 0` have animated lighting:
```kotlin
var dynamicLuminosityFunction: Int = 2 // Torch flicker
```
**Functions:**
- `0` — Static (no animation)
- `1` — Slow pulse
- `2` — Torch flicker
- `3` — Fast flicker
- `4-7` — Various patterns
**Implementation:**
The engine creates virtual tiles for each dynamic light block, pre-generating random luminosity variations. At runtime, blocks sample from these virtual tiles based on their world coordinates to create spatially-coherent animated lighting.
### Structural Support
Some blocks have weight limits:
```kotlin
var maxSupport: Int = 100 // Can support 100 blocks above
```
Used for:
- Collapsing structures
- Realistic building constraints
- Puzzle mechanics
### Tile Connecting
Control how blocks connect to neighbours:
```kotlin
val isSolidForTileCnx: Boolean
get() = if (tags.contains("DORENDER") || !isActorBlock) isSolid else false
```
- Actor blocks normally don't connect
- `DORENDER` tag forces connection
- Used for trees, decorations
### Block Extra Data
Store custom module data:
```kotlin
block.extra["mymod:special_property"] = "custom value"
block.extra["mymod:spawn_rate"] = 0.5
```
## Block Items
Every block automatically becomes a placeable item.
**Automatic registration:**
When `ModMgr.GameBlockLoader.invoke()` runs, it:
1. Creates `BlockProp` instances from CSV
2. Calls `blockRegisterHook()` for each block
3. The hook generates a `GameItem` via `makeNewItemObj()`
4. Registers the item in `ItemCodex` with the same ID as the block
```kotlin
// Internal process (you don't write this)
fun blockRegisterHook(tile: BlockProp) {
// makeNewItemObj() creates appropriate block item
val item = makeNewItemObj(tile, isWall = tile.isWall)
ItemCodex[tile.id] = item
}
```
**You never manually create block items** — defining a block in CSV automatically creates its placeable item counterpart.
**Generated block item properties:**
- Same ID as block (`basegame:2` for both block and item)
- Mass calculated from block density
- Stackable (up to 999)
- Primary use places the block in world
**Placement:**
```kotlin
// Place terrain block
INGAME.world.setTileTerrain(x, y, blockID, true)
// Place wall block
INGAME.world.setTileWall(x, y, wallID, true)
```
## Querying Blocks
### By ID
```kotlin
val stone = BlockCodex["basegame:2"]
val customOre = BlockCodex["mymod:200"]
```
### By Tags
```kotlin
// All ore blocks
val ores = BlockCodex.blockProps.values.filter { it.hasTag("ORE") }
// All glowing blocks
val glowing = BlockCodex.blockProps.values.filter {
it.baseLumColR > 0 || it.baseLumColG > 0 || it.baseLumColB > 0
}
// All natural stone
val naturalStone = BlockCodex.blockProps.values.filter {
it.hasAllTagsOf("STONE", "NATURAL")
}
```
### By Property
```kotlin
// All blocks stronger than iron
val hardBlocks = BlockCodex.blockProps.values.filter { it.strength > 150 }
// All high-density blocks
val heavyBlocks = BlockCodex.blockProps.values.filter { it.density > 5000 }
// All reflective surfaces
val mirrors = BlockCodex.blockProps.values.filter { it.reflectance > 0.7 }
```
## World Interaction
### Getting Blocks
```kotlin
// Get terrain block at position
val terrainTile = world.getTileFromTerrain(x, y)
// Get wall block at position
val wallTile = world.getTileFromWall(x, y)
// Check if solid
val isSolid = BlockCodex[terrainTile].isSolid
```
### Setting Blocks
```kotlin
// Place terrain
world.setTileTerrain(x, y, "basegame:2", true) // true = update neighbours
// Place wall
world.setTileWall(x, y, "basegame:131", true)
// Remove block (place air)
world.setTileTerrain(x, y, Block.AIR, true)
```
### Mining Blocks
```kotlin
val tile = world.getTileFromTerrain(x, y)
val prop = BlockCodex[tile]
// Calculate mining time
val toolStrength = player.toolStrength
val miningTime = prop.strength / toolStrength
// Drop item when destroyed
val dropItem = prop.drop
if (dropItem.isNotEmpty() && Math.random() < dropProbability) {
world.spawnDroppedItem(x, y, dropItem)
}
// Remove block
world.setTileTerrain(x, y, Block.AIR, true)
```
## Best Practises
1. **Use appropriate strength values** — Match mining difficulty to tier
2. **Set correct density** — Affects physics and realism
3. **Tag comprehensively** — Enable flexible queries
4. **Match drops to blocks** — Logical item drops
5. **Use real material IDs** — Don't create fake materials
6. **Test autotiling** — Verify connections work correctly
7. **Balance luminosity** — Don't make everything glow
8. **Consider friction** — Ice should be slippery
9. **Namespace IDs** — Use `modulename:id` format
10. **Test in all seasons** — Verify seasonal atlas variations
## Common Pitfalls
- **Wrong solid/wall flags** — Blocks behave incorrectly
- **Missing drop items** — Blocks vanish when mined
- **Excessive luminosity** — Blindingly bright blocks
- **Zero friction** — Players slide uncontrollably
- **Inconsistent density** — Wood heavier than stone
- **Missing tags** — Can't query blocks properly
- **Wrong material** — Incorrect sounds and behaviour
- **Forgetting neighbours** — Autotiling breaks
- **Numeric ID conflicts** — Multiple blocks with same ID
- **Not testing placement** — Block items don't work
## See Also
- [[Modules-Codex-Systems#BlockCodex]] — BlockCodex reference
- [[Modules-Setup]] — Creating modules with blocks
- [[Items]] — Block items and placement
- [[World]] — World generation with blocks
- [[Tile Atlas System]] — Block rendering and atlases
- [[Autotiling in Depth]] — Autotiling implementation

@@ -1,24 +1,374 @@
This is a place for the developers, a hub for documentation and code conventions.
# Developer Portal
## Concepts
* [[Glossary]]
* [[Modules]]
This is a place for developers, a hub for documentation and code conventions for the Terrarum game engine.
## Ingame
* [[Actors]]
* [[World]]
* [[Inventory]]
## Getting Started
## Serialisation
* [[Save and Load]]
* [[Glossary]] — Essential terminology and concepts
* [[Modules]] — Understanding the module system
* [[Modules:Setup]] — Setting up your first module
## Internationalisation and Localisation
* [[Languages]]
* [[Keyboard Layout and IME]]
## Core Engine Systems
## Considerations for Basegame and Modules
* [[OpenGL Considerations]]
### Game World
* [[World]] — World structure, chunks, and layers
* [[Actors]] — The actor system and lifecycle
* [[Inventory]] — Items and inventory management
* [[Save and Load]] — Serialisation and persistence
## Child Repositories
* [Terrarum Sans Bitmap](https://github.com/minjaesong/Terrarum-sans-bitmap): The font used in this game
* [TerranVirtualDisk](https://github.com/minjaesong/TerranVirtualDisk): File archival format used in this game
### Rendering
* [[Rendering Pipeline]] — Graphics system and tile rendering
* [[OpenGL Considerations]] — GL 3.2 requirements and shader syntax
### Physics
* [[Physics Engine]] — AABB collision and Newtonian simulation
### Audio
* [[Audio System]] — Sound, music, and spatial audio
### User Interface
* [[UI Framework]] — Canvas-based UI system and components
### Internationalisation and Localisation
* [[Languages]] — Translation system and multilingual support
* [[Keyboard Layout and IME]] — Input methods for complex scripts
## Engine Internals (For Maintainers)
**Advanced technical documentation for engine developers:**
### Rendering & Graphics
* [[Tile Atlas System]] — Six-season atlas generation, subtiling, and dynamic texture management
* [[Autotiling In-Depth]] — Connection algorithms, lookup tables, and subtiling patterns
### Audio
* [[Audio Engine Internals]] — Mixer architecture, DSP pipeline, spatial audio, and streaming
### Coming Soon
* **Fluid Simulation Internals** — Cellular automata and flow algorithms
* **Lighting Engine** — RGB+UV light propagation and transmittance
* **Save Format Specification** — TerranVirtualDisk binary format details
* **Physics Internals** — AABB collision resolution and spatial partitioning
* **Networking** — Multiplayer synchronisation and packet protocol
## Making Modules
### Content Creation
* [[My Little Dagger|Modules:Items]] — Creating custom items
* [[My Precious Block|Modules:Blocks]] — Creating custom blocks
* [[Let It Snow|Modules:Weather]] — Custom weather systems
* [[Art Forgery|Modules:Tapestries]] — Adding decorative images
* [[Let There Be Non-Stationary Thingies|Modules:Actors]] — Creating custom actors
* [[Texture Packs|Modules:Retextures]] — Retexturing existing content
### Advanced Topics
* [[Items in-depth|Development:Items]] — Advanced item implementation
* [[Actors in-depth|Development:Actors]] — Deep dive into actor systems
* [[Creature RAW|Development:RAW]] — Data-driven creature definitions
* [[Faction|Development:Faction]] — Faction system and relationships
## Engine Architecture
### Core Components
* **App.java** — Application entry point and initialisation
* **Terrarum.kt** — Engine singleton and codex management
* **IngameInstance.kt** — Base class for game screens
* **ModMgr.kt** — Module loading and management
### Code Organisation
```
src/net/torvald/terrarum/
├── gameactors/ # Actor system
├── gameworld/ # World structure
├── gameitems/ # Item system
├── blockproperties/ # Block definitions
├── itemproperties/ # Item properties
├── worlddrawer/ # Rendering
├── audio/ # Audio system
├── ui/ # User interface
├── serialise/ # Save/load
├── savegame/ # Virtual disk
├── langpack/ # Localisation
├── gamecontroller/ # Input handling
├── modulebasegame/ # Base game implementation
└── shaders/ # GLSL shaders
```
## Data Structures
### Codices
Codices are global registries for game content:
* **BlockCodex** — All blocks and terrain types
* **ItemCodex** — All items
* **WireCodex** — Wire and conduit types
* **MaterialCodex** — Material definitions
* **FactionCodex** — Faction data
* **CraftingCodex** — Crafting recipes
* **AudioCodex** — Audio assets
* **WeatherCodex** — Weather types
* **FluidCodex** — Fluid properties
* **OreCodex** — Ore definitions
### Key Data Types
* **ActorID** — Integer identifier for actors
* **ItemID** — String identifier for items (`item@module:id`)
* **BlockAddress** — Long integer for block positions
* **RGBA8888** — 32-bit colour value
* **Cvec** — Colour vector with RGBUV channels
## Rendering Details
### Tile Atlas Formats
* **16×16** — Single tile, no autotiling
* **64×16** — Wall stickers (4 variants)
* **128×16** — Platforms (8 variants)
* **112×112** — Full autotiling (49 variants)
* **224×224** — Seasonal autotiling (4 seasons)
### Render Order Layers
1. FAR_BEHIND — Wires and conduits
2. BEHIND — Tapestries, background particles
3. MIDDLE — Actors (players, NPCs, creatures)
4. MIDTOP — Projectiles, thrown items
5. FRONT — Front walls and barriers
6. OVERLAY — Screen overlays (unaffected by lighting)
### Lighting System
* **RGB+UV** — Four independent light channels
* **Transmittance** — Light propagation through blocks
* **Dynamic lights** — Time-varying luminosity functions
* **Corner occlusion** — Shader-based depth enhancement
## Physics Constants
* **TILE_SIZE** — 16 pixels (configurable at compile time)
* **METER** — 25 pixels (1 metre in game units)
* **PHYS_TIME_FRAME** — 25.0 (physics time step)
* **PHYS_REF_FPS** — 60.0 (reference frame rate)
## Input Handling
### Keyboard Layouts
* **Low Layer** — Base layout (QWERTY, Colemak, etc.)
* **High Layer** — Language-specific IME (Hangul, Kana, etc.)
Keyboard layouts are stored in `assets/keylayout/` as `.key` files (Low Layer) and `.ime` files (High Layer).
### Controller Support
* GDX controllers (via LibGDX)
* XInput devices (via JXInput)
* Custom controller mappings
## Modding API Guidelines
### Module Structure
```
<module>/
├── metadata.properties # Required: module info
├── default.json # Optional: default config
├── icon.png # Optional: 48×48 icon
├── <module>.jar # Optional: compiled code
├── blocks/ # Block definitions
│ └── blocks.csv
├── items/ # Item definitions
│ └── itemid.csv
├── materials/ # Material definitions
│ └── materials.csv
├── locales/ # Translations
│ ├── en/
│ ├── koKR/
│ └── ...
├── crafting/ # Crafting recipes
│ └── *.json
└── retextures/ # Texture packs
└── ...
```
### Entry Point
If your module has a JAR file, create an entry point:
```kotlin
class MyModuleEntryPoint : ModuleEntryPoint() {
override fun init() {
// Load blocks
ModMgr.GameBlockLoader.loadAll(moduleInfo)
// Load items
ModMgr.GameItemLoader.loadAll(moduleInfo)
// Load materials
ModMgr.GameMaterialLoader.loadAll(moduleInfo)
// Load languages
ModMgr.GameLanguageLoader.loadAll(moduleInfo)
// Load crafting recipes
ModMgr.GameCraftingRecipeLoader.loadAll(moduleInfo)
}
}
```
### Dependency Management
Specify module dependencies in `metadata.properties`:
```properties
dependency=basegame 0.4.0+;othermod 1.2.*
```
Version syntax:
* `a.b.c` — Exact version
* `a.b.c+` — Version a.b.c to a.b.65535
* `a.b.*` — Any version a.b.x
* `a.b+` — Version a.b.0 to a.255.65535
* `a.*` — Any version a.x.x
* `*` — Any version (testing only!)
## Code Conventions
### Naming
* **Classes** — PascalCase (`ActorPlayer`, `BlockStone`)
* **Functions** — camelCase (`update`, `getTileFromTerrain`)
* **Constants** — SCREAMING_SNAKE_CASE (`TILE_SIZE`, `METER`)
* **Properties** — camelCase (`hitbox`, `baseMass`)
### Language Usage
* **Kotlin** — Preferred for new game logic and systems
* **Java** — Used for performance-critical code and LibGDX integration
* **Comments** — Required for public APIs
### File Organisation
* One public class per file
* Filename matches class name
* Group related classes in packages
## Asset Guidelines
### Image Formats
* **With transparency** — Export as TGA
* **Without transparency** — Export as PNG
* **Colour space** — sRGB, white point D65
### Audio Formats
* **Music** — OGG Vorbis, `-q 10` quality
* **SFX** — OGG Vorbis or WAV
* **Sample rate** — 44100 Hz
### Text Files
* **Encoding** — UTF-8
* **Line endings** — Unix (LF)
* **JSON** — Properly formatted with indentation
## Debugging Tools
### Console Commands
Access the in-game console (typically F12) for:
* Spawning items and actors
* Teleportation
* Time manipulation
* Debug overlays
### Debug Flags
```kotlin
App.IS_DEVELOPMENT_BUILD = true // Enable debug features
```
Debug features include:
* Hitbox visualisation
* Performance metrics
* Verbose logging
* Collision testing modes
### Logging
```kotlin
import net.torvald.terrarum.App.printdbg
printdbg(this, "Debug message")
printdbgerr(this, "Error message")
```
## Performance Profiling
### DebugTimers
```kotlin
DebugTimers.start("MyOperation")
// ... code to profile
DebugTimers.end("MyOperation")
```
View results in the debug UI or logs.
### Memory Monitoring
* Java heap — Standard JVM memory
* Unsafe allocation — Custom memory management
* Texture memory — GPU texture usage
## Testing
### Manual Testing
* Test with multiple modules loaded
* Test module loading/unloading
* Verify save/load compatibility
* Check language switching
* Test at different resolutions
### Asset Validation
* Check for missing textures
* Verify audio file integrity
* Test all translations
* Validate JSON syntax
## Common Pitfalls
1. **Forgetting @Transient** — Sprites and UI cannot serialise
2. **Not calling reload()** — Transient fields stay null
3. **Hardcoding TILE_SIZE** — Use the constant
4. **Ignoring frame rate** — Scale by delta time
5. **Creating circular references** — Breaks serialisation
6. **Not disposing resources** — Memory leaks
7. **Assuming English** — Support all languages
## Best Practises
1. **Follow existing patterns** — Check neighbouring code
2. **Document public APIs** — Help other developers
3. **Test with base game** — Ensure compatibility
4. **Handle missing data gracefully** — Don't crash on errors
5. **Provide fallbacks** — Always have English translations
6. **Version your content** — Use semver strictly
7. **Profile performance** — Identify bottlenecks early
## External Resources
### Child Repositories
* [Terrarum Sans Bitmap](https://github.com/minjaesong/Terrarum-sans-bitmap) — The font used in this game
* [TerranVirtualDisk](https://github.com/minjaesong/TerranVirtualDisk) — File archival format used in saves
### Libraries
* [LibGDX](https://libgdx.com/) — Game framework
* [LWJGL3](https://www.lwjgl.org/) — OpenGL bindings
* [dyn4j](https://dyn4j.org/) — Physics library (for Vector2)
* [GraalVM JavaScript](https://www.graalvm.org/) — Scripting engine
### Documentation
* [OpenGL 3.2 Reference](https://www.khronos.org/opengl/wiki/OpenGL_3.2)
* [GLSL 1.50 Reference](https://www.khronos.org/opengl/wiki/Core_Language_(GLSL))
* [Kotlin Documentation](https://kotlinlang.org/docs/home.html)
## Sample Projects
* [Terrarum Sample Module](https://github.com/curioustorvald/terrarum-sample-module-project) — Template for creating modules
## Getting Help
* Check this documentation portal
* Review sample module code
* Search existing modules for examples
* Open an issue on GitHub for bugs
* Join the community for questions
---
**Happy modding!**

629
Fixtures.md Normal file

@@ -0,0 +1,629 @@
# Fixtures
**Audience:** Engine maintainers and module developers working with placeable objects and electrical systems.
Fixtures are stationary, placeable objects in the world such as furniture, machines, and decorative items. They occupy tiles, interact with the player, and can have complex behaviours including electrical connectivity and music playback.
## Overview
Fixtures provide:
- **Tile occupation** — Reserve space in the world with BlockBox
- **Interaction** — Player can click to use or open UI
- **Inventory** — Store items internally
- **Electrical connectivity** — Wire-based power and signals
- **Music playback** — Play audio files through fixtures
- **Spawn validation** — Check for floor/wall/ceiling requirements
## FixtureBase
The base class for all fixtures.
### Class Hierarchy
```
Actor
└─ ActorWithBody
└─ FixtureBase
├─ Electric (fixtures with wire connectivity)
│ ├─ FixtureJukebox
│ ├─ FixtureMusicalTurntable
│ ├─ FixtureLogicSignalEmitter
│ └─ ...
├─ FixtureTapestry
├─ FixtureWorkbench
└─ ...
```
### Core Properties
```kotlin
class FixtureBase : ActorWithBody, CuedByTerrainChange {
// Spawn requirements
@Transient open val spawnNeedsWall: Boolean = false
@Transient open val spawnNeedsFloor: Boolean = true
@Transient open val spawnNeedsStableFloor: Boolean = false
@Transient open val spawnNeedsCeiling: Boolean = false
// Physical presence
lateinit var blockBox: BlockBox
@Transient var blockBoxProps: BlockBoxProps
var worldBlockPos: Point2i? // Tile position in world, null if not placed
// Interaction
@Transient var nameFun: () -> String
@Transient var mainUI: UICanvas?
var inventory: FixtureInventory?
// State
@Transient var inOperation = false
@Transient var spawnRequestedTime: Long
}
```
### BlockBox
Defines the physical space a fixture occupies:
```kotlin
data class BlockBox(
val collisionType: ItemID = NO_COLLISION,
val width: Int = 0, // Tile-wise width
val height: Int = 0 // Tile-wise height
)
```
**Collision Types:**
```kotlin
companion object {
const val NO_COLLISION = Block.ACTORBLOCK_NO_COLLISION
const val FULL_COLLISION = Block.ACTORBLOCK_FULL_COLLISION
const val ALLOW_MOVE_DOWN = Block.ACTORBLOCK_ALLOW_MOVE_DOWN // Platform
const val NO_PASS_RIGHT = Block.ACTORBLOCK_NO_PASS_RIGHT
const val NO_PASS_LEFT = Block.ACTORBLOCK_NO_PASS_LEFT
val NULL = BlockBox() // No tile occupation
}
```
**Example BlockBoxes:**
```kotlin
// 2×3 fixture with no collision (decorative painting)
BlockBox(BlockBox.NO_COLLISION, 2, 3)
// 1×2 fixture with full collision (statue)
BlockBox(BlockBox.FULL_COLLISION, 1, 2)
// 2×1 fixture that acts as platform (table)
BlockBox(BlockBox.ALLOW_MOVE_DOWN, 2, 1)
```
### Constructor
```kotlin
constructor(
blockBox0: BlockBox,
blockBoxProps: BlockBoxProps = BlockBoxProps(0),
renderOrder: RenderOrder = RenderOrder.MIDDLE,
nameFun: () -> String,
mainUI: UICanvas? = null,
inventory: FixtureInventory? = null,
id: ActorID? = null
)
```
**Example Fixture:**
```kotlin
class FixtureWorkbench : FixtureBase(
BlockBox(BlockBox.FULL_COLLISION, 3, 2), // 3 tiles wide, 2 tiles tall
nameFun = { Lang["ITEM_WORKBENCH"] },
mainUI = UIWorkbench()
) {
init {
makeNewSprite(getSpritesheet("basegame", "sprites/fixtures/workbench.tga", 48, 32))
density = 600.0
actorValue[AVKey.BASEMASS] = 150.0
}
}
```
### Spawning
#### Spawn Requirements
Fixtures specify their spawning constraints:
```kotlin
// Requires solid floor below (default)
spawnNeedsFloor = true
spawnNeedsStableFloor = false
// Requires stable floor (no platforms)
spawnNeedsFloor = false
spawnNeedsStableFloor = true
// Requires solid wall behind
spawnNeedsWall = true
// Requires solid ceiling above (hanging fixtures)
spawnNeedsCeiling = true
// Requires wall OR floor (either satisfies requirement)
spawnNeedsWall = true
spawnNeedsFloor = true
```
#### Spawning Process
```kotlin
fun spawn(posX0: Int, posY0: Int, installersUUID: UUID?): Boolean {
// posX0, posY0: tile-wise bottom-centre position
// 1. Calculate top-left position
val posXtl = (posX0 - blockBox.width.minus(1).div(2)) fmod world.width
val posYtl = posY0 - blockBox.height + 1
// 2. Check spawn validity
if (!canSpawnHere(posX0, posY0)) {
return false // Blocked by tiles or lacks required floor/wall
}
// 3. Place filler blocks in terrain layer
placeActorBlocks()
// 4. Set hitbox and position
worldBlockPos = Point2i(posXtl, posYtl)
hitbox.setFromWidthHeight(
posXtl * TILE_SIZED,
posYtl * TILE_SIZED,
blockBox.width * TILE_SIZED,
blockBox.height * TILE_SIZED
)
// 5. Add to world
INGAME.queueActorAddition(this)
// 6. Make placement sound and dust particles
makeNoiseAndDust(posXtl, posYtl)
// 7. Call custom spawn logic
onSpawn(posX0, posY0)
return true
}
```
### Interaction
#### Click Interaction
```kotlin
open fun onInteract(mx: Double, my: Double) {
// Fired on mouse click
// Do NOT override if fixture has mainUI (handled automatically)
}
```
**Example:**
```kotlin
override fun onInteract(mx: Double, my: Double) {
// Toggle torch on/off
isLit = !isLit
playSound("torch_toggle")
}
```
#### UI Integration
If `mainUI` is set, clicking the fixture opens the UI automatically:
```kotlin
class FixtureWorkbench : FixtureBase(
blockBox = BlockBox(BlockBox.FULL_COLLISION, 3, 2),
nameFun = { Lang["ITEM_WORKBENCH"] },
mainUI = UIWorkbench() // Automatically opens on click
)
```
### Quick Lookup Parameters
Add tooltip information visible on hover:
```kotlin
init {
// With label
addQuickLookupParam("STAT_TEMPERATURE") {
"${temperature}°C"
}
// Without label (dynamic text only)
addQuickLookupParam {
if (isPowered) "§o§Powered§.§" else ""
}
}
```
### Despawning
```kotlin
override fun despawn() {
if (canBeDespawned) {
// Remove filler blocks
forEachBlockbox { x, y, _, _ ->
world.setTileTerrain(x, y, Block.AIR, true)
}
worldBlockPos = null
mainUI?.dispose()
super.despawn()
}
}
override val canBeDespawned: Boolean
get() = inventory?.isEmpty() ?: true
```
Fixtures with non-empty inventory **cannot be despawned** until emptied.
### BlockBox Iteration
```kotlin
// Iterate over all tiles occupied by fixture
forEachBlockbox { x, y, offsetX, offsetY ->
// x, y: world tile coordinates
// offsetX, offsetY: offset from fixture's top-left corner
world.setTileTerrain(x, y, Block.AIR, true)
}
// Get all positions as list
val positions: List<Pair<Int, Int>>? = everyBlockboxPos
```
### Serialisation
**Transient fields** (not serialised):
- `@Transient var nameFun`
- `@Transient var mainUI`
- `@Transient var blockBoxProps`
- Sprite and rendering data
**Serialised fields:**
- `blockBox`
- `inventory`
- `worldBlockPos`
- Custom state variables
**Reconstruction:**
```kotlin
override fun reload() {
super.reload()
// Reconstruct transient state
nameFun = { Lang["ITEM_WORKBENCH"] }
mainUI = UIWorkbench()
sprite = loadSprite()
}
```
## Electric Fixtures
Electric fixtures support wire-based connectivity for power and signals.
### Class Definition
```kotlin
open class Electric : FixtureBase {
// Wire emitters (outputs)
@Transient val wireEmitterTypes: HashMap<BlockBoxIndex, WireEmissionType>
val wireEmission: HashMap<BlockBoxIndex, Vector2>
// Wire sinks (inputs)
@Transient val wireSinkTypes: HashMap<BlockBoxIndex, WireEmissionType>
val wireConsumption: HashMap<BlockBoxIndex, Vector2>
// Energy storage
val chargeStored: HashMap<String, Double>
}
```
### Wire Types
Common wire emission types:
- **`"appliance_power"`** — AC power (230V/120V)
- **`"digital_bit"`** — Digital signal (0.0 = LOW, 1.0 = HIGH)
- **`"heating_power"`** — Thermal energy
- **`"analog_audio"`** — Audio signal
### Configuring Wires
```kotlin
class FixtureJukebox : Electric {
init {
// Set sink at (0, 2) in BlockBox for appliance power
setWireSinkAt(0, 2, "appliance_power")
// Power consumption: 350W real, 0VAR reactive
setWireConsumptionAt(0, 2, Vector2(350.0, 0.0))
}
}
```
```kotlin
class FixtureSolarPanel : Electric {
init {
// Set emitter at (1, 0) for appliance power
setWireEmitterAt(1, 0, "appliance_power")
// Power generation updated per frame
setWireEmissionAt(1, 0, Vector2(powerGenerated, 0.0))
}
}
```
### Digital Signals
Electric fixtures can respond to digital signals:
```kotlin
class FixtureLogicSignalEmitter : Electric {
init {
setWireSinkAt(0, 0, "digital_bit")
setWireEmitterAt(1, 0, "digital_bit")
}
// Called when signal rises from LOW to HIGH
override fun onRisingEdge(readFrom: BlockBoxIndex) {
println("Signal went HIGH at index $readFrom")
// Set output HIGH
setWireEmissionAt(1, 0, Vector2(1.0, 0.0))
}
// Called when signal falls from HIGH to LOW
override fun onFallingEdge(readFrom: BlockBoxIndex) {
println("Signal went LOW at index $readFrom")
// Set output LOW
setWireEmissionAt(1, 0, Vector2(0.0, 0.0))
}
// Called after edge detection
override fun updateSignal() {
// Update internal logic
}
}
```
### Signal Thresholds
```kotlin
companion object {
const val ELECTRIC_THRESHOLD_HIGH = 0.6666666666666666 // 66.7%
const val ELECTRIC_THRESHOLD_LOW = 0.3333333333333333 // 33.3%
const val ELECTRIC_THRESHOLD_EDGE_DELTA = 0.33333333333333337
}
```
**Signal Detection:**
- **Rising edge** — Signal crosses from below `THRESHOLD_LOW` to above `THRESHOLD_HIGH`
- **Falling edge** — Signal crosses from above `THRESHOLD_HIGH` to below `THRESHOLD_LOW`
### Reading Wire State
```kotlin
// Get wire state at offset (x, y) for given type
val state: Vector2 = getWireStateAt(offsetX, offsetY, "digital_bit")
// Check if digital signal is HIGH
if (isSignalHigh(0, 0)) {
// Input is HIGH
}
// Check if digital signal is LOW
if (isSignalLow(0, 0)) {
// Input is LOW
}
```
### Wire Emission
```kotlin
override fun updateImpl(delta: Float) {
super.updateImpl(delta)
// Update power generation based on sunlight
val sunlight = world.globalLight
val powerGenerated = sunlight * 500.0 // Up to 500W
setWireEmissionAt(0, 0, Vector2(powerGenerated, 0.0))
}
```
### Complex Example: Logic Gate
```kotlin
class FixtureLogicAND : Electric {
init {
// Two inputs
setWireSinkAt(0, 0, "digital_bit")
setWireSinkAt(1, 0, "digital_bit")
// One output
setWireEmitterAt(2, 0, "digital_bit")
}
override fun updateSignal() {
// AND gate logic
val inputA = isSignalHigh(0, 0)
val inputB = isSignalHigh(1, 0)
val output = if (inputA && inputB) 1.0 else 0.0
setWireEmissionAt(2, 0, Vector2(output, 0.0))
}
}
```
## PlaysMusic Interface
A marker interface for fixtures that can play music.
```kotlin
interface PlaysMusic {
// Marker interface (no methods)
}
```
**Purpose:** Identifies fixtures that play music for game systems:
- Music service coordination (prevent multiple music sources)
- Audio priority management
- Jukebox/radio functionality
**Usage:**
```kotlin
class FixtureJukebox : Electric, PlaysMusic {
@Transient var musicNowPlaying: MusicContainer? = null
val musicIsPlaying: Boolean
get() = musicNowPlaying != null
fun playDisc(index: Int) {
val disc = discInventory[index]
val musicFile = (ItemCodex[disc] as? ItemFileRef)?.getAsGdxFile()
musicNowPlaying = MusicContainer(title, musicFile.file()) {
// Callback when music ends
stopPlayback()
}
MusicService.playMusicalFixture(
action = { startAudio(musicNowPlaying!!) },
musicFinished = { !musicIsPlaying },
onSuccess = { /* ... */ },
onFailure = { /* ... */ }
)
}
}
```
**Built-in PlaysMusic Fixtures:**
- `FixtureJukebox` — Plays music discs
- `FixtureMusicalTurntable` — Vinyl record player
- `FixtureRadio` — Streams music
## Common Patterns
### Creating a Simple Fixture
```kotlin
class FixtureTikiTorch : FixtureBase(
BlockBox(BlockBox.NO_COLLISION, 1, 2),
nameFun = { Lang["ITEM_TIKI_TORCH"] }
) {
@Transient var isLit = true
init {
makeNewSprite(getSpritesheet("basegame", "sprites/fixtures/tiki_torch.tga", 16, 32))
density = 500.0
actorValue[AVKey.BASEMASS] = 5.0
}
override fun onInteract(mx: Double, my: Double) {
isLit = !isLit
}
override fun updateImpl(delta: Float) {
super.updateImpl(delta)
sprite?.currentRow = if (isLit) 0 else 1
}
override fun reload() {
super.reload()
// Reconstruct sprite
makeNewSprite(getSpritesheet("basegame", "sprites/fixtures/tiki_torch.tga", 16, 32))
}
}
```
### Creating an Electric Fixture
```kotlin
class FixtureLamp : Electric {
constructor() : super(
BlockBox(BlockBox.NO_COLLISION, 1, 1),
nameFun = { Lang["ITEM_LAMP"] }
)
@Transient var isPowered = false
init {
makeNewSprite(getSpritesheet("basegame", "sprites/fixtures/lamp.tga", 16, 16))
// Require power at bottom tile
setWireSinkAt(0, 0, "appliance_power")
setWireConsumptionAt(0, 0, Vector2(60.0, 0.0)) // 60W lamp
}
override fun updateImpl(delta: Float) {
super.updateImpl(delta)
// Check if receiving power
val powerReceived = getWireStateAt(0, 0, "appliance_power")
isPowered = powerReceived.x > 10.0 // At least 10W
// Update sprite
sprite?.currentRow = if (isPowered) 0 else 1
}
}
```
### Fixture with Inventory
```kotlin
class FixtureChest : FixtureBase(
BlockBox(BlockBox.FULL_COLLISION, 2, 2),
nameFun = { Lang["ITEM_CHEST"] },
mainUI = UIChest(),
inventory = FixtureInventory(20) // 20 slots
) {
init {
makeNewSprite(getSpritesheet("basegame", "sprites/fixtures/chest.tga", 32, 32))
(mainUI as UIChest).setInventory(inventory!!)
}
override val canBeDespawned: Boolean
get() = inventory?.isEmpty() ?: true
}
```
## Best Practises
1. **Always call `super.updateImpl(delta)`** — Required for fixture systems to work
2. **Mark UI/sprites as `@Transient`** — They cannot be serialised
3. **Implement `reload()`** — Reconstruct all transient fields
4. **Use `forEachBlockbox` for tile operations** — Handles BlockBox correctly
5. **Check `canBeDespawned` before despawn** — Prevent loss of inventory contents
6. **Set `inOperation` when active** — Enables chunk anchoring for machines
7. **Use `makeNoiseAndDust()` on spawn** — Provides visual/audio feedback
8. **Don't override `onInteract()` if using `mainUI`** — Handled automatically
## Common Pitfalls
- **Forgetting to call `placeActorBlocks()`** — Fixture doesn't occupy tiles
- **Not implementing `reload()`** — Crashes when loading save
- **Setting wire emissions in constructor** — Use `updateImpl()` instead
- **Mixing up BlockBox coordinates** — Use `forEachBlockbox` for clarity
- **Despawning fixtures with inventory** — Check `canBeDespawned` first
- **Directly modifying `worldBlockPos`** — Use `spawn()` instead
## See Also
- [[Actors]] — Base actor system
- [[Animation-Description-Language]] — ADL for humanoid sprites
- [[Inventory]] — Inventory system
- [[World]] — World tile management

260
Glossary.md Normal file

@@ -0,0 +1,260 @@
# Glossary
This page defines the key terminology used throughout the Terrarum game engine.
## Core Concepts
### Actor
An entity in the game world that can be updated and rendered. Actors are the fundamental interactive objects in the game, including players, NPCs, particles, projectiles, and fixtures. All actors extend the `Actor` base class and have a unique **Reference ID**.
See also: [[Actors]]
### Reference ID (Actor ID)
A unique integer identifier assigned to each Actor in the game. Valid Reference IDs range from 16,777,216 to 0x7FFF_FFFF. The Reference ID is used to track and retrieve actors during the game's lifecycle.
### Render Order
Determines the drawing layer for actors. From back to front:
- **FAR_BEHIND** — Wires and conduits
- **BEHIND** — Tapestries, some particles (obstructed by terrain)
- **MIDDLE** — Standard actors (players, NPCs, creatures)
- **MIDTOP** — Projectiles, thrown items
- **FRONT** — Front walls (blocks that obstruct actors)
- **OVERLAY** — Screen overlays, not affected by lightmap
### ActorWithBody
An Actor that has physics properties, including position, velocity, hitbox, and collision. Most interactive game entities inherit from ActorWithBody rather than the base Actor class.
### Hitbox
An axis-aligned bounding box (AABB) that defines an actor's collision area. The engine uses an AABB-based physics system optimised for handling millions of tile bodies.
### Block
A tile in the game world. Blocks can be terrain, walls, platforms, or other tile-based objects. Each block has properties such as strength, density, material, luminosity, and collision behaviour.
### Wall
A block placed in the wall layer, typically behind terrain. Walls do not provide collision by default but can affect lighting and provide visual background.
### Terrain
A block placed in the terrain layer. Terrain blocks typically provide collision and are what players interact with directly.
### Tile
A single grid position in the game world. The default tile size is 16×16 pixels (though this can be configured via `TILE_SIZE`). Used interchangeably with "block" in some contexts.
### Item
A game object that can exist in inventories, be dropped in the world, or be used by actors. Items extend the `GameItem` class and have unique **Item IDs**.
### Item ID
A string identifier for items following the format `item@<module>:<id>` (e.g., `item@basegame:1`). For blocks used as items, the format is `<module>:<id>` (e.g., `basegame:32`).
### Fixture
A special type of actor representing interactable world objects such as doors, chests, crafting stations, and other furniture. Fixtures often have associated UI interactions.
## World & Generation
### GameWorld
The container for all world data, including terrain layers, fluid simulation, lighting, wirings, and environmental properties. A GameWorld represents a single playable world/dimension.
### Chunk
A subdivision of the world used for efficient storage and generation. Chunks are `CHUNK_W × CHUNK_H` tiles in size. The world generates chunks on-demand rather than all at once, enabling fast world creation for vast world sizes.
### Block Layer
A 2D array storing block data for the world. The engine uses multiple specialised layers:
- **layerTerrain** — Foreground blocks with collision
- **layerWall** — Background wall blocks
- **layerOres** — Ore deposits overlaid on terrain
- **layerFluids** — Fluid simulation data
### Spawn Point
The tilewise coordinate where players initially appear in a world. Stored as `spawnX` and `spawnY` in the GameWorld.
### World Time
The in-game time system. Measured in seconds since the world's epoch. The engine simulates day/night cycles, seasons, and celestial events based on world time.
### Gravitation
The gravitational acceleration vector applied to physics bodies. Currently, only downward gravity is supported. Default is approximately 9.8 m/s² in game units.
## Modules & Loading
### Module
A package of game content (blocks, items, actors, code) that can be loaded by the engine. Modules are the modding/extension system for Terrarum. See also: [[Modules]]
### Internal Module
A module shipped with the game, stored in `<game exec dir>/assets/mods/`. These modules form the "base game" and become read-only when packaged.
### User Module
A user-created mod stored in `<appdata>/Modules/`. These can be freely modified by end users.
### Entry Point
A class extending `ModuleEntryPoint` that initialises and registers a module's content with the game. The entry point is specified in the module's `metadata.properties`.
### ModMgr
The Module Manager (ModMgr) singleton that handles module loading, dependency resolution, and content registration. It provides loaders for blocks, items, materials, languages, and other game content.
### Codex
A registry that stores game content by ID. The engine maintains several codices:
- **BlockCodex** — All blocks and terrain types
- **ItemCodex** — All items
- **WireCodex** — All wire/conduit types
- **MaterialCodex** — All material definitions
- **FactionCodex** — All faction definitions
- **CraftingCodex** — All crafting recipes
- **AudioCodex** — All audio assets
- **WeatherCodex** — All weather types
- **FluidCodex** — All fluid types
- **OreCodex** — All ore types
## UI System
### UICanvas
The base class for all UI screens and windows. A UICanvas can contain UIItems and sub-UIs, and manages its own show/hide animations and event handling.
### UIItem
An individual UI element (button, slider, text field, etc.) that can be added to a UICanvas. UIItems have event listeners for clicks, drags, keyboard input, and updates.
### UIHandler
Manages the lifecycle and state of a UICanvas, including opening/closing animations, positioning, and visibility state.
### IngameInstance
A game screen that represents an active game session. Extends LibGDX's `Screen` interface and manages the game world, actors, UI, and rendering pipeline. The title screen and ingame view are both IngameInstances.
## Graphics & Rendering
### Autotiling
A system that automatically selects the correct sprite variant for blocks based on neighbouring blocks. Terrarum uses a 7×7 tile atlas (112×112 pixels) for full autotiling support with connection rules.
### Tile Atlas
A texture atlas containing all variants of a block's sprite for autotiling. The engine supports several atlas formats: 16×16 (single tile), 64×16 (wall stickers), 128×16 (platforms), 112×112 (full autotiling), and 224×224 (seasonal autotiling).
### Lightmap
A texture representing the light levels (RGB+UV) for each tile in the visible area. The engine simulates realistic light propagation with transmittance and supports both static and dynamic light sources.
### Dynamic Light Function (dlfn)
Defines how a luminous block's light output changes over time:
- **0** — Static (constant brightness)
- **1** — Torch flicker
- **2** — Current global light (sun, star, moon)
- **3** — Daylight at noon
- **4** — Slow breath (gentle pulsing)
- **5** — Pulsating (rhythmic)
### FlippingSpriteBatch
A custom SpriteBatch implementation used throughout the engine for rendering sprites and textures.
### WorldCamera
The orthographic camera that determines what portion of the world is currently visible on screen.
### Shader
GLSL programs running on the GPU to render special effects. Terrarum uses OpenGL 3.2 Core Profile with GLSL version 1.50. Shaders are stored in `src/shaders/`.
## Physics & Collision
### AABB
Axis-Aligned Bounding Box. The collision detection system used by the engine. All hitboxes are rectangular and aligned to the world axes (not rotated).
### Phys Properties
Physical properties of an ActorWithBody, including:
- **Mass** — Affects momentum and collision response
- **Friction** — How much the actor resists movement on surfaces
- **Immobile** — Whether the actor can move or is fixed in place
- **Density** — Mass per unit volume (water = 1000 in game units)
### METER
A fundamental constant defining the relationship between pixels and physics simulation. As of the current version, 1 metre equals 25 pixels. This constant is used throughout physics calculations.
### PHYS_TIME_FRAME
The time step used by the physics simulator. Currently set to the value of METER for convenient 1:1 correspondence between game units and SI units.
## Audio
### AudioMixer
The central audio system managing all sound output. Supports spatial audio, effects processing, and mixer buses.
### Track
An audio channel in the mixer. The engine provides multiple dynamic tracks for music and sound effects, each with independent volume control, filters, and effects.
### MusicContainer
A wrapper for music assets in the AudioBank. Manages music playback with crossfading and looping.
### Spatial Audio
3D sound positioning system that adjusts volume and panning based on the listener's position relative to the sound source.
## Serialisation & Save/Load
### VirtualDisk
The file archival format used for save games. Based on the TerranVirtualDisk library. Each savegame consists of a world disk and a player disk.
### Disk Skimmer
A utility for reading savegame metadata without loading the entire save file. Used for displaying save information in load screens.
### Transient
A Kotlin annotation (`@Transient`) marking fields that should not be serialised when saving the game state. Transient fields must be reconstructed when loading.
### Reload
A method called on actors after deserialisation to reconstruct transient fields and re-establish references.
## Internationalisation
### Lang
The language system managing text translations. Supports 20+ languages with dynamic font switching.
### IME
Input Method Editor. Allows entry of complex scripts (e.g., Chinese, Japanese, Korean) using keyboard layouts. See also: [[Keyboard Layout and IME]]
### Low Layer
The base keyboard layout (typically Latin characters like QWERTY) used by the IME.
### High Layer
The advanced keyboard layout for complex scripts managed by the IME. Can be phonetic (reading from Low Layer) or independent.
### Terrarum Sans Bitmap
The custom bitmap font system supporting multilingual text rendering. Maintained in a separate repository.
## Configuration
### App Config
Global engine configuration stored in `config.json` in the appdata directory. Includes graphics settings, controls, audio settings, and debug options.
### Module Config
Configuration specific to a module. Default values are in `<module>/default.json`, with user modifications stored in the global config file with the module name prefix (e.g., `basegame:config_key`).
### TILE_SIZE
A configurable constant defining the pixel dimensions of one tile. Default is 16 pixels. Changing this requires engine recompilation.
## Debugging
### Debug Mode
Enabled when `IS_DEVELOPMENT_BUILD` is true. Activates additional logging, assertions, and debug visualisations.
### Console
An in-game developer console accessible during gameplay (typically via F12). Allows executing commands, spawning items/actors, and manipulating game state.
### hs_err_pid
Log files generated by the JVM when it crashes. Useful for debugging native crashes during development.
## Miscellaneous
### Faction
A group affiliation system for actors. Factions can have relationships (friendly, neutral, hostile) that affect AI behaviour and combat.
### Weather
Environmental effects like rain, snow, or fog. Managed by the WeatherMixer and can affect visibility, lighting, and gameplay.
### Wire / Conduit
A special type of block placed in the FAR_BEHIND render layer that connects devices and transmits signals or resources. Multiple wire types can occupy the same tile position.
### Wirings
A hash map storing wire connection data for the world. Each block position can have multiple conduit types with different connection states.
### Canister
A container type for storing fluids, gases, or other substances. Defined in the CanistersCodex.
### RAW
Raws And Wiki-syntax. A data definition format inspired by Dwarf Fortress, used for defining creature properties and other complex game data. See: [[Creature RAW|Development:RAW]]
### Tapestry
Decorative images that can be placed in the world as background art. See: [[Art Forgery|Modules:Tapestries]]
### RGBA8888
A type alias for Int, representing a 32-bit colour value in RGBA format (8 bits per channel).
### Cvec
Colour vector. A colour representation used for lighting calculations, supporting RGB plus UV (ultraviolet) channels.

524
Inventory.md Normal file

@@ -0,0 +1,524 @@
# Inventory
The inventory system manages storage and organisation of items for actors, fixtures, and the world itself. It handles item stacking, equipment, encumbrance, and item persistence.
## Overview
Terrarum uses a flexible inventory system with several specialisations:
1. **FixtureInventory** — Base inventory for storage containers
2. **ActorInventory** — Extended inventory for actors with equipment and quickslots
3. **ItemTable** — Dynamic item storage for worlds
## GameItem
All items in the game extend the `GameItem` base class.
### Item Identification
Items use string-based IDs:
```kotlin
typealias ItemID = String
```
**Item ID formats:**
- **Static items:** `item@<module>:<id>` (e.g., `item@basegame:1`)
- **Blocks as items:** `<module>:<id>` (e.g., `basegame:32`)
- **Dynamic items:** `item@<module>:D<number>` (e.g., `item@basegame:D4096`)
- **Actor items:** Special prefix for actors in pockets
### Original ID vs. Dynamic ID
Items have two ID fields:
```kotlin
val originalID: ItemID // The base item type (e.g., "item@basegame:1")
var dynamicID: ItemID // The specific instance (may be same as originalID)
```
**Static items** have `originalID == dynamicID` and can stack.
**Dynamic items** get unique `dynamicID` values and represent individual instances (e.g., a worn pickaxe).
### Key Properties
```kotlin
// Basic Properties
var baseMass: Double // Mass in kilograms
var baseToolSize: Double? // Physical size for tools
var inventoryCategory: String // "weapon", "tool", "armour", etc.
// Display
var originalName: String // Translation key
var name: String // Displayed name (with custom names)
var nameColour: Color // Item name colour (rarity, etc.)
var nameSecondary: String // Additional description
// Behaviour
val canBeDynamic: Boolean // Can create unique instances
val isCurrentlyDynamic: Boolean // Is this a dynamic instance
var stackable: Boolean // Can stack in inventory
val isConsumable: Boolean // Destroyed on use
var isUnique: Boolean // Only one can exist
var equipPosition: Int // Where to equip (EquipPosition enum)
```
### Item Categories
The `inventoryCategory` classifies items:
- **`weapon`** — Melee and ranged weapons
- **`tool`** — Pickaxes, axes, hammers
- **`armour`** — Protective equipment
- **`block`** — Placeable blocks
- **`wire`** — Wires and conduits
- **`fixture`** — Furniture and fixtures
- **`generic`** — Miscellaneous items
- **`quest`** — Quest-related items
### Equipment Positions
Items can be equipped in specific slots defined by `EquipPosition`:
```kotlin
EquipPosition.HAND_PRIMARY // Main hand
EquipPosition.HAND_SECONDARY // Off hand
EquipPosition.HEADGEAR // Helmet
EquipPosition.ARMOUR // Body armour
EquipPosition.LEGGINGS // Leg armour
EquipPosition.BOOTS // Footwear
EquipPosition.ACCESSORY_1 // Ring, amulet, etc.
EquipPosition.ACCESSORY_2
// ... more accessory slots
EquipPosition.NULL // Not equippable
```
### Dynamic vs. Static Items
**Static items** are identical instances that stack:
- Blocks
- Consumables
- Resources
- Stackable materials
**Dynamic items** have individual state:
- Tools with durability
- Weapons with enchantments
- Armour with damage
- Custom-named items
Creating a dynamic instance:
```kotlin
val dynamicItem = staticItem.makeDynamic(inventory)
```
Dynamic items get assigned IDs in the range 32768..1048575.
### Durability
Dynamic items can have durability:
```kotlin
var durability: Float // Current durability (0.0 = broken)
val maxDurability: Float // Maximum durability when new
```
When durability reaches 0, the item is destroyed.
### Material System
Items are made from materials:
```kotlin
val materialId: String // Material identifier
val material: Material // Full material object
```
Materials affect item properties like mass, durability, and value. See `MaterialCodex` for available materials.
### Combustibility
Items with the `COMBUSTIBLE` tag can be used as fuel:
```kotlin
var calories: Double // Energy content (game-calories)
var smokiness: Float // Smoke emission rate
```
1 game-calorie ≈ 5 watt-hours. An item burning for 80 seconds at 60 FPS has 4800 calories.
### Tags
Items use tags to mark special properties:
```kotlin
interface TaggedProp {
fun hasTag(tag: String): Boolean
fun addTag(tag: String)
}
```
Common tags:
- **`TOOL`** — Can be used as a tool
- **`WEAPON`** — Can be used as a weapon
- **`COMBUSTIBLE`** — Can be burned as fuel
- **`MAGIC`** — Has magical properties
- **`TREASURE`** — Valuable loot
## FixtureInventory
The base inventory class for storage containers.
### Creating an Inventory
```kotlin
val inventory = FixtureInventory(
maxCapacity = 1000L, // Maximum capacity
capacityMode = CAPACITY_MODE_COUNT
)
```
### Capacity Modes
```kotlin
CAPACITY_MODE_COUNT // Limit by item count
CAPACITY_MODE_WEIGHT // Limit by total weight
CAPACITY_MODE_NO_ENCUMBER // Unlimited capacity
```
### Adding Items
```kotlin
// Add an item
inventory.add(item: GameItem, count: Long = 1L)
// Add by ID
inventory.add(itemID: ItemID, count: Long)
```
Items automatically stack if they're stackable and have matching IDs.
### Removing Items
```kotlin
// Remove by ID
val removedCount = inventory.remove(itemID: ItemID, count: Long): Long
// Remove by item
val removedCount = inventory.remove(item: GameItem, count: Long): Long
```
Returns the actual number removed (may be less than requested).
### Querying Items
```kotlin
// Check if item exists
val has: Boolean = inventory.has(itemID: ItemID, minCount: Long = 1L)
// Search by ID
val pair: InventoryPair? = inventory.searchByID(itemID: ItemID)
// Get item count
val count: Long = inventory.count(itemID: ItemID)
```
### InventoryPair
Inventory items are stored as pairs:
```kotlin
data class InventoryPair(
val itm: ItemID, // Item ID
var qty: Long // Quantity
)
```
### Item List
Access all items in the inventory:
```kotlin
val itemList: List<InventoryPair> = inventory.itemList
```
**Important:** Don't modify the item list directly. Use `add()` and `remove()` methods.
### Capacity Tracking
```kotlin
val capacity: Long // Current used capacity
val maxCapacity: Long // Maximum capacity
val encumberment: Double // 0.0-1.0+ (over 1.0 = overencumbered)
```
### Clearing Inventory
```kotlin
val removedItems: List<InventoryPair> = inventory.clear()
```
Returns all items that were removed.
## ActorInventory
Extended inventory for actors (players, NPCs) with equipment and quickslots.
### Creating an Actor Inventory
```kotlin
val inventory = ActorInventory(
actor = playerActor,
maxCapacity = 10000L,
capacityMode = CAPACITY_MODE_WEIGHT
)
```
### Equipment System
Actors can equip items in specific slots:
```kotlin
// Equipment array (indexed by EquipPosition)
val itemEquipped: Array<ItemID?> = inventory.itemEquipped
// Equip an item
actor.equipItem(itemID: ItemID)
// Unequip an item
actor.unequipItem(itemID: ItemID)
```
**Important:** Equipped items must also exist in the main `itemList`. The equipment array stores references (`dynamicID`), not separate copies.
### Quickslots
Actors have a quickslot bar for fast access:
```kotlin
val quickSlot: Array<ItemID?> = inventory.quickSlot // 10 slots by default
// Set quickslot
inventory.setQuickslotItem(slot: Int, dynamicID: ItemID?)
// Get quickslot item
val pair: InventoryPair? = inventory.getQuickslotItem(slot: Int?)
```
Quickslots reference items in the main inventory by their `dynamicID`.
### Dynamic Capacity
For actors, capacity can scale with actor size:
```kotlin
val maxCapacityByActor: Double // Scaled by actor's scale squared
```
This means larger actors can carry more.
### Consuming Items
```kotlin
inventory.consumeItem(item: GameItem, amount: Long = 1L)
```
This method:
1. Removes consumable items
2. Applies durability damage to tools/weapons
3. Auto-equips replacements when tools break
4. Handles dynamic item unpacking
### Item Durability Management
When using tools/weapons, durability decreases:
```kotlin
// Damage calculation
val baseDamage = actor.avStrength / 1000.0
item.durability -= damageAmount
// When durability reaches 0
if (item.durability <= 0) {
// Item is removed and auto-replaced if available
}
```
## ItemCodex
The global registry of all items:
```kotlin
object ItemCodex {
// Look up item by ID
operator fun get(itemID: ItemID): GameItem?
// Register a new item
fun registerItem(item: GameItem)
}
```
Access items via the global codex:
```kotlin
val item = ItemCodex[itemID]
```
## Item Images
Items have associated sprites:
```kotlin
// Get item image (use this, not item.itemImage directly)
val texture: TextureRegion = ItemCodex.getItemImage(item)
```
**Never read `item.itemImage` directly** due to initialization order issues. Always use `ItemCodex.getItemImage()`.
## Inventory UI
ActorInventory integrates with several UI components:
### UIQuickslotBar
Displays and manages quickslot items.
### UIInventoryFull
Full inventory screen with equipment, storage, and crafting.
### InventoryTransactionNegotiator
Handles item transfers between inventories (drag-and-drop, shift-click, etc.).
## Best Practises
1. **Use dynamic IDs for equipment** — Store `dynamicID` in equipment/quickslot arrays, not `originalID`
2. **Check canBeDynamic before creating instances** — Only dynamic items can have individual state
3. **Clean up equipment on item removal** — ActorInventory does this automatically
4. **Validate item existence** — Always check `ItemCodex[id]` returns non-null
5. **Use ItemCodex.getItemImage()** — Never access `item.itemImage` directly
6. **Scale capacity by actor size** — Use `maxCapacityByActor` for realistic encumbrance
7. **Handle item consumption properly** — Use `consumeItem()` for tools to manage durability
8. **Respect capacity modes** — Check `encumberment` to limit player movement when overencumbered
## Common Patterns
### Adding a Block to Inventory
```kotlin
// Blocks use module:id format, not item@module:id
val blockID = "basegame:32"
inventory.add(blockID, 64L) // Add 64 blocks
```
### Equipping a Tool
```kotlin
val pickaxe = ItemCodex["item@basegame:pickaxe_iron"]
if (pickaxe != null && pickaxe.canBeDynamic) {
// Create dynamic instance
val dynamicPickaxe = pickaxe.makeDynamic(actor.inventory)
actor.inventory.add(dynamicPickaxe)
// Equip it
actor.equipItem(dynamicPickaxe.dynamicID)
// Add to quickslot
actor.inventory.setQuickslotItem(0, dynamicPickaxe.dynamicID)
}
```
### Checking Crafting Requirements
```kotlin
fun hasRequiredItems(inventory: FixtureInventory, requirements: Map<ItemID, Long>): Boolean {
return requirements.all { (itemID, count) ->
inventory.has(itemID, count)
}
}
```
### Transferring Items Between Inventories
```kotlin
fun transferItem(from: FixtureInventory, to: FixtureInventory, itemID: ItemID, count: Long): Long {
val removed = from.remove(itemID, count)
if (removed > 0) {
to.add(itemID, removed)
}
return removed
}
```
### Finding All Tools
```kotlin
val tools = actor.inventory.itemList
.mapNotNull { ItemCodex[it.itm] }
.filter { it.hasTag("TOOL") }
```
## Serialisation
Inventories are serialisable for save games. However:
- **`actor` field is `@Transient`** — Reconstructed on reload
- **Equipment and quickslots** are saved
- **Dynamic items** preserve their unique state
On reload, call:
```kotlin
inventory.actor = restoredActor
```
## Advanced Topics
### Custom Item Classes
Create custom items by extending GameItem:
```kotlin
class MyCustomItem : GameItem("item@mymod:custom_item") {
override var baseMass = 1.0
override var baseToolSize: Double? = null
override val canBeDynamic = true
override val materialId = "iron"
override var inventoryCategory = "generic"
init {
originalName = "ITEM_CUSTOM_NAME"
equipPosition = EquipPosition.HAND_PRIMARY
}
override fun startPrimaryUse(actor: ActorWithBody, delta: Float) {
// Custom usage logic
}
}
```
### Inventory Filtering
Filter items by category or tag:
```kotlin
val weapons = inventory.itemList
.mapNotNull { ItemCodex[it.itm] }
.filter { it.inventoryCategory == "weapon" }
```
### Weight Calculation
Calculate total inventory weight:
```kotlin
val totalWeight = inventory.itemList.sumOf { (itemID, quantity) ->
(ItemCodex[itemID]?.baseMass ?: 0.0) * quantity
}
```
## See Also
- [[Glossary]] — Item and inventory terminology
- [[Modules:Items]] — Creating items in modules
- [[Development:Items]] — In-depth item implementation
- [[Actors]] — Actor system that uses inventories

839
Items.md Normal file

@@ -0,0 +1,839 @@
# Items
**Audience:** Module developers creating tools, consumables, equipment, and custom items.
Items are objects that actors can carry, use, equip, and interact with. This guide covers the item system, properties, effects, dynamic items, and creating custom items.
## Overview
Items provide:
- **Tools** — Pickaxes, axes, shovels for resource gathering
- **Weapons** — Swords, bows, guns for combat
- **Armour** — Helmets, chestplates, boots for protection
- **Consumables** — Food, potions, ammunition
- **Blocks** — Placeable terrain and walls
- **Fixtures** — Workbenches, chests, decorations
- **Special items** — Keys, quest items, books
## Item System Architecture
### GameItem Abstract Class
All items extend the `GameItem` abstract class:
```kotlin
abstract class GameItem(val originalID: ItemID) : Comparable<GameItem>, Cloneable, TaggedProp {
// Identity
open var dynamicID: ItemID = originalID
open var originalName: String = "" // Translation key
var newName: String = "" // Custom name (renamed items)
var isCustomName = false
// Physical properties
abstract var baseMass: Double // Mass in kg
abstract var baseToolSize: Double? // Tool size/weight
abstract var inventoryCategory: String // "tool", "weapon", "armor", etc.
abstract val materialId: String // 4-letter material code
// Item type
abstract val canBeDynamic: Boolean // Can have unique instances?
var stackable: Boolean = true // Can stack in inventory?
val isConsumable: Boolean // Consumed on use?
get() = stackable && !canBeDynamic
// Durability
open var maxDurability: Int = 0 // Max durability (0 = none)
open var durability: Float = 0f // Current durability
// Equipment
open var equipPosition: Int = EquipPosition.NULL
// Appearance
var itemImage: TextureRegion? // Item sprite
var itemImagePixmap: Pixmap? // Pixmap data
open val itemImageGlow: TextureRegion? = null
open val itemImageEmissive: TextureRegion? = null
// Tags and data
var tags = HashSet<String>() // Item tags
var modifiers = HashSet<String>() // Dynamic modifiers
open val extra = Codex() // Custom module data
var itemProperties = ItemValue() // Item-specific values
// Effects (override these)
open fun effectWhileInPocket(actor: ActorWithBody, delta: Float) { }
open fun effectOnPickup(actor: ActorWithBody) { }
open fun effectWhileEquipped(actor: ActorWithBody, delta: Float) { }
open fun effectOnUnequip(actor: ActorWithBody) { }
open fun effectOnThrow(actor: ActorWithBody) { }
open fun startPrimaryUse(actor: ActorWithBody, delta: Float): Long = -1
open fun startSecondaryUse(actor: ActorWithBody, delta: Float): Long = -1
open fun endPrimaryUse(actor: ActorWithBody, delta: Float): Boolean = false
open fun endSecondaryUse(actor: ActorWithBody, delta: Float): Boolean = false
}
```
### ItemCodex Singleton
All items are registered in the global `ItemCodex`:
```kotlin
object ItemCodex {
val itemCodex = ItemTable() // Static items
val dynamicItemInventory = ItemTable() // Dynamic items
val dynamicToStaticTable = HashMap<ItemID, ItemID>()
operator fun get(id: ItemID?): GameItem?
fun registerNewDynamicItem(dynamicID: ItemID, item: GameItem)
}
```
**Access pattern:**
```kotlin
val pickaxe = ItemCodex["basegame:1"] // Copper pickaxe
println("Mass: ${pickaxe?.mass} kg")
println("Tool size: ${pickaxe?.toolSize}")
```
## Item Types
### Static Items
**Static items** are shared instances defined in module files:
```csv
id;classname;tags
1;net.torvald.terrarum.modulebasegame.gameitems.PickaxeCopper;TOOL,PICK
```
**Characteristics:**
- Single shared instance
- Cannot be modified per-instance
- Stackable
- ID range: `item@module:id`
**Examples:**
- Blocks (stone, dirt, wood)
- Consumables (food, potions)
- Ammunition (arrows, bullets)
### Dynamic Items
**Dynamic items** are unique instances created at runtime:
```kotlin
// Create from static template
val baseSword = ItemCodex["basegame:sword_iron"]
val customSword = baseSword.copy()
// Customise
customSword.name = "§o§Excalibur§.§" // Formatted name
customSword.durability = customSword.maxDurability * 0.5f // Half durability
customSword.tags.add("LEGENDARY")
// Register with unique ID
val dynamicID = "dyn:${System.nanoTime()}"
ItemCodex.registerNewDynamicItem(dynamicID, customSword)
```
**Characteristics:**
- Unique instance per item
- Can be modified (durability, enchantments, names)
- NOT stackable
- ID range: `dyn:*`
**Examples:**
- Tools (worn pickaxes)
- Weapons (enchanted swords)
- Armour (damaged chestplate)
- Named items (signed books)
### Item Categories
Items have categories for organisation:
```kotlin
abstract var inventoryCategory: String
```
**Standard categories:**
- `"tool"` — Pickaxes, axes, shovels
- `"weapon"` — Swords, bows, guns
- `"armor"` — Helmets, chestplates, boots, shields
- `"block"` — Placeable blocks
- `"fixture"` — Workbenches, chests, doors
- `"consumable"` — Food, potions, single-use items
- `"material"` — Raw materials, ingots, gems
- `"misc"` — Uncategorised items
## Creating Custom Items
### Step 1: Define Item Class
Create a Kotlin class extending `GameItem`:
```kotlin
package net.torvald.mymod.items
import net.torvald.terrarum.gameitems.GameItem
import net.torvald.terrarum.gameitems.ItemID
import net.torvald.terrarum.gameactors.ActorWithBody
import net.torvald.terrarum.gameactors.AVKey
class FireSword(originalID: ItemID) : GameItem(originalID) {
override var baseMass = 2.5
override var baseToolSize: Double? = 2.0
override val canBeDynamic = true
override var inventoryCategory = "weapon"
override val materialId = "IRON"
override var maxDurability = 1000
init {
originalName = "ITEM_FIRE_SWORD"
tags.add("WEAPON")
tags.add("SWORD")
tags.add("FIRE")
durability = maxDurability.toFloat()
equipPosition = EquipPosition.HAND_GRIP
}
override fun effectWhileEquipped(actor: ActorWithBody, delta: Float) {
// Give fire resistance while equipped
if (actor.actorValue.getAsBoolean("mymod:fire_sword.buffGiven") != true) {
actor.actorValue[AVKey.FIRE_RESISTANCE] =
(actor.actorValue.getAsDouble(AVKey.FIRE_RESISTANCE) ?: 0.0) + 50.0
actor.actorValue["mymod:fire_sword.buffGiven"] = true
}
}
override fun effectOnUnequip(actor: ActorWithBody) {
// Remove fire resistance when unequipped
if (actor.actorValue.getAsBoolean("mymod:fire_sword.buffGiven") == true) {
actor.actorValue[AVKey.FIRE_RESISTANCE] =
(actor.actorValue.getAsDouble(AVKey.FIRE_RESISTANCE) ?: 0.0) - 50.0
actor.actorValue.remove("mymod:fire_sword.buffGiven")
}
}
override fun startPrimaryUse(actor: ActorWithBody, delta: Float): Long {
// Attack with fire damage
val target = findTargetInRange(actor)
if (target != null) {
target.takeDamage(15.0) // Normal damage
target.setOnFire(5.0) // 5 seconds of fire
// Reduce durability
durability -= 1f
if (durability <= 0f) {
// Item breaks
return 1 // Remove from inventory
}
return 0 // Successfully used, don't remove
}
return -1 // Failed to use
}
}
```
### Step 2: Register in CSV
Add to `items/itemid.csv`:
```csv
id;classname;tags
fire_sword;net.torvald.mymod.items.FireSword;WEAPON,SWORD,FIRE
```
### Step 3: Add Translation
Add to `locales/en/items.txt`:
```
ITEM_FIRE_SWORD=Fire Sword
ITEM_FIRE_SWORD_DESC=A blazing blade that sets enemies aflame.
```
### Step 4: Create Sprite
Add sprite to `items/items.tga` spritesheet at the appropriate index.
### Step 5: Load in Module
In your `EntryPoint`:
```kotlin
override fun invoke() {
// Load item sprites
CommonResourcePool.addToLoadingList("mymod.items") {
ItemSheet(ModMgr.getGdxFile("mymod", "items/items.tga"))
}
CommonResourcePool.loadAll()
// Load items
ModMgr.GameItemLoader.invoke("mymod")
}
```
## Item Properties
### Mass and Scale
Items have mass that scales with size:
```kotlin
override var baseMass = 2.5 // Base mass at scale 1.0
// Apparent mass scales cubically
open var mass: Double
get() = baseMass * scale * scale * scale
```
**Typical masses:**
- Small items (arrows): 0.02 kg
- Tools (pickaxe): 2-5 kg
- Blocks (stone): 10-50 kg
- Heavy equipment: 20+ kg
### Tool Size
Tools have a size/weight property:
```kotlin
override var baseToolSize: Double? = 2.0 // null for non-tools
// Scales cubically like mass
open var toolSize: Double?
get() = if (baseToolSize != null) baseToolSize!! * scale * scale * scale else null
```
**Uses:**
- Mining speed calculation
- Attack damage
- Stamina drain
- Tool tier comparison
### Durability
Items can wear down with use:
```kotlin
override var maxDurability: Int = 1000
open var durability: Float = 1000f
```
**Durability values:**
- `0` — Infinite durability (blocks)
- `100-500` — Fragile (glass tools)
- `500-2000` — Normal (iron tools)
- `2000+` — Durable (diamond tools)
**Reducing durability:**
```kotlin
override fun startPrimaryUse(actor: ActorWithBody, delta: Float): Long {
// Use tool
performAction()
// Reduce durability
durability -= 1f
if (durability <= 0f) {
// Item breaks
playBreakSound()
return 1 // Remove 1 from inventory
}
return 0 // Used successfully
}
```
### Materials
Every item has a material:
```kotlin
override val materialId = "IRON" // 4-letter code
```
**Material affects:**
- Tool effectiveness vs. blocks
- Durability calculations
- Sound effects
- Value/rarity
- Thermal properties
**Common materials:**
- `WOOD` — Wooden tools (tier 0)
- `ROCK` — Stone tools (tier 1)
- `COPR` — Copper tools (tier 2)
- `IRON` — Iron tools (tier 3)
- `GOLD` — Gold tools (tier 4)
- `DIAM` — Diamond tools (tier 5)
### Equipment Positions
Items can be equipped to specific slots:
```kotlin
open var equipPosition: Int = EquipPosition.HAND_GRIP
```
**Equipment positions:**
- `EquipPosition.NULL` — Cannot be equipped
- `EquipPosition.HAND_GRIP` — Main hand (tools, weapons)
- `EquipPosition.HAND_GRIP_SECONDARY` — Off-hand
- `EquipPosition.HEAD` — Helmet
- `EquipPosition.BODY` — Chestplate
- `EquipPosition.LEGS` — Leggings
- `EquipPosition.FEET` — Boots
**Equipping items:**
```kotlin
item equipTo actor // Infix notation
actor.equipItem(item)
```
## Item Effects
### Effect Hooks
Items can react to various events:
#### effectWhileInPocket
Called every frame while item is in inventory:
```kotlin
override fun effectWhileInPocket(actor: ActorWithBody, delta: Float) {
// Passive effects just from having item
if (actor.actorValue.getAsBoolean("mymod:amulet.protection") != true) {
actor.actorValue[AVKey.DEFENCEBUFF] += 10.0
actor.actorValue["mymod:amulet.protection"] = true
}
}
```
**Uses:**
- Passive buffs
- Curses
- Background effects
#### effectOnPickup
Called once when item is picked up:
```kotlin
override fun effectOnPickup(actor: ActorWithBody) {
// Immediate pickup effects
actor.heal(20.0)
playPickupSound()
}
```
**Uses:**
- Immediate healing
- Buffs
- Achievements/triggers
#### effectWhileEquipped
Called every frame while item is equipped:
```kotlin
override fun effectWhileEquipped(actor: ActorWithBody, delta: Float) {
// Active equipment effects
if (actor.actorValue.getAsBoolean("mymod:ring.speedGiven") != true) {
actor.actorValue[AVKey.SPEEDBUFF] += 2.0
actor.actorValue["mymod:ring.speedGiven"] = true
}
}
```
**Uses:**
- Stat boosts
- Continuous effects
- Visual effects
**Important:** Use a custom flag to apply effects only once:
```kotlin
if (actor.actorValue.getAsBoolean("mymod:item.buffGiven") != true) {
// Apply buff
actor.actorValue["mymod:item.buffGiven"] = true
}
```
#### effectOnUnequip
Called once when item is unequipped:
```kotlin
override fun effectOnUnequip(actor: ActorWithBody) {
// Remove equipment effects
if (actor.actorValue.getAsBoolean("mymod:ring.speedGiven") == true) {
actor.actorValue[AVKey.SPEEDBUFF] -= 2.0
actor.actorValue.remove("mymod:ring.speedGiven")
}
}
```
**Uses:**
- Remove buffs
- Clean up state
- Penalties
#### effectOnThrow
Called once when item is discarded:
```kotlin
override fun effectOnThrow(actor: ActorWithBody) {
// Throwing effects
if (hasTag("EXPLOSIVE")) {
createExplosion(actor.position)
}
}
```
**Uses:**
- Grenades
- Throwable potions
- Item-specific discard behaviour
### Use Handlers
Items can respond to player input:
#### startPrimaryUse
Called while primary button (left mouse/RT) is held:
```kotlin
override fun startPrimaryUse(actor: ActorWithBody, delta: Float): Long {
// Perform action
val success = doAction(actor)
if (success) {
durability -= 1f
if (durability <= 0f) {
return 1 // Remove 1 from inventory (item consumed/broken)
}
return 0 // Successfully used, keep item
}
return -1 // Failed to use
}
```
**Return values:**
- `0` or greater — Amount to remove from inventory (success)
- `-1` — Failed to use (no removal)
**Uses:**
- Mining blocks (pickaxe)
- Attacking (sword)
- Placing blocks
- Using consumables
#### startSecondaryUse
Called while secondary button (right mouse) is held:
```kotlin
override fun startSecondaryUse(actor: ActorWithBody, delta: Float): Long {
// Alternative action
if (canPerformSecondary(actor)) {
performSecondaryAction()
return 0
}
return -1
}
```
**Note:** Secondary use is less commonly used due to control scheme limitations.
#### endPrimaryUse / endSecondaryUse
Called when button is released:
```kotlin
override fun endPrimaryUse(actor: ActorWithBody, delta: Float): Boolean {
// Finish action (e.g., shoot charged bow)
if (chargeTime > 1.0f) {
shootArrow(actor, chargeTime)
chargeTime = 0f
return true
}
return false
}
```
**Uses:**
- Charged attacks
- Bow drawing
- Cleanup actions
## Item Tags
Tags enable flexible categorisation:
```kotlin
tags.add("WEAPON")
tags.add("SWORD")
tags.add("LEGENDARY")
```
### Common Tags
**Item types:**
- `TOOL` — General tools
- `WEAPON` — Combat items
- `ARMOR` — Protection gear
- `CONSUMABLE` — Single-use items
- `BLOCK` — Placeable blocks
**Tool subtypes:**
- `PICK` — Pickaxes
- `AXE` — Axes
- `SHOVEL` — Shovels
- `HAMMER` — Hammers
**Weapon subtypes:**
- `SWORD` — Swords
- `BOW` — Bows
- `GUN` — Firearms
- `MAGIC` — Magic weapons
**Materials:**
- `WOOD` — Wooden items
- `STONE` — Stone items
- `METAL` — Metal items
- `GEM` — Gem items
**Special:**
- `LEGENDARY` — Rare/unique items
- `QUEST` — Quest items
- `KEY` — Key items
- `COMBUSTIBLE` — Can be used as fuel
**Usage:**
```kotlin
// Check tags
if (item.hasTag("WEAPON")) {
println("This is a weapon")
}
// Query items by tag
val allWeapons = ItemCodex.itemCodex.values.filter { it.hasTag("WEAPON") }
val allLegendary = ItemCodex.itemCodex.values.filter { it.hasTag("LEGENDARY") }
```
## Dynamic Item System
### Creating Dynamic Items
Transform static items into unique instances:
```kotlin
// Get static template
val ironSword = ItemCodex["basegame:sword_iron"]
// Create dynamic copy
val enchantedSword = ironSword.copy()
// Customise
enchantedSword.name = "Sword of Flames"
enchantedSword.nameColour = Color.RED
enchantedSword.durability = enchantedSword.maxDurability * 0.75f
enchantedSword.modifiers.add("FIRE_DAMAGE")
enchantedSword.itemProperties["enchantment_level"] = 3
// Register
val dynamicID = "dyn:${System.nanoTime()}"
enchantedSword.dynamicID = dynamicID
ItemCodex.registerNewDynamicItem(dynamicID, enchantedSword)
```
### Dynamic Item Lifecycle
1. **Creation** — Copy from static template
2. **Customisation** — Modify properties
3. **Registration** — Add to `dynamicItemInventory`
4. **Serialisation** — Save to disk
5. **Deserialisation** — Reload from disk
6. **Reload** — Re-sync properties after loading
**Reload hook:**
```kotlin
override fun reload() {
// Called after loading from save
// Re-sync derived values
if (durability > maxDurability) {
durability = maxDurability.toFloat()
}
}
```
### Dynamic Item Storage
Dynamic items are stored separately:
```kotlin
object ItemCodex {
val itemCodex = ItemTable() // Static items
val dynamicItemInventory = ItemTable() // Dynamic items
val dynamicToStaticTable = HashMap<ItemID, ItemID>() // Dynamic → Static mapping
}
```
**Lookup:**
```kotlin
val item = ItemCodex["dyn:123456789"] // Checks dynamic inventory first
val originalID = ItemCodex.dynamicToStaticTable["dyn:123456789"] // "basegame:sword_iron"
```
## Example Items
### Simple Consumable
```kotlin
class HealthPotion(originalID: ItemID) : GameItem(originalID) {
override var baseMass = 0.2
override var baseToolSize: Double? = null
override val canBeDynamic = false
override var inventoryCategory = "consumable"
override val materialId = "GLAS"
init {
originalName = "ITEM_HEALTH_POTION"
tags.add("CONSUMABLE")
tags.add("POTION")
stackable = true
}
override fun startPrimaryUse(actor: ActorWithBody, delta: Float): Long {
actor.heal(50.0)
playDrinkSound()
return 1 // Consume one potion
}
}
```
### Durable Tool
```kotlin
class DiamondPickaxe(originalID: ItemID) : GameItem(originalID) {
override var baseMass = 3.0
override var baseToolSize: Double? = 3.5
override val canBeDynamic = true
override var inventoryCategory = "tool"
override val materialId = "DIAM"
override var maxDurability = 5000
init {
originalName = "ITEM_PICKAXE_DIAMOND"
tags.add("TOOL")
tags.add("PICK")
durability = maxDurability.toFloat()
equipPosition = EquipPosition.HAND_GRIP
}
override fun startPrimaryUse(actor: ActorWithBody, delta: Float): Long {
// Mining handled by PickaxeCore
val result = PickaxeCore.startPrimaryUse(actor, delta, this, mouseX, mouseY)
if (result >= 0) {
// Successfully mined
durability -= 1f
if (durability <= 0f) {
playBreakSound()
return 1 // Tool breaks
}
}
return result
}
}
```
### Equipment with Buffs
```kotlin
class IronHelmet(originalID: ItemID) : GameItem(originalID) {
override var baseMass = 1.5
override var baseToolSize: Double? = null
override val canBeDynamic = true
override var inventoryCategory = "armor"
override val materialId = "IRON"
override var maxDurability = 2000
init {
originalName = "ITEM_HELMET_IRON"
tags.add("ARMOR")
tags.add("HELMET")
durability = maxDurability.toFloat()
equipPosition = EquipPosition.HEAD
}
override fun effectWhileEquipped(actor: ActorWithBody, delta: Float) {
if (actor.actorValue.getAsBoolean("armor:iron_helmet.defenceGiven") != true) {
actor.actorValue[AVKey.DEFENCEBUFF] += 15.0
actor.actorValue["armor:iron_helmet.defenceGiven"] = true
}
}
override fun effectOnUnequip(actor: ActorWithBody) {
if (actor.actorValue.getAsBoolean("armor:iron_helmet.defenceGiven") == true) {
actor.actorValue[AVKey.DEFENCEBUFF] -= 15.0
actor.actorValue.remove("armor:iron_helmet.defenceGiven")
}
}
}
```
## Best Practises
1. **Use appropriate mass values** — Match real-world weights
2. **Set correct durability** — Balance tool lifespans
3. **Tag comprehensively** — Enable flexible queries
4. **Implement effect cleanup** — Always remove buffs in effectOnUnequip
5. **Use flags for one-time effects** — Prevent duplicate buff application
6. **Return correct values from use handlers** — Respect the return value contract
7. **Handle edge cases** — Check for null, zero durability, etc.
8. **Test dynamic items** — Verify serialisation/deserialisation
9. **Balance tool sizes** — Affects mining/attack speed
10. **Namespace IDs** — Use `modulename:itemname` format
## Common Pitfalls
- **Forgetting effectOnUnequip** — Buffs persist after unequipping
- **Not using flags** — Buffs applied multiple times per frame
- **Wrong return values** — Items disappear or duplicate unexpectedly
- **Infinite durability** — Set maxDurability > 0 for tools
- **Null tool size** — Set baseToolSize for tools
- **Missing sprites** — Items render as missing texture
- **Wrong equipment position** — Items can't be equipped
- **Not checking actor validity** — Crashes when actor is null
- **Forgetting stackable flag** — Consumables don't stack
- **Hardcoded IDs** — Use string constants or config
## See Also
- [[Modules-Codex-Systems#ItemCodex]] — ItemCodex reference
- [[Modules-Setup]] — Creating modules with items
- [[Blocks]] — Block items and placement
- [[Actors]] — Actor inventory system
- [[Fixtures]] — Fixture items

505
Languages.md Normal file

@@ -0,0 +1,505 @@
# Languages
Terrarum features comprehensive internationalisation (i18n) support with over 20 languages, a custom multilingual font system, and sophisticated text rendering including support for complex scripts.
## Overview
The language system provides:
- **20+ supported languages** — From English to Japanese to Hindi
- **Terrarum Sans Bitmap** — Custom multilingual bitmap font
- **String templates** — Dynamic text formatting
- **Pluralisation** — Language-specific plural rules
- **Postpositions** — Korean/Japanese grammatical particles
- **Fallback system** — Graceful handling of missing translations
- **Polyglot support** — Integration with Polyglot format
## Language Codes
Languages are identified by ISO locale codes:
### Supported Languages
| Code | Language |
|------|----------|
| `en` | English |
| `koKR` | Korean |
| `jaJP` | Japanese |
| `zhCN` | Chinese (Simplified) |
| `de` | German |
| `frFR` | French |
| `es` | Spanish |
| `it` | Italian |
| `ptBR` | Portuguese (Brazilian) |
| `ruRU` | Russian |
| `plPL` | Polish |
| `nlNL` | Dutch |
| `daDK` | Danish |
| `noNB` | Norwegian (Bokmål) |
| `fiFI` | Finnish |
| `isIC` | Icelandic |
| `csCZ` | Czech |
| `huHU` | Hungarian |
| `bgBG` | Bulgarian |
| `elGR` | Greek |
| `hiIN` | Hindi |
## Lang Object
The `Lang` singleton manages all translations:
### Getting Translations
```kotlin
// Basic usage
val text = Lang["STRING_ID"]
// With capitalisation
val capitalised = Lang["string_id", capitalise = true]
// Safe access (returns null if missing)
val text = Lang.getOrNull("STRING_ID")
```
### String ID Format
Translation keys follow a naming convention:
```
CONTEXT_SPECIFIC_DESCRIPTION
Examples:
MENU_LANGUAGE_THIS // Menu item
BLOCK_STONE_NAME // Block name
ITEM_PICKAXE_DESC // Item description
UI_INVENTORY_TITLE // UI text
```
## Translation Files
### File Structure
Translations are organised by language:
```
assets/locales/
├── en/
│ ├── main.json
│ ├── blocks.json
│ └── items.json
├── koKR/
│ ├── main.json
│ └── ...
└── jaJP/
└── ...
```
### Regular Format
Standard translation files use simple JSON:
```json
{
"MENU_NEW_GAME": "New Game",
"MENU_LOAD_GAME": "Load Game",
"MENU_OPTIONS": "Options",
"BLOCK_STONE_NAME": "Stone",
"ITEM_PICKAXE_NAME": "Pickaxe"
}
```
### Polyglot Format
For Polyglot-compatible files (prefix: `Polyglot-100_*.json`):
```json
{
"resources": {
"polyglot": { /* metadata */ },
"data": [
{
"n": "STRING_ID",
"s": "Translated Text"
},
{
"n": "ANOTHER_STRING_ID",
"s": "Another Translation"
}
]
}
}
```
## String Templates
Templates allow dynamic text insertion:
### Basic Template Syntax
```kotlin
// Translation file:
"WELCOME_MESSAGE": "Welcome, %s!"
// Usage:
val text = String.format(Lang["WELCOME_MESSAGE"], playerName)
// Result: "Welcome, Steve!"
```
### Bind Operator
The `>>=` operator binds strings to templates:
```kotlin
// Define template
"WALL_NAME_TEMPLATE": "%s Wall"
// Use bind operator in item names
item.originalName = "BLOCK_STONE>>=WALL_NAME_TEMPLATE"
// Result: "Stone Wall"
```
This is particularly useful for generating variants:
```kotlin
"BLOCK_WOOD_OAK" // "Oak Wood"
"BLOCK_WOOD_OAK>>=WALL_NAME_TEMPLATE" // "Oak Wood Wall"
```
## Advanced Features
### Korean Postpositions
Korean requires dynamic postposition selection based on the final character:
```kotlin
// Automatic postposition selection
val name = "사과" // Apple (ends with vowel)
Lang.formatKoreanPostposition(name, "은는") // Returns "는"
val name2 = "" // Stone (ends with consonant)
Lang.formatKoreanPostposition(name2, "은는") // Returns "은"
```
The system supports:
- **은/는** — Topic marker
- **이/가** — Subject marker
- **을/를** — Object marker
- **로/으로** — Direction marker
### Pluralisation
Different languages have different plural rules:
#### English
```kotlin
// Regular plurals: add 's'
"apple" -> "apples"
// Irregular handled specially
"photo", "demo" -> normal 's' plural
```
#### French
```kotlin
// Special words with normal 's' plural
"bal", "banal", "fatal", "final"
// Others follow French rules
```
The pluralisation system is extensible for each language.
### Capitalisation
Automatic capitalisation respects language rules:
```kotlin
val text = Lang["menu_item", capitalise = true]
// English: "Menu Item"
// German: "Menü-Element" (noun capitalisation rules)
```
## Terrarum Sans Bitmap
The game uses a custom bitmap font supporting all languages:
### Font Features
- **Multilingual** — Latin, Cyrillic, Greek, CJK, Hangul, Devanagari
- **Bitmap-based** — Pixel-perfect rendering at any size
- **Fallback chains** — Graceful degradation for missing glyphs
- **SDF rendering** — Smooth scaling with signed distance fields
### Font Sizes
The font system provides multiple sizes:
- **24pt** — UI text
- **32pt** — Large text, headings
- **48pt** — Very large text
- **BigAlphNum** — Numbers and basic letters
- **TinyAlphNum** — Tiny fallback font
See: [Terrarum Sans Bitmap Repository](https://github.com/minjaesong/Terrarum-sans-bitmap)
## Adding Translations
### For Modules
Add language files in your module:
```
<module>/locales/
├── en/
│ └── mymod.json
└── koKR/
└── mymod.json
```
Register them with the language loader:
```kotlin
ModMgr.GameLanguageLoader.loadAll(
moduleInfo,
File("<module>/locales/")
)
```
### Translation File Example
```json
{
"MYMOD_ITEM_SPECIAL_SWORD": "Special Sword",
"MYMOD_ITEM_SPECIAL_SWORD_DESC": "A sword with special properties",
"MYMOD_BLOCK_MAGIC_STONE": "Magic Stone",
"MYMOD_UI_CRAFTING_TITLE": "Magical Crafting"
}
```
### Best Practises
1. **Use prefixes** — Namespace your string IDs with module name
2. **Group by context** — Separate files for items, blocks, UI
3. **Provide fallbacks** — Always have English (`en`) translations
4. **Keep keys stable** — Don't change IDs once released
5. **Use templates** — Avoid duplicating similar strings
6. **Test all languages** — Ensure text fits in UI elements
## Missing Translations
The system handles missing translations gracefully:
```kotlin
val text = Lang["NONEXISTENT_KEY"]
// Returns: "$NONEXISTENT_KEY" (with $ prefix)
```
The `$` prefix indicates a missing translation. Check logs for warnings.
### Fallback Chain
1. Try requested language (e.g., `koKR`)
2. Fall back to English (`en`)
3. Return `$KEY` if not found
## Language Selection
### Setting Language
```kotlin
App.GAME_LOCALE = "koKR" // Set to Korean
Lang.load(File("./assets/locales/")) // Reload translations
```
### Current Language
```kotlin
val currentLang = App.GAME_LOCALE
```
### Available Languages
```kotlin
val languages: Set<String> = Lang.languageList
```
## Caching System
The Lang object caches decoded strings for performance:
```kotlin
// Cached per locale
private val decodeCache = HashMap<String, HashMap<String, String>>()
```
Cache is cleared when language changes.
## Name Generation
The system includes name generators for various cultures:
### Namesets
Namesets are CSV files defining name components:
```
assets/locales/nameset_scandinavian_m.csv
assets/locales/nameset_scandinavian_f.csv
assets/locales/nameset_russian_m.csv
assets/locales/nameset_russian_f.csv
assets/locales/nameset_dwarven.csv
assets/locales/nameset_exotic_deities.csv
```
### Using Namesets
```kotlin
val name = RandomWordsName.generate("scandinavian_m")
// Returns: "Ragnar", "Bjorn", "Olaf", etc.
```
## Language Properties
Language-specific properties are defined in `langprop.csv`:
```csv
lang,direction,font,features
en,ltr,latin,plural_s
koKR,ltr,hangul,postposition
jaJP,ltr,japanese,
ar,rtl,arabic,rtl_text
```
Properties include:
- **direction** — Text direction (ltr/rtl)
- **font** — Primary font family
- **features** — Language-specific features
## Advanced Usage
### Dynamic Text Generation
Generate text with multiple variables:
```kotlin
val template = Lang["CRAFTING_RECIPE_YIELDS"]
// "Crafting %s with %s yields %d %s"
val text = String.format(
template,
item1Name,
item2Name,
quantity,
resultName
)
```
### Formatting Numbers
Use locale-aware number formatting:
```kotlin
val formatter = NumberFormat.getInstance(Locale(App.GAME_LOCALE))
val formatted = formatter.format(12345.67)
// English: "12,345.67"
// German: "12.345,67"
// French: "12 345,67"
```
### Handling Long Text
For UI elements, check text width:
```kotlin
val font = App.fontGame
val width = font.getWidth(translatedText)
if (width > maxWidth) {
// Truncate or wrap text
}
```
## Keyboard Input
See also: [[Keyboard Layout and IME]]
The IME system allows input in any language:
- **Low Layer** — Base keyboard (e.g., QWERTY)
- **High Layer** — Language-specific input (e.g., Korean Hangul)
## Common Patterns
### Item Names with Templates
```kotlin
class MyBlock : BlockBase() {
init {
originalName = "BLOCK_MYBLOCK>>=BLOCK_WALL_NAME_TEMPLATE"
// Automatically generates "My Block Wall"
}
}
```
### UI Text with Variables
```kotlin
fun showInventoryCount(count: Int) {
val text = String.format(
Lang["UI_INVENTORY_ITEMS_COUNT"],
count
)
// "Items: 42"
}
```
### Safe Translation Access
```kotlin
val text = Lang.getOrNull("OPTIONAL_STRING_ID") ?: "Default Text"
```
## Debugging
### Finding Missing Translations
Search for `$` in rendered text:
```kotlin
if (text.startsWith("$")) {
println("Missing translation: ${text.substring(1)}")
}
```
### Logging Untranslated Strings
Enable debug logging:
```kotlin
App.IS_DEVELOPMENT_BUILD = true
```
Missing translations will be logged to console.
## Performance Considerations
1. **Cache translations** — Don't call `Lang[]` every frame
2. **Preload common strings** — Load frequently-used text at init
3. **Avoid string concatenation** — Use templates instead
4. **Batch font measurements** — Measure multiple strings together
## Limitations
- **No RTL support yet** — Right-to-left languages not fully implemented
- **Limited plural forms** — Only basic plural rules
- **Static at runtime** — Language changes require restart
- **No gender agreement** — Some languages need grammatical gender
## See Also
- [[Glossary]] — Language-related terminology
- [[Keyboard Layout and IME]] — Input methods for complex scripts
- [Terrarum Sans Bitmap](https://github.com/minjaesong/Terrarum-sans-bitmap) — Font repository
- [[Modules]] — Adding translations to modules

@@ -37,3 +37,4 @@ The user config will have a key with `<module name>:` prepended: for example, if
## Recondite References
* [[Creature RAW|Development:RAW]]
* [[Faction|Development:Faction]]
* [[Codex System|Modules:Codex Systems]]

606
Modules:Codex-Systems.md Normal file

@@ -0,0 +1,606 @@
# 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:
```kotlin
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:
```kotlin
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.
```csv
"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
```kotlin
// 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
```kotlin
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.
```csv
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`:
```kotlin
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
```kotlin
// 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:
```kotlin
// 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.
```csv
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
```kotlin
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.
```csv
"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
```kotlin
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.
```csv
"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
```kotlin
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`
```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
```kotlin
// 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 (`/* */`).
```json
{
"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
```kotlin
// 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.
```csv
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
```kotlin
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
```kotlin
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
```kotlin
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:
```kotlin
// Register custom GUI
ModMgr.registerExtraGUI("mymod:custom_hud") {
MyCustomHUDCanvas()
}
```
### Retexturing
Allows texture pack overrides:
```kotlin
// 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**:
```kotlin
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 naming**`modulename: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

@@ -1,21 +1,106 @@
## Metadata
Every module must be able to introduce themselves through the `metadata.properties` file.
# Module Setup
The metadata file is a standard-issue Java properties file with some required keys.
**Audience:** Module developers creating new game content or total conversion mods.
### metadata.properties
A metadata must have the following keys:
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.
- **propername** — the displayed name of the module
- **description** — a short text describing the module
- **author** — the author of the module
- **package** — the root package of the module
- **entrypoint** — the fully-qualified class name of the Entry Point (see Contents § Entry Point)
- **version** — the version of the module. The version format must strictly follow the [Semver 2.0.0](https://semver.org/) scheme
- **releasedate** — the release date of the module of the version. The date format must be `YYYY-MM-DD`
- **jar** — the name of the Jar file the module's codes are contained. If there is none, leave it as a blank
- **jarhash** — the Sha256sum of the Jar file. If there is no Jar file, leave it as a blank
- **dependency** — list the other modules that this module requires
## 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
```properties
# 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
@@ -38,133 +123,546 @@ For example, if your module requires `basegame` 0.3 or higher, the dependency st
- 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.
### Jar File
A module may have one Jar file. Two or more Jars are not allowed: if your module requires external libraries, package them as a Fatjar.
## default.json
## Contents
Except for the [[retextures|Modules:Retextures]], there are *technically* no strict directory structure required, certain ingame elements loaded by the engine expects certain paths.
Provides default configuration values.
A [sample project](https://github.com/curioustorvald/terrarum-sample-module-project) is available!
### Format
### Entry Point
If your module has a Jar file, it must have a single class that extends `net.torvald.terrarum.ModuleEntryPoint`: such class is called an Entry Point. The Entry Point initialises and installs your module to the game. What the Entry Point does to load your module depends on you, but the engine provides the **Modmgr** for installing ingame elements to the game.
```json
{
"enableFeatureX": true,
"debugMode": false,
"difficultyScale": 1.0,
"customSettings": {
"spawnRate": 0.5,
"maxEnemies": 100
}
}
```
### The Modmgr
**Access in code:**
The Modmgr provides the functions to load the following ingame elements:
- ModMgr.**GameBlockLoader** — loads the blocks and wires
- ModMgr.**GameItemLoader** — loads the items
- ModMgr.**GameMaterialLoader** — loads the materials that come with the blocks and items
- ModMgr.**GameLanguageLoader** — loads the text translations
- ModMgr.**GameRetextureLoader** — loads the retextures
- ModMgr.**GameCraftingRecipeLoader** — loads the crafting recipes
```kotlin
val enabled = App.getConfigBoolean("mymod.enableFeatureX")
val difficulty = App.getConfigDouble("mymod.difficultyScale")
```
The Modmgr will load the blocks/items, assign them the Item IDs, and register them to their respective *Codex*.
Configuration is automatically namespaced with module name.
#### GameBlockLoader
## Module Entry Point
The GameBlockLoader looks for the `<module root>/blocks/blocks.csv` for the list of blocks and the `<module root>/wires/wires.csv` for the list of wires.
The `ModuleEntryPoint` is invoked when the module loads.
Every block within a module gets a unique number as an ID. A texture of the block must be named as the ID, with the extension of `.tga`.
### Structure
The Item ID for the blocks will be `<module name>:<id defined in the csv>` e.g. `basegame:32`
```kotlin
package net.torvald.mymod
##### blocks.csv
import net.torvald.terrarum.*
Every block's property is defined on the csv file with following columns:
class EntryPoint : ModuleEntryPoint() {
- id: ID of this block
- drop: Which item the DroppedItem actually adds to your inventory
- spawn: Which item the DroppedItem should impersonate when spawned
- name: String identifier of the block
- shdr: Shade Red (light absorption). Valid range 0.01.0+
- shdg: Shade Green (light absorption). Valid range 0.01.0+
- shdb: Shade Blue (light absorption). Valid range 0.01.0+
- shduv: Shade UV (light absorbtion). Valid range 0.01.0+
- lumr: Luminosity Red (light intensity). Valid range 0.01.0+
- lumg: Luminosity Green (light intensity). Valid range 0.01.0+
- lumb: Luminosity Blue (light intensity). Valid range 0.01.0+
- lumuv: Luminosity UV (light intensity). Valid range 0.01.0+
- str: Strength of the block
- dsty: Density of the block. Water have 1000 in the in-game scale
- mate: Material of the block (defined in the MaterialCodex; see GameMaterialLoader)
- solid: Whether the file has full collision (0/1)
- plat: Whether the block should behave like a platform (0/1)
- wall: Whether the block can be used as a wall (0/1)
- grav: Whether the block should fall through the empty space. N/A to not make it fall; 0 to fall immediately (e.g. Sand), non-zero to indicate that number of floating blocks can be supported (e.g. Scaffolding)
- dlfn: Dynamic Light Function. 0=Static. Please see <strong>notes</strong>
- fv: Vertical friction when player slide on the cliff. 0 means not slide-able
- fr: Horizontal friction. &lt;16:slippery; 16:regular; &gt;16:sticky
- colour: [Fluids] Colour of the block in hexadecimal RGBA.
- vscs: [Fluids] Viscocity of the block. 16 for water.
- refl: [NOT Fluids] Reflectance of the block, used by the light calculation. Valid range 0.01.0
- tags: Tags used by the crafting system and the game's internals
private val moduleName = "mymod"
Notes on `dlfn`:
override fun getTitleScreen(batch: FlippingSpriteBatch): IngameInstance? {
// Return custom title screen (optional)
// Only the first module's title screen is used
return MyTitleScreen(batch)
}
- dlfn stands for the Dynamic Luminosity Function.
- 0—static; 1—torch flicker; 2—current global light (sun, star, moon); 3—daylight at noon; 4—slow breath; 5—pulsating
override fun invoke() {
// Called when module loads
// Register content here
loadContent()
registerActors()
setupCustomSystems()
}
##### Block Texture Format
override fun dispose() {
// Called when game shuts down
// Clean up resources
}
Block spritesheet must be one of following sizes (assuming global TILE_SIZE of 16): 16x16, 64x16, 128x16, 112x112, 224x224. Different sheet size denotes different format of the sheet.
private fun loadContent() {
// Load assets
CommonResourcePool.addToLoadingList("$moduleName.items") {
ItemSheet(ModMgr.getGdxFile(moduleName, "items/items.tga"))
}
CommonResourcePool.loadAll()
- 16x16: Just a single tile. No autotiling will be performed, and the other tiles won't connect to it.
- 64x16: A "wall sticker". Used by torches. Indices: free-floating, planted-on-left, planted-on-right, planted-on-bottom
- 128x16: A "platforms". Used by platforms. Indices: middle, right-end, left-end, planted-on-left-middle, planted-on-left-end, planted-on-right-middle, planted-on-right-end, single-piece
- 112x112: The full autotiling.
// 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)
}
}
```
![(if this image does not load, please look at the ingame asset located on assets/mods/basegame/blocks/33.tga)](autotiling1.png)
## Codex Loading
- The "empty" tile at the (6,5) work as a "barcode" to assign properties. Each row on the square encodes a number in binary: pixel plotted = 1
- Topmost row: the connection type
- …0000—connect mutually (every tile tagged as 0 will connect to each other); …0001—connect to self
- Second row: the mask type
- …0010—use the autotiling (this row is reserved for future expansion; for now just mark it accordingly)
- 224x224: Same as 112x112, but has seasonal variations. On render, colours from the current and the next seasons will be blended according to the ingame calendar time.
- Top left—summer; Top right—autumn; Bottom right—winter; Bottom left—spring
Codices are loaded via `ModMgr.Game*Loader` objects.
#### GameItemLoader
### Loading Order
The GameItemLoader looks for the `<module root>/items/itemid.csv` for the list of items.
**Critical:** Load in dependency order!
The Item ID for the items will be `item@<module name>:<id defined in the csv>` e.g. `item@basegame:1`
```kotlin
// GROUP 0: No dependencies
ModMgr.GameMaterialLoader.invoke(moduleName)
ModMgr.GameFluidLoader.invoke(moduleName)
##### itemid.csv
// GROUP 1: Depends on materials
ModMgr.GameItemLoader.invoke(moduleName)
Every item's id-classname pair is defined on the csv file with following columns:
// GROUP 2: Depends on items and materials
ModMgr.GameBlockLoader.invoke(moduleName)
ModMgr.GameOreLoader.invoke(moduleName)
- id: ID of this item
- classname: the full classname of the item which extends `net.torvald.terrarum.gameitems.GameItem`
// 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)
```
#### GameMaterialLoader
### Loader Details
The GameMaterialLoader looks for the `<module root>/materials/materials.csv` for the list of materials.
Each loader expects specific files:
TODO
| 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 |
#### GameLanguageLoader
## Custom Code
The GameMaterialLoader looks for the `<module root>/locales/` for the translations.
Modules can include custom Kotlin/Java code.
TODP
### JAR Structure
#### GameRetextureLoader
```
mymod.jar
└── net/torvald/mymod/
├── EntryPoint.class
├── actors/
│ └── MyCustomActor.class
├── items/
│ └── MyCustomItem.class
└── fixtures/
└── MyCustomFixture.class
```
The GameRetextureLoader looks for the `<module root>/retextures/` for the retextures.
### Registering Custom Actors
For the retexture file formats, please refer to the [[Modules:Retextures]].
```kotlin
class EntryPoint : ModuleEntryPoint() {
override fun invoke() {
// Register custom actor
ActorRegistry.register("mymod:custom_npc") {
MyCustomNPC()
}
}
}
#### GameCraftingRecipeLoader
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)
}
The GameCraftingRecipeLoader looks for the `<module root>/crafting/*.json` for the crafting recipes.
override fun updateImpl(delta: Float) {
super.updateImpl(delta)
// Custom AI logic
}
}
```
TODQ
### Registering Custom Items
### The Title Screen
```kotlin
class MyCustomSword(originalID: ItemID) : GameItem(originalID) {
override var baseMass = 1.5
For your set of modules to be recognised as a fully-fledged game, the first module in the Load Order must have a Title Screen, which extends `net.torvald.terrarum.IngameInstance`. When a game is being loaded, the Title Screen will be presented first to the end users. Other than that, a title screen is simply a standard `com.badlogic.gdx.Screen`.
init {
itemImage = ItemCodex.getItemImage(this)
tags.add("WEAPON")
tags.add("LEGENDARY")
originalName = "mymod:legendary_sword"
}
Only the first module will be inspected for loading the Title Screen; other instances of it on the subsequent modules, if any, will be ignored.
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
```kotlin
// 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
```kotlin
// 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:**
```kotlin
val name = Lang["ITEM_CUSTOM_SWORD"]
val description = Lang["ITEM_CUSTOM_SWORD_DESC"]
```
## Module Dependencies
Specify required modules in `metadata.properties`:
```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`
```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:
```kotlin
// 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`:**
```properties
configplan=enableNewFeature,spawnRateMultiplier,debugLogging
```
**`default.json`:**
```json
{
"enableNewFeature": true,
"spawnRateMultiplier": 1.0,
"debugLogging": false
}
```
### Accessing Config
```kotlin
// 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`:
```javascript
config.enableScriptMods = true;
config.developerMode = true;
```
### Logging
```kotlin
App.printdbg(this, "Module loaded successfully")
App.printmsg(this, "Important message")
if (App.IS_DEVELOPMENT_BUILD) {
println("[MyMod] Detailed debug info")
}
```
### Error Handling
```kotlin
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 code**`mymod.jar`
2. **Organise assets** → directory structure
3. **Write `metadata.properties`**
4. **Create `default.json`** (if using config)
5. **Test with load order**
6. **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`
```properties
version=1.2.3
```
- **MAJOR** — Breaking changes
- **MINOR** — New features (backwards-compatible)
- **PATCH** — Bug fixes
## Example: Complete Module
### metadata.properties
```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
```kotlin
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
```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.
```csv
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:
```kotlin
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
```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 IDs**`modulename: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
- [[Modules:Codex Systems]] — Detailed Codex documentation
- [[Blocks]] — Creating custom blocks
- [[Items]] — Creating custom items
- [[Actors]] — Creating custom actors
- [[Fixtures]] — Creating custom fixtures

555
Physics-Engine.md Normal file

@@ -0,0 +1,555 @@
# Physics Engine
Terrarum features a custom AABB-based physics system optimised for handling millions of tile bodies alongside dynamic actors. The system simulates Newtonian motion, collision detection, and gravitational forces.
## Overview
The physics engine provides:
- **AABB collision detection** — Axis-Aligned Bounding Box system
- **Tile-based optimisation** — Efficient handling of millions of static blocks
- **Newtonian simulation** — Velocity, acceleration, momentum
- **Gravity system** — Configurable gravitational acceleration
- **Friction and damping** — Surface and air resistance
- **Multi-threading support** — Scalable actor updates (currently disabled for performance)
## Core Concepts
### Units and Scale
**METER Constant:**
```kotlin
companion object {
const val METER = 25.0 // 1 metre = 25 pixels
}
```
This fundamental constant defines the relationship between pixels and physics simulation:
- **1 metre** = 25 pixels
- **1 tile** = 16 pixels = 0.64 metres
### Time Step
**PHYS_TIME_FRAME** (from `Terrarum.kt`):
```kotlin
const val PHYS_TIME_FRAME: Double = METER // = 25.0
```
The physics time step is set to match METER, creating a 1:1 correspondence between game units and SI units in calculations.
**Frame Rate:**
Velocity and acceleration scale with delta time to maintain consistent physics at any frame rate. The physics simulation assumes a reference of 60 FPS, with values scaling proportionally:
```kotlin
// Velocity units: pixels per 1/60 seconds
// When resolving velocity, account for actual framerate:
// v_resolved = v * (60 * delta) where delta is frame time in seconds
```
## PhysProperties
Physical objects have properties defining their physics behaviour:
```kotlin
class PhysProperties(
var immobile: Boolean = false,
var usePhysics: Boolean = true,
var movementType: Int = PhysicalTypes.NORMAL,
// ... more properties
)
```
### Common Presets
```kotlin
PhysProperties.HUMANOID_DEFAULT() // Standard character physics
PhysProperties.FLYING() // Flying entities
PhysProperties.PROJECTILE() // Bullets, arrows
PhysProperties.STATIONARY() // Fixed objects
```
### Movement Types
```kotlin
object PhysicalTypes {
const val NORMAL = 0 // Standard gravity physics
const val FLYING = 1 // No gravity, free movement
const val RIDING = 2 // Mounted on vehicle
const val SWIMMING = 3 // Fluid dynamics
}
```
## ActorWithBody Physics
Actors inheriting from `ActorWithBody` have full physics simulation.
### Velocity System
ActorWithBody maintains two velocity vectors:
#### External Velocity
Physics-driven movement (gravity, collisions, forces):
```kotlin
internal val externalV: Vector2 // pixels per frame at 60 FPS
```
#### Controller Velocity
Player/AI-controlled movement:
```kotlin
var controllerV: Vector2? // Only for Controllable actors
```
### Applying Forces
Add acceleration to velocity:
```kotlin
// Apply gravity (delta is frame time, 60.0 is reference FPS)
externalV.y += world.gravitation.y * delta * 60.0
// Apply horizontal force
externalV.x += pushForce
// Apply damping
externalV.scl(dampingFactor)
```
### Velocity Limits
```kotlin
private val VELO_HARD_LIMIT = 100.0 // Maximum velocity magnitude
// Clamp velocity
if (externalV.len() > VELO_HARD_LIMIT) {
externalV.nor().scl(VELO_HARD_LIMIT)
}
```
## Collision Detection
### AABB System
All collision uses Axis-Aligned Bounding Boxes:
```kotlin
class Hitbox(
var startX: Double, // Left edge
var startY: Double, // Top edge
var width: Double, // Width
var height: Double // Height
) {
val endX: Double get() = startX + width
val endY: Double get() = startY + height
val centerX: Double get() = startX + width / 2
val centerY: Double get() = startY + height / 2
}
```
### Hitbox Queries
```kotlin
fun containsPoint(x: Double, y: Double): Boolean
fun intersects(other: Hitbox): Boolean
fun overlaps(x: Double, y: Double, width: Double, height: Double): Boolean
```
### Tile Collision
#### Half-Integer Tilewise Hitbox
For physics collision with tiles:
```kotlin
val hIntTilewiseHitbox: Hitbox
```
This hitbox represents the actor's position in "half-integer" tile coordinates, aligned to tile centres for physics calculations.
#### Integer Tilewise Hitbox
For block occupation checks:
```kotlin
val intTilewiseHitbox: Hitbox
```
This represents which tiles the actor fully or partially occupies.
### Collision Iteration
Check collision with surrounding tiles:
```kotlin
for (tx in hIntTilewiseHitbox.startX.toInt()..hIntTilewiseHitbox.endX.toInt()) {
for (ty in hIntTilewiseHitbox.startY.toInt()..hIntTilewiseHitbox.endY.toInt()) {
val block = world.getTileFromTerrain(tx, ty)
if (BlockCodex[block]?.isSolid == true) {
// Handle collision
}
}
}
```
## Mass and Momentum
### Mass Calculation
Actor mass scales with volume (cubic scaling):
```kotlin
val mass: Double = baseMass * scale³
```
Where:
- **baseMass** — Mass at scale = 1.0 (in kilograms)
- **scale** — Actor's size multiplier
### Momentum
```kotlin
val momentum: Vector2 = velocity * mass
```
Larger, heavier actors have more momentum and are harder to stop.
## Gravity System
### World Gravity
Each world defines gravitational acceleration:
```kotlin
var gravitation: Vector2 = DEFAULT_GRAVITATION
```
Default gravity approximates Earth's 9.8 m/s²:
```kotlin
// Typical value (not a defined constant in codebase):
// val DEFAULT_GRAVITATION = Vector2(0.0, 9.8 * METER / (60.0 * 60.0))
// This converts 9.8 m/s² to game units (pixels per frame²)
```
### Applying Gravity
Gravity is applied each physics update:
```kotlin
if (physProp.usePhysics && physProp.movementType == PhysicalTypes.NORMAL) {
externalV.add(world.gravitation * delta * 60.0) // delta = frame time in seconds
}
```
### Gravity Exceptions
- **Flying actors** (movementType = FLYING) — No gravity
- **Immobile actors** (immobile = true) — No movement
- **Riding actors** (movementType = RIDING) — Follow mount's motion
## Friction
### Surface Friction
Blocks define horizontal friction:
```kotlin
val frictionCoeff: Float // <16: slippery, 16: normal, >16: sticky
```
Friction is applied when actors touch ground:
```kotlin
if (onGround) {
val friction = groundBlock.frictionCoeff / 16.0
controllerV.x *= friction
}
```
### Vertical Friction
For wall sliding:
```kotlin
val verticalFriction: Float // 0 = not slideable
```
Applied when actor is against a wall:
```kotlin
if (againstWall && block.verticalFriction > 0) {
externalV.y *= block.verticalFriction
}
```
### Air Resistance
Damping applied to all movement:
```kotlin
val airDamping = 0.99 // Slight damping
externalV.scl(airDamping)
controllerV?.scl(airDamping)
```
## Advanced Physics
### Platform Collision
Platforms are special blocks:
```kotlin
val isPlatform: Boolean // One-way collision
```
Actors can jump through from below but land on top:
```kotlin
if (block.isPlatform && actor.externalV.y <= 0 && actor.bottomY <= blockTopY) {
// Land on platform
} else if (actor.externalV.y > 0) {
// Pass through from below
}
```
### Falling Blocks
Blocks can fall due to gravity:
```kotlin
val gravitation: Int?
// null: Don't fall
// 0: Fall immediately (sand)
// N: Support N floating blocks (scaffolding)
```
Falling blocks become temporary actors and settle when they hit solid ground.
### Fluid Dynamics
Actors in fluids experience:
- **Buoyancy** — Upward force based on density difference
- **Viscosity** — Movement resistance
- **Drag** — Velocity-dependent resistance
```kotlin
if (inFluid) {
val fluid = FluidCodex[fluidID]
val buoyancy = (fluid.density - actor.density) * volume * gravitation
externalV.y -= buoyancy * delta
// Apply fluid drag
val drag = fluid.viscosity / 16.0
externalV.scl(drag)
}
```
## Collision Response
### Separation
When actors collide, separate them:
```kotlin
fun separate(actor: ActorWithBody, block: Hitbox) {
val overlapX = min(actor.hitbox.endX - block.startX, block.endX - actor.hitbox.startX)
val overlapY = min(actor.hitbox.endY - block.startY, block.endY - actor.hitbox.startY)
if (overlapX < overlapY) {
// Separate horizontally
if (actor.hitbox.centerX < block.centerX) {
actor.translatePosition(-overlapX, 0.0)
} else {
actor.translatePosition(overlapX, 0.0)
}
actor.externalV.x = 0.0
} else {
// Separate vertically
if (actor.hitbox.centerY < block.centerY) {
actor.translatePosition(0.0, -overlapY)
} else {
actor.translatePosition(0.0, overlapY)
}
actor.externalV.y = 0.0
}
}
```
### Bouncing
Apply elasticity on collision:
```kotlin
val restitution = 0.5 // 0 = no bounce, 1 = perfect bounce
if (collisionWithGround) {
externalV.y = -externalV.y * restitution
if (abs(externalV.y) < minBounceVelocity) {
externalV.y = 0.0 // Stop bouncing
}
}
```
## Performance Optimisations
### Spatial Partitioning
The engine uses PR-Tree for efficient actor queries:
```kotlin
val actorTree: PRTree<ActorWithBody>
// Query actors in area
val nearbyActors = actorTree.find(
minX, minY,
maxX, maxY
)
```
### Sleeping Actors
Stationary actors can be flagged as sleeping:
```kotlin
var isStationary: Boolean = true
```
Sleeping actors skip expensive physics calculations.
### PHYS_EPSILON_DIST
Small epsilon for floating-point precision:
```kotlin
companion object {
const val PHYS_EPSILON_DIST = 1.0 / 256.0
}
```
Used to avoid edge cases in collision detection.
## Common Patterns
### Jumping
```kotlin
fun jump(jumpStrength: Double) {
if (onGround) {
externalV.y = -jumpStrength
onGround = false
}
}
```
### Walking
```kotlin
fun moveHorizontal(direction: Double, speed: Double) {
controllerV?.x = direction * speed
}
```
### Applying Knockback
```kotlin
fun applyKnockback(angle: Double, force: Double) {
externalV.x += cos(angle) * force
externalV.y += sin(angle) * force
}
```
### Ground Check
```kotlin
fun isOnGround(): Boolean {
val feetY = hitbox.endY
val checkY = (feetY + PHYS_EPSILON_DIST) / TILE_SIZE
for (tx in intTilewiseHitbox.startX.toInt()..intTilewiseHitbox.endX.toInt()) {
val block = world.getTileFromTerrain(tx, checkY.toInt())
if (BlockCodex[block]?.isSolid == true) {
return true
}
}
return false
}
```
## Multi-Threading
The engine supports multi-threaded actor updates:
```kotlin
class ThreadActorUpdate(
val actors: List<ActorWithBody>,
val delta: Float
) : Runnable {
override fun run() {
actors.forEach { it.update(delta) }
}
}
```
However, multi-threading is currently disabled as it's slower for typical actor counts (< tens of thousands).
## Debug Visualisation
### Render Hitboxes
```kotlin
if (App.IS_DEVELOPMENT_BUILD) {
shapeRenderer.begin(ShapeRenderer.ShapeType.Line)
shapeRenderer.setColor(Color.RED)
shapeRenderer.rect(
hitbox.startX.toFloat(),
hitbox.startY.toFloat(),
hitbox.width.toFloat(),
hitbox.height.toFloat()
)
shapeRenderer.end()
}
```
### Velocity Vectors
```kotlin
shapeRenderer.line(
position.x, position.y,
position.x + externalV.x * 10, position.y + externalV.y * 10
)
```
## Best Practises
1. **Use controller velocity for input** — Keep player control separate from physics
2. **Apply forces gradually** — Large instantaneous forces cause clipping
3. **Check TILE_SIZE boundaries** — Ensure coordinates are in correct space
4. **Clamp velocities** — Prevent excessive speeds causing tunnelling
5. **Mark immobile actors** — Optimise performance for static objects
6. **Use epsilon for comparisons** — Account for floating-point precision
7. **Profile physics calculations** — Identify expensive collision checks
## Limitations
- **No rotation** — All hitboxes are axis-aligned
- **Gravity is downward only** — No reverse or lateral gravity
- **Single-threaded by default** — Multi-threading disabled for performance
- **Discrete collision** — Fast-moving objects can tunnel through thin obstacles
## Future Enhancements
Possible improvements:
- Continuous collision detection
- Rotated hitboxes
- Soft-body physics
- Better fluid simulation
- Multi-directional gravity
## See Also
- [[Glossary]] — Physics terminology
- [[Actors]] — Actor system using physics
- [[World]] — World structure and tile properties
- [[Modules:Blocks]] — Defining block physics properties

633
Rendering-Pipeline.md Normal file

@@ -0,0 +1,633 @@
# Rendering Pipeline
The Terrarum rendering pipeline is built on LibGDX with OpenGL 3.2 Core Profile, featuring tile-based rendering, dynamic lighting, and sophisticated visual effects.
## Overview
The rendering system provides:
- **Tile-based world rendering** with autotiling
- **RGB+UV lightmap** system with transmittance
- **Multi-layer sprite rendering** (up to 64 layers)
- **Custom shaders** (GLSL 1.50)
- **Post-processing effects**
- **Weather and particle systems**
## OpenGL Configuration
### Version Requirements
Terrarum targets **OpenGL 3.2 Core Profile** for maximum compatibility, especially with macOS:
```kotlin
val appConfig = Lwjgl3ApplicationConfiguration()
appConfig.setOpenGLEmulation(
Lwjgl3ApplicationConfiguration.GLEmulation.GL30,
3, 2 // OpenGL 3.2
)
```
### GLSL Version
All shaders must use **GLSL 1.50**:
```glsl
#version 150
```
See also: [[OpenGL Considerations]]
### Shader Syntax Changes
GLSL 1.50 requires modern syntax:
**Vertex Shaders:**
- `attribute``in`
- `varying``out`
**Fragment Shaders:**
- `varying``in`
- `gl_FragColor` → custom `out vec4 fragColor`
## Render Layers
Actors and world elements are rendered in ordered layers:
### Layer Order (Back to Front)
1. **FAR_BEHIND** — Wires and conduits
2. **BEHIND** — Tapestries, background particles
3. **MIDDLE** — Actors (players, NPCs, creatures)
4. **MIDTOP** — Projectiles, thrown items
5. **FRONT** — Front walls and barriers
6. **OVERLAY** — Screen overlays (unaffected by lighting)
Each actor's `renderOrder` property determines its layer.
## World Rendering
### BlocksDrawer
The `BlocksDrawer` class handles tile rendering:
```kotlin
class BlocksDrawer(
val world: GameWorld,
val camera: WorldCamera
)
```
#### Tile Rendering Process
1. **Calculate visible area** from camera
2. **Iterate visible tiles** within camera bounds
3. **Fetch block data** from world layers
4. **Determine sprite variant** using autotiling
5. **Apply lighting** from lightmap
6. **Batch draw** tiles to screen
### Tile Atlas System
Blocks use texture atlases for autotiling variants:
#### Atlas Formats
- **16×16** — Single tile (no autotiling)
- **64×16** — Wall stickers (4 variants: free, left, right, bottom)
- **128×16** — Platforms (8 variants)
- **112×112** — Full autotiling (7×7 grid = 49 variants)
- **224×224** — Seasonal autotiling (4 seasons × 49 variants)
#### Autotiling Algorithm
```kotlin
// Calculate tile connections (8 neighbours)
val connections = calculateConnections(x, y, blockType)
// Map to atlas coordinates
val atlasX = connectionPattern and 0x7 // 0-6
val atlasY = (connectionPattern shr 3) and 0x7 // 0-6
// Select sprite region
val sprite = atlas.get(atlasX * 16, atlasY * 16, 16, 16)
```
#### Connection Rules
Autotiling tiles have a "barcode" pixel in the atlas (position 6,5) encoding:
**Top row (connection type):**
- `0000` — Connect mutually (all tagged tiles connect)
- `0001` — Connect to self only
**Second row (mask type):**
- `0010` — Use autotiling
### Seasonal Variation
224×224 atlases support seasonal colour blending:
```
┌─────────┬─────────┐
│ Summer │ Autumn │
├─────────┼─────────┤
│ Spring │ Winter │
└─────────┴─────────┘
```
Current and next season textures blend based on in-game time.
## Lighting System
### RGB+UV Lightmap
Terrarum simulates four light channels:
- **R** — Red light
- **G** — Green light
- **B** — Blue light
- **UV** — Ultraviolet light
Each channel propagates independently with transmittance.
### LightmapRenderer
```kotlin
class LightmapRenderer(
val world: GameWorld,
val camera: WorldCamera
)
```
#### Light Calculation
1. **Initialize with global light** (sun/moon/ambient)
2. **Add static luminous blocks** (torches, lava, etc.)
3. **Add dynamic lights** (actors, particles)
4. **Propagate light** through tiles
5. **Apply transmittance** based on block properties
6. **Render to lightmap texture**
### Block Lighting Properties
Blocks define lighting behaviour:
```kotlin
// Light absorption (0.0 = transparent, 1.0 = opaque)
val shadeR: Float
val shadeG: Float
val shadeB: Float
val shadeUV: Float
// Light emission (0.0 = none, 1.0+ = bright)
val lumR: Float
val lumG: Float
val lumB: Float
val lumUV: Float
// Reflectance for non-fluids (0.0-1.0)
val reflectance: Float
```
### Dynamic Light Functions
Luminous blocks can have time-varying brightness:
```kotlin
enum class DynamicLightFunction {
STATIC, // 0: Constant brightness
TORCH_FLICKER, // 1: Flickering torch
GLOBAL_LIGHT, // 2: Sun/moon light
DAYLIGHT_NOON, // 3: Fixed noon brightness
SLOW_BREATH, // 4: Gentle pulsing
PULSATING // 5: Rhythmic pulsing
}
```
### Corner Occlusion
The lighting system includes corner darkening as a visual effect using a simple shader, enhancing depth perception.
## Sprite Rendering
### FlippingSpriteBatch
Terrarum uses a custom `FlippingSpriteBatch` for all sprite rendering:
```kotlin
val batch = FlippingSpriteBatch()
batch.begin()
// Draw sprites
batch.end()
```
This custom implementation adds engine-specific optimisations.
### Actor Sprites
Actors use the `SpriteAnimation` system:
```kotlin
actor.sprite = SheetSpriteAnimation(
spriteSheet,
frameWidth, frameHeight,
frameDuration
)
// Update animation
actor.sprite.update(delta)
// Render
batch.draw(actor.sprite.currentFrame, x, y)
```
### Multi-Layer Sprites
Actors support multiple sprite layers:
```kotlin
@Transient var sprite: SpriteAnimation? // Base sprite
@Transient var spriteGlow: SpriteAnimation? // Glow layer
@Transient var spriteEmissive: SpriteAnimation? // Emissive layer
```
Layers are composited in order:
1. Base sprite (affected by lighting)
2. Glow layer (additive blend)
3. Emissive layer (full brightness)
### Blend Modes
```kotlin
enum class BlendMode {
NORMAL, // Standard alpha blending
ADDITIVE, // Additive blending (glow effects)
MULTIPLY, // Multiplicative blending (shadows)
}
actor.drawMode = BlendMode.ADDITIVE
```
## Camera System
### WorldCamera
The camera determines the visible area:
```kotlin
class WorldCamera(
val width: Int, // Screen width
val height: Int // Screen height
) : OrthographicCamera()
```
#### Camera Properties
```kotlin
val xTileStart: Int // First visible tile X
val yTileStart: Int // First visible tile Y
val xTileEnd: Int // Last visible tile X
val yTileEnd: Int // Last visible tile Y
```
#### Camera Movement
```kotlin
camera.position.set(targetX, targetY, 0f)
camera.update()
```
### Screen Zoom
IngameInstance supports camera zoom:
```kotlin
var screenZoom: Float = 1.0f
val ZOOM_MINIMUM = 1.0f
val ZOOM_MAXIMUM = 4.0f
// Apply zoom
camera.zoom = 1f / screenZoom
```
## Shader System
### ShaderMgr
The shader manager loads and manages GLSL programs:
```kotlin
object ShaderMgr {
fun get(shaderName: String): ShaderProgram
}
```
### Custom Shaders
Shaders are stored in `src/shaders/`:
```
src/shaders/
├── default.vert // Default vertex shader
├── default.frag // Default fragment shader
├── lightmap.frag // Lightmap shader
└── postprocess.frag // Post-processing
```
### Shader Example
**Vertex Shader (default.vert):**
```glsl
#version 150
in vec2 a_position;
in vec2 a_texCoord0;
in vec4 a_color;
out vec2 v_texCoords;
out vec4 v_color;
uniform mat4 u_projTrans;
void main() {
v_texCoords = a_texCoord0;
v_color = a_color;
gl_Position = u_projTrans * vec4(a_position, 0.0, 1.0);
}
```
**Fragment Shader (default.frag):**
```glsl
#version 150
in vec2 v_texCoords;
in vec4 v_color;
out vec4 fragColor;
uniform sampler2D u_texture;
void main() {
fragColor = texture(u_texture, v_texCoords) * v_color;
}
```
### Applying Shaders
```kotlin
val shader = ShaderMgr["myshader"]
batch.shader = shader
shader.setUniformf("u_customParam", 1.0f)
```
## Frame Buffer System
### FrameBufferManager
Manages off-screen rendering targets:
```kotlin
object FrameBufferManager {
fun createBuffer(width: Int, height: Int): FrameBuffer
}
```
### Render-to-Texture
```kotlin
val fbo = FrameBuffer(Pixmap.Format.RGBA8888, width, height, false)
fbo.begin()
// Render to framebuffer
renderWorld()
fbo.end()
// Use as texture
val texture = fbo.colorBufferTexture
batch.draw(texture, x, y)
```
### Post-Processing
```kotlin
// Render scene to FBO
sceneFBO.begin()
renderScene()
sceneFBO.end()
// Apply post-processing
batch.shader = postProcessShader
batch.draw(sceneFBO.colorBufferTexture, 0f, 0f)
```
## Particle System
### Creating Particles
```kotlin
fun createParticle(
x: Double,
y: Double,
velocityX: Double,
velocityY: Double,
lifetime: Float,
sprite: TextureRegion
): ActorWithBody
```
### Block Break Particles
```kotlin
createRandomBlockParticle(
world,
blockX,
blockY,
blockID,
particleCount = 8
)
```
Particles are lightweight actors rendered in appropriate layers.
## Weather Effects
### WeatherMixer
Manages weather rendering:
```kotlin
val weatherBox = world.weatherbox
weatherBox.currentWeather // Active weather type
weatherBox.windSpeed // Wind speed
weatherBox.windDir // Wind direction
```
Weather effects (rain, snow, fog) are rendered as particles with physics simulation.
## UI Rendering
UI elements render on top of the game world:
```kotlin
// Render game world
renderWorld()
// Render UI (unaffected by lighting)
batch.shader = null // Reset shader
uiCanvas.render(batch)
```
See also: [[UI Framework]]
## Performance Optimisations
### Batch Rendering
Minimise draw calls by batching:
```kotlin
batch.begin()
// Draw many sprites
for (tile in visibleTiles) {
batch.draw(tile.texture, x, y)
}
batch.end() // Single draw call
```
### Culling
Only render visible elements:
```kotlin
if (actor.hitbox.intersects(camera.bounds)) {
actor.render(batch)
}
```
### Texture Atlas
Use texture atlases to reduce texture switches:
```kotlin
val atlas = TextureAtlas("sprites/packed.atlas")
```
### Level of Detail
Reduce detail for distant objects:
```kotlin
val distance = actor.position.dst(camera.position)
if (distance > LOD_THRESHOLD) {
renderSimplified(actor)
} else {
renderDetailed(actor)
}
```
## Debug Rendering
### Debug Overlays
```kotlin
if (App.IS_DEVELOPMENT_BUILD) {
renderHitboxes()
renderTileGrid()
renderLightValues()
}
```
### Shape Renderer
Use `ShapeRenderer` for debug visuals:
```kotlin
val shapes = ShapeRenderer()
shapes.begin(ShapeRenderer.ShapeType.Line)
shapes.rect(hitbox.x, hitbox.y, hitbox.width, hitbox.height)
shapes.end()
```
## Common Patterns
### Rendering a Tile
```kotlin
val blockID = world.getTileFromTerrain(x, y)
val block = BlockCodex[blockID]
val sprite = CreateTileAtlas.getSprite(blockID, x, y)
val light = lightmap.getLight(x, y)
batch.setColor(light.r, light.g, light.b, 1f)
batch.draw(sprite, x * TILE_SIZE, y * TILE_SIZE)
batch.setColor(1f, 1f, 1f, 1f)
```
### Rendering an Actor
```kotlin
actor.sprite?.update(delta)
val frame = actor.sprite?.currentFrame
val light = getLightAtPosition(actor.hitbox.centerX, actor.hitbox.centerY)
batch.setColor(light.r, light.g, light.b, 1f)
batch.draw(
frame,
actor.hitbox.startX,
actor.hitbox.startY
)
batch.setColor(1f, 1f, 1f, 1f)
```
### Custom Shader Effect
```kotlin
val shader = ShaderMgr["wave"]
batch.shader = shader
shader.setUniformf("u_time", gameTime)
shader.setUniformf("u_amplitude", 0.1f)
batch.draw(texture, x, y)
batch.shader = null // Reset to default
```
## Best Practises
1. **Batch draw calls** — Group similar sprites together
2. **Use texture atlases** — Reduce texture binding overhead
3. **Cull off-screen objects** — Don't render invisible actors
4. **Cache sprite references** — Don't recreate textures every frame
5. **Reset batch colour** — Always reset to white after tinting
6. **Profile rendering** — Identify bottlenecks with timing
7. **Minimise shader switches** — Group by shader when possible
8. **Use appropriate precision**`lowp`/`mediump` in shaders when sufficient
## Troubleshooting
### Black Screen
- Check OpenGL version support
- Verify shader compilation (check logs)
- Ensure camera is positioned correctly
### Flickering
- Z-fighting: Ensure proper layer ordering
- Missing batch.begin()/end() calls
- Texture binding issues
### Performance Issues
- Too many draw calls (use atlases)
- Large lightmap calculations
- Excessive particle count
- Missing culling
## See Also
- [[OpenGL Considerations]] — GL 3.2 requirements
- [[Glossary]] — Rendering terminology
- [[World]] — World data structure
- [[Actors]] — Actor rendering

524
Save-and-Load.md Normal file

@@ -0,0 +1,524 @@
# Save and Load
Terrarum uses a custom save game format called **TerranVirtualDisk** (TEVD), which stores game data as virtual disk images. Each save consists of two separate disk files: one for the world and one for the player.
## Overview
The save system provides:
- **Chunked world storage** — Efficient storage for massive worlds
- **Version tracking** — Snapshot numbers for save compatibility
- **Compression** — Multiple compression algorithms (Gzip, Zstd, Snappy)
- **Integrity checking** — CRC-32 verification and SHA-256 hashing
- **Metadata** — Save type, game mode, creation time
## Save File Structure
### Two-Disk System
Each save game consists of two virtual disks:
1. **World Disk** — Contains the world, terrain, actors, and environment
2. **Player Disk** — Contains player data, inventory, and stats
This separation allows:
- Sharing worlds between players
- Moving players between worlds
### File Locations
Saves are stored in the appdata directory:
```
<appdata>/Savegames/
├── WorldName_w.tvd # World disk
└── PlayerName_p.tvd # Player disk
```
## VirtualDisk Format
### TEVD Specification
VirtualDisk implements the **TerranVirtualDisk** format (version 254), a custom archival format designed for game saves.
#### Header Structure (300 bytes)
```
Offset Size Field
0 4 Magic: "TEVd"
4 6 Disk size (48-bit, max 256 TiB)
10 32 Disk name (short)
42 4 CRC-32 checksum
46 1 Version (0xFE)
47 1 Marker byte (0xFE)
48 1 Disk properties flags
49 1 Save type flags
50 1 Kind (player/world)
51 1 Origin flags
52 2 Snapshot number
54 1 Game mode
55 9 Reserved extra info bytes
64 236 Rest of disk name (long)
```
### Entry Structure
Each file in the disk is stored as an entry:
```
EntryID (8 bytes) // Unique file identifier
Size (6 bytes) // File size in bytes
Timestamp (6 bytes) // Creation timestamp
Compression (1 byte) // Compression method
Data (variable) // File contents
```
### Entry IDs
Special entry IDs are reserved for specific purposes:
- **0x00** — Metadata entry
- **0x01..0xFFFFFFFE** — User data entries
- **0xFEFEFEFE** — Originally reserved for footer (now freed)
- **0xFFFFFFFF** — Invalidated entry marker
## Save Types
### World Saves
World disks (`VDSaveKind.WORLD_DATA`) contain:
- **WorldInfo** — Dimensions, time, spawn points, seeds
- **Terrain layers** — Terrain, wall, ore, fluid data (chunked)
- **Actors** — All actors in the world (except the player)
- **Weather** — Current weather state
- **Wirings** — Wire/conduit networks
- **Extra fields** — Module-specific data
### Player Saves
Player disks (`VDSaveKind.PLAYER_DATA`) contain:
- **PlayerInfo** — Position, stats, actor values
- **Inventory** — Items, equipment, quickslots
- **Player-specific data** — Quests, achievements, etc.
### Full Save vs. Quicksave
Saves can be marked as quicksaves:
```kotlin
// Save type flags
0b0000_00ab
b: 0 = full save, 1 = quicksave
a: 1 = autosave
```
#### How Quicksaves Work
**Quicksave (append-only):**
1. Modified entries are written to the **end** of the disk file
2. This creates **duplicate entries** with the same Entry ID at different offsets
3. When reading the disk, the entry at the **later offset** takes precedence
4. Earlier duplicates are **ignored**
5. The disk becomes "dirty" with obsolete entries still taking up space
**Example:**
```
Initial disk:
Offset 0x1000: Entry ID 42 (original world data)
Offset 0x2000: Entry ID 43 (original player data)
After quicksave:
Offset 0x1000: Entry ID 42 (original - IGNORED on read)
Offset 0x2000: Entry ID 43 (original)
Offset 0x3000: Entry ID 42 (updated - USED on read) ← Appended by quicksave
```
**Full Save (rebuild):**
1. Disk is **completely rebuilt** from scratch
2. Each entry appears **only once**
3. All obsolete/deleted entries are **removed**
4. Disk is compacted to minimum size
5. No duplicate entries exist
This makes quicksaves faster (no rebuild) but results in larger file sizes over time. Periodic full saves compact the disk by removing duplicate and deleted entries.
## Snapshot System
### Snapshot Numbers
Saves are versioned using snapshot numbers in the format: `YYwWWX`
```kotlin
// Format: 0b A_yyyyyyy wwwwww_aa
// Example: 23w40f
// 23 = year 2023
// w40 = ISO week 40
// f = revision 6 (a=1, b=2, ..., f=6)
```
This allows tracking save compatibility and detecting version mismatches.
### Version Compatibility
When loading a save, the engine checks:
1. **Snapshot number** — Warn if from newer version
2. **GENVER** — Game version that created the save
3. **Module versions** — Check module compatibility
## Serialisation
### JSON-Based Serialisation
Game state is serialised to JSON using LibGDX's JSON library with custom serialisers.
#### Custom Serialisers
The engine provides serialisers for:
- **BigInteger** — Arbitrary precision integers
- **BlockLayerGenericI16** — Terrain layers with SHA-256 hashing
- **WorldTime** — Temporal data
- **HashArray** — Sparse arrays
- **HashedWirings** — Wire networks
- **ZipCodedStr** — Compressed strings
#### Example Serialisation
```kotlin
val json = Common.jsoner
val jsonString = json.toJson(gameWorld)
val loadedWorld = json.fromJson(GameWorld::class.java, jsonString)
```
### Transient Fields
Fields marked `@Transient` are not serialised:
```kotlin
@Transient var sprite: SpriteAnimation? = null
@Transient lateinit var actor: Pocketed
```
**These must be reconstructed in the `reload()` method after deserialisation.**
### Reload Method
After loading, actors and objects call `reload()` to reconstruct transient state:
```kotlin
override fun reload() {
super.reload()
// Reconstruct sprites, UI, caches, etc.
sprite = loadSprite()
actor = retrieveActorReference()
}
```
## DiskSkimmer
DiskSkimmer provides efficient read/write access to virtual disks without loading the entire disk into memory.
### Creating a Skimmer
```kotlin
val skimmer = DiskSkimmer(File("save.tvd"))
```
### Reading Entries
```kotlin
// Get entry by ID
val data: ByteArray64 = skimmer.requestFile(entryID)
// Read specific entry without building DOM
val inputStream = data.getAsInputStream()
```
### Writing Entries
```kotlin
// Write new entry
skimmer.appendEntry(entryID, data, compression = Common.COMP_ZSTD)
```
### Rebuilding (Compaction)
Dirty disks accumulate duplicate and deleted entries from quicksaves. **Full saves** perform compaction:
```kotlin
skimmer.rebuild() // Rebuild entry table, removing duplicates and deleted entries
skimmer.sync() // Write clean disk to file
```
The `rebuild()` process:
1. Scans all entries in the disk
2. For duplicate Entry IDs, keeps only the **latest** (highest offset)
3. Removes entries marked as deleted (ID = 0xFFFFFFFF)
4. Writes a clean, compacted disk with no duplicates
This is what distinguishes a **full save** from a **quicksave**.
## Compression
### Supported Algorithms
```kotlin
COMP_NONE = 0 // No compression
COMP_GZIP = 1 // Gzip compression
COMP_ZSTD = 3 // Zstandard (recommended)
COMP_SNAPPY = 4 // Snappy (fast, lower ratio)
```
### Using Compression
```kotlin
// Compress data before writing
val compressedStream = ZstdOutputStream(outputStream)
compressedStream.write(data)
compressedStream.close()
// Decompress on read
val decompressedStream = ZstdInputStream(inputStream)
val data = decompressedStream.readBytes()
```
**Zstandard (COMP_ZSTD)** provides the best balance of speed and compression ratio for save games.
## SavegameCollection
The SavegameCollection manages all save files:
```kotlin
object SavegameCollection {
// List all world saves
fun getWorlds(): List<SaveMetadata>
// List all player saves
fun getPlayers(): List<SaveMetadata>
// Load world
fun loadWorld(worldFile: File): GameWorld
// Load player
fun loadPlayer(playerFile: File): IngamePlayer
}
```
## Save and Load Workflow
### Saving a Game
**Quicksave (append-only, faster):**
```kotlin
// 1. Prepare save data
val world = INGAME.world
val player = INGAME.actorNowPlaying as IngamePlayer
// 2. Open existing disk (don't rebuild)
val worldDisk = DiskSkimmer(File("${worldName}_w.tvd"))
// 3. Serialise changed data to JSON
val worldJson = Common.jsoner.toJson(world)
val compressedWorld = compress(worldJson, Common.COMP_ZSTD)
// 4. Append to disk (creates duplicate entries)
worldDisk.appendEntry(0x00, compressedWorld) // Entry 0x00 now exists twice!
// 5. Sync without rebuilding
worldDisk.sync() // Disk is now "dirty" with duplicates
```
**Full Save (rebuild, compacted):**
```kotlin
// Same as quicksave, but with rebuild before sync:
// 4. Append entries
worldDisk.appendEntry(0x00, compressedWorld)
// 5. Rebuild and sync (removes duplicates)
worldDisk.rebuild() // Compact: remove old duplicates and deleted entries
worldDisk.sync() // Write clean disk
```
### Loading a Game
```kotlin
// 1. Open disks
val worldDisk = DiskSkimmer(File("${worldName}_w.tvd"))
val playerDisk = DiskSkimmer(File("${playerName}_p.tvd"))
// 2. Read and decompress (automatically uses latest entry if duplicates exist)
val worldData = worldDisk.requestFile(0x00) // Gets entry at highest offset
val worldJson = decompress(worldData)
// 3. Deserialise from JSON
val world = Common.jsoner.fromJson(GameWorld::class.java, worldJson)
// 4. Reload transient fields
world.layerTerrain.reload()
world.actors.forEach { actor -> actor.reload() }
// 5. Repeat for player
// ...
// 6. Initialise game
INGAME.world = world
INGAME.actorNowPlaying = player
```
## Integrity Checking
### CRC-32 Verification
Virtual disks store a CRC-32 checksum in the header:
```kotlin
// Calculate CRC
val crc = CRC32()
entries.map { it.crc }.sorted().forEach { crc.update(it) }
val diskCRC = crc.value
// Verify on load
if (diskCRC != storedCRC) {
throw IOException("Disk CRC mismatch: corrupted save file")
}
```
### SHA-256 Hashing
Block layers are hashed to detect corruption:
```kotlin
// During save
val hash = SHA256(blockLayer.bytes)
writeHash(hash)
// During load
val loadedHash = SHA256(blockLayer.bytes)
if (loadedHash != storedHash) {
throw BlockLayerHashMismatchError(storedHash, loadedHash, blockLayer)
}
```
## Chunked World Storage
Worlds are stored in chunks to reduce save/load time:
### Chunk System
- Only **modified chunks** are saved
- **Unmodified chunks** are regenerated on load
- **Chunk flags** track modification state
### Saving Chunks
```kotlin
for (chunk in world.chunks) {
if (chunk.isModified) {
saveChunk(chunk)
}
}
```
### Loading Chunks
```kotlin
for (chunk in world.chunks) {
if (chunkExistsInSave(chunk)) {
loadChunk(chunk)
} else {
regenerateChunk(chunk) // Use world generator
}
}
```
## Best Practises
1. **Mark UI and sprites as @Transient** — They cannot be serialised
2. **Implement reload() for all custom classes** — Reconstruct transient fields
3. **Use Zstd compression** — Best performance for game saves
4. **Use quicksaves for autosaves** — Fast append-only saves during gameplay
5. **Use full saves when exiting** — Rebuild and compact the disk when player quits
6. **Rebuild disks periodically** — Compact after multiple quicksaves to prevent file bloat
7. **Validate snapshot versions** — Warn users of version mismatches
8. **Hash critical data** — Detect corruption in block layers
9. **Chunk world saves** — Only save modified chunks
10. **Separate world and player** — Allow sharing and portability
## Common Pitfalls
- **Forgetting to call reload()** — Leads to null sprite/UI crashes
- **Not marking fields @Transient** — Causes serialisation errors
- **Saving entire worlds** — Use chunking for large worlds
- **Only using quicksaves** — Disk files bloat with duplicate entries; use full saves periodically
- **Rebuilding on every save** — Too slow for autosaves; use quicksaves during gameplay
- **Ignoring CRC errors** — Corrupted saves can crash the game
- **Not compressing data** — Saves become unnecessarily large
- **Hardcoding entry IDs** — Use constants or enums
## Advanced Topics
### Dynamic Item Persistence
Dynamic items (tools, weapons) require special handling:
```kotlin
// World stores dynamic item table
world.dynamicItemInventory: ItemTable
// Map dynamic IDs to static IDs
world.dynamicToStaticTable: ItemRemapTable
// When loading, remap IDs
item.dynamicID = world.dynamicToStaticTable[oldDynamicID]
```
### Module Compatibility
Saves store module versions. On load:
```kotlin
// Check each module
for (module in save.modules) {
if (!ModMgr.isCompatible(module.name, module.version)) {
warn("Module ${module.name} version mismatch")
}
}
```
### Custom Save Data
Modules can add custom save data using extra fields:
```kotlin
// Add extra field
world.extraFields["mymod:custom_data"] = MySerializable()
// Retrieve on load
val customData = world.extraFields["mymod:custom_data"] as? MySerializable
```
### Autosave
Implement autosave by periodically calling save in a background thread:
```kotlin
// Every N seconds
if (timeSinceLastSave > autosaveInterval) {
Thread {
saveGame(isAutosave = true, isQuicksave = true)
}.start()
}
```
Mark autosaves in the save type flags (`a` bit).
## See Also
- [[Glossary]] — Save/load terminology
- [[World]] — World structure and management
- [[Actors]] — Actor serialisation and reload
- [[Inventory]] — Inventory persistence
- [TerranVirtualDisk](https://github.com/minjaesong/TerranVirtualDisk) — Virtual disk format library

634
Tile-Atlas-System.md Normal file

@@ -0,0 +1,634 @@
# Tile Atlas System (Engine Internals)
**Audience:** Engine maintainers and advanced developers working on core rendering systems.
This document describes the internal implementation of Terrarum's tile atlas system, including six-season blending, subtiling, and dynamic atlas generation.
## Overview
The tile atlas system (`CreateTileAtlas`) dynamically builds texture atlases from block textures across all loaded modules, supporting:
- **Six seasonal variations** with temporal blending
- **Subtiling system** for 8×8 pixel subdivisions
- **Multiple atlas formats** (16×16, 64×16, 128×16, 112×112, 168×136, 224×224)
- **Glow and emissive layers** for self-illumination
- **Dynamic retexturing** via module overrides
- **Item image generation** from world tiles
## Six-Season System
### Season Atlases
The engine maintains **six separate texture atlases** for seasonal variation:
```kotlin
lateinit var atlasPrevernal: Pixmap // Pre-spring (late winter transitioning)
lateinit var atlasVernal: Pixmap // Spring
lateinit var atlasAestival: Pixmap // Summer
lateinit var atlasSerotinal: Pixmap // Late summer
lateinit var atlasAutumnal: Pixmap // Autumn
lateinit var atlasHibernal: Pixmap // Winter
```
### Season Names
| Atlas | Season | Calendar Period |
|-------|--------|-----------------|
| Prevernal | Pre-spring | Late Month 12 to Early Month 1 |
| Vernal | Spring | Months 1-2 |
| Aestival | Summer | Months 3-5 |
| Serotinal | Late Summer | Months 6-7 |
| Autumnal | Autumn | Months 8-10 |
| Hibernal | Winter | Months 11-12 |
### Seasonal Blending
The rendering shader can blend between two seasonal atlases using the `tilesBlend` uniform:
```glsl
// In tiling.frag shader
uniform sampler2D tilesAtlas; // Primary season atlas
uniform sampler2D tilesBlendAtlas; // Secondary season atlas for blending
uniform float tilesBlend = 0.0; // Blend factor [0..1]
vec4 tileCol = texture(tilesAtlas, finalUVCoordForTile);
vec4 tileAltCol = texture(tilesBlendAtlas, finalUVCoordForTile);
vec4 finalColor = mix(tileCol, tileAltCol, tilesBlend);
```
Game code selects which two seasonal atlases to use and sets the blend factor. By default, `atlasVernal` (spring) is used.
### Seasonal Texture Formats
#### Standard Blocks (112×112)
Single-season blocks populate all six atlases with identical textures.
#### Seasonal Blocks (224×224 or 336×224)
**Four-season format** (224×224) provides four seasons in a quad layout:
```
┌───────────┬───────────┐
│ Aestival │ Autumnal │ 112×112 each
│ (Summer) │ (Autumn) │
├───────────┼───────────┤
│ Vernal │ Hibernal │
│ (Spring) │ (Winter) │
└───────────┴───────────┘
```
For blocks using this format, the same texture is reused for Prevernal and Serotinal seasons
#### Six-Season Layout
For textures with full six-season support (336×224 for standard blocks, or 3× width for subtiled blocks):
```
Horizontal layout (336 × 224):
┌─────────┬─────────┬─────────┐
│Prevernal│ Vernal │Aestival │ 112×112 each (top row)
├─────────┼─────────┼─────────┤
│Hibernal │Autumnal │Serotinal│ 112×112 each (bottom row)
└─────────┴─────────┴─────────┘
```
The atlas creation code extracts each 112×112 region and places it into the corresponding seasonal atlas. No interpolation occurs—all six seasons are explicitly provided in the source texture.
## Subtiling System
### Subtile Size
Subtiles are subdivisions of standard tiles:
```kotlin
const val TILE_SIZE = 16 // Standard tile
const val SUBTILE_SIZE = 8 // Half-tile subdivision
```
### Subtile Grid
Each tile divides into a 2×2 subtile grid:
```
┌────┬────┐
│ 0 │ 1 │ 8×8 each
├────┼────┤
│ 3 │ 2 │
└────┴────┘
```
Subtile indices: 0 (top-left), 1 (top-right), 2 (bottom-right), 3 (bottom-left).
### Subtile Atlases
Two special atlas formats use subtiling:
#### Generic Subtile Atlas (104×136)
```kotlin
val W_SUBTILE_GENERIC = 104 // Width in subtiles
val H_SUBTILE = 136 // Height in subtiles
```
Total: 13 tiles wide × 17 tiles tall = 221 tile positions.
#### Grass Subtile Atlas (168×136)
```kotlin
val W_SUBTILE_GRASS = 168 // Width in subtiles
val H_SUBTILE = 136 // Height in subtiles
```
Total: 21 tiles wide × 17 tiles tall = 357 tile positions.
### Subtile Layout
Subtile atlases store tile variants at subtile resolution for advanced tiling patterns.
**Example grass subtiling:**
- Positions (0,0) to (3,0): Top-left subtiles for different connection patterns
- Positions (0,1) to (3,1): Top-right subtiles
- And so on...
### Subtile Offset Vectors
```kotlin
val subtileOffsetVectors = arrayOf(
Point2i(0, 0), // Top-left
Point2i(SUBTILE_SIZE, 0), // Top-right
Point2i(SUBTILE_SIZE, SUBTILE_SIZE), // Bottom-right
Point2i(0, SUBTILE_SIZE) // Bottom-left
)
```
Used for extracting and compositing subtiles from atlases.
### Tiling Modes
```kotlin
const val TILING_FULL = 0 // Standard autotiling with flip/rotation
const val TILING_FULL_NOFLIP = 1 // Standard autotiling without flip/rotation
const val TILING_BRICK_SMALL = 2 // Small brick pattern (4 rows per tile)
const val TILING_BRICK_SMALL_NOFLIP = 3 // Small brick without flip/rotation
const val TILING_BRICK_LARGE = 4 // Large brick pattern (2 rows per tile)
const val TILING_BRICK_LARGE_NOFLIP = 5 // Large brick without flip/rotation
```
Each mode defines different subtile selection patterns and whether flip/rotation variants are applied.
### Item Image Generation from Subtiles
When creating item icons from subtiled blocks:
```kotlin
val tileOffsetsForItemImageFromSubtile = arrayOf(
intArrayOf(4*2, 4*5, 4*8, 4*11), // TILING_FULL
intArrayOf(4*2, 4*5+2, 4*8+2, 4*11), // TILING_BRICK_SMALL
intArrayOf(4*2+2, 4*5, 4*8+2, 4*11) // TILING_BRICK_LARGE
)
```
These offsets select representative subtiles to composite into a 16×16 item icon.
## Atlas Generation Process
### Initialisation
```kotlin
operator fun invoke(updateExisting: Boolean = false) {
// 1. Create six season atlases
atlasPrevernal = Pixmap(TILES_IN_X * TILE_SIZE, TILES_IN_X * TILE_SIZE, RGBA8888)
// ... create all six + glow + emissive
// 2. Load init.tga (predefined tiles: air, breakage stages, etc.)
drawInitPixmap()
// 3. Scan all modules for block textures
val tgaList = collectBlockTextures()
// 4. Process each texture into atlases
tgaList.forEach { (modname, file) ->
fileToAtlantes(modname, file, glowFile, emissiveFile)
}
// 5. Generate item images
generateItemImages()
// 6. Create terrain colour map
generateTerrainColourMap()
}
```
### Texture Collection
The engine scans three directories:
- **blocks/** — Standard terrain blocks
- **ores/** — Ore overlays
- **fluid/** — Fluid textures
Only textures with corresponding codex entries are loaded:
```kotlin
dir.list()
.filter { it.extension() == "tga" &&
BlockCodex["$modname:${it.nameWithoutExtension()}"] != null }
.forEach { fileToAtlantes(modname, it) }
```
### File to Atlas Processing
```kotlin
private fun fileToAtlantes(
modname: String,
diffuse: FileHandle,
glow: FileHandle?,
emissive: FileHandle?,
prefix: String? // e.g., "ores" or "fluid" for non-block textures
) {
val sourcePixmap = Pixmap(diffuse)
// Determine atlas format from dimensions
when {
sourcePixmap.width == 16 && sourcePixmap.height == 16 ->
processSingleTile(sourcePixmap)
sourcePixmap.width == 112 && sourcePixmap.height == 112 ->
processAutotilingBlock(sourcePixmap)
sourcePixmap.width == 224 && sourcePixmap.height == 224 ->
processSeasonalBlock(sourcePixmap)
sourcePixmap.width == W_SUBTILE_GENERIC * SUBTILE_SIZE ->
processSubtiledBlock(sourcePixmap, SUBTILE_GENERIC)
sourcePixmap.width == W_SUBTILE_GRASS * SUBTILE_SIZE ->
processSubtiledBlock(sourcePixmap, SUBTILE_GRASS)
// ... other formats
}
}
```
### Barcode System
112×112 autotiling textures embed metadata in a "barcode" region at position (6,5):
```
Grid position (6, 5) = pixel column 96-111, row 80-95
```
#### Barcode Encoding
Two rows of 16 pixels encode properties as binary:
**Row 1 (top):** Connection type
- Bits read left-to-right as binary number
- `0000` = Connect mutually (CONNECT_MUTUAL)
- `0001` = Connect to self only (CONNECT_SELF)
**Row 2:** Mask type
- `0010` = Use autotiling (MASK_47)
- Other values for special tiling modes
**Reading the barcode (right-to-left):**
```kotlin
// In fileToAtlantes() for 112×112 textures
var connectionType = 0
var maskType = 0
for (bit in 0 until TILE_SIZE) { // TILE_SIZE = 16
val x = (7 * TILE_SIZE - 1) - bit // Read right-to-left: x = 111 down to 96
val y1 = 5 * TILE_SIZE // y1 = 80 (connection type row)
val y2 = y1 + 1 // y2 = 81 (mask type row)
val pixel1 = (tilesPixmap.getPixel(x, y1).and(255) >= 128).toInt(bit)
val pixel2 = (tilesPixmap.getPixel(x, y2).and(255) >= 128).toInt(bit)
connectionType += pixel1
maskType += pixel2
}
// Helper extension: fun Boolean.toInt(shift: Int) = if (this) (1 shl shift) else 0
```
The barcode is read **right-to-left** (from pixel x=111 to x=96), with each white pixel representing a binary 1.
### RenderTag Generation
Each block gets a RenderTag storing atlas metadata:
```kotlin
data class RenderTag(
val tileNumber: Int, // Position in atlas (0-based index)
val connectionType: Int, // CONNECT_SELF, CONNECT_MUTUAL, etc.
val maskType: Int, // MASK_47, MASK_PLATFORM, etc.
val tilingMode: Int, // TILING_FULL, TILING_BRICK_SMALL, etc.
val postProcessing: Int // POSTPROCESS_NONE, POSTPROCESS_DEBLOCKING, etc.
)
```
Stored in hash maps:
```kotlin
lateinit var tags: HashMap<ItemID, RenderTag> // By block ID
lateinit var tagsByTileNum: HashArray<RenderTag> // By tile number
```
## Item Image Generation
### Terrain and Wall Images
Separate item textures for terrain and wall placement:
```kotlin
lateinit var itemTerrainTexture: Texture
lateinit var itemWallTexture: Texture
```
### Generation Process
```kotlin
tags.forEach { (id, tag) ->
val itemSheetNum = tileIDtoItemSheetNumber(id)
if (tag.maskType >= 16) {
// Subtiled block - composite from subtiles
val subtilePositions = tileOffsetsForItemImageFromSubtile[tag.tilingMode / 2]
compositeSub tiles(subtilePositions, itemSheetNum)
} else {
// Standard block - copy representative tile
val atlasPos = tag.tileNumber + maskOffsetForItemImage(tag.maskType)
copyTileToItemSheet(atlasPos, itemSheetNum)
}
}
```
### Wall Darkening
Wall item images are darkened for visual distinction:
```kotlin
val WALL_OVERLAY_COLOUR = Color(.72f, .72f, .72f, 1f)
for (y in 0 until itemWallPixmap.height) {
for (x in 0 until itemWallPixmap.width) {
val color = Color(itemWallPixmap.getPixel(x, y))
color.mul(WALL_OVERLAY_COLOUR)
itemWallPixmap.drawPixel(x, y, color.toRGBA())
}
}
```
## Glow and Emissive Layers
### Separate Atlases
```kotlin
lateinit var atlasGlow: Pixmap // Additive glow layer
lateinit var atlasEmissive: Pixmap // Full-brightness layer
```
### File Naming Convention
For a block texture `32.tga`:
- **Main texture:** `32.tga`
- **Glow layer:** `32_glow.tga` (optional)
- **Emissive layer:** `32_emsv.tga` (optional)
### Rendering
Glow and emissive layers render separately:
```kotlin
// 1. Render base tile (affected by lighting)
batch.draw(baseTexture, x, y)
// 2. Render glow (additive blend)
batch.setBlendFunction(GL_SRC_ALPHA, GL_ONE)
batch.draw(glowTexture, x, y)
// 3. Render emissive (full brightness)
batch.setColor(1f, 1f, 1f, 1f) // Ignore lighting
batch.draw(emissiveTexture, x, y)
batch.setBlendFunction(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
```
## Terrain Colour Map
Pre-calculated average colours for each block:
```kotlin
lateinit var terrainTileColourMap: HashMap<ItemID, Cvec>
```
### Generation
```kotlin
for (id in itemSheetNumbers) {
val tileNum = itemSheetNumbers[id]
val tx = (tileNum % TILES_IN_X) * TILE_SIZE
val ty = (tileNum / TILES_IN_X) * TILE_SIZE
var r = 0f; var g = 0f; var b = 0f; var a = 0f
// Average all pixels in the tile
for (y in ty until ty + TILE_SIZE) {
for (x in tx until tx + TILE_SIZE) {
val pixel = itemTerrainPixmap.getPixel(x, y)
r += extractRed(pixel)
g += extractGreen(pixel)
b += extractBlue(pixel)
a += extractAlpha(pixel)
}
}
val pixelCount = TILE_SIZE * TILE_SIZE
terrainTileColourMap[id] = Cvec(
r / pixelCount,
g / pixelCount,
b / pixelCount,
a / pixelCount
)
}
```
### Usage
Colour maps are used for:
- Minimap rendering
- Particle colours when blocks break
- LOD (Level of Detail) rendering
- Fast block type identification
## Atlas Size and Limits
### Configuration
```kotlin
var MAX_TEX_SIZE = 2048 // Configurable atlas dimension
var TILES_IN_X = MAX_TEX_SIZE / TILE_SIZE // = 128 tiles
var SUBTILES_IN_X = MAX_TEX_SIZE / SUBTILE_SIZE // = 256 subtiles
```
### Capacity
With 2048×2048 atlases:
- **Standard tiles:** 128 × 128 = 16,384 tile positions
- **Subtiles:** 256 × 256 = 65,536 subtile positions
### Dynamic Resizing
The init.tga can be wider than MAX_TEX_SIZE. The engine tiles it vertically:
```kotlin
// If init.tga is 4096 pixels wide but MAX_TEX_SIZE = 2048:
// Split into two 2048×16 strips stacked vertically
val stripsNeeded = ceil(initPixmap.width / MAX_TEX_SIZE)
for (strip in 0 until stripsNeeded) {
val srcX = strip * MAX_TEX_SIZE
val destY = strip * TILE_SIZE
atlas.drawPixmap(initPixmap, srcX, 0, MAX_TEX_SIZE, TILE_SIZE, 0, destY, MAX_TEX_SIZE, TILE_SIZE)
}
```
## Atlas Cursor and Allocation
### Atlas Cursor
Tracks next available tile position:
```kotlin
private var atlasCursor = 66 // First 66 tiles reserved
```
Reserved tiles (0-65):
- Tile 0: Air (transparent)
- Tiles 1-15: Reserved for engine use
- Tiles 16-65: Breakage stages, update markers, etc.
### Allocation
```kotlin
private fun allocateTileSpace(tilesNeeded: Int): Int {
val allocated = atlasCursor
atlasCursor += tilesNeeded
if (atlasCursor >= TOTAL_TILES) {
throw Error("Atlas full! Needed $tilesNeeded more tiles but only ${TOTAL_TILES - allocated} remaining.")
}
return allocated
}
```
### Item Sheet Cursor
Separate cursor for item sheet allocation:
```kotlin
private var itemSheetCursor = 16 // Reserve first 16 item positions
```
## Retexturing System
### Alt File Paths
Modules can override textures from other modules:
```kotlin
val altFilePaths: HashMap<String, FileHandle> = ModMgr.GameRetextureLoader.altFilePaths
```
### Override Resolution
```kotlin
val originalFile = Gdx.files.internal("basegame/blocks/32.tga")
val overrideFile = altFilePaths.getOrDefault(originalFile.path(), originalFile)
// Use overrideFile for atlas generation
```
Texture packs populate `altFilePaths` to replace textures without modifying original modules.
## Performance Considerations
### Memory Usage
Six season atlases + glow + emissive = **8 atlases**
With 2048×2048 RGBA8888:
- Per atlas: 2048 × 2048 × 4 bytes = 16 MB
- Total: 8 × 16 MB = **128 MB** of atlas memory
Plus item sheets (~2048×2048 × 6 = 48 MB).
**Total texture memory: ~176 MB**
### Generation Time
Atlas generation is expensive:
- Runs once at startup
- Can be cached (planned feature)
- Blocks game startup until complete
### Optimisations
1. **Lazy subtile generation** — Only process subtiled blocks that exist in BlockCodex
2. **Parallel texture loading** — Could load multiple modules concurrently
3. **Atlas caching** — Save generated atlases to disk (not yet implemented)
4. **Compression** — Use texture compression (DXT/ETC) on GPUs that support it
## Debugging
### Export Atlases
Uncomment debug code to export atlases:
```kotlin
PixmapIO2.writeTGA(Gdx.files.absolute("atlas_0_prevernal.tga"), atlasPrevernal, false)
PixmapIO2.writeTGA(Gdx.files.absolute("atlas_1_vernal.tga"), atlasVernal, false)
// ... export all six
```
### Verify Barcodes
Check barcode region is transparent except for set bits:
```kotlin
for (y in barcodeY until barcodeY + 2) {
for (x in barcodeX until barcodeX + 16) {
val pixel = pixmap.getPixel(x, y)
if (!isTransparent(pixel) && !isWhite(pixel)) {
println("WARNING: Barcode contains non-binary pixel at ($x, $y)")
}
}
}
```
### Atlas Visualisation
Render atlas to screen with grid overlay:
```kotlin
batch.draw(atlasTexture, 0f, 0f)
shapeRenderer.begin(ShapeRenderer.ShapeType.Line)
for (i in 0 until TILES_IN_X) {
shapeRenderer.line(i * TILE_SIZE, 0, i * TILE_SIZE, MAX_TEX_SIZE)
shapeRenderer.line(0, i * TILE_SIZE, MAX_TEX_SIZE, i * TILE_SIZE)
}
shapeRenderer.end()
```
## Potential Enhancements
1. **Atlas caching** — Save generated atlases to disk to avoid regeneration on startup
2. **GPU texture compression** — Use DXT/ETC formats to reduce VRAM usage
3. **Mipmap generation** — For better filtering at distance
4. **Animated block textures** — Frame-by-frame animation support
5. **Normal maps** — For advanced lighting effects
6. **Parallax mapping** — Create depth illusion
Note: Atlas expansion is already implemented (see `expandAtlantes()` method).
## See Also
- [[Rendering Pipeline]] — How atlases are used during rendering
- [[Autotiling In-Depth]] — Detailed autotiling algorithm
- [[Modules:Blocks]] — Creating block textures for modules

642
UI-Framework.md Normal file

@@ -0,0 +1,642 @@
# UI Framework
Terrarum features a comprehensive UI framework built on a canvas-based architecture with event handling, animations, and a rich set of pre-built UI components.
## Overview
The UI system provides:
- **UICanvas** — Base class for all UI screens
- **UIItem** — Individual UI elements (buttons, sliders, text fields)
- **UIHandler** — Manages lifecycle and animations
- **Event system** — Mouse, keyboard, and gamepad input
- **Layout management** — Positioning and sizing
- **Built-in components** — Buttons, lists, sliders, and more
## Architecture
### UICanvas
The base class for all UI screens and windows:
```kotlin
abstract class UICanvas(
toggleKeyLiteral: String? = null,
toggleButtonLiteral: String? = null,
customPositioning: Boolean = false
) : Disposable {
abstract var width: Int
abstract var height: Int
val handler: UIHandler
val uiItems: List<UIItem>
abstract fun updateImpl(delta: Float)
abstract fun renderImpl(batch: SpriteBatch, camera: OrthographicCamera)
}
```
### UIHandler
Manages the canvas lifecycle:
```kotlin
class UIHandler(
val toggleKey: String?,
val toggleButton: String?,
val customPositioning: Boolean
) {
var posX: Int
var posY: Int
var isOpened: Boolean
var isOpening: Boolean
var isClosing: Boolean
}
```
## Creating a UI
### Basic UICanvas Example
```kotlin
class MyUI : UICanvas() {
override var width = 800
override var height = 600
init {
// Position the UI
handler.initialX = (App.scr.width - width) / 2
handler.initialY = (App.scr.height - height) / 2
// Add UI items
addUIItem(
UIItemTextButton(
this,
text = "Click Me",
x = 50,
y = 50,
width = 200,
clickOnceListener = { _, _ ->
println("Button clicked!")
}
)
)
}
override fun updateImpl(delta: Float) {
// Update logic
}
override fun renderImpl(batch: SpriteBatch, camera: OrthographicCamera) {
// Custom rendering
batch.color = Color.WHITE
// Draw UI background, decorations, etc.
}
override fun dispose() {
// Clean up resources
}
}
```
## UI Lifecycle
### Show/Hide Timeline
The UI lifecycle follows this sequence:
```
User triggers open
show() -- Called once when opening starts
doOpening() -- Called every frame while opening
doOpening()
endOpening() -- Called once when fully opened
(UI is open)
User triggers close
doClosing() -- Called every frame while closing
doClosing()
endClosing() -- Called once when fully closed
hide() -- Called once when hidden
```
### Override Points
```kotlin
override fun show() {
super.show()
// Initialize when opening
}
override fun hide() {
super.hide()
// Clean up when closing
}
override fun doOpening(delta: Float) {
super.doOpening(delta)
// Opening animation logic
}
override fun doClosing(delta: Float) {
super.doClosing(delta)
// Closing animation logic
}
override fun endOpening(delta: Float) {
super.endOpening(delta)
// Finalize opening
}
override fun endClosing(delta: Float) {
super.endClosing(delta)
// Finalize closing
}
```
## UIItem Components
UIItems are individual UI elements added to a canvas.
### Base UIItem
```kotlin
abstract class UIItem(
val parentUI: UICanvas,
var posX: Int,
var posY: Int
) {
abstract var width: Int
abstract var height: Int
var clickOnceListener: ((Int, Int) -> Unit)? = null
var touchDraggedListener: ((Int, Int, Int, Int) -> Unit)? = null
var keyDownListener: ((Int) -> Unit)? = null
// ... more event listeners
}
```
### Built-in Components
#### UIItemTextButton
Clickable button with text:
```kotlin
UIItemTextButton(
parentUI = this,
text = "OK",
x = 100,
y = 100,
width = 150,
clickOnceListener = { _, _ ->
// Handle click
}
)
```
#### UIItemImageButton
Button with an image:
```kotlin
UIItemImageButton(
parentUI = this,
image = texture,
x = 50,
y = 50,
width = 64,
height = 64,
clickOnceListener = { _, _ ->
// Handle click
}
)
```
#### UIItemHorzSlider
Horizontal slider:
```kotlin
UIItemHorzSlider(
parentUI = this,
x = 50,
y = 100,
width = 200,
min = 0.0,
max = 100.0,
initial = 50.0,
changeListener = { newValue ->
println("Value: $newValue")
}
)
```
#### UIItemTextLineInput
Single-line text input:
```kotlin
UIItemTextLineInput(
parentUI = this,
x = 50,
y = 50,
width = 300,
placeholder = "Enter text...",
textCommitListener = { text ->
println("Entered: $text")
}
)
```
#### UIItemTextArea
Multi-line text area:
```kotlin
UIItemTextArea(
parentUI = this,
x = 50,
y = 50,
width = 400,
height = 200,
readOnly = false
)
```
#### UIItemList
Scrollable list:
```kotlin
UIItemList<String>(
parentUI = this,
x = 50,
y = 50,
width = 300,
height = 400,
items = listOf("Item 1", "Item 2", "Item 3"),
itemRenderer = { item, y ->
// Render item
},
clickListener = { item ->
println("Selected: $item")
}
)
```
#### UIItemToggleButton
Toggle button (on/off):
```kotlin
UIItemToggleButton(
parentUI = this,
x = 50,
y = 50,
width = 150,
labelText = "Enable Feature",
initialState = false,
changeListener = { isOn ->
println("Toggle: $isOn")
}
)
```
## Event Handling
### Mouse Events
```kotlin
val button = UIItemTextButton(...)
button.clickOnceListener = { mouseX, mouseY ->
// Single click
}
button.touchDownListener = { screenX, screenY, pointer, button ->
// Mouse button pressed
}
button.touchUpListener = { screenX, screenY, pointer, button ->
// Mouse button released
}
button.touchDraggedListener = { screenX, screenY, deltaX, deltaY ->
// Mouse dragged while holding
}
```
### Keyboard Events
```kotlin
item.keyDownListener = { keycode ->
when (keycode) {
Input.Keys.ENTER -> handleEnter()
Input.Keys.ESCAPE -> handleEscape()
}
}
item.keyTypedListener = { character ->
handleTypedChar(character)
}
```
### Scroll Events
```kotlin
item.scrolledListener = { amountX, amountY ->
// Mouse wheel scrolled
scrollOffset += amountY * scrollSpeed
}
```
## Positioning
### Absolute Positioning
```kotlin
// Position relative to canvas
uiItem.posX = 100
uiItem.posY = 50
```
### Relative Positioning
```kotlin
// Center horizontally
uiItem.posX = (width - uiItem.width) / 2
// Align to right
uiItem.posX = width - uiItem.width - margin
// Align to bottom
uiItem.posY = height - uiItem.height - margin
```
### Custom Positioning
```kotlin
override fun updateUI(delta: Float) {
// Dynamic positioning
followButton.posX = targetX - followButton.width / 2
followButton.posY = targetY - followButton.height / 2
}
```
## Sub-UIs
UICanvases can contain child UIs:
```kotlin
class ParentUI : UICanvas() {
val childUI = ChildUI()
init {
addSubUI(childUI)
}
}
```
Child UIs:
- Have their own lifecycle
- Can be opened/closed independently
- Positioned relative to parent
## Animations
### Opening Animation
```kotlin
override fun doOpening(delta: Float) {
// Fade in
opacity = min(1.0f, opacity + delta * fadeSpeed)
// Slide in from top
handler.posY = lerp(startY, targetY, openProgress)
super.doOpening(delta)
}
```
### Closing Animation
```kotlin
override fun doClosing(delta: Float) {
// Fade out
opacity = max(0.0f, opacity - delta * fadeSpeed)
// Slide out to bottom
handler.posY = lerp(targetY, endY, closeProgress)
super.doClosing(delta)
}
```
## Mouse Control
UIs can be mouse-controlled:
```kotlin
interface MouseControlled {
fun touchDragged(screenX: Int, screenY: Int, pointer: Int): Boolean
fun touchDown(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean
fun touchUp(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean
fun scrolled(amountX: Float, amountY: Float): Boolean
}
```
## Keyboard Control
UIs can be keyboard-navigable:
```kotlin
interface KeyControlled {
fun keyDown(keycode: Int): Boolean
fun keyUp(keycode: Int): Boolean
fun keyTyped(character: Char): Boolean
}
```
## Tooltips
UIItems can display tooltips on hover:
```kotlin
override fun getTooltipText(mouseX: Int, mouseY: Int): String? {
return if (mouseOver) {
"This is a helpful tooltip"
} else {
null
}
}
```
The tooltip system automatically displays text near the cursor.
## Toolkit Utilities
The `Toolkit` object provides UI utilities:
```kotlin
object Toolkit {
val drawWidth: Int // UI render area width
val drawHeight: Int // UI render area height
fun drawBoxBorder(batch: SpriteBatch, x: Int, y: Int, w: Int, h: Int)
fun drawBoxFilled(batch: SpriteBatch, x: Int, y: Int, w: Int, h: Int)
}
```
## Common Patterns
### Modal Dialog
```kotlin
class ConfirmDialog(
val message: String,
val onConfirm: () -> Unit
) : UICanvas() {
override var width = 400
override var height = 200
init {
// Center on screen
handler.initialX = (App.scr.width - width) / 2
handler.initialY = (App.scr.height - height) / 2
// Yes button
addUIItem(UIItemTextButton(
this, "Yes",
x = 50, y = 150, width = 100,
clickOnceListener = { _, _ ->
onConfirm()
handler.setAsClose()
}
))
// No button
addUIItem(UIItemTextButton(
this, "No",
x = 250, y = 150, width = 100,
clickOnceListener = { _, _ ->
handler.setAsClose()
}
))
}
override fun renderImpl(batch: SpriteBatch, camera: OrthographicCamera) {
// Draw background
Toolkit.drawBoxFilled(batch, posX, posY, width, height)
// Draw message
App.fontGame.draw(batch, message, posX + 50f, posY + 50f)
}
}
```
### Settings Screen
```kotlin
class SettingsUI : UICanvas() {
val volumeSlider = UIItemHorzSlider(...)
val fullscreenToggle = UIItemToggleButton(...)
init {
addUIItem(volumeSlider)
addUIItem(fullscreenToggle)
// Apply button
addUIItem(UIItemTextButton(
this, "Apply",
clickOnceListener = { _, _ ->
applySettings()
}
))
}
private fun applySettings() {
App.audioMixer.masterVolume = volumeSlider.value.toFloat()
setFullscreen(fullscreenToggle.isOn)
}
}
```
### Inventory Grid
```kotlin
class InventoryUI : UICanvas() {
val inventoryGrid = UIItemInventoryItemGrid(
parentUI = this,
inventory = player.inventory,
x = 50,
y = 50,
columns = 10,
rows = 5,
cellSize = 48
)
init {
addUIItem(inventoryGrid)
}
}
```
## Best Practises
1. **Dispose resources** — Always implement dispose() properly
2. **Cache calculations** — Don't recalculate positions every frame
3. **Use event listeners** — Don't poll mouse state manually
4. **Batch rendering** — Group draw calls efficiently
5. **Handle edge cases** — Check bounds, null items, empty lists
6. **Provide keyboard navigation** — Accessibility and convenience
7. **Test at different resolutions** — Ensure UI scales properly
## Performance Considerations
1. **Minimize UIItem count** — Too many items slow updates
2. **Cull off-screen items** — Skip rendering invisible elements
3. **Pool objects** — Reuse UIItems when possible
4. **Optimize render calls** — Batch similar draw operations
5. **Lazy initialization** — Create heavy UIs only when needed
## Debugging
### Debug Overlays
```kotlin
if (App.IS_DEVELOPMENT_BUILD) {
// Draw UIItem bounds
shapeRenderer.rect(posX, posY, width, height)
// Show mouse position
println("Mouse: ($mouseX, $mouseY)")
}
```
### UI State Logging
```kotlin
println("UI opened: ${handler.isOpened}")
println("UI opening: ${handler.isOpening}")
println("UI closing: ${handler.isClosing}")
```
## See Also
- [[Glossary]] — UI terminology
- [[Inventory]] — Inventory UI system
- [[Modules]] — Adding UIs to modules
- [[Languages]] — Localising UI text

483
World-Time-and-Calendar.md Normal file

@@ -0,0 +1,483 @@
# World Time and Calendar
**Audience:** Terrarum the game maintainers working with ingame time-based systems.
Terrarum the game uses a custom calendar system tailored specifically for the ingame lore. This guide covers the calendar structure, time representation, date formatting, and working with world time programmatically.
## Overview
The world time system provides:
- **Custom calendar** — 4 seasons, 30 days each, no leap years
- **8-day week** — Inspired by The World Calendar
- **24-hour clock** — No AM/PM, forced 24-hour format
- **Real-time mapping** — One game day = 22 real minutes
- **ISO 8601 compliance** — Time intervals follow international standard
- **Predictable equinoxes** — Always occur on the 15th of each month
## Calendar Structure
### The Yearly Calendar
A year consists of 4 seasons (months), each lasting exactly 30 days. There are no leap years.
```
=========================
|Mo|Ty|Mi|To|Fr|La|Su|Ve|
|--|--|--|--|--|--|--|--|
| 1| 2| 3| 4| 5| 6| 7| | <- Spring
| 8| 9|10|11|12|13|14| |
|15|16|17|18|19|20|21| |
|22|23|24|25|26|27|28| |
|29|30| 1| 2| 3| 4| 5| | <- Summer
| 6| 7| 8| 9|10|11|12| |
|13|14|15|16|17|18|19| |
|20|21|22|23|24|25|26| |
|27|28|29|30| 1| 2| 3| | <- Autumn
| 4| 5| 6| 7| 8| 9|10| |
|11|12|13|14|15|16|17| |
|18|19|20|21|22|23|24| |
|25|26|27|28|29|30| 1| | <- Winter
| 2| 3| 4| 5| 6| 7| 8| |
| 9|10|11|12|13|14|15| |
|16|17|18|19|20|21|22| |
|23|24|25|26|27|28|29|30|
=========================
```
### Key Calendar Facts
- **Year length:** 120 days
- **Week length:** 7 or 8 days
- **Seasons:** Spring, Summer, Autumn, Winter (30 days each)
- **Week starts:** Monday (Mondag)
- **8th day of the week:** Verddag (Winter 30th) — New Year's Eve holiday
- **New Year:** Spring 1st (Mondag)
- **Equinoxes/Solstices:** Always on the 15th of each season
### Day Names
| Day | Name | Abbreviation |
|-----|------|--------------|
| 1 | Mondag | Mo |
| 2 | Tysdag | Ty |
| 3 | Middag | Mi |
| 4 | Torsdag | To |
| 5 | Fredag | Fr |
| 6 | Lagsdag | La |
| 7 | Sundag | Su |
| 8 | Verddag | Ve |
**Note:** Verddag only occurs on Winter 30th as the year's 8th-day week completion.
### Season Names
| Season | Number | Days | Notes |
|--------|--------|------|-------|
| Spring | 1 | 1-30 | Spring 1st is New Year |
| Summer | 2 | 1-30 | |
| Autumn | 3 | 1-30 | |
| Winter | 4 | 1-30 | Winter 30th is New Year's Eve (Verddag) |
## Time Representation
### WorldTime Class
Time is managed by the `WorldTime` class:
```kotlin
class WorldTime(initTime: Long = 0L) {
var TIME_T = 0L // Time in seconds since epoch
// Time components
val seconds: Int // 0-59
val minutes: Int // 0-59
val hours: Int // 0-23
// Date components
val days: Int // 1-30
val months: Int // 1-4 (Spring=1, Summer=2, Autumn=3, Winter=4)
val years: Int // Year number
val dayOfWeek: Int // 0-7 (0=Mondag, 7=Verddag)
val dayOfYear: Int // 0-119
}
```
### Time Constants
```kotlin
const val MINUTE_SEC = 60 // 60 seconds per minute
const val HOUR_MIN = 60 // 60 minutes per hour
const val HOURS_PER_DAY = 22 // 22 hours per day
const val HOUR_SEC = 3600 // 3600 seconds per hour
const val DAY_LENGTH = 79200 // 22 * 3600 seconds per day
const val YEAR_DAYS = 120 // 120 days per year
```
### Epoch
The epoch (time zero) is:
- **Year 1, Spring 1st, 00:00:00** (Mondag)
- Represented as: `0001-01-01` or `00010101`
## Date Formatting
### Human-Readable Format
Format: `Year-MonthName-Date`
```kotlin
// Examples:
"0125-Spring-07" // Year 125, Spring 7th
"0125-Summ-15" // Year 125, Summer 15th (solstice)
"0125-Autu-22" // Year 125, Autumn 22nd
"0125-Wint-30" // Year 125, Winter 30th (Verddag)
```
**Usage:**
```kotlin
val formattedTime = worldTime.getFormattedTime()
// Returns: "0125-Spring-07"
```
### Number-Only Format
Format: `Year-Month-Date` where months are numbered 1-4:
```kotlin
// Examples:
"0125-01-07" // Year 125, Spring 7th
"0125-02-15" // Year 125, Summer 15th
"0125-03-22" // Year 125, Autumn 22nd
"0125-04-30" // Year 125, Winter 30th
```
### Computerised Format
Format: `YearMonthDate` (no separators)
```kotlin
// Examples:
01250107 // Year 125, Spring 7th
01250215 // Year 125, Summer 15th
01250322 // Year 125, Autumn 22nd
01250430 // Year 125, Winter 30th
```
**Usage:**
```kotlin
val filenameTime = worldTime.getFilenameTime()
// Returns: 01250107
```
### Short Time Format
Abbreviated for UI display:
```kotlin
val shortTime = worldTime.getShortTime()
// Returns: "Spr-07" or "Wint-30"
```
## Working with Time
### Getting Current Time
```kotlin
val worldTime = INGAME.world.worldTime
val currentHour = worldTime.hours
val currentDay = worldTime.days
val currentMonth = worldTime.months
val currentYear = worldTime.years
```
### Advancing Time
```kotlin
// Advance by 1 second
worldTime.addTime(1)
// Advance by 1 minute
worldTime.addTime(WorldTime.MINUTE_SEC)
// Advance by 1 hour
worldTime.addTime(WorldTime.HOUR_SEC)
// Advance by 1 day
worldTime.addTime(WorldTime.DAY_LENGTH)
```
### Setting Specific Time
```kotlin
// Set to a specific timestamp
worldTime.TIME_T = targetTime
// Set to specific date/time
worldTime.setTime(year = 125, month = 1, day = 15, hour = 12, minute = 30)
```
### Time Calculations
#### Day of Week
```kotlin
val dayOfWeek = worldTime.dayOfWeek
when (dayOfWeek) {
0 -> println("Mondag")
1 -> println("Tysdag")
2 -> println("Middag")
3 -> println("Torsdag")
4 -> println("Fredag")
5 -> println("Lagsdag")
6 -> println("Sundag")
7 -> println("Verddag") // Only on Winter 30th
}
```
#### Day of Year
```kotlin
val dayOfYear = worldTime.dayOfYear // 0-119
val season = dayOfYear / 30 // 0=Spring, 1=Summer, 2=Autumn, 3=Winter
val dayInSeason = (dayOfYear % 30) + 1 // 1-30
```
#### Time Until Event
```kotlin
// Calculate time until next sunrise (hour 6)
val currentHour = worldTime.hours
val hoursUntilSunrise = if (currentHour < 6) {
6 - currentHour
} else {
(22 - currentHour) + 6 // Tomorrow
}
val secondsUntilSunrise = hoursUntilSunrise * WorldTime.HOUR_SEC
```
## Solar Cycle
### Day/Night Cycle
The day lasts 22 hours with the following structure:
```kotlin
// Approximate solar positions
0h - 5h Night (dark)
6h - 8h Dawn (sunrise)
9h - 13h Day (bright)
14h - 16h Afternoon
17h - 19h Dusk (sunset)
20h - 21h Evening (twilight)
```
### Solar Angle Calculation
```kotlin
fun getSolarAngle(time: WorldTime): Double {
val hourAngle = (time.hours + time.minutes / 60.0) / 22.0
return hourAngle * 2 * Math.PI
}
fun isDaytime(time: WorldTime): Boolean {
return time.hours in 6..18
}
fun isNighttime(time: WorldTime): Boolean {
return time.hours < 6 || time.hours > 18
}
```
## Seasonal Effects
### Season Detection
```kotlin
fun getCurrentSeason(time: WorldTime): Season {
return when (time.months) {
1 -> Season.SPRING
2 -> Season.SUMMER
3 -> Season.AUTUMN
4 -> Season.WINTER
else -> Season.SPRING
}
}
enum class Season {
SPRING, SUMMER, AUTUMN, WINTER
}
```
### Equinoxes and Solstices
```kotlin
fun isEquinoxOrSolstice(time: WorldTime): Boolean {
return time.days == 15
}
fun getCelestialEvent(time: WorldTime): String? {
if (time.days != 15) return null
return when (time.months) {
1 -> "Spring Equinox"
2 -> "Summer Solstice"
3 -> "Autumn Equinox"
4 -> "Winter Solstice"
else -> null
}
}
```
## Real-Time Mapping
### Game Time vs. Real Time
One game day = 22 real minutes
```kotlin
const val REAL_SECONDS_PER_GAME_DAY = 22 * 60 // 1320 real seconds
const val GAME_SECONDS_PER_REAL_SECOND = WorldTime.DAY_LENGTH / REAL_SECONDS_PER_GAME_DAY
// = 86400 / 1320 ≈ 65.45
// One real second ≈ 65 game seconds = 1 game minute
```
### Time Scale Calculations
```kotlin
// Convert real seconds to game seconds
fun realToGameTime(realSeconds: Float): Long {
return (realSeconds * GAME_SECONDS_PER_REAL_SECOND).toLong()
}
// Convert game seconds to real seconds
fun gameToRealTime(gameSeconds: Long): Float {
return gameSeconds / GAME_SECONDS_PER_REAL_SECOND.toFloat()
}
// Example: 5 real minutes
val fiveRealMinutes = 5 * 60 // 300 real seconds
val gameTime = realToGameTime(300f) // 18000 game seconds = 5 game hours
```
## Time-Based Events
### Scheduling Events
```kotlin
class ScheduledEvent(
val triggerTime: Long,
val action: (WorldTime) -> Unit
)
val eventQueue = PriorityQueue<ScheduledEvent> { a, b ->
a.triggerTime.compareTo(b.triggerTime)
}
// Schedule event
fun scheduleEvent(worldTime: WorldTime, hoursFromNow: Int, action: (WorldTime) -> Unit) {
val triggerTime = worldTime.TIME_T + (hoursFromNow * WorldTime.HOUR_SEC)
eventQueue.add(ScheduledEvent(triggerTime, action))
}
// Process events
fun processEvents(worldTime: WorldTime) {
while (eventQueue.isNotEmpty() && eventQueue.peek().triggerTime <= worldTime.TIME_T) {
val event = eventQueue.poll()
event.action(worldTime)
}
}
```
### Daily Events
```kotlin
// Check if it's a specific hour
fun checkDailyEvent(worldTime: WorldTime, targetHour: Int, action: () -> Unit) {
if (worldTime.hours == targetHour && worldTime.minutes == 0 && worldTime.seconds == 0) {
action()
}
}
// Example: Daily shop restock at 6:00
if (worldTime.hours == 6 && worldTime.minutes == 0) {
restockShop()
}
```
### Seasonal Events
```kotlin
// Check for season start
fun checkSeasonChange(worldTime: WorldTime, previousMonth: Int) {
if (worldTime.months != previousMonth && worldTime.days == 1) {
onSeasonChange(getCurrentSeason(worldTime))
}
}
// Example: Seasonal weather change
fun onSeasonChange(season: Season) {
when (season) {
Season.SPRING -> weatherSystem.setSpringWeather()
Season.SUMMER -> weatherSystem.setSummerWeather()
Season.AUTUMN -> weatherSystem.setAutumnWeather()
Season.WINTER -> weatherSystem.setWinterWeather()
}
}
```
## Serialisation
### Saving Time
```kotlin
fun saveTime(worldTime: WorldTime): ByteArray {
return worldTime.TIME_T.toByteArray()
}
```
### Loading Time
```kotlin
fun loadTime(data: ByteArray): WorldTime {
val timeT = data.toLong()
return WorldTime(timeT)
}
```
## Best Practises
1. **Use WorldTime instance** — Don't calculate time manually
2. **Check equinoxes on day 15** — Predictable solar events
3. **Handle Verddag correctly** — 8th day only on Winter 30th
4. **Use constants** — Don't hardcode time values
5. **Schedule via TIME_T** — Use absolute timestamps for events
6. **Format consistently** — Use provided formatting functions
7. **Test year boundaries** — Ensure proper rollover from Winter 30 to Spring 1
8. **Consider time zones** — Game uses single global time
9. **Handle pause** — Time stops when game is paused
10. **Sync with world save** — Always serialise TIME_T
## Common Pitfalls
- **Assuming 24-hour days** — Days are 22 hours
- **Forgetting Verddag** — 8th day only occurs once per year
- **Hardcoding month numbers** — Use season constants
- **Not handling year rollover** — Winter 30 → Spring 1
- **Assuming AM/PM** — 24-hour clock enforced
- **Miscalculating real-time mapping** — 1 real second = 1 game minute
- **Ignoring equinox timing** — Always on 15th, not varying
- **Using wall-clock time** — Game time is independent
- **Forgetting to update events** — Process event queue regularly
- **Not serialising properly** — TIME_T must be saved
## See Also
- [[World]] — World generation and management
- [[Actors]] — Time-based actor behaviour
- [[Weather]] — Seasonal weather systems
- [[Modules-Setup]] — Module time integration

502
World.md Normal file

@@ -0,0 +1,502 @@
# World
The **GameWorld** is the container for all environmental data in Terrarum, including terrain, fluids, lighting, weather, and time. Each GameWorld represents a single playable dimension or planet.
## Overview
A GameWorld consists of:
- **Block Layers** — Terrain, walls, ores, and fluids
- **World Time** — Day/night cycle and calendar
- **Environmental Properties** — Gravity, temperature, lighting
- **Metadata** — Dimensions, spawn points, creation time
- **Weather System** — Wind, precipitation, and atmospheric effects
- **Wirings** — Electrical/conduit connections between blocks
## Creating a World
### World Construction
To create a new world programmatically:
```kotlin
val world = GameWorld(
width = 8192, // Width in tiles
height = 2048, // Height in tiles
creationTIME_T = App.getTIME_T(),
lastPlayTIME_T = App.getTIME_T()
)
```
### World Dimensions
World dimensions are measured in **tiles**. Common world sizes:
- **Small:** 4096 × 1024
- **Medium:** 8192 × 2048
- **Large:** 16384 × 4096
- **Massive:** 32768 × 8192
The engine supports millions of tiles through its chunked loading system.
**Important:** Dimensions must be positive integers. Worlds cannot be resized after creation.
### World Indices
Each world has a unique UUID:
```kotlin
val worldIndex: UUID // Unique identifier for this world
```
This index distinguishes worlds in multi-world save games.
## Block Layers
GameWorld uses multiple specialised layers to store different types of blocks:
### Layer Types
#### layerTerrain
Foreground blocks that provide collision and form the main playable surface:
```kotlin
val layerTerrain: BlockLayerGenericI16
```
- Stores 16-bit block IDs
- Provides collision for actors
- Rendered in front of walls
#### layerWall
Background wall blocks:
```kotlin
val layerWall: BlockLayerGenericI16
```
- Stores 16-bit block IDs
- Typically non-solid
- Rendered behind terrain
- Affects lighting propagation
#### layerOres
Ore deposits overlaid on terrain blocks:
```kotlin
val layerOres: BlockLayerOresI16I8
```
- Stores 16-bit ore type + 8-bit ore coverage
- Rendered as overlay on terrain blocks
- Shares damage values with terrain
#### layerFluids
Fluid simulation data:
```kotlin
val layerFluids: BlockLayerFluidI16F16
```
- Stores 16-bit fluid type + 16-bit fluid amount
- Uses cellular automata for flow simulation
- Fluids have viscosity, colour, and density
### Accessing Blocks
Block layers use tile coordinates (integers):
```kotlin
// Get a terrain block
val blockID: ItemID = world.getTileFromTerrain(x, y)
// Set a terrain block
world.setTileTerrain(x, y, blockID, false)
// Get a wall block
val wallID: ItemID = world.getTileFromWall(x, y)
// Set a wall block
world.setTileWall(x, y, wallID, false)
```
**Coordinates:**
- **x** — Horizontal tile position (0 to width-1)
- **y** — Vertical tile position (0 to height-1)
- **Origin** — Top-left corner is (0, 0)
### Block Damage
Blocks can be partially damaged:
```kotlin
val terrainDamages: HashArray<Float> // 0.0 = undamaged, 1.0 = destroyed
val wallDamages: HashArray<Float>
```
Damage is indexed by block address (computed from x, y coordinates).
## Chunk System
Worlds are divided into **chunks** for efficient storage and generation:
### Chunk Dimensions
```kotlin
val CHUNK_W = 128 // Chunk width in tiles
val CHUNK_H = 128 // Chunk height in tiles
```
Chunks enable:
1. **Lazy Generation** — Generate terrain on-demand as players explore
2. **Memory Efficiency** — Only load visible/nearby chunks
3. **Save Optimisation** — Only save modified chunks
### Chunk Flags
Each chunk has a byte of flags:
```kotlin
val chunkFlags: Array<ByteArray>
```
Chunk flags track:
- Generation status
- Modification state
- Active/inactive state
The world generates partially during creation, then generates additional chunks as needed during gameplay, enabling fast world creation even for massive world sizes.
## Tile Number Mapping
GameWorld maintains bidirectional mappings between block names (ItemIDs) and internal numbers:
```kotlin
// Name to number (for setting blocks)
val tileNameToNumberMap: HashMap<ItemID, Int>
// Number to name (for getting blocks)
val tileNumberToNameMap: HashArray<ItemID>
```
### Special Tile Numbers
- **0** — `Block.AIR` (empty space)
- **1** — `Block.UPDATE` (triggers block update)
- **65535** — `Block.NOT_GENERATED` (chunk not yet generated)
This mapping system allows worlds to persist blocks even when mods are added or removed, using the `dynamicToStaticTable` for remapping.
## Spawn Points
### Player Spawn
The tilewise coordinates where players initially appear:
```kotlin
var spawnX: Int
var spawnY: Int
// Convenience property
var spawnPoint: Point2i
```
### Portal Point
Optional alternative spawn point (e.g., for teleportation):
```kotlin
var portalPoint: Point2i?
```
## World Time
GameWorld includes a sophisticated time system:
```kotlin
val worldTime: WorldTime
```
### Time Units
WorldTime uses **seconds** as the base unit:
```kotlin
const val SECOND_SEC = 1L
const val MINUTE_SEC = 60L
const val HOUR_SEC = 3600L
const val DAY_SEC = 86400L
```
### Calendar System
The world uses a 360-day calendar:
- **12 months** of 30 days each
- **Days** are 24 hours
- **Year 125 (EPOCH)** is the default starting year
### Time Properties
```kotlin
worldTime.timeDelta // Seconds elapsed since last update
worldTime.TIME_T // Total seconds since epoch
```
### Celestial Calculations
WorldTime calculates sun/moon positions and phases:
```kotlin
worldTime.solarNoonTime // Time of solar noon today
worldTime.lunarPhase // Current moon phase (0.0-1.0)
worldTime.eclipticLongitude // Sun's position on ecliptic
```
The time system simulates realistic seasons, equinoxes, and solstices.
### Day/Night Cycle
The day/night cycle affects:
1. **Global lighting** (`world.globalLight`)
2. **Actor behaviour** (day/night AI changes)
3. **Block properties** (dynamic light sources)
## Environmental Properties
### Gravity
Gravitational acceleration vector:
```kotlin
var gravitation: Vector2 = DEFAULT_GRAVITATION
```
Default gravity is approximately 9.8 m/s² downward. Currently, only downward gravity is supported.
### Global Light
The baseline lighting level for the world:
```kotlin
var globalLight: Cvec // RGB+UV colour vector
```
Global light represents:
- Sunlight during day
- Moonlight/starlight at night
- Ambient light in caves (typically near-zero)
Global light is **additive** with block luminosity when calculating the final lightmap.
### Average Temperature
The world's baseline temperature in Kelvin:
```kotlin
var averageTemperature: Float = 288f // 15°C
```
This affects environmental generation and could be used for climate simulation.
## Weather System
GameWorld includes a weather simulation:
```kotlin
var weatherbox: Weatherbox
```
### Weatherbox
The weatherbox manages:
- **Current weather type** (clear, rain, snow, fog, etc.)
- **Wind direction and speed** (with temporal smoothing)
- **Precipitation intensity**
- **Atmospheric effects**
Weather types are defined in the **WeatherCodex**.
### Wind
Wind is simulated with temporal coherence using control points:
```kotlin
weatherbox.windDir // Wind direction (0.0-1.0 representing angles)
weatherbox.windSpeed // Wind speed (m/s)
```
Both properties use a point-interpolation system (pM3, pM2, pM1, p0, p1, p2, p3) for smooth transitions.
## Wiring and Conduits
GameWorld supports wire/conduit networks for connecting devices:
```kotlin
val wirings: HashedWirings
```
### Wire Types
Multiple wire types can occupy the same tile position:
- Electrical wires
- Fluid pipes
- Logic gates
- Data cables
Each wire type is stored separately in the wirings hash map.
### Wire Connections
Wires connect in four directions (up, down, left, right) using bit flags:
```kotlin
// Connection bit positions
WIRE_POS_MAP = [1, 2, 4, 8] // Up, Right, Down, Left
```
## Dynamic Items
Worlds can have dynamic item inventories that persist:
```kotlin
internal val dynamicItemInventory: ItemTable
```
This stores items that are unique to this world (e.g., procedurally generated equipment).
## Game Rules
Arbitrary world-specific configuration:
```kotlin
val gameRules: KVHashMap
```
Game rules can store custom world settings such as:
- Difficulty modifiers
- Gameplay options
- World-specific flags
## World Lifecycle
### Creation Time
```kotlin
internal var creationTime: Long // Unix timestamp
```
Records when the world was first created.
### Play Time
```kotlin
internal var lastPlayTime: Long // Last time the world was played
internal var totalPlayTime: Long // Cumulative play time in seconds
```
These track how long the world has been played.
### Disposal
When switching worlds or exiting, dispose of the world properly:
```kotlin
world.dispose()
```
This frees:
- Block layer memory
- Chunk data
- Cached resources
**Warning:** Once disposed, a world cannot be used again. Set `world.disposed` flag to prevent accidental access.
## Advanced Topics
### SimpleGameWorld
For small, fixed-size worlds (e.g., mini-games), use `SimpleGameWorld`:
```kotlin
class SimpleGameWorld(width: Int, height: Int) : GameWorld(width, height)
```
SimpleGameWorld stores all data in a single file rather than using chunked storage.
### Random Seeds
Worlds store 128 random seeds for deterministic generation:
```kotlin
val randSeeds: LongArray // 256 longs = 128 128-bit seeds
```
Specific ranges are reserved:
- Seeds 0-1: Roguelike randomiser
- Seeds 2-3: Weather mixer
- Seeds 4+: Available for custom use
### Generator Seed
The primary seed used for world generation:
```kotlin
var generatorSeed: Long
```
This seed determines the initial terrain, caves, ores, and structures.
## Best Practises
1. **Always check bounds** before accessing blocks: `x in 0 until world.width`
2. **Use spawn points** rather than hardcoding coordinates
3. **Dispose worlds** when switching or exiting to prevent memory leaks
4. **Update world time** every frame using `worldTime.timeDelta`
5. **Respect chunk boundaries** when implementing generation algorithms
6. **Use tile number mapping** for mod compatibility
7. **Set appropriate global light** for the world's environment
## Common Patterns
### Iterating Over Visible Tiles
```kotlin
val camera: WorldCamera = ...
for (y in camera.yTileStart until camera.yTileEnd) {
for (x in camera.xTileStart until camera.xTileEnd) {
val block = world.getTileFromTerrain(x, y)
// Process block
}
}
```
### Finding Safe Spawn Point
```kotlin
fun findSafeSpawnY(world: GameWorld, x: Int): Int {
for (y in 0 until world.height) {
val terrain = world.getTileFromTerrain(x, y)
val above = world.getTileFromTerrain(x, y - 1)
if (BlockCodex[terrain].isSolid && !BlockCodex[above].isSolid) {
return y - 1 // One tile above solid ground
}
}
return world.spawnY // Fallback
}
```
### Checking Block Properties
```kotlin
fun isBlockSolid(world: GameWorld, x: Int, y: Int): Boolean {
val blockID = world.getTileFromTerrain(x, y)
return BlockCodex[blockID]?.isSolid ?: false
}
```
## See Also
- [[Glossary]] — World-related terminology
- [[Save and Load]] — Persisting worlds to disk
- [[Modules:Blocks]] — Defining custom blocks
- [[Modules:Weather]] — Creating weather types
- [[Development:RAW]] — Data-driven world generation