Skip to content

Script hooks

This page is part of Writing Lua scripts. It is the single list of the functions the server calls on a mobile script or an item script: what each one is called with, when it fires, whether it runs inside the action or after it, and what its answer means. The two pages above explain each one with examples.

Every function is optional: a script defines the ones it wants, and a hook no script defines costs the server a single lookup in a cache.

The first argument is always the serial of the object that owns the script (the NPC, or the item). After it come the mobile that did the thing, the actor, and then the details of what it did:

(serial, actor, payload...)

New hooks name their arguments actor, target, item, container and amount. Hooks that existed before this convention keep their argument names and order, because a script written against them would break: they are marked “legacy” in the table below, and their order will not change.

A hook is one of three kinds:

  • can_* is a veto. It is asked before the action and answered at once; only false refuses. A missing function, an error, a wait() or any other value lets the action go on, so a broken script never locks the game.
  • on_* is a notice of something that happened. Its answer is ignored.
  • A few on_* are answers: the server reads the return value, such as true for “handled”.

A hook runs sync, inside the action, on the game loop, and must be quick; or it is queued, posted to the next turn of the game loop after the action and its packets, so the script sees the world as the action left it and may delete or move the item. A hook marked mixed is sync, except when a script caused the action: a running script cannot start another, so the server queues it.

The server protects itself from a script that breaks:

  • An error in a veto or answer hook counts as “no hook”: the action goes on. The script error is reported as any other, and the server logs once a minute for each script and function that the hook was treated as absent.
  • Veto hooks, immediate answer hooks (not item on_use) and on_delete can run while a script is running, as when a spell script harms an NPC: the script that acts is set aside and goes on when the hook has answered. A hook that must answer immediately gives no usable answer if it calls wait(): the action does not wait for its eventual return.
  • A veto or answer hook is not called again for an object while the same function of that object runs: a handler that damages, heals or moves its own object gets “no hook” from the nested call. Another function of the same object still runs, so an item that deletes itself from can_pick_up still hears on_delete.
  • A hook asked from inside a hook nests at most 8 deep; one that would be the ninth is not run and counts as “no hook”, logged once a minute like any failing hook. A pack of NPCs that share damage with combat.harm must stop before that.
  • What a script defines is looked up once for each script and function, and again after the script is reloaded with script reload.
  • Choose the timing by what the hook has to do. A sync hook runs inside the action, so it is the only kind that may refuse (can_die, can_buy) or change a number (on_damage); keep it short, since the game loop waits for it. A queued hook runs after the action, when the world already shows the result, so it may do the slow or the destructive thing, such as deleting the item.
  • A function that is not defined costs a lookup, and one that is defined is called every time: do not add an empty function “just in case”, and make the cheap test first (if kind ~= "melee" then return end).
  • The recursion guard protects the server, not your logic: a veto or answer hook that changes its own object, such as on_damage calling combat.harm on the same NPC, gets “no hook” from the nested call, so the second blow is not changed. Write the handler so that it does not need to be asked twice.
  • In on_delete the item is still there and can be read, but it is going: read what you need, stop its timers with item.stop_timer and clear what it owns. Do not delete or move the item or its container, and do not put anything into it. Each item inside a container hears its own on_delete before the container does: when a container decays, is removed with .remove or is cleaned up with its corpse, it is already empty when its own on_delete runs. When an NPC is removed its gear and what it carries go contents first too, but its whole inventory still lies where it was until every on_delete has run. An item is told once: one that another handler deleted meanwhile is not told again.
  • Use the Wait column below, not the timing label alone. sync describes when the first call begins; it does not mean every such hook forbids suspension. Queued notices and coroutine-based notices may call wait(seconds). Item on_use may wait too: it counts as handled immediately and its later return does not change the default action.
  • Every veto and immediate answer hook must return before waiting; on_think must also finish immediately (waiting logs a warning), and on_delete must clean up before the item disappears. on_death must finish without waiting, because NPC removal follows its first return or suspension; use a timer for later work on its corpse. The scheduler can still resume a handler that breaks these rules, but its late answer cannot affect the finished action. After any permitted wait, check that the item or mobile still exists, and that its owner, location and other assumptions still hold.
  • can_buy and can_sell are asked before the transaction, while the window is open: they must not open or close a vendor window, move or consume goods, or move the player. A script that does is not trusted: a purchase whose window was replaced is dropped, and a sale whose goods changed or whose player left the vendor pays nothing.
