Skip to content

Events and timers

This page is part of Writing Lua scripts. The functions are listed in the reference: events and timer.

Save this listener as scripts/common/character_log.lua:

local character_log = {}
character_log.handle = events.on("character_created", function(e)
log.info("New character {Name}", e.name)
end)
return character_log

Append require("common.character_log") to the existing scripts/init.lua, keeping its other code. Start the server or enter script reload init.lua in the server console. Create a new character from the client: the console log should show New character followed by its name. Logging in with an existing character does not raise character_created.

Keep the returned module table if you need to unsubscribe later. To remove the listener deliberately, run this from code that requires the same module:

local character_log = require("common.character_log")
events.off(character_log.handle) -- returns false when the handle is unknown

The second block is cleanup; do not append it to the first test, or the listener will be removed before a character can be created.

  • Each handler runs on the game loop as a coroutine, so it may call wait().
  • Handlers of one event run in the order they subscribed. Subscribing or unsubscribing inside a handler takes effect from the next event.
  • Every handler receives its own table; changing it does not affect other handlers.
  • Events are notifications: a handler cannot cancel or change what happened. An error in a handler is reported like any script error, and the other handlers still run.
  • Subscriptions belong to the file that made them. Reloading or invalidating the file removes them, like its timers; stopping the engine removes all of them.
  • An unknown event name raises an error in events.on. The generated definitions.lua lists the valid names as the EventName alias, so editors complete them.
Event Fields
character_created serial, account_id, name, race and gender (numbers of the race and the gender; scripts get no enum tables for them), map, x, y, z. Raised after a new character and its starting items are saved.
character_deletion_requested serial, account_id, name. Raised after a player asks to delete a character; it stays restorable until removed.
character_entered_world serial, account_id, name, map, x, y, z. Raised after a character entered the world and the client’s login completed.
player_say serial, name, text, type. Raised after a player’s character said something and the players and NPCs around heard it; text is what they heard and type how it was said, a number such as SpeechType.Yell. A command (text starting with a dot) raises nothing.
character_left_world serial, account_id, name, map, x, y, z. Raised after a character left the world because its session closed, once its save was attempted.
player_region_changed serial, name, previous, current, map, x, y, z. Raised when a player’s character walks or is teleported from a region into another, or changes map; previous and current are the regions’ names, nil outside every region, and previous is also nil when the character just entered the world
item_created serial, template (nil when the item has none). Raised when a new item entered the world: made by a script, a vendor, a spawn or a craft. What is loaded at startup or at a login, the rest of a split stack, the items .decorate places and the items a book attachment settles raise nothing.
item_deleted serial, template. Raised when an item was deleted: decay, a merge into a stack, item.delete, the gear of a removed NPC. Logging out raises nothing.
mobile_created serial, template. Raised when an NPC was spawned, also a pet that came out of a stable, a ridden pet that was dismounted or a bonded pet raised from its corpse: the creature has a new serial each time. The NPCs loaded at startup raise nothing, and an ethereal statuette makes no creature.
mobile_deleted serial, template. Raised when an NPC left the world: after its death, .remove, a pet that went into a stable, or a pet that was mounted and became the mount item its rider wears. A pet that comes back has a new serial.
mobile_died serial, killer, corpse (0 for none). Raised when a player or an NPC died; a human body raises it when its fall is over.
pet_tamed serial (the pet), tamer. Raised when a creature became a player’s own, by pet.tame or .tame.
craft_succeeded actor, craft (the id of the craft), item (the serial of what was made). Raised by the shared crafting script when a try succeeded; a failed try raises nothing.
corpse_carved serial (the corpse), carver, items (how many items were made, inside the corpse or beside it, a stack counting as one). Raised when a knife took items off a corpse (carve.corpse); a carve that found nothing raises nothing.
party_invite party_id, leader, invitee. Raised when a leader invited a player; party_id is the number of the party, kept while the server runs.
party_joined party_id, member. Raised when an invited player accepted and joined the party.
party_left party_id, member, reason (left, kicked or logout). Raised when a member is no longer in the party, also the one whose leaving ends it.
party_disbanded party_id, reason (too_small when fewer than two members remained, emptied when the only member’s last invitation ended). Raised when a party is gone.

The last twelve are notifications of the world, not of a character: the server publishes each one only while a script listens to it, so an event nobody subscribed to costs one lookup. Script hooks lists them next to the hooks and says what is not built.

Each delivery is one work item of the game loop. A mass fill with a listener on item_created, such as a spawn or a script that makes thousands of items, posts one work item for each item; when the loop’s queue is full the events are dropped, with a warning in the log.

Hosts and plugins publish a bus event to Lua with an explicit registration, before the engine starts:

container.AddScriptEvent<MyEvent>(
"my_event",
e => new Dictionary<string, object?> { ["name"] = e.Name, ["amount"] = e.Amount });

The name must be snake_case and unique, and each event type is published once. The mapping runs on the publishing thread and must only read the event. It may return strings, booleans, numbers, enums (sent as numbers) or null. A mapping that fails is logged, and the event is skipped for Lua only. Events published while no script is subscribed cost one lookup and are not queued.

All timer delays and wait arguments are in seconds.

timer.after runs a function once, timer.every repeats it and timer.cancel stops either; wait parks the coroutine that calls it.

Call wait from a scheduled coroutine, such as a timer callback, not at the top level of init.lua or a required module. It needs a positive, finite delay that fits the timer range. It yields the coroutine rather than blocking the thread. Each repeating timer occurrence starts a coroutine: if one calls wait for longer than the repeat interval, multiple suspended invocations can coexist. Cancelling the timer prevents later starts; it does not cancel an already-started coroutine. For sequences that must not overlap, use a one-shot callback that schedules its next run only after its work finishes.

Made with ❤️ by Tom

Buy me a coffee