Skip to main content

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.

ModuleEnablesReturns
inventoryc.inventoryInventoryModule
petc.petPetModule
fishingc.fishingFishingModule
spellc.spellSpellModule
gardenc.gardencapability module
dropsc.drops — see Dropscapability module
equipmentc.equipmentEquipmentModule 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.

Using a sub-object before its require raises an error

c.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.

The sandbox has no other require

package 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:

FieldTypeNotes
indexnumberThe item's own index
namestringTemplate name
display_namestringWhat the player reads
display_keystringLang code behind display_name
debug_namestring
slotstringOne of the slots below, or '' if unslotted
equippedbooleanCurrently worn
template_id, item_idnumberThe same value under two names
global_idnumber
addressnumberMemory address
class_name, template_type, object_typestringType introspection
quantitynumberAbsent 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 keyAliases
indexitem_index, item
global_idgid, id
template_iditem_id
nameitem_name, display_name
matchitem_matchcontains (default), exact, prefix
slotequipment_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:

  • match defaults to contains, the opposite of CastSpell's default. A short name matches broadly here unless you ask for exact.
  • An index selector accepts either numbering. It matches an item whose own index field is that number or whose 1-based position in GetItems() 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.