Skip to content

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.

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.

  1. Create templates/items/tutorial_greeting_stone.toml:
[[item]]
id = "tutorial_greeting_stone"
name = "greeting stone"
item_id = 0x0ED4
movable = true
script_id = "tutorial_greeting_stone"
  1. 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 true
end
  1. Restart the server to load the new template and script. Templates are read at startup; script reload alone does not load a new TOML template.
  2. In the game client, say .add tutorial_greeting_stone and target the ground beside your character. The command should report Added greeting stone with its serial and location. See add for targeting and permissions.
  3. Double click the stone within 2 tiles and in sight. Your character should read The stone welcomes you. The true return 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.

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 on
function 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
end
end
-- 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 pack
pedestal = {}
function pedestal.can_drop_on(serial, dropper, thing)
if item.get_prop(thing, "cursed") then
return false
end
end
function pedestal.on_dropped_on(serial, dropper, thing)
item.delete(thing)
end

The 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 true
end

The 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

Buy me a coffee