Automation scripts for Razor Enhanced, the Ultima Online assistant. Written for its embedded IronPython 3.4 runtime and targeted at a RunUO/ServUO-derived freeshard.
| Script | What it does |
|---|---|
Scripts/TameAndFill.py |
Reads the taming order deeds in your pack and hunts only those species. Walks to each one, tames it, and puts it in the deed for its species. |
Scripts/COVFarm.py |
Camps the Slasher of Veils, kills it from range, and loots it. Waits for the spawn, opens with Wildfire, then holds 4-5 tiles while spamming Nether Blast — the standoff is enforced mid-cast, not just between casts. Confirms the kill with a corpse rather than the monster vanishing, walks onto it, says [grab, and returns to the starting spot for the next spawn. Breaks off below a configurable health percentage. |
Scripts/petcommandcenter2.py |
Deploy and shrink your pets by speaking a phrase. Say one phrase to release every pet statue in your pack and issue the guard command; say another to shrink the nearest pets back in. Set SETUP_MODE = True and it walks you through targeting your statues and shrink tool, then prints a finished config block to paste in — no reading item IDs out of the inspector. |
Scripts/harvest_runner.py |
Mining + lumberjacking on one script. Account-runebook travel, per-job rune routes, smelting, drop-off runs, vendor rounds, meditation-backed mana, and the Greyskull call-out. |
Scripts/mining_runner.py |
Superseded by harvest_runner.py. Kept as a working fallback until the integrated script is confirmed in-game — delete it once it is. |
Scripts/resource_order_runner.py |
Fills resource orders. Counts the chest's ingots by hue, takes orders it can afford out of the book, fills them, then recalls to RO > RO and drags the completed deeds to the Resource Gatherer. Keeps 100 of each metal behind. Confirmed end-to-end in game. |
Several of these scripts were built alongside diag_*.py troubleshooting
companions — they dump what Razor actually sees (gump layouts, NPC tooltips,
pack contents, creature bodies) instead of guessing. Those are not in this
repo, which is kept to the scripts that get run day to day, but they are what
found nearly every root cause recorded in docs/. If you are debugging one of
these scripts, writing a throwaway dump script first is almost always faster
than reasoning about it.
- Copy the
.pyfile into your Razor Enhanced scripts directory (the folder configured under the Scripting tab). - In Razor Enhanced, open Scripting, hit Reload, select the script.
- Optionally bind it to a hotkey.
There is nothing to configure and no prompts. Put your taming order deeds in your backpack and run it. It reads each deed, works out which species it is for, and hunts only those. No unicorn deed means unicorns are never approached.
- Scan the pack. Each deed's name and tooltip is matched against a catalogue of
112 tameable species — see
docs/tameable-animals.md. - The creature search filter is built from just those species' body values.
- Chase the nearest match, tame it, put it in that species' deed.
- Rescan after every success and every
DEED_RESCAN_MS, so a deed that fills up drops out of the hunt and a newly added one joins it.
- Taming order deeds in your backpack.
- Enough Animal Taming for the species those deeds name. Species above your skill are reported and skipped rather than attempted.
- At least one free follower slot.
Some species are indistinguishable by body: a nightmare and a dread warhorse
are both body 0x74, and there are 14 other collisions. For those the script
reads the creature's name before touching it, and walks away if the name won't
load. A missed tame beats a tamed pet with no deed to hold it.
Name matching tolerates punctuation and spacing, so a deed saying Ki-Rin,
Ki Rin or kirin all resolve to the same species, and longer names win over
shorter ones — hell cat never resolves as cat, dread warhorse never as
horse.
Confirmed against a live deed via Razor's Enhanced Item Inspector:
Name: A Taming Order ItemID: 0x2258
Serial: 0x4302A461 Root Container: 0x41D40F58 (the backpack)
A Taming Order
Weight: 1 Stone
Level: 2Creature Type: KirinFilled: 24/60Gold: 100%Runics:
Two things about that text drive the implementation:
- Properties arrive concatenated —
KirinFilled,2Creature. There is no separator between one property's value and the next one's label. The script inserts a space at each lower/digit → upper seam before matching, otherwisekirinfillednever matches the specieski-rin. Creature Type:is read as a field, not found by scanning the whole tooltip. The value ends where the next field's label starts. Field labels are configurable viaDEED_SPECIES_FIELDS.
Filled: 24/60 is read too — a deed at 60/60 is full and is dropped from the
hunt list (SKIP_FULL_DEEDS), and progress is shown in the DEBUG scan output.
An item only counts as a deed if its name or tooltip contains one of
DEED_NAME_HINTS (default order, deed, contract) and it names a
species in the catalogue. Run diag_deeds.py —
it prints every item you carry, the exact tooltip text read from it, and which of
those two checks failed. It changes nothing, so it's safe to run any time.
Deed discovery:
| Setting | Default | Effect |
|---|---|---|
DEED_NAME_HINTS |
["order","deed","contract"] |
Words that mark an item as a deed. [] accepts any item naming a species. |
DEED_SPECIES_FIELDS |
["creature type", …] |
Tooltip fields naming the species, most specific first. |
DEED_PROGRESS_FIELDS |
["filled", …] |
Tooltip fields holding 24/60 style progress. |
SKIP_FULL_DEEDS |
True |
Drop deeds already at capacity from the hunt list. |
DEED_GRAPHICS |
[] |
Restrict the scan to these item IDs. Learned automatically otherwise. |
DEED_RESCAN_MS |
60000 |
How often to re-read the pack. |
NARROW_RESCAN |
True |
After the first scan, only re-check graphics that already matched. Faster. |
MAX_PACK_SCAN |
300 |
Tooltip-read cap for very full packs. |
What to hunt:
| Setting | Default | Effect |
|---|---|---|
ONLY_ANIMALS |
[] |
Whitelist. Restrict to these species even if you hold other deeds. |
NEVER_ANIMALS |
[] |
Blacklist. Never hunt these even holding a deed. |
EXTRA_ANIMALS |
[] |
Shard-custom species: ("name", [body, ...], min_tame). Body collisions with the catalogue are detected automatically and name-verified. |
SKIP_ABOVE_SKILL |
True |
Skip species whose minimum taming skill you lack. |
Distance handling — the creature wanders while being tamed, and the server re-checks both range and line of sight on every taming tick, so the script stays adjacent rather than trailing:
| Setting | Default | Effect |
|---|---|---|
STAY_DIST |
1 |
Distance held during an attempt. 1 = adjacent. Re-closes every POLL_MS (150 ms). |
TAME_START_DIST |
2 |
Will open or continue an attempt from here without re-approaching. The server itself allows 3. |
LEASH_DIST |
7 |
Server's own cutoff. Past this the attempt is abandoned and a pathfound approach is redone. |
SETTLE_STEPS |
6 |
Steps at TAME_START_DIST without improving before accepting it — stops the script grinding against terrain trying to touch a creature it can't reach. |
STALL_STEPS |
40 |
Steps without getting any closer before writing the creature off. |
Other behaviour: SCAN_RANGE (18 tiles), MAX_TAME_ATTEMPTS,
TAME_ATTEMPT_TIMEOUT, and the DEED_* timings. DEBUG is on by default and
prints the pack scan plus every journal line the deed produces — turn it off once
things are confirmed working.
Verified in-game via diag_deed_target.py, sequence 1:
- Cancel any stale target cursor and confirm
Target.HasTarget()is false. Items.UseItem(deed).Target.WaitForTarget(4000, False).- Pause
DEED_SETTLE_MS(400 ms). Target.TargetExecute(petSerial).- Pause
DEED_RESULT_MS(1500 ms), then confirm the pet is gone.
Steps 1 and 4 are load-bearing. Step 1 was the original bug:
Target.WaitForTarget returns True for a cursor that is already open, so a
cursor leaked by an earlier action swallowed the deed's target — the deed
double-clicked, a cursor sat open, and nothing was added. The taming loop leaked
one every time its own WaitForTarget timed out and it re-issued UseSkill.
The combined Items.UseItem(deed, pet) form is deliberately not used: the
Razor docs warn its built-in target "may not work on some free shards."
If the deed step ever breaks again, stand next to one already-tamed animal and run
diag_deed_target.py. It tries five
sequences and stops at the first that consumes the pet, dumping the deed tooltip
and journal at each step.
- It clears your journal before every taming attempt. That is how it avoids reading a stale result. Don't run it when you need your journal history.
- It assumes the deed consumes the pet. If your deed leaves the pet standing, the script reports that the deed did not take.
- It does not fight. Creatures that need to be subdued first are skipped.
- Ruled-out creatures go on Razor's global ignore list.
Misc.ClearIgnore()or a restart resets it.
Mining and lumberjacking on one script, driven by the account runebook ([ar).
Both original scripts are by Cral; this merges them so travel, mana, drop-off,
the vendor round and the Greyskull response are shared rather than duplicated.
JOBS = [
{"enabled": True, "name": "Mining", "folder": ['Mining'], "task": "mine"},
{"enabled": True, "name": "Lumberjacking", "folder": ['Lumber'], "task": "lumber"},
]Each job names a runebook folder and a harvesting task. A job runs until its whole rune route has been worked, then the next job starts.
JOB_ROTATION |
Switches jobs… |
|---|---|
"route" (default) |
after every rune in the folder has been worked once |
"dropoff" |
after each drop-off run |
"timer" |
every JOB_TIME_MS |
"never" |
never — stays on the first enabled job |
Use "route". Wood is far heavier than ore, so a lumber run fills the pack
after one or two trees. Under "dropoff" that meant: chop one spot → pack full →
go home → rotate away to mining, with the rest of the lumber route never
visited. Under "route" a full pack is just a trip home — the script unloads,
returns to the same spot, and carries on for as many trips as it takes.
Unloading happens inside the job, so the route position survives it. Returning
home no longer resets or skips waypoints. A vendor round or Greyskull call
resumes the lap rather than restarting it — run_job(job, resume=True).
DROPOFF_BETWEEN_JOBS (on by default) empties the pack when switching jobs.
Measured from a real trace: mining handed lumberjacking 225 of its 297 usable
stones, and the wood storage only accepts wood — so lumber had two chops of
headroom and spent the route in a full/unload cycle instead of chopping.
PACK_HANDOVER_LEVEL (0.15) is the threshold that triggers it.
| Setting | Default | Effect |
|---|---|---|
ABORT_ON_HOSTILES |
True |
Skip to the next rune when something hostile is close. |
HOSTILE_RANGE |
8 |
Tiles. Not optional — see below. |
HOSTILE_NOTORIETIES |
[4, 5, 6] |
Criminal, enemy, murderer. |
HOSTILE_SKIP_LIMIT |
3 |
Consecutive skips before harvesting anyway. |
The filter originally had no RangeMax, so it reported any criminal, enemy
or murderer anywhere the client could see — 18–25 tiles. In an area with
wandering spawns that is permanently true, and every hit skipped a waypoint, so
a route could burn from rune 1 to rune 9 without a single swing and then report
itself complete.
HOSTILE_SKIP_LIMIT is the backstop: after three skips in a row the script
harvests anyway rather than silently consuming the route, and says so.
Found by graphic first (AXE_IDS, from ServUO), then by remembered serial,
then by name. Names are last because an item's Name is often empty until its
properties load — a name-only search returns nothing once the axe has been
stowed, which aborted the whole lumber job.
Meditation no longer disarms pre-emptively. It only frees hands if the server
actually answers Your hands must be free to cast spells or meditate., so a
low-mana moment doesn't put your axe back in the pack for no reason.
Set one job's enabled to False and the script behaves exactly like the
single-purpose version it came from.
Jobs are validated at startup — a missing folder or an unknown task is named and skipped rather than failing silently mid-run.
| Area | Before | Now |
|---|---|---|
| Runebook navigation | Two copies, only the lumber one page-aware | One page-aware navigator |
| Routes / waypoints | Single global | Per-job, so each folder keeps its own position |
| Weight check | Mining indexed tooltip line 2; lumber searched for "Contents" | Lumber's method, with mining's as fallback |
| Key restock | Two separate routines | One RESTOCK_KEYS table covering both |
| Hostile check | Lumber only | Shared, via ABORT_ON_HOSTILES |
Three bugs carried over from the originals were fixed:
Player.UnEquipItemByLayer(layer, wait)takes a Boolean, not a timeout — the original passed5000.- Item names can be null. The axe search called
.Name.lower()unguarded and would throw on any unnamed item in the pack. - Page-walk duplication. If the
Page X/Yfooter claims more pages than the next button delivers, the walk re-parsed the same page and duplicated every rune on it. The walk now verifies the page actually advanced.
HOUSE_DEPOSITS hands the taming and resource order books in on every drop-off
run — regardless of pack weight, which is why they aren't just RESTOCK_KEYS
entries (those only fire when the pack is full).
HOUSE_DEPOSITS = [
{"enabled": True, "label": "Taming orders", "serial": 0x4057CC3A},
{"enabled": True, "label": "Resource orders", "serial": 0x404AC332},
]
HOUSE_DEPOSIT_CONTEXT = ["Refill from stock"]
HOUSE_DEPOSIT_GUMP = 0x06ABCE12The books use the same Refill from stock entry as every other key, and
pressing it deposits everything of that type at once. So this goes through the
same context_select as the keys and vendors, with the same exact-match-first
and CONTEXT_NEVER guards.
No amount is ever sent. The recorded macro ended with:
Gumps.SendAdvancedAction(0x6abce12, 0, [], [0], ["100"])
That is deliberately not reproduced. The deposit completes on the context reply;
the gump is just the book's window and its text field is for withdrawing, so
writing "100" into it risks pulling 100 items back out. The window is closed
instead, and a test asserts SendAdvancedAction appears nowhere in the deposit
path.
Both books share one gump id, so each deposit closes any stale window first —
WaitForGump returns True for an already-open gump, which would otherwise make
the second book answer the first one's window.
Deposits run before the chest sweep, same as the wood storage — specific consumers get first refusal.
Deeds received from the scribe are dragged into a carried Bulk Order Book. No per-character editing is needed — the book is found automatically:
BOD_BOOK_BY_CHARACTER = {} # optional: {"Hattori Hanzo": 0x413F54D6}
BOD_BOOK_SERIAL = 0 # optional: a specific book
BOD_BOOK_ID = 0x2259 # otherwise: first book of this graphic in the packResolution is per-character map → explicit serial → graphic in the pack. The last one is the default, so all three characters run the same file unedited. The startup log says which book was found and how, plus its current deed count.
A bulk order deed and a taming order share ItemID 0x2258. Two things keep
them apart:
HOUSE_DEPOSITSruns first, soRefill from stockhas already taken the taming and resource orders out of the pack.BOD_EXCLUDE_TEXTskips anything whose tooltip marks it as one of those.
The book itself also refuses taming and resource orders, so (2) is only there to avoid pointless drag attempts — an empty list is safe, just noisier in the log.
Filing runs after the vendor round as well as at drop-off, since that's when
deeds are handed over. Every deed moved is logged by name, and the book's
Deeds In Book: N count is read before and after so a silent rejection shows up.
RESTOCK_KEYS lists containers that swallow harvested resources. Each is
single-clicked and answered with Refill from stock.
{
"label": "Wood Storage",
"serial": 0x4290200A, # tried first
"id": 0x1BD9, "hue": 0x0058, # fallback if the serial is gone
"where": "world", "range": 12, # on the ground, not in the pack
}where matters. The Wood Storage is locked down on the ground at the
house — the Item Inspector shows Container: None, Root Container: None,
Ground: Yes. The original searched Player.Backpack.Serial for it, so it could
never be found. "world" searches the ground within range tiles; "pack"
searches the backpack.
The wood storage is configured by four settings at the very top of the file, so
you can move it without touching RESTOCK_KEYS:
WOOD_STORAGE_WHERE = "world" # "world" = locked down at the house
# "pack" = carried, empties on the spot
WOOD_STORAGE_SERIAL = 0x4290200A
WOOD_STORAGE_ID = 0x1BD9
WOOD_STORAGE_HUE = 0x0058A carried key never triggers a trip home. When the pack fills, the script
first tries only storage it can find on the player, decided by where the item
actually is — not by WOOD_STORAGE_WHERE. If the key is in your pocket it
empties at the tree and the route carries straight on; the drop-off run only
happens when nothing carried can take the load.
Order in dropoff() is storage first, then the chest. PURGE_ID includes
logs and boards as a sweep, so running the chest first would empty the wood into
it before the Wood Storage ever saw it.
This reverses the original mining order, where the chest ran before the keys. Ingots now get offered to the keys first. If that's wrong for mining, swap the two calls back in
dropoff().
Needs an axe or hatchet in hand or pack; it equips one automatically.
AXE_WORDS / AXE_EXCLUDE control matching — war axe is excluded, large battle axe is not.
Messages are from ServUO Lumberjacking.cs, plus the shard's own You chop
success line the original relied on:
| Message | Cliloc | Handling |
|---|---|---|
You chop |
shard | keep chopping |
You hack at the tree for a while |
500495 | failed swing, keep chopping |
There's not enough wood here to harvest |
500493 | next rune |
You can't use an axe on that |
500489 | next rune |
You can't place any wood into your backpack |
500497 | drop off |
You broke your axe |
500499 | find another |
The server's lumberjacking MaxRange is 2 tiles, so each rune has to land
within 2 tiles of a tree.
Mining's own detection is deliberately left exactly as the working original — the verified ServUO strings are listed in a comment for future tuning, but nothing that currently works was rewritten.
Two API signatures changed underneath the script, which is why it stopped working without being edited. Both are now called through shims that try the current form and fall back to the old one:
| Was | Now |
|---|---|
Player.ChatSay(msg) |
Player.ChatSay(colour, msg) |
Gumps.GetLineList(gumpId) |
Gumps.GetLineList(gumpId, dataOnly) |
Player.ChatSay is what sends [ar to open the runebook, and
Gumps.GetLineList is what reads the folder and destination names out of it —
so between them they account for travel failing entirely.
Global chat reaches the journal like this:
System: <Public> Fred Kruger: By The Power Of Greyskull!
Note that entry.Name is System — the actual speaker is buried in the
text. The script parses the channel and caller out of the line itself, so caller
filtering works on Fred Kruger rather than on System.
| Setting | Default | Effect |
|---|---|---|
GREYSKULL_PHRASES |
["by the power of greyskull"] |
Matched case-insensitively as substrings. |
GREYSKULL_ALLOWED_CALLERS |
[] |
Empty = anyone can call it. Add names only to restrict. |
GREYSKULL_REQUIRE_CHANNEL |
"" |
Empty = any channel. Set to "Public" to accept only <Public>. |
GREYSKULL_IGNORE_SELF |
False |
Off, so calling it out yourself still works. |
GREYSKULL_HOLD_MS |
20000 |
How long to hold at the circle. |
Two bugs were fixed here:
Case-sensitive matching. The old code did an exact Journal.Search for
"By The Power Of Greyskull!". Since that phrase is typed by hand, any variation
in capitalisation missed silently. Matching is now case-insensitive.
Long waits swallowed the chant. checkGreyskull() was only called from three
places, none of which run during meditation — and ensure_mana() can block for
90 seconds. Detection is now split from the response: poll_greyskull() only
raises a flag and is safe to call anywhere, including from inside travel waits,
while checkGreyskull() acts on the flag at the top of the main loop. Every long
pause now uses interruptible_pause(), which keeps listening. With a chant
pending, meditation also stops at the travel floor instead of topping up to full.
Detection uses a journal timestamp cursor rather than Search + Clear, so
one chant fires exactly once, chants said before the script started are ignored,
and the mining and meditation journal checks are left undisturbed.
If it still doesn't trigger, run diag_journal.py
and say it — that prints every journal line with its Type, speaker and text, and
says whether it would have matched.
Nothing travels on an empty pool. ensure_mana() runs before every recall and
retries a travel that bounced off "Insufficient mana".
| Setting | Default | Effect |
|---|---|---|
MIN_MANA_TO_TRAVEL |
20 |
Floor before any recall. Recall costs 11 on stock RunUO; the rest is headroom for a failed cast. |
MANA_TARGET |
0 |
Meditate up to this. 0 means to full. |
MEDITATION_TIMEOUT |
90000 |
Total ms to spend recovering before giving up. |
DISARM_FOR_MEDITATION |
True |
Stow held items — meditation refuses to start with full hands. |
Two things stop meditation outright, both detected and handled:
Your hands must be free to cast spells or meditate.— the script stows whatever is held and retries.Regenative forces cannot penetrate your armor!— metal armour blocks meditation completely. There is no way around it in script; it falls back to standing still for passive regeneration and says so once. If you mine in metal, swap to leather or expect slow recovery.
This is the first thing in the script, above every other setting, because it is the config most likely to need editing. Every NPC the script talks to is listed there and nothing else needs changing to add, remove or rename one.
{
"enabled": True,
"label": "Inscription Orders",
"folder": ['Inscription'], # runebook folder path
"point": 'Inscription', # rune name
"names": ["Sahale the scribe", "Sahale", "scribe"], # NPC, substring
"context": ["Bulk Order Info", "Bulk Order", "Talk"], # tried in order
"gump": (0x9bade6ea, 1), # optional follow-up
}Each entry is self-contained — folder and rune name are inline, so there are no separate constants elsewhere to keep in sync.
| Field | Notes |
|---|---|
enabled |
False skips a stop without deleting it. |
folder |
Folder path, e.g. ['RO'] or ['Work', 'RO'] when nested. |
point |
Rune name, matched case-insensitively as a substring. |
names |
Matched case-insensitively as substrings against the NPC's name and its tooltip. List several; first match wins. This is the field that usually needs fixing. |
context |
Menu entries tried in order until one is accepted. Prefer the exact label. |
gump |
(gumpid, buttonid) to answer afterwards, or None. |
Confirmed from diag_vendors.py:
| NPC | Menu | Entry used |
|---|---|---|
| Sherri | Open Paperdoll, Stable Pet, Talk, Buy, Sell, Train Animal Lore, Train Animal Taming, Train Veterinary | Talk |
| Edie | Open Paperdoll, Bulk Order Info, Bribe, Claim Rewards, Buy, Sell, Train Evaluating Intelligence, Train Inscription | Bulk Order Info |
Note what sits alongside them: Buy, Sell, Bribe, Open Bankbox and Train <skill> — all of which cost gold. Selection therefore runs in two passes:
- An exact label match, always honoured. If you configured it verbatim, you meant it.
- Otherwise a substring match, which refuses anything hitting
CONTEXT_NEVER(buy,sell,bribe,open bankbox,train).
So a sloppy context value of "Taming" will not buy Train Animal Taming,
while configuring "Train Animal Taming" exactly still works if that is what you
want.
What gets sent to Misc.ContextReply is the real label read from the menu, not
your search string — so "Bulk Order Info" still works if the live entry reads
slightly differently.
gump accepts a list of (gumpid, buttonid) pairs, tried in order, because
large and small bulk orders may not use the same gump id:
"gump": [(0x9bade6ea, 1)],Two things address bulk orders intermittently not being accepted:
- Stale gumps are closed first.
Gumps.WaitForGumpreturnsTruefor a gump that is already open — the same trap asTarget.WaitForTarget. A window left over from the previous vendor made the script answer the wrong one, which looks random from the outside. - The whole interaction retries
VENDOR_RETRIEStimes.
If the expected gump still doesn't appear, the log names the id that opened instead:
[Harvest] Inscription Orders: expected gump 0x9BADE6EA but 0xXXXXXXXX opened
instead. Add (0xXXXXXXXX, <button>) to this vendor's "gump" list.
Add that pair to the list. If the button number is also unknown, the Enhanced Gump Inspector's response log shows it when you click Accept by hand.
A vendor's title is usually in its tooltip, not its name. Confirmed with the Enhanced Mobile Inspector:
| NPC | Name |
Properties |
|---|---|---|
| Resource Gatherer | Davin the Resource Gatherer |
(empty) |
| Animal Trainer | Sherri |
Animal Trainer, Quest Giver |
| Scribe | Edie |
Scribe |
Matching only the name could never find Sherri or Edie. find_vendors now checks
the name first (cheap) and falls back to the tooltip, so match on the title
rather than a given name the shard may change — Sahale the scribe is already
gone, replaced by Edie.
Mobiles.Filter().Name is also an exact match, which is why the original
loop silently did nothing the moment an NPC was renamed.
When a lookup fails, the log now lists every NPC standing there with its tooltip, so the real title is visible without running a separate diagnostic.
Startup validation. The table is printed and checked when the script starts. An entry missing its NPC names, rune name, or context entries is called out and skipped rather than failing silently mid-round:
[Mine] Vendor round:
[Mine] Resource Orders RO -> RO NPC: Resource Gatherer, Resource
[Mine] Taming Deeds SKIPPED - no NPC names
[Mine] Fill it in at the top of this script, or set "enabled": False.
Run diag_vendors.py beside an NPC to get its
real name and context entries verbatim.
Confirmed by gump inspection — full protocol in
docs/account-runebook-gump.md:
| Button | Action |
|---|---|
504 |
Page forward |
503 |
Page back |
5 |
Back to root |
Paging is server-side: each click returns a fresh gump carrying only that
page's nine entries, so pages must genuinely be walked. The Page X/Y footer
gives an exact page count, so walking is bounded rather than guessed.
This was the actual bug. Folder lookup and the mining route both only ever saw
page 1 — so Arcane (page 2 of the root) was unreachable, and a 3-page mining
folder ran the same 9 runes forever instead of all 12.
Two details the parser depends on:
- A rune is followed by a coordinate line, a folder is not. That's how they are told apart, rather than by guessing at button ids.
- Entries are found by their
N. Nametext, then paired with the page's entry buttons in display order. The inspector shows text but not button ids, so it's unknown whether page 2 restarts at button10or continues at19— pairing by display order is correct either way, and the tests assert both.
If the runebook changes, re-run diag_ar_gump.py.
Its click-probe stays off by default — blind-clicking runebook buttons can recall
you or spend a charge. Both diagnostics write a full dump to %TEMP% and print
the path.
Set DIAGNOSTIC_MODE = True at the top of
Scripts/harvest_runner.py and run it. It walks
every rune of every job, takes one harvest swing at each, and prints exactly
what the server replied — then stops. No rotation, no vendor round, no drop-off.
This debugs the real code path rather than a parallel copy, and writes the whole
trace to %TEMP%\harvest_diag.txt.
Per waypoint it reports the axe found, pack and mana, the task's return value, every journal line verbatim, and — the important one — which message bucket matched:
======== Lumberjacking waypoint 3 of 9 ========
[Harvest] Lumberjacking waypoint 3/9: Lumber (Malas)
[Harvest] axe: gargish axe (0x402119CB, id 0x48B2)
[Harvest] pack: 40/125 items, 210/400 stones mana: 74/100
[Harvest] task returned: ok
[Harvest] journal: You chop some logs and put them in your pack.
[Harvest] matched: SUCCESS
If it says matched: NOTHING, the shard's wording isn't in any LUMBER_* list
and that is the bug — copy the raw line into the right one. It also unloads when
the pack fills so the trace keeps producing real swings rather than a run of
full.
Set it back to False for normal running.
python tests/test_tame_animals.py
python tests/test_harvest_runner.py
python tests/sim_harvest_runner.py # -v for every script log lineBoth run under normal CPython 3 — no client, no Razor. Each loads the real script with stub Razor globals and calls the actual functions, so the tests cannot drift from the implementation.
test_tame_animals.py— 43 checks: species matching, the live deed tooltip, container ownership, catalogue invariants.sim_harvest_runner.py— a full-loop simulation. Builds a fake world (paged runebook, a pack that fills, a wood key, trees that run out) and runs the realrun_job()end to end for both jobs. Covers: each job working all 9 runes, the wood key carried vs at the house, two jobs rotating over two laps, a hostile permanently in range, and the diagnostic mode itself. Where the unit tests check functions in isolation, this checks that a job actually finishes its route.test_harvest_runner.py— 79 checks against a fake runebook whose pages are the verbatim text captured from the Gump Inspector, plus a fake journal fed the realSystem: <Public> Fred Kruger: ...line format. Covers page parsing, folder/rune discrimination, cross-page folder lookup,goNextvisiting all 12 runes across 3 pages before wrapping, chat-line parsing, case-insensitive call-out matching, any-caller-triggers behaviour, single-fire per chant, and vendor-table validation.test_resource_order_runner.py— 41 checks on the filler: hue-keyed stock, the per-metal reserve, row/button pairing including the amt-0 row that opens every page, deed tooltip parsing, and the config values that go live.test_extract_re_api.py— 9 checks on the API extractor intools/, one per C# declaration shape that an earlier version of the parser silently dropped (nested class with the brace on the next line,public static int Hits { get,= new()field initialisers, single-line method bodies, instance methods).
- Harvest & taming handoff — current state, unmerged live config, confirmed shard facts, open items.
- Account Runebook gump —
[arprotocol: page buttons, page structure, folder vs rune, entry/button pairing. - Resource Order Book gump — both gumps mapped button by button, the row-button numbering across pages, the ore hue table, and the Razor Enhanced empty-string bug that shifts gump text ids.
- Tameable animals — 112 species, body values, min taming skill, and the 15 body collisions.
- API cheatsheet — the calls these
scripts use, with the shard-specific caveats, for
Mobiles,Player,Journal,Target,Items,Misc,PathFinding,Gumps,Spells,TimerandStatics. - Generated API reference — the complete
surface: 68 classes, every overload, extracted from the Razor Enhanced C#
source at tag
v1.0.0.14. Ground truth when a signature is in doubt. - Animal taming messages — cliloc table and range rules.
- Official Razor Enhanced API docs —
built from 1.0.0.11, so it lags the current release, but the API has not moved
since; good for the prose descriptions. The older
razorenhanced.readthedocs.iois stale enough to be misleading — avoid it.
Razor Enhanced has a history of changing signatures between builds, which breaks scripts with no edits. The reference is regenerated from source rather than transcribed:
git clone --depth 1 --filter=blob:none --sparse --branch v1.0.0.14 https://github.com/RazorEnhanced/RazorEnhanced.git REgit -C RE sparse-checkout set Razor/RazorEnhancedRE_TAG=v1.0.0.14 python tools/extract_re_api.py RE/Razor/RazorEnhanced docs/api-reference-generated.mdThe installed build is whatever version folder exists under
%LOCALAPPDATA%\www.razorenhanced.net\RazorEnhanced.exe_Url_*\ — currently
1.0.0.12, whose API is identical to 1.0.0.14.