v1.40.0
What's new
Royal Revenue
v1.40.0 (2026-10-19)
- v1.40.0: forecast vs actual, charge schedule, enchanting exists again, and a crash from infinity
Five approved ideas plus four bug fixes, one of them the first crash reported by
someone other than Rafael.
Acerto tab (forecast vs actual). The addon forecast profit all the time and
nothing ever checked whether it came true. The comparison engine already existed
in Craft/Sales.lua, left over from when the Sales tab moved to the Livro, and
nobody called GetRows(). For every item actually sold in the window it now shows
forecast against realized, the error with colour, and the impact in gold --
quantity times difference, net of the cut -- which is the default sort, because
300% off on one unit is noise and 20% off on 400 units is money. The headline is
the volume-weighted error, not the mean of loose errors: on the fixture the two
disagree in direction (-35.15% weighted against +12.85% unweighted).
TSM in the harness. Three price fixes had shipped with zero diff in the suite,
not because they were right but because without TSM root.TSMPrice returns nil
and the whole path never runs. tests/tsmstub.lua reads the price language
(first, min, max, source * N) from prices the addon itself recorded in
r.audit.prices, so 755 of 965 recipes get a sale price under test.
Charge schedule. Charges capped the plan since v1.37.3 but nothing said a charge
was sitting at the cap, and charges come back ~1/day and do not stack past it.
Sorted by profit per charge, not by "is full": sorting by full buried the best
recipe under four full transmutes all losing gold. The warning counts only
charges worth spending -- 18 of his 20 full charges lose money at today's prices
-- with a 1g floor, so a 30/30 item at 0.79g per charge does not nag.
Price by realm came and went. Built as a tab, then measured on real data: it
flagged 156 of 558 items and 73% of them sell under 1/day against 24% in the
population, and he had sold exactly zero of the 156. The filter selects the
cause of divergence, which is illiquidity. The useful kernel moved into
ns.Provenance, which now reports the sale price as weak when the realm is far
off the region -- the delve recipe that showed 200k of unrealizable profit.
Ledger: a crafting order you placed is not income. Every item arriving in order
mail was booked as revenue, but receiving the output of an order you paid for is
production, not income, and the auction sale comes later. 19,005.74g of phantom
revenue on one item. C_Mail.GetCraftingOrderMailInfo tells the two apart, and it
now books the customer side as a conversion. Auction returns were already
correct; the test pins that down.
Own.lua read the invoice count from the 10th return of GetInboxInvoiceInfo and
it is the 11th -- the 10th is etaMin, which is 0. And count or 1 does not fall
back when count is 0, because 0 is true in Lua. Every mail sale was stored with
qty 0 and the total in the unit price field.
Enchanting did not exist for the addon. An enchant produces an effect, not an
item; applied to a vellum it produces an item with its own id and name, so
GetOutputItemID found no output and the scan dropped the recipe. Of 36 stored
Enchanting recipes, zero were enchants. The game does not expose the vellum id
(CraftSim keeps a hand-built private table), so the addon learns it: from the
craft result, which already carried the number and threw it away; from the item
cache by name; from bags; and from the auction scan. Quality comes only from a
definitive source -- GetItemCraftedQualityByItemInfo, the same pair of APIs TSM
uses -- never from id order, which is wrong for 7 of CraftSim's 103 enchants.
RetierEnchants fixes what was already stored at the wrong tier.
The reported crash: 17x integer overflow attempting to store nan, with
money = Infinite reaching the game's GetMoneyString. r.perConc divided
concProfit by concEff inside a guard that read "if r.concCost and r.concSale",
and concCost = 0 passes that, because 0 is true in Lua -- the third time in this
codebase. concCost = 0 is the recipe whose higher quality needs no
concentration, so gold per point does not exist there; it is a normal craft at a
better quality, now marked concFree with its profit preserved. Guarded in three
places: the division, EffectiveConc returning nil instead of zero, and
Pricing.Finito rejecting nan and infinity in the formatters, so a future
division by zero shows a dash instead of taking the UI down.
Also: the measured-yield tooltip divided items by n, where n counts crafts whose
output was not observed, so it read 7.43 per craft while the calculation used
7.69. On a 0.5g margin that is half the profit. The multicraft model itself was
verified against 279 of his crafts and is correct: 34.64% modelled proc chance
against 34.77% measured.
Cleanup: the dead History and Bets tab code, the empty Sales.Refresh, and
recoMinShare documented rather than removed -- it still filters the recipe list,
it is only bypassed for the concentration plan while the market cap is on.
Tests: t128 through t135. 104 tests, zero failures, 13 explained diffs.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - v1.39.4: a Portuguese column on an English client, and the blind spot behind it
Screenshot with the Simulate tab open: tabs and columns in English (Recipes,
Cost, Skill, Reagents) and one column in Portuguese -- "Qualidade".
The key had no EN translation, and the strict check.sh I turned on in v1.37.1
precisely for this did not catch it. The reason is written in CLAUDE.md as a
known limit: the checker only sees L["literal"]. The column label travels as a
variable into Head, which resolves L[label] inside, so it escaped.
tools/loccheck.py now follows indirect labels: for OHead and Head (the addon's
two functions that resolve L[] internally) it splits the arguments respecting
quotes and parentheses and checks the label. Verified in practice -- removing
"Qualidade" from the locale makes the check report
[Craft\Simulate.lua (via Head)] Qualidade and fail. The limit is no longer a
limit. It also picked up 34 labels it had not been counting, all already
translated.
Simulate's summary line: in the screenshot the cheapest set needs ~396
concentration and he has 290, so it yields 0 crafts. Saying "the best makes 601g
more than the cheapest" when the cheapest makes nothing at all is a strange way
to put it. In that case the line now says the cheapest set does not fit his
concentration, what it needs and what he has.
Test: t126.lua gained the insufficient-concentration scenario from the
screenshot.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - v1.39.3: the quality mark is now a texture, like the game's
Two screenshots side by side, one from the addon and one from the game: "I want
the addon's icons to show quality the same way".
Why v1.39.2 was not enough: there I fixed the atlas proportion, but the mark was
still a FontString with markup. A FontString carries font metrics -- the line
has its own height and the glyph sits centred in it with slack around it. That
is why the mark never reached the corner or the game's size, however high the
number went. The game draws a texture anchored to the corner, and now so does
the addon.
Canvas:Icon gained btn.qtex, an OVERLAY texture anchored TOPLEFT with 1px of
overhang like the game's. The new option is qTier/qMax (numbers); the old
quality(text) stays for places where the mark appears inside a sentence.
ns.QAtlas(q, maxQ) picks the atlas meant for icons -- it prefers
Professions-Icon-Quality-... (what the game uses over reagents) over ChatIcon,
which is built for text. ns.QAtlasW returns the width that keeps the ratio.
The 11 calls that draw a mark over an icon (Queue, Plan, Invest, Simulate,
Recipes) were converted. ns.QIconOn, added in v1.39.2, became unused and was
removed -- the dead-function sweep is back to zero.
The proportion is 50% of the icon height, which is what the game uses on
reagents (ns.QICON_FRAC). On a 32px icon the mark is 14x16; on a 26px one,
11x13. Without the atlas in the client it still falls back to text, with no
error.
Test: t127.lua now tests the path that actually runs -- the atlas chosen, the
proportion at 6 icon sizes, and Canvas:Icon showing the texture and clearing the
text (and hiding the texture when there is no quality).
Note: outside t127 the suite does not change, since the harness has no game
atlas. The effect only shows in game; if the mark reads too big or too small it
is a single number (ns.QICON_FRAC, currently 0.50).
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - v1.39.2: the quality mark was squashed and a different size on every screen
Asked for: "can you make the quality icon bigger, the same or similar to what
the game uses -- the one the addon makes has a different proportion from the
game's default -- and if possible keep it the same for all icons in the addon."
Cause of the wrong proportion: ns.QIcon built the mark with
CreateAtlasMarkup(atlas, size, size) -- square. The game's mark is not: the link
the game itself generates uses 17 high by 15 wide, so forcing a square squashed
it. The height now leads and the width follows the atlas's real ratio via
C_Texture.GetAtlasInfo; at height 16 the mark comes out 16x14, like the game's.
Without that API it still falls back to text ("Q2"), with no error.
One standard size (ns.QICON_SIZE = 16, was 14) instead of 10, 11, 12 and 14
picked by hand at each call. The 11 text marks scattered through the addon now
use the default.
For a mark drawn over an item icon, ns.QIconOn(q, maxQ, iconSize) uses a fixed
proportion (ns.QICON_FRAC = 0.45), as the game does. The size used to be
arbitrary: 12 over a 34px icon is 35%, over a 22px icon is 55% -- the same mark
looked different on every screen. The 11 places that draw a mark over an icon
(Queue, Plan, Invest, Simulate, Recipes) were converted.
Test: t127.lua (the proportion at 4 sizes, the standard size, the fixed
proportion at 6 icon sizes, a source scan that fails if anyone passes a loose
size again, and the path with no atlas API).
Note: the suite does not change outside t127 because the harness has no game
atlas and the mark falls back to text, which is why t127 stubs the API
explicitly. The visual effect only shows in game.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - v1.39.1: item icons in the Simulate tab's reagent column
Asked for: "can you put the item icon with its quality? from what I see there
is still plenty of screen available, we do not need to save space."
The reagent column showed only the stars, which forces you to memorise the slot
order. It now shows the icon of each chosen item, with the quality mark in the
corner, the quantity and the rarity border, so the combination reads at a
glance. Each icon carries its own tooltip (item, quantity and that slot's cost),
and the row's tooltip area starts after them so it does not steal the mouse. The
text label stays on the row: it is what sorts and what the tests read, since a
test cannot see a texture.
Roomier, as asked: rows went from 18 to 32px, the icon is 26px, and the number
columns were widened (Cost 78->92, Profit 86->102, Profit/point 80->96, Total
96->112). There was dead space between the reagents and Cost, and a cramped gold
figure is a figure that gets misread. The reagent column grows with the slot
count: 5 slots need 168px and fit with 68px to spare before Cost.
Test: t126.lua now checks that the icons appear (4 combinations x 2 slots plus
the header one = 9) and that each row knows the item and quality per slot.
Against the previous code it reports 1 icon instead of 9.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - v1.39.0: a Simulate tab for every reagent quality combination
Asked for: "which is better, crafting the light potion with the cheapest
materials or with the best ones, and every combination of material quality, and
how that affects concentration and how many more times I could craft if I
change the material."
Why this cannot be done in your head: a combination moves three things at once
-- the cost of the reagents, the skill (and with it the quality that comes out
WITHOUT spending concentration), and the concentration cost of going up one
quality. Since concentration is the scarce resource, cheap reagents that demand
more concentration yield fewer crafts and can make less gold overall.
Craft/Simulate.lua enumerates the cartesian product of each slot's qualities and
asks the game's own API about each one (Scanner.TryOp, the same call the
profession panel uses) -- not a formula of our own, which is the only way to
match what the game will actually do.
Columns: Reagents, Cost, Skill, Quality, Concentration, Profit, Profit/point,
Crafts and Total, all sortable. The best by total gets a green background, and a
line at the top says how much more the best set makes than the cheapest and with
how many more crafts.
"Crafts" is the direct answer to the request: floor((concentration - full cost)
/ effective cost) + 1. In the test, moving one slot up a quality costs 8g more
in reagents and buys 1 more craft and 170.50g more overall.
A combination that needs no concentration is not limited by it: it shows as
"free" rather than competing on the same axis. Saying "0 crafts" because the
bags are empty would be a lie -- the limit there is materials and gold, which
can be bought.
It follows the selected recipe just like the audit tab: both read
ns.auditOut.row, so there is no second selection mechanism to learn. Capped at
243 combinations (3^5), since each one is an API call and this is an on-demand
screen.
Test: t126.lua. t110 and t124 change on purpose: the new tab joins both geometry
sweeps, clean.
Note: the .toc gained a file, so WoW needs a restart -- /reload is not enough
this time.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com - v1.38.3: a price that exists on paper and cannot be realised
Reported: "it counted 200k gold of profit, but it is a gain I cannot realise,
the recipe does not sell for that on Goldrinn, and on others the value drops to
1/4" -- it was an enchanting formula that dropped in a delve.
Where it was: not craft profit. Uriuri's journal holds a 167,865.42g entry on
14/10, account 3.2.03, item 256753, from "The Labyrinth of Kindo'jan · Delves".
That is the ledger valuing the drop.
Cause: Livro/Price.lua used first(DBMarket, DBRegionMarketAvg, DBMinBuyout) --
your own realm first. In a thin market (a rare formula) a high number on
Goldrinn becomes "market value", which is exactly what he described when he
said it is worth a quarter of that elsewhere.
Fix: the value is now the lower of the realm price and the region average,
falling back to whichever exists when only one does. It is the same rule the
craft module already uses in saleSource. The ledger is accounting: what goes in
has to be what can be realised, not the best number that exists somewhere. The
file already recognised this trap for gear ("a single absurd listing becomes the
market value"); now it holds for any item.
A hole on the craft side, found on the way: saleSource is already min(realm,
region), but gear is priced by item string with the exact item level, and the
region rarely has data for that -- so min() does not resolve, the code falls to
SALE_FALLBACK and takes DBMinBuyout raw. In Uriuri's data DBMinBuyout runs 3.6x
DBMarket on several leather pieces. Pricing.BoundBySale caps a listing at the
realm's market value when it exceeds saleMaxOverMarket times it (default 2, 0
disables).
Test: t125.lua (the delve case with the real numbers, both sides, and the
configurable cap).
Caveats: the 14/10 entry stays as it is -- the change applies going forward, and
rewriting a past entry is his call. And the suite showed no differences at all,
because the harness has no TSM and this path is never exercised; t125 tests the
rule in isolation but the real effect can only be seen in game. That is a known
coverage gap.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
This mod has no additional files

