Item scripts
This page is part of Writing Lua scripts. The functions a script calls on its item are in
the reference: item.
Every hook the server calls, with its arguments and whether it runs inside the action, is in the single list of
Script hooks.
An item template names its script with script_id, the name of a global Lua table
defined by scripts/items/<script_id>.lua. The server loads the files directly in
scripts/items/ at startup, after the bootstrap.
Create and try a scripted item
Section titled “Create and try a scripted item”Use a running game or standalone shard and a character with GameMaster or Administrator access. Paths below are relative to the server root. These names create new content; they do not replace the shipped potion script.
- Create
templates/items/tutorial_greeting_stone.toml:
[[item]]id = "tutorial_greeting_stone"name = "greeting stone"item_id = 0x0ED4movable = truescript_id = "tutorial_greeting_stone"- Create
scripts/items/tutorial_greeting_stone.lua:
tutorial_greeting_stone = {}
function tutorial_greeting_stone.on_use(serial, user) item.message(serial, user, "The stone welcomes you.") return trueend- Restart the server to load the new template and script. Templates are read at
startup;
script reloadalone does not load a new TOML template. - In the game client, say
.add tutorial_greeting_stoneand target the ground beside your character. The command should reportAdded greeting stonewith its serial and location. Seeaddfor targeting and permissions. - Double click the stone within 2 tiles and in sight. Your character should read
The stone welcomes you.Thetruereturn marks the use as handled.
After changing only the Lua file, enter script reload items/tutorial_greeting_stone.lua
in the server console and double click the same item again. If the command reports
an unknown template, verify the TOML path and restart; if the item appears but says
nothing, check the Lua errors and that its file name, table and script_id match.
Hook reference
Section titled “Hook reference”| Function | When |
|---|---|
on_use(serial, user) |
A player double clicks the item, carried (worn or in its containers) or on the ground within 2 tiles (the template’s use_range) and in sight; farther, the player reads “That is too far away.” and nothing runs. Items inside a container lying on the ground cannot be used yet: the player reads “That is too far away.”. A missing on_use, or one that raises an error, lets the default action follow. Return true to stop the default action, such as opening a container; return nothing to let it follow. A handler that calls wait counts as handled; after the wait the item may have moved, so check it again, for example item.owner(serial) == user. |
on_ghost_use(serial, user) |
A dead player double clicks the item (the ankh uses it). Without it the player reads that the dead cannot use it. |
on_npc_use(serial, npc) |
An NPC walking to a place finds the item in its way and may use it; a door opens (up to three tries). The call is queued, so what it returns is discarded: the NPC is not told whether it worked. |
on_move_over(serial, mobile) |
A player stepped onto the cell of the item, lying on the ground at the player’s height, up to 14 above its feet, or below them and tall enough to reach them (ModernUO’s rule). It runs after the step was acknowledged and shown to the players around; an NPC runs on_npc_move_over instead. Once a script moved the player off the cell, the other items of the cell are not run. Arriving by teleport does not trigger it, so two teleporters that point at each other do not loop. |
on_npc_move_over(serial, npc) |
An NPC stepped onto the cell of the item, by the same height rule. It runs on the turn of the game loop after the step, so the NPC may already have moved on: check where it is |
on_speech(serial, speaker, text, keywords, type) |
A player said text within hearing of the item, lying on the ground (commands are not heard): 15 cells aloud or as an emote, 1 cell for a whisper, 18 for a yell. speaker is the player’s serial; keywords the speech keywords the client found, an array of numbers; type how it was said, such as SpeechType.Whisper. Every scripted ground item in range is asked, after the NPCs, so a script checks its own range and words. It may call wait. |
on_equip(serial, wearer) |
The item went onto a layer of the mobile wearer, dropped on the paperdoll. A worn item lifted and bounced back never left its layer, and items loaded or spawned already dressed raise nothing. It cannot refuse the item: can_equip does. |
on_unequip(serial, wearer) |
The item left the layer of wearer: dropped in a container or on the ground, merged into a stack, or taken out of the world while worn, by decay, item.delete, item.consume, .remove, a mount statuette used or an NPC’s gear going into its corpse. The item may be gone by then, so item.* gives nil. Logging out, removing an NPC or deleting a mobile with its items raise nothing. |
on_pickup(serial, picker) |
The player picker lifts the item from a container, the paperdoll or the ground; lifting part of a stack lifts this item, and the rest left behind is not new. While it is held, item.consume and item.delete refuse it. A held item ends in on_drop, in on_equip when it is worn by a new wearer, or in nothing: when it bounces back, is worn again on the layer it came from, or its player logs out holding it. |
on_drop(serial, dropper) |
The player dropper puts the held item down: into a container, on the ground, or onto a stack (the item is gone then, so item.* gives nil). Not when it bounces back or is worn. A worn item put down runs on_unequip first, then on_drop. |
can_pick_up(serial, picker) |
The player picker is about to lift the item, from a container, the paperdoll or the ground, once every rule of the server allows it and before a stack is split. Return false to refuse: the item stays where it is and the client shows no message of its own. A worn item is lifted before it is taken off, so this is also where a script keeps an item on its wearer |
can_drop(serial, dropper, destination) |
The player dropper is about to put the held item down, anywhere: on the ground, into a container or onto a stack. destination is the serial the client names for the drop: a container, an item, a mobile, or 4294967295 for the ground. Return false to refuse: the item goes back where it was lifted from |
can_equip(serial, wearer) |
The held item is about to be worn by wearer, once the layer is free and the rules allow it. Return false to refuse: the item goes back where it was lifted from and on_equip does not run. Not asked of a worn item lifted and put back on its layer, which it never left |
can_insert(serial, mobile, item) |
Asked of a container: the player mobile is about to put item into it, or onto a stack that lies directly in it, once the rules allow the drop. serial is the container, carried or lying on the ground; a container holding that container is not asked. Return false to refuse: the item goes back where it was lifted from. It is asked after the item’s own can_drop, and once for each drop |
on_context_menu(serial, player) |
That player asks for the item’s context menu: an item it carries, or one on the ground or in a container there, in view. Return the entries the item adds, a table of { id, cliloc, range, enabled }, or nothing; it answers at once and must not wait. |
on_context_menu_select(serial, player, id) |
The player chose one of those entries: id is the entry’s own. Called only for an entry the player was shown and is in range of. |
on_timer(serial, name) |
A timer of the item, started with item.start_timer, is due. Timers are checked once a second. The timer is gone when the function runs: start it again there for something that repeats. One that came due while the server was down, or while the character carrying the item was offline, runs as soon as the item is in the world again. It may call wait |
on_darkness(serial, dark) |
Every 30 seconds, and right after .globallight, on a lamp post (template decoration_light, prop decoration_type LampPost1 to LampPost3) whose spot turned dark (true) or light (false) |
on_create(serial) |
A newly created item enters the world: the equipment, backpack and loot of a spawned NPC, before that NPC’s on_spawn, and a chest a spawn region makes, with everything inside it. A new character’s starting items, the rest of a split stack, and items made by item.give, item.create, item.add_loot or .decorate raise nothing. |
on_delete(serial) |
The item is being deleted, whichever the way: decay, a stack that absorbs it, item.delete, item.consume of all of it, .remove, its container deleted with its contents, the gear of a removed NPC. Asked once for each item, before it leaves the world: the item, its props and its container can still be read. A container’s items are asked before the container (a decaying container is already empty when its own runs; the gear of a removed NPC goes contents first too, with the inventory still in place until all have run). An item that never entered the world is not asked. Use it to clean up what the item owns, such as a timer or a prop of another item. It must not delete or move the item or its container: a handler that does cannot stop the deletion. Nothing runs when a player logs out |
can_decay(serial) |
The decay time of an item lying on the ground is due. Return false to keep it: it is asked again after one more decay time, decay_minutes of its template (60 by default), not at every check. Asked of the item that decays, not of what lies inside it, and not of an item that never decays |
on_added(serial, container, actor) |
The item entered a container, or the backpack of a mobile, by any path: a drag and drop, item.move_into, item.give, a purchase, a crafted piece, loot, a created item put straight inside. actor is the mobile that caused it: the player who dropped it, the buyer of a purchase, the mobile item.give gave it to; 0 when none did, as for loot put in a corpse, a script’s item.move_into or the contents of a chest. Not for a worn item, which has on_equip, nor for a stack that grows or splits, nor for items loaded at startup or at login, placed by .decorate, or made by a book attachment, which comes into the backpack silently |
on_removed(serial, container, actor) |
The item left a container or a backpack without being deleted: it was moved to another container, put on the ground or worn. Lifting it is not leaving, since a lifted item stays where it is until it is dropped (on_pickup). A deleted item runs on_delete instead |
on_item_added(serial, item, actor) |
Asked of a container: item entered it, by any path of on_added. serial is the container. The backpack of a mobile has no script and is asked nothing; a container whose script lacks the function costs nothing |
on_item_removed(serial, item, actor) |
Asked of a container: item left it, or was deleted while inside it (then actor is 0). item may be gone by the time it runs |
on_move_off(serial, mobile) |
A player or an NPC stepped off the cell of the item, by the height rule of on_move_over. For an NPC it runs on the turn after the step, with the mobile already on the new cell, and not at all if the item left the cell meanwhile; for a player it runs at once, inside the step, before the on_move_over of the cell entered. A teleport raises nothing |
on_mobile_near(serial, mobile, distance) |
A player or an NPC came within the watch_range of the item, a template field of 1 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 every step inside it; distance is the larger of the two axes. A hidden mobile and a ghost are not noticed |
on_tooltip(serial, viewer) |
The property list of the item is built for the player viewer. Return a list of lines to append after the server’s own: a string, or { cliloc = 1060658, args = "Name\tvalue" }. At most 16 are taken, and at most five of them plain strings: the client shows only five free-text lines, fewer when the server’s own lines use some already (cliloc lines have only the 16). One that is not well formed is left out, and an error adds nothing. It answers at once, must not wait, and is asked every time the client asks for the list, only for an item whose script defines it: opening a container runs it once for each scripted item inside. A single click shows the name only and does not run it |
can_drop_on(serial, dropper, item) |
Asked of an item that is not a container: the player dropper is about to drop the held item on it, which lies within reach or is carried. Return false to send the item back where it was lifted from. Asked after the item’s own can_drop and before the other rules, so before a stack merges. Containers use can_insert |
on_carving(serial, carver, items) |
Asked of the script of a corpse (scripts/items/corpse.lua): a knife is about to take items off it, before any item is made. items is the roll of the corpse’s carve table, an array of { item = template id, amount = n }. Return false to cancel (nothing is made, the corpse is not carved and the carver is told it cannot use the blade on that), a table of the same shape to replace the roll, or anything else to keep it. It answers at once and must not wait; an entry keeps the message it came with. See Carving |
on_dropped_on(serial, dropper, item) |
The drop on this item was allowed and made: item was put beside it, or merged into it (then it no longer exists). Also when nothing else took the item and no container refused it: the item is then back where it was lifted from, and the script may take it from there with item.consume or item.delete. A drop refused for the weight or the room of a container raises nothing |
on_equip, on_unequip, on_pickup, on_drop, on_create, on_added, on_removed,
on_item_added, on_item_removed, on_move_off, on_mobile_near and on_dropped_on run right after what
caused them, on the next turn of the game loop, once the players have seen it: a script
may then delete or consume the item. Not all of them are called when the item was deleted meanwhile:
on_added, on_removed, on_item_added, on_item_removed, on_mobile_near, on_dropped_on and the on_move_off of an
NPC’s step are dropped when the item they are asked of (the container, for on_item_added and on_item_removed) is
gone, while on_equip, on_unequip,
on_pickup, on_drop and on_create are still called, and item.* then gives nil for the item. The on_move_off
of a player’s step is not queued: it runs at once, inside the step, before the on_move_over of the cell it entered.
They are notifications: none can refuse the move. on_delete is the exception: it runs inside the deletion, before the
item is gone, so that it can still be read.
can_pick_up, can_drop, can_equip, can_insert, can_drop_on and can_decay are questions, asked
before the move and answered at once: only false refuses. A missing function, an error, a call to
wait or any other value lets the move follow, so a broken script never locks an item. Tell
the player why with mobile.message before returning false. They are asked for the moves
a player makes with the client, staff included; a script that moves an item itself
(item.move_to, item.move_into) is not asked. While a question is asked the item counts as held, so
item.delete, item.consume, item.move_into and the functions that change it refuse it:
answer the question there, and act on the item in on_pickup, on_drop or on_equip.
-- a cursed ring: once worn, it stays onfunction ring.can_pick_up(serial, picker) -- worn: it has an owner and lies in no container if item.owner(serial) == picker and item.container(serial) == nil then mobile.message(picker, "The ring will not come off.") return false endend-- a pressure plate that watches 3 tiles around it (watch_range = 3 in its template)plate = {}
function plate.on_mobile_near(serial, mobile, distance) item.set_prop(serial, "last", mobile)end
function plate.on_tooltip(serial, viewer) return { "Armed", { cliloc = 1060658, args = "Charges\t3" } }end
-- a pedestal that eats a gem dropped on it, once the gem is back in the packpedestal = {}
function pedestal.can_drop_on(serial, dropper, thing) if item.get_prop(thing, "cursed") then return false endend
function pedestal.on_dropped_on(serial, dropper, thing) item.delete(thing)endThe script acts on its item with the item module, passing its serial; user is
the serial of the player. A minimal item script, as the distribution’s scripts/items/potion.lua began; the full one
is described in Potions:
potion = {}
function potion.on_use(serial, user) item.message(serial, user, "You drink the potion.") item.consume(serial)
return trueendThe scripts the distribution ships for doors, lights, food, teleporters, moongates, clocks and containers
are described in Shipped scripts; engraved_stone.lua there is an example of can_pick_up
that keeps an item in the hands of its owner (see Engraved stones).
The scripts that listen with events.on are told when any item is created or deleted, through the events item_created
and item_deleted: they cost nothing while nobody listens, and are on the bus events
table.
Made with ❤️ by Tom