Hook Owner Kind Arguments Runs Wait When Returns
can_die NPC veto serial, killer sync No A blow or a poison tick, after on_damage, would leave the NPC with no hit points, before its death. Not asked for .kill and mobile.kill, which are explicit kills, nor for players. false keeps the NPC alive at 1 hit point: the blow counts as one that did not kill (the NPC fights back, on_health_low may fire) and the next lethal blow asks again.
on_attack NPC answer serial, target sync No The NPC is about to swing at its target, once the swing delay is due. Must not wait. false cancels this swing: its delay is spent, so the hook is asked again only when the next swing is due, nothing is played or hurt, there is no on_miss, and the time of the fight is not renewed by the swing.
on_combat_end NPC notice serial, target, reason queued Yes The NPC lost its target. reason is killed (the target died), lost (it left the world or the map), expired (ultima.combat.combatant_seconds without a swing), fled (the fight was ended with combat.stop while the NPC runs from blows, combat.flees or combat.passive) or stopped (anything else: combat.stop, an error in the fight). Not raised when the NPC itself dies, on_death says that. Ignored.
on_combat_start NPC notice serial, target queued Yes The NPC got a target while it had none: it attacked, or it was hit or aggressed and fights back. Changing target in a fight raises nothing. Ignored.
on_context_menu NPC answer serial, player sync No A player asked for the context menu of the NPC. Must not wait. The entries to add, a table of { id, cliloc, range, enabled }, or nothing.
on_context_menu_select NPC notice serial, player, id queued Yes The player chose one of the entries the script added. Ignored.
on_damage NPC answer serial, attacker, amount, kind sync No The NPC is about to lose hit points, before they change: a blow (melee, or ranged for a bow), harm done with combat.harm (spell by default, other for a field, an explosion or a trap), a poison tick (poison) or the share of a blow that Reactive Armor sends back (other). attacker is 0 when no one did it; for a poison tick it is the mobile that applied the poison, or 0 when that is not known. Also when a script, such as a spell, does the harm. A number replaces the damage (whole, 0 or more); false is no damage; nil, true or anything else leaves it. A damage of 0 hurts for nothing: the hurt animation plays, there is no blood and no on_hit.
on_death NPC notice serial, corpse, killer mixed No The NPC died, after its corpse exists and before it leaves the world. Legacy: the corpse comes before the killer. Ignored.
on_drag_drop NPC answer serial, giver, item sync No A player dropped an item on the NPC from 2 tiles or closer. true: the script took the item. Anything else gives it back.
on_enter_region NPC notice serial, region queued Yes The NPC stepped, was teleported or appeared (spawned, brought back) inside a named region that is not the one it was in; region is its name, as world.region gives it. A region with no name raises nothing, and an NPC whose script defines neither this nor on_leave_region costs nothing per step. Ignored.
on_heal NPC answer serial, healer, amount sync No The NPC is about to get hit points back through mobile.heal(patient, amount, healer), which bandages (the veterinary too) and the heal spells use; healer is 0 when none. Not for regeneration, which is not a heal, nor for mobile.set_stats, nor for potions, which only players drink. As on_damage: a number replaces the amount (whole, 0 or more), false gives none, anything else leaves it.
on_health_low NPC notice serial, hits, hits_max, attacker queued Yes The hit points fell under the template’s low_health_at percent, from damage that did not kill. Legacy: the attacker comes last. Ignored.
on_hit NPC notice serial, target, damage queued Yes The blow of the NPC landed and hurt, with the damage that was dealt after on_damage of the target. A blow that was absorbed to nothing raises neither this nor on_miss. Ignored.
on_leave_region NPC notice serial, region queued Yes The NPC stepped or was teleported out of a named region; region is the name of the one it left. Raised before on_enter_region of the one it entered. Removing the NPC from the world raises nothing. Ignored.
on_mana_low NPC notice serial, mana, mana_max, needed queued Yes The mana of a casting NPC fell under what its cheapest spell costs. Ignored.
on_miss NPC notice serial, target queued Yes The swing of the NPC missed. Ignored.
on_mobile_in_range NPC notice serial, other queued Yes Another mobile came within ultima.npcs.sense_range, by a step or by entering the world; once per arrival. Ignored.
on_mobile_killed NPC notice serial, killed, killer queued Yes A mobile was killed within sense range; the killed one is not told. Legacy: the killer comes last. Ignored.
on_release NPC notice serial, owner queued Yes A pet is nobody’s any more: the owner let it go (pet.release), it disobeyed down to no loyalty and went wild, or it deserted. The script sees it already wild; owner is the one that had it. Stabling, a death and the staff giving it to another raise nothing. Ignored.
on_resurrect NPC notice serial, healer queued Yes An NPC is back from its corpse: a bandage on the corpse of a bonded pet, mobile.resurrect(corpse, healer) and .resurrect. The NPC that rises is a new one, born where the corpse lay, with its own serial; healer is 0 when none. Players raised by an ankh, a healer or a spell raise nothing. Ignored.
on_spawn NPC notice serial queued Yes Once, after the NPC is spawned with .spawn, in the world and shown. Not for the saved NPCs loaded at startup. Ignored.
on_speech NPC notice serial, speaker, text, keywords, type sync Yes A player spoke within hearing of the NPC; commands are not heard. Ignored.
on_tame NPC notice serial, tamer queued Yes A creature became a player’s own: pet.tame succeeded, or the staff gave it with .tame. The script sees it already owned. Ignored.
on_think NPC notice serial sync No Every think of the NPC (ultima.npcs.think_interval_ms) while a player is within the 5×5 sectors around it. Must not wait: the server warns once per script. Ignored.
can_decay Item veto serial sync No The decay time of an item lying on the ground is due, before it decays. Asked of the item that decays, not of what lies inside it. No script runs during a world save. Nothing is asked of an item that never decays. false keeps the item, and it is asked again after one more decay time (decay_minutes of its template, 60 by default).
can_drop Item veto serial, dropper, destination sync No A player is about to put the held item down, anywhere. false sends the item back where it was lifted from.
can_drop_on Item veto serial, dropper, item sync No Asked of an item that is not a container: a player is about to drop the held item on it, from 2 tiles or closer if it lies on the ground, or carrying it. serial is the target. Asked after can_drop of the item and before every other rule, including a stack merging and the placing beside it. Containers use can_insert. false sends the item back where it was lifted from; nothing is raised.
can_equip Item veto serial, wearer sync No The held item is about to be worn by wearer. false sends the item back.
can_insert Item veto serial, mobile, item sync No Asked of a container: a player is about to put item into it, or onto a stack lying directly in it. false sends the item back.
can_pick_up Item veto serial, picker sync No A player is about to lift the item, once every rule of the server allows it. false refuses.
on_added Item notice serial, container, actor queued Yes The item entered a container or the backpack of a mobile, by any path: a drag and drop, item.move_into, item.give, a created item put straight inside (loot, a purchase, a crafted piece, the contents of a chest, the gear in the backpack of a spawned NPC). actor is the mobile that caused it: the player who dropped the item, the buyer of a purchase, the mobile that item.give gave it to; 0 when none did, such as loot put in a corpse, item.move_into from a script, the contents of a chest or the gear of a spawned NPC. Not for a worn item (on_equip covers it), not for a stack that grows or splits, not for items loaded at startup or at login, placed by the staff’s .decorate, or made by a book attachment (a letter’s bearer attachments), which come into the backpack silently. Dropped if the item was deleted meanwhile. Ignored.
on_carving Item answer serial, carver, items sync No A knife is about to take items off the corpse, once the corpse is known to be carvable (an NPC’s that is no pet, lying on the ground, not carved before) and its carve table was rolled; the corpse is judged again after the answer; asked of the script of the corpse (scripts/items/corpse.lua), before any item is made. items is the roll, an array of { item = template id, amount = n }, empty for a corpse with no table. Must not wait. false cancels: nothing is made, the corpse is not marked carved and the carver is told it cannot use the blade on that. A table of the same shape replaces the roll: an entry that is not well formed or names no item template is left out. Anything else keeps it.
on_context_menu Item answer serial, player sync No A player asked for the context menu of an item in view. Must not wait. The entries to add, as for an NPC.
on_context_menu_select Item notice serial, player, id queued Yes The player chose one of the entries the script added. Ignored.
on_create Item notice serial queued Yes A new item entered the world: the gear and loot of a spawned NPC, or the contents of a chest a spawn region made. Ignored.
on_darkness Item notice serial, dark queued Yes The spot of a lamp post turned dark (true) or light (false). Ignored.
on_delete Item notice serial sync No The item is being deleted, by any way: decay, being merged into a stack, item.delete, item.consume, .remove, its container being deleted with its contents, the gear of a removed NPC, the cleanup of a corpse. Asked once for each item, before it leaves the world: the item, its props and its container can still be read. An item that is not in the world, such as a chest a spawn could not finish, is not asked, and one a handler deleted from inside its own hook (can_pick_up, can_decay) or from another item’s on_delete is asked only once. Use it to clean up timers and props. A handler must not delete or move the item or its container: one that does cannot stop the deletion. Logging out raises nothing. Ignored.
on_drop Item notice serial, dropper queued Yes A player put the held item down; not when it bounces back or is worn. Ignored.
on_dropped_on Item notice serial, dropper, item queued Yes The target of a drop of the held item on an item that is no container, once can_drop_on allowed it: after the drop was made (the item was put beside the target in its container, or merged into the target, in which case it no longer exists), or when nothing else took it and no container refused it, in which case the item has gone back where it was lifted from and the script may take it from there (for example item.consume or item.delete, then give something back). A drop refused by the weight or the room of a container raises nothing. Dropped if the target was deleted meanwhile. Ignored.
on_equip Item notice serial, wearer queued Yes The item went onto a layer of wearer. Items loaded or spawned dressed raise nothing. Ignored.
on_ghost_use Item notice serial, user sync Yes A dead player double clicked the item. Without it the player reads that the dead cannot use it. Ignored.
on_item_added Item notice serial, item, actor queued Yes Asked of a container: an item entered it, by any path of on_added. serial is the container; item may have moved on by the time it runs. A container whose script lacks the function costs nothing, and the backpack of a mobile has no script. Dropped if the container was deleted meanwhile. Ignored.
on_item_removed Item notice serial, item, actor queued Yes Asked of a container: an item left it (moved, put on the ground, worn) or was deleted while inside it (decay, a stack that absorbed it, item.delete, item.consume); then actor is 0. serial is the container. Dropped if the container was deleted meanwhile. Ignored.
on_mobile_near Item notice serial, mobile, distance queued Yes A player or an NPC came within the watch_range of the item (a template field, 0 to 8 tiles, 0 or unset watches nothing and costs nothing): by a step, a teleport or by entering the world. Raised once per entry, from outside the range, not at each step inside it; distance is the larger of the distances on the two axes. Hidden mobiles and ghosts are not noticed, and an item that appears where a mobile already stands tells nothing until that mobile moves. Only items lying on the ground watch. Dropped if the item was picked up meanwhile. Ignored.
on_move_off Item notice serial, mobile queued Yes A player or an NPC stepped off the cell of the item, by walking or running; the counterpart of on_move_over, with the same rule for the height. Not by teleport. It is raised for the step that left, before on_move_over of the item on the cell it entered, also for a player, whose two hooks run at once, in that order, inside the step. For an NPC it is queued and dropped if the item left the cell meanwhile. The mobile is already on the new cell. Ignored.
on_move_over Item notice serial, mobile sync Yes A player stepped onto the cell of the item; not by teleport. Ignored.
on_npc_move_over Item notice serial, npc queued Yes An NPC stepped onto the cell of the item. It may have moved on already. Ignored.
on_npc_use Item notice serial, npc queued Yes An NPC walking a path found the item in its way, such as a door, up to three tries. Ignored: the call is queued, so the NPC is not told whether it worked.
on_pickup Item notice serial, picker queued Yes A player lifted the item from a container, the paperdoll or the ground. Ignored.
on_removed Item notice serial, container, actor queued Yes The item left a container or a backpack without being deleted: moved to another container, put on the ground, worn. Lifting it does not raise it, since a lifted item stays where it is until it is dropped (on_pickup). A deleted item raises on_delete instead. Dropped if the item was deleted meanwhile. Ignored.
on_speech Item notice serial, speaker, text, keywords, type sync Yes A player spoke within hearing of an item lying on the ground. Ignored.
on_timer Item notice serial, name sync Yes A timer started with item.start_timer is due; the timer is gone when it runs. Ignored.
on_tooltip Item answer serial, viewer sync No The property list of the item is built for the player viewer, when the client asks for it. Must be quick and must not wait. The lines are asked every time, never cached, and only for an item whose script defines the function; the list the server sends for the other items is shared. The client asks again whenever the item is shown or changes (the revision it is told is the one of the lines the server writes itself). A list of lines to append after the server’s own: a string, or a table { cliloc = number, args = "text" } (args may hold several arguments separated by a tab). At most 16 are taken, and at most five of them plain strings, since the client shows only five free-text lines (fewer when the server’s own lines already use some); the cliloc lines have the 16 only. One that is not well formed is left out; an error or no answer adds nothing. The hook runs once for each scripted item every time a container is opened, since the revision the client is told is the one of the server’s own lines; a single click shows the name only and does not run it.
on_unequip Item notice serial, wearer queued Yes The item left the layer of wearer: put in a container or on the ground, merged into a stack, or taken out of the world while worn (decay, item.delete, item.consume, .remove, a mount statuette, an NPC’s gear going into its corpse). Logging out or removing the NPC raise nothing. Ignored.
on_use Item answer serial, user sync Yes A player double clicked the item, also by Telekinesis. true, or a wait(): handled, no default action.
can_buy Vendor veto serial, actor, item, amount, price sync No Asked of the script of an NPC vendor for each line of a purchase, once the window’s lines are checked and before any gold or goods move. It must not open a window or move goods. item is the template id of the goods and price the gold of the whole line (amount pieces). Players are never asked. false refuses that line: it spends nothing and moves nothing, the vendor says so once and the other lines go on; with none left the window closes.
can_sell Vendor veto serial, actor, item, amount, price sync No As can_buy, for each line of what a player sells, before the gold is paid and the goods taken. false refuses that line, as can_buy: it stays with the player.
on_bought Vendor notice serial, actor, item, amount, price queued Yes One line of a purchase is done: the goods are delivered and the gold taken. Once for each line, after the whole purchase; a refused purchase raises nothing. Ignored.
on_sold Vendor notice serial, actor, item, amount, price queued Yes One line a player sold is done: the gold paid and the goods taken. Once for each line. Ignored.

