Opt-in modules
Seven capability modules are preloaded but inactive. A plugin that never opens the backpack should not pay for reading it, so these cost nothing until you ask:
require('inventory')
require('pet')
require('fishing')
require('spell')
require('equipment')
require('garden')
require('drops')
The require does two things: it returns a module table, and it enables the matching
sub-object on every client. Most of the time you only care about the second effect, so the
return value goes unused:
require('garden')
-- ... later
local plots, err = c.garden:Plots()
Call require once at file scope, not inside on_tick.
| Module | Enables | Returns |
|---|---|---|
inventory | c.inventory | InventoryModule |
pet | c.pet | PetModule |
fishing | c.fishing | FishingModule |
spell | c.spell | SpellModule |
garden | c.garden | capability module |
drops | c.drops — see Drops | capability module |
equipment | c.equipment | EquipmentModule with real methods |
equipment is the odd one. Its returned table carries usable functions that default to the
active client, while c.equipment scopes to a specific one. Both work after the same
require.
require raises an errorc.pet without require('pet') does not return nil — it raises
pet module not required - call require("pet") first, which aborts the hook you are in. You
cannot probe for a module by testing the field; the require must come first.
The flag is per-VM and permanent, so one require at file scope covers every client and every
later tick.
requirepackage is replaced with a version that only resolves preloaded modules. require('socket')
or require('./helpers') fails with module 'socket' is not available; require() it only for known modules. A plugin is one file.
inventory
c.inventory:Items() --> Item[]
c.inventory:Count() --> number
c.inventory:FreeSpace() --> number
c.inventory:IsFull() --> boolean
Item fields: name, template_id, quantity, type. Methods: Name(), TemplateID(),
Quantity(), Type().
Item types: reagent, treasure_card, mount, pet, jewel, equipment, unknown.
name is the item template's display name, and is '' when it could not be resolved. A field
whose read failed is simply absent from the table, so guard with tonumber(item.quantity) or 0
rather than assuming every field is present.
None of these four methods reports errors: a failed read gives {}, 0, 0, and false
respectively. IsFull() returning false is therefore not proof of free space — check
FreeSpace() too if you are about to discard something.
require('inventory')
local function backpack_pressure(c)
if c.inventory:IsFull() then
log('backpack full, stopping')
state.enabled = false
return
end
if c.inventory:FreeSpace() < 10 then
log_throttled('space', c.inventory:FreeSpace() .. ' slots left')
end
end
A farm bot that fills its backpack and keeps farming is wasting drops. Checking FreeSpace()
occasionally is cheap insurance.
pet
c.pet:Energy() --> current, max
c.pet:EnergyPercent() --> number
c.pet:HasEnergy(n) --> boolean
c.pet:IsEnergyFull() --> boolean
c.pet:RefillEnergy() --> ok, err
A failed read gives 0, 0 from Energy(), 0 from EnergyPercent(), and false from
HasEnergy/IsEnergyFull — so a zero-energy reading may just be a bad read. Only
RefillEnergy() reports an error, as false, err.
Pet games cost energy and it regenerates slowly. HasEnergy(n) before starting one avoids
opening a minigame you cannot afford:
require('pet')
if not c.pet:HasEnergy(6) then
log_throttled('petenergy', 'pet energy at ' .. math.floor(c.pet:EnergyPercent()) .. '%')
return
end
fishing
c.fishing:Fish() --> Fish[]
c.fishing:Count() --> number
c.fishing:Catchable() --> Fish[]
c.fishing:HasActive() --> boolean
c.fishing:BobberPosition() --> Position|nil
Fish fields: school, rank, is_catchable, is_chest. Methods: School(), Rank(),
IsCatchable(), IsChest().
Catchable() pre-filters to fish you can actually hook with your current lure.
BobberPosition() returns nil when no bobber is in the water or the read fails — always
check it before indexing .x. The other four degrade to {}, 0, {}, and false.
spell
c.spell:Spellbook() --> SpellEntry[]
c.spell:SpellIDs() --> number[]
c.spell:Count() --> number
c.spell:Knows(id) --> boolean
c.spell:Deck() --> DeckCard[]
c.spell:GetDeck() --> string
SpellEntry: spell_id, is_retired, tiered_group_index.
DeckCard: template_id, quantity, enchantment.
GetDeck() returns a serialized "templateID:quantity,..." token. It is read-only — there is
no SetDeck, because libwiz has no deck write path. Reading the deck to verify a manual setup
is the realistic use.
Knows(id) takes the numeric spell id from SpellIDs(), not a name. Like the rest of this
controller it answers false/0/{}/'' on a failed read rather than reporting one.
equipment
Two access shapes:
local equipment = require('equipment')
equipment.GetItems() -- active client
c.equipment:GetItems() -- explicit client
c.equipment:GetItems() --> EquipmentItem[]
c.equipment:FindItem(selector) --> EquipmentItem|nil, err
c.equipment:EquipItem(selector) --> ok, EquipmentItem|err
EquipmentItem fields:
| Field | Type | Notes |
|---|---|---|
index | number | The item's own index |
name | string | Template name |
display_name | string | What the player reads |
display_key | string | Lang code behind display_name |
debug_name | string | |
slot | string | One of the slots below, or '' if unslotted |
equipped | boolean | Currently worn |
template_id, item_id | number | The same value under two names |
global_id | number | |
address | number | Memory address |
class_name, template_type, object_type | string | Type introspection |
quantity | number | Absent when the game reported none |
Selectors accept an index, a name, or a table:
c.equipment:EquipItem(3)
c.equipment:EquipItem('Sword of')
c.equipment:EquipItem({ name = 'Sword of Kings', match = 'contains', slot = 'wand' })
| Selector key | Aliases |
|---|---|
index | item_index, item |
global_id | gid, id |
template_id | item_id |
name | item_name, display_name |
match | item_match — contains (default), exact, prefix |
slot | equipment_slot |
Slots: hat, robe, boots, wand, athame, amulet, ring, deck, mount.
Constraining by slot matters when names are ambiguous — several items match "Ring", only
one of them goes on your finger.
Three behaviours worth knowing:
matchdefaults tocontains, the opposite ofCastSpell's default. A short name matches broadly here unless you ask forexact.- An index selector accepts either numbering. It matches an item whose own
indexfield is that number or whose 1-based position inGetItems()is. When the two disagree you get whichever matches first, so prefer a name or id for anything that must be exact. - An unequipped match wins. Among items that match, the first not-currently-equipped one
is chosen; an already-equipped match is only used if nothing else matched. This is what makes
EquipItem('Ring of')swap rather than re-equip what you are wearing.
FindItem returns nil, err when nothing matches (equipment item not found) or the
backpack is empty (no equipment items). EquipItem returns true, item on success — the
second value is the item table, not an error — and false, err on failure.
garden
c.garden:Plots() --> GardenPlot[], err
c.garden:PlotCount() --> integer, err
c.garden:IsPlanted(i) --> boolean, err
GardenPlot: x, y, z, templateID (0 when empty), empty.
Plots come back in memory order, and IsPlanted takes a 1-based index into that list. An
index outside it returns false, 'plot index out of range' rather than raising.
This is the one controller that reports its errors, but note the shape: on failure Plots()
returns an empty table and an error, not nil — so #plots == 0 is ambiguous and err
is the value to test. Within a plot, a field whose read failed is absent, so a plot table with
no empty key is a failed read rather than a full plot.
require('garden')
local plots, err = c.garden:Plots()
if err then
log('cannot read garden: ' .. tostring(err))
return
end
if #plots == 0 then
log('no plots found. either no garden is loaded, or GardeningBehavior resolved to the wrong object')
return
end
local empty = {}
for i, plot in ipairs(plots) do
if plot.empty then empty[#empty + 1] = i end
end
log(#empty .. ' empty plot(s) of ' .. #plots)
An empty plot list is ambiguous — either there genuinely is no garden loaded, or the behaviour object resolved to something else. Say both in the log; you will thank yourself.
Plot positions are ground-level. Placing a seed usually needs a Z offset, which is why the
gardening plugin carries local Z_OFFSET = 125.
drops
require('drops')
c.drops:Start()
The drop logger has a page of its own — see Drops for the full
controller, the JSONL format, and the drop event.
Checking a module is active
local m = require('inventory')
utils.Log(m.name()) -- 'inventory'
name() is the only function on the plain capability modules — inventory, pet, fishing,
spell, garden, and drops all return a table carrying just that. The real API is on the
client sub-object; the returned table is a receipt, not an interface. equipment is the
exception, and carries working GetItems/FindItem/EquipItem.
Not usually needed — require fails loudly if the module does not exist. Worth having when a
plugin is meant to run under more than one host.