Modding
AnvilHeart loads Lua mods at startup. Mods use the same scripting API that our own gameplay is written in, so you can add items, villagers, events, interface screens and whole new systems. Everything lives under the global digi: 152 calls across 17 namespaces, listed below.
Mods run in a sandbox: there is no io, os is cut down to clock and date functions, and the string metatable is locked, and each mod gets its own environment and require.
Entities are opaque handles. Pass them back into the API, compare them with == and check them with digi.valid before use after a frame boundary.
Installing mods
Put each mod in its own folder inside the mods folder next to the game, then start AnvilHeart. Mods with a higher priority load first, and a mod with a broken manifest is skipped with a warning in the log instead of stopping the game.
Your first mod
A mod is a folder with a mod.json manifest and an entry script. Higher priority mods load first; ties load in folder name order.
mods/
better_forge/
mod.json
main.lua
assets/
{
"name": "Better Forge",
"version": "1.0.0",
"description": "Faster heating, hotter coals.",
"entry": "main.lua",
"priority": 10
}
Events
Subscribe with digi.on(name, fn) and raise your own with digi.emit(name, ...).
| Event | Arguments | When |
|---|---|---|
update | dt | Every frame. |
sceneLoaded | After a scene and its mods finish loading. | |
areaChanged | area | The local player entered a different area. |
npcArrived | entity | An NPC reached the end of its path. |
npcBlocked | entity | An NPC could not find or follow a path. |
netJoining | A join attempt started. | |
netConnected | This client connected to a host. | |
netDisconnected | reason | The session ended or the host went away. |
netRejected | reason | The host refused the join. |
netPlayerJoined | peer, name, uid | A player joined the session. |
netPlayerLeft | peer | A player left the session. |
net:<name> | payload, from | A custom message sent with digi.net.send. |
digi
Entities, events, transforms, spawning and the core game loop.
| Call | Returns |
|---|---|
digi.assetExists(path: string) | |
digi.component(name: string, definition: table) | |
digi.dashPlayer(vx: number, vz: number, seconds: number) | |
digi.data(path: string) | any |
digi.destroy(entity: entity) | |
digi.emit(event: string, ...) | |
digi.facePlayer(x: number, z: number) | |
digi.find(name: string) | any |
digi.findAll(name: string) | |
digi.log | |
digi.model(entity: entity) | string |
digi.name(entity: entity) | string |
digi.on(event: string, function: function) | |
digi.parent(entity: entity) | any |
digi.paused() | |
digi.player() | any |
digi.playerForward() | |
digi.playerInput() | |
digi.playerPosition() | |
digi.position(entity: entity) | |
digi.quit() | |
digi.reload() | |
digi.rotation(entity: entity) | |
digi.scale(entity: entity) | |
digi.scene() | |
digi.setLight(entity: entity, intensity: number, range?: number) | |
digi.setModel(entity: entity, path: string) | |
digi.setPaused(paused: boolean) | |
digi.setPlayerInput(enabled: boolean) | |
digi.setPlayerMotion(speed: number, traction: number) | |
digi.setPosition(entity: entity, x: number, y: number, z: number, localOnly?: boolean) | |
digi.setRotation(entity: entity, pitch: number, yaw: number, roll: number, localOnly?: boolean) | |
digi.setScale(entity: entity, x: number, y?: number, z?: number, localOnly?: boolean) | |
digi.setVisible(entity: entity, visible: boolean, localOnly?: boolean) | |
digi.spawn(description: any, x?: number, y?: number, z?: number) | any |
digi.spawnLocal(description: table) | any |
digi.time() | |
digi.valid(entity: entity) | |
digi.warn(message: string) |
digi.animator
Animation graph state and triggers on skinned entities.
| Call | Returns |
|---|---|
digi.animator.set(entity: entity, parameter: string, value: any) | |
digi.animator.state(entity: entity) | string |
digi.animator.trigger(entity: entity, name: string) |
digi.appearance
Per-entity model swaps, mesh masks and material overrides.
| Call | Returns |
|---|---|
digi.appearance.get(entity: entity) | |
digi.appearance.meshes(entity: entity) | |
digi.appearance.set(entity: entity, appearance: any) |
digi.area
Interiors and other areas that live inside the one world.
| Call | Returns |
|---|---|
digi.area.current() | |
digi.area.enter(id: string, x: number, y: number, z: number, facingDegrees?: number) | |
digi.area.list() | |
digi.area.move(entity: entity, id: string, x: number, y: number, z: number, facingDegrees?: number) | |
digi.area.name(id?: string) | string |
digi.area.of(entity: entity) | string |
digi.audio
Sound playback and volume.
| Call | Returns |
|---|---|
digi.audio.play(path: string, options?: table) | |
digi.audio.play3d(path: string, x: number, y: number, z: number, options?: table) | |
digi.audio.setVolume(bus: string, volume: number) | |
digi.audio.stop(id: any, fade?: number) | |
digi.audio.volume(bus: string) |
digi.camera
Camera position, projection and scripted overrides.
| Call | Returns |
|---|---|
digi.camera.override(x: number, y: number, z: number, yawDegrees: number, pitchDegrees: number, fovDegrees?: number) | |
digi.camera.position() | |
digi.camera.project(x: number, y: number, z: number) | any |
digi.camera.release() |
digi.environment
Time of day, sky, fog and weather.
| Call | Returns |
|---|---|
digi.environment.get() | |
digi.environment.hour() | |
digi.environment.set(values: table) | |
digi.environment.setHour(hour: number) | |
digi.environment.setTimeScale(scale: number) | |
digi.environment.timeScale() |
digi.input
Actions, axes, rebinding and gamepad state.
| Call | Returns |
|---|---|
digi.input.axis(axis: string) | |
digi.input.axisKey(axis: string, index: integer) | |
digi.input.bindings(action: string) | |
digi.input.cancelCapture() | |
digi.input.capture(mode?: string) | |
digi.input.captured() | any |
digi.input.capturing() | |
digi.input.down(action: string) | |
digi.input.hasGamepad() | |
digi.input.mouseCaptured() | |
digi.input.padFace(button: string) | |
digi.input.padFamily() | |
digi.input.pressed(action: string) | |
digi.input.released(action: string) | |
digi.input.setMouseCaptured(captured: boolean) | |
digi.input.usingGamepad() |
digi.memory
Values kept across scene reloads for the session.
| Call | Returns |
|---|---|
digi.memory.get(key: string) | any |
digi.memory.set(key: string, value: any) |
digi.nav
Navigation grids and pathfinding.
| Call | Returns |
|---|---|
digi.nav.build(id: string, minX: number, minZ: number, maxX: number, maxZ: number, options?: table) | |
digi.nav.buildArea(id: string, options?: table) | integer |
digi.nav.path(id: string, x1: number, y1: number, z1: number, x2: number, y2: number, z2: number) | any |
digi.nav.walkable(id: string, x: number, y: number, z: number) |
digi.net
Co-op sessions, players, authority and custom messages.
| Call | Returns |
|---|---|
digi.net.appearance(peer: integer) | |
digi.net.authority() | |
digi.net.host(port?: any) | |
digi.net.isClient() | |
digi.net.isHost() | |
digi.net.join(address: string, port?: any) | |
digi.net.leave() | |
digi.net.localPeer() | |
digi.net.name(peer?: integer) | string |
digi.net.playerArea(peer: integer) | string |
digi.net.playerPosition(peer: integer) | any |
digi.net.players() | |
digi.net.role() | |
digi.net.send(target: entity, name: string, payload?: any) | |
digi.net.setAppearance(appearance: any) | |
digi.net.setName(name: string) | |
digi.net.transport() |
digi.npc
Moving kinematic NPCs along navigation paths.
| Call | Returns |
|---|---|
digi.npc.face(entity: entity, x: number, z: number) | |
digi.npc.moveTo(entity: entity, x: number, y: number, z: number, speed?: number) | |
digi.npc.moving(entity: entity) | |
digi.npc.stop(entity: entity) |
digi.physics
Raycasts, overlaps, velocity and impulses.
| Call | Returns |
|---|---|
digi.physics.gravity() | |
digi.physics.impulse(entity: entity, x: number, y: number, z: number) | |
digi.physics.overlapBox(cx: number, cy: number, cz: number, hx: number, hy: number, hz: number, yawDegrees?: number) | |
digi.physics.raycast(ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, distance: number) | any |
digi.physics.setGravity(x: number, y: number, z: number) | |
digi.physics.setVelocity(entity: entity, x: number, y: number, z: number) | |
digi.physics.velocity(entity: entity) |
digi.settings
Player settings with defaults, validation and saving.
| Call | Returns |
|---|---|
digi.settings.all() | |
digi.settings.defaults() | |
digi.settings.get(key: string) | |
digi.settings.reset(section?: string) | |
digi.settings.resolutions() | |
digi.settings.save() | |
digi.settings.set(key: string, value: any) |
digi.steam
Steam identity, friends, lobbies, presence and achievements.
| Call | Returns |
|---|---|
digi.steam.achieved(name: string) | |
digi.steam.available() | |
digi.steam.deck() | |
digi.steam.friends() | |
digi.steam.id() | |
digi.steam.invite() | |
digi.steam.join(friendId: string) | |
digi.steam.keyboard(x: integer, y: integer, width: integer, height: integer) | |
digi.steam.lobby() | string |
digi.steam.name() | |
digi.steam.setPresence(key: string, value: string) | |
digi.steam.unlock(name: string) |
digi.storage
Save slots in the game's save folder.
| Call | Returns |
|---|---|
digi.storage.exists(name: string) | |
digi.storage.load(name: string) | any |
digi.storage.remove(name: string) | |
digi.storage.save(name: string, value: any) |
digi.ui
RmlUi documents, text, classes and gamepad navigation.
| Call | Returns |
|---|---|
digi.ui.activate(doc: string) | |
digi.ui.clearNavigation(doc: string) | |
digi.ui.focusRect(doc: string) | any |
digi.ui.hide(doc: string) | |
digi.ui.navigate(doc: string, dx?: integer, dy?: integer) | |
digi.ui.on(doc: string, id: string, event: string, handler: function) | |
digi.ui.setClass(doc: string, id: string, name: string, enabled: boolean) | |
digi.ui.setProperty(doc: string, id: string, property: string, value: string) | |
digi.ui.setRml(doc: string, id: string, rml: string) | |
digi.ui.setText(doc: string, id: string, text: any) | |
digi.ui.show(doc: string, modal?: boolean) | |
digi.ui.value(doc: string, id: string) | |
digi.ui.visible(doc: string) |