Some things are not asked of one script but told to every script that listens, with events.on (see Events and timers). They are notifications, they are not in the table above and the catalog does not list them; a script gets one only if it subscribed, and an event nobody listens to costs the server a single lookup.

Event Fields When
item_created serial, template A new item entered the world: made by a script, a vendor, a spawn, a craft or a command. Items loaded at startup or at a login, the rest of a split stack, the items the staff’s .decorate places and the items a book attachment settles raise nothing.
item_deleted serial, template An item was deleted, by any of the ways of on_delete; the handler runs on a later turn, when the item is gone. Logging out raises nothing.
mobile_created serial, template An NPC was spawned: a spawn region, .spawn, a script, a guard, a pet that came out of a stable, a ridden pet that was dismounted, a bonded pet raised from its corpse. The creature has a new serial each time. Those loaded at startup raise nothing, and an ethereal statuette makes no creature.
mobile_deleted serial, template An NPC was removed from the world: after its death, .remove, a script, a pet that went into a stable, or a pet that was mounted and became the mount item its rider wears. The serial is gone for good; a pet that comes back is a new mobile_created.
mobile_died serial, killer, corpse A player or an NPC died. killer and corpse are 0 when there is none. For a human body it follows the fall, as on_death; for an NPC mobile_deleted follows it.
pet_tamed serial, tamer A creature became a player’s own, by pet.tame or .tame.
corpse_carved serial, carver, items A corpse gave items to a knife (carve.corpse); serial is the corpse and items how many items were made, inside the corpse or beside it, a stack counting as one. A carve that gave nothing raises nothing.
craft_succeeded actor, craft, item A craft made an item. craft is the id of the craft and item the serial of what it made, which may be the stack it joined. Failed tries raise nothing.
party_invite party_id, leader, invitee A leader invited a player to the party.
party_joined party_id, member An invited player accepted and joined the party.
party_left party_id, member, reason A member is no longer in the party; reason is left, kicked or logout. The member whose leaving ends the party is told as well.
party_disbanded party_id, reason A party is gone: too_small when fewer than two members remained, emptied when its only member’s last invitation ended.

Not built: resource_harvested (harvesting is split over the pickaxe, axe and fishing scripts and the server never knows who harvested), and no bus event is raised for a failed craft.

These hooks were built before the convention and keep their order; a new hook follows the convention:

  • on_death puts the corpse before the killer, and on_mobile_killed the killer last.
  • on_health_low puts the attacker after the hit points.
  • The name of the actor changes with the hook: giver, dropper, picker, wearer, user, speaker, mobile, other, player and npc. They are all the serial of a mobile.
  • A skill script’s on_use and on_snoop, the on_close and on_button of a gump and the on_start, on_end and on_login of a seasonal event do not begin with the serial of an object; they are not in this table, which lists the hooks of NPC and item scripts only.

Hooks the server calls and no shipped script defines are the ones a script of your own may use first: on_mobile_killed, on_health_low, on_mana_low, on_damage, can_die, on_heal, on_attack, on_hit, on_miss, on_combat_start, on_combat_end, on_resurrect, on_tame, on_release, on_enter_region, on_leave_region, on_equip, on_unequip, on_pickup, on_drop, can_equip, on_delete, can_decay, on_added, on_removed, on_item_added, on_item_removed, on_move_off, on_mobile_near, on_tooltip, can_drop_on, on_dropped_on, on_carving, can_buy, can_sell, on_bought and on_sold.

Hook Shipped scripts
on_think wander, guard, healer, orione, vega, hireling, and the scripts made with common/creature.lua (animal, monster, scared_animal, guard, guard_outsider)
on_spawn wander, and the scripts made with common/creature.lua
on_speech (NPC) banker, healer, hireling, quest_giver, shopkeeper, stablemaster, vega, wander, and the scripts made with common/creature.lua
on_speech (item) keyword_teleport
on_mobile_in_range wander, quest_giver
on_death hireling, and the scripts made with common/creature.lua
on_release hireling
on_drag_drop banker, healer, hireling, quest_giver, shopkeeper, stablemaster, and the scripts made with common/creature.lua
on_context_menu, on_context_menu_select (NPC) banker, healer, hireling, quest_giver, shopkeeper, stablemaster
on_use most of the scripts of scripts/items/, the tools, food, potions, doors and gates among them
on_ghost_use ankh
on_npc_use door
on_move_over teleporter, magic_field, moongate, public_moongate
on_npc_move_over teleporter, magic_field
can_pick_up, can_drop engraved_stone
on_tooltip engraved_ring
can_insert treasure_chest
on_timer door, magic_field, moongate
on_darkness light
on_create treasure_chest

The Lua functions a script calls from inside a hook are in the reference.

Made with ❤️ by Tom

Buy me a coffee