The hook comes back #50

Manually merged
tvidal merged 9 commits from feat/050-the-hook-comes-back into master 2026-07-16 14:29:50 +02:00
Owner

The hook comes back

Along the way:

  • The weapon loadouts come out
  • The addon draws in a face it ships rather than one the client had
  • The figure is yours to turn
  • A name is read on a gradient rather than on the grass
  • The socket carries its gem rather than a mark saying one is there
  • The sheet is one page and the tab strip goes with the others
  • The world darkens behind the sheet
  • The line under an item's name says what is on the item

Touches 32 files: scripts/bake-glyphs.sh, scripts/check.sh, scripts/harness/client/01-widgets.lua, scripts/harness/client/02-text.lua, scripts/harness/client/03-player.lua, scripts/harness/client/13-character.lua and 26 more.

The hook comes back Along the way: - The weapon loadouts come out - The addon draws in a face it ships rather than one the client had - The figure is yours to turn - A name is read on a gradient rather than on the grass - The socket carries its gem rather than a mark saying one is there - The sheet is one page and the tab strip goes with the others - The world darkens behind the sheet - The line under an item's name says what is on the item Touches 32 files: `scripts/bake-glyphs.sh`, `scripts/check.sh`, `scripts/harness/client/01-widgets.lua`, `scripts/harness/client/02-text.lua`, `scripts/harness/client/03-player.lua`, `scripts/harness/client/13-character.lua` and 26 more.
praghunathan requested changes 2026-07-15 06:30:41 +02:00
Dismissed
praghunathan left a comment

The wash is lovely and the argument against baking the ramp into a texture file convinced me before I got to the third reason. Holding this on the stub, which is a smaller thing than it sounds.

The wash is lovely and the argument against baking the ramp into a texture file convinced me before I got to the third reason. Holding this on the stub, which is a smaller thing than it sounds.
@ -191,0 +195,4 @@
-- would be this file claiming to know a shape it has not read.
function _G.CreateColor(r, g, b, a)
local color = { r = r, g = g, b = b, a = a or 1 }
function color:GetRGB() return self.r, self.g, self.b end
Owner

This hands back a colour object we invented, so every section that goes through it proves our stub agrees with itself and nothing else. What does the real CreateColor return on 2.5.6, and is alpha the fourth return of GetRGBA or a field on the table? If we have that backwards the harness stays green and the sheet draws every name at full alpha.

This hands back a colour object we invented, so every section that goes through it proves our stub agrees with itself and nothing else. What does the real `CreateColor` return on 2.5.6, and is alpha the fourth return of `GetRGBA` or a field on the table? If we have that backwards the harness stays green and the sheet draws every name at full alpha.
Author
Owner

The shape is off ColorMixin in the client's own SharedColor.lua, which is where I copied both methods from, and alpha is the fourth return rather than a field. You are right that nothing here proves it, and a harness on this machine cannot: the stub now carries the file and the build number I read it in, which is the most honest thing available.

The shape is off ColorMixin in the client's own SharedColor.lua, which is where I copied both methods from, and alpha is the fourth return rather than a field. You are right that nothing here proves it, and a harness on this machine cannot: the stub now carries the file and the build number I read it in, which is the most honest thing available.
mdoorn approved these changes 2026-07-16 08:30:00 +02:00
mdoorn left a comment

The weapon loadouts coming out is the part I care about. Three files, 764 lines, ten secure buttons made at login whether or not anybody owned a loadout, and a macro rewritten onto each of them on every stance change. That is the biggest thing anyone has taken off the login path this year.

The weapon loadouts coming out is the part I care about. Three files, 764 lines, ten secure buttons made at login whether or not anybody owned a loadout, and a macro rewritten onto each of them on every stance change. That is the biggest thing anyone has taken off the login path this year.
Author
Owner

Also moved the two methods onto a shared metatable instead of making closures per call. A full run makes a few thousand of these and it was the last allocation left in that file.

Also moved the two methods onto a shared metatable instead of making closures per call. A full run makes a few thousand of these and it was the last allocation left in that file.
praghunathan left a comment

Naming the build you read it in is worth more to me than the code change was. When this breaks it will break on a client nobody here has installed, and the comment is the only thing that will tell that person where to look.

Naming the build you read it in is worth more to me than the code change was. When this breaks it will break on a client nobody here has installed, and the comment is the only thing that will tell that person where to look.
45e56e7 took scripts/hooks/pre-commit out with it. Nothing in that commit
wanted it gone and its message does not mention it, so it rode along on a
staged deletion. core.hooksPath still names scripts/hooks, so what was left
was a repo whose local gate resolves to a directory that is not there: every
commit after it goes in ungated and nothing says so.

That is the failure mode the two layers exist to catch, and it is the one
neither layer catches on its own. A missing hook is silent by construction,
and check.sh cannot see it because check.sh only reads src.

Restored as it was.
Three files, 764 lines, ten secure buttons made at login whether or not
anybody had a loadout, a macro rewritten onto each of them on every apply,
and a tab on the character sheet to drive it. What it bought was a pair of
weapons and a stance on a key. The charge macro already swaps a weapon out
of a line of macro text with no secure button, no saved list and no page,
and that is the shape the one real use had.

The folder goes with the tab that hosted it, in one commit rather than two.
The registry runs at file load, so a folder dropped from the toc while
Character/Window.lua still called ns.LoadoutPage.Build would be a Lua error
at login, and this client hides those: the gate would pass and the sheet
would silently not appear. So the pane, the TABS entry and the LOADOUTS
constant come out in the same change as the files.

The stumps are the part worth the sweep. The saved variables, the slash
word, the help lines and the status line were all inside the deleted
Feature.lua, so they left with it, but ten global button names sat in
.luacheckrc, an allow-list entry sat in the options harness saying why the
part declared no switch, the section list in the runner named a section
that no longer exists, and fourteen files carried comments pointing at
Loadouts/Loadouts.lua for the shape of something. An allow-list entry for a
part that is gone is the same debt as a warning nobody fixes, and a comment
citing a deleted file is a wrong answer to whoever reads it next, so both
were retargeted at something still on disk rather than left.

The bar loadout in Class/*.lua is a different thing with the same name and
nothing about it changed.
Arial Narrow was never chosen. It was what every client already has, and
that is a supply argument rather than a design one. Media/Sans.ttf is Noto
Sans Regular under the SIL OFL, licence beside it the way Glyphs.ttf has
one, and UI/Text.lua's PATH is the single line that puts every string in
the addon into it.

Noto Sans over Source Sans Pro, which is the face Narcissus reaches for and
was the obvious answer, and the two were weighed at eleven pixels rather
than on a page. At eleven the only measurement that matters is the
x-height, because that is the letter and the em box is not. Arial Narrow is
0.519 em and draws 5.7 pixels of letter; Noto Sans is 0.536 and draws 5.9;
Source Sans Pro is 0.486 and draws 5.3, so the whole addon would have come
out smaller on the screen at the sizes it already asks for, which is the
opposite of the point. The copy of Source Sans Pro on this disk also ships
no fpgm, prep, cvt or gasp at all, and UI/Text.lua already carries a
gravestone for what an unhinted face does at these sizes.

The cost is 27 per cent. Noto Sans averages 0.481 em per glyph over the
addon's own strings against Arial Narrow's 0.379, and two places had been
sized against the narrow one. Seven of the options window's forty five
section titles ran off the end of their line, so the rail is 216 rather
than 180, which is the letters' growth and not the row's. A five figure
crit ran out of the feed's number column, so that is 52 rather than 42.
Both were widened rather than the font dropped a pixel to make the old
numbers hold, because a label that fits because the text got smaller is
this change undone. The tooltip's wrap went to 266 for the other reason:
its comment argues for forty characters a line and 210 buys thirty six of
this face, so the pixels moved to keep the sentence the same shape.

The harness models advance width and that model was Arial Narrow's. It is
0.53 now, which is the same eleven per cent of slack 0.42 left over 0.379,
so a width assertion that moved moved by a ratio. Nothing else moved, and
the ones that did not are the ones that had room.

Two gates. Every one of the two and a half thousand strings the addon draws
is now asserted to be in a face the addon ships, which is how a face change
is known to have reached forty windows rather than the six anybody thinks
to screenshot; a caller that reaches SetFont itself lands on something
legible and is invisible without it. And the readback that covers a client
refusing the file is driven, because the face lives in the addon folder now
and can actually go missing, where Fonts\ARIALN.TTF could not. That
fallback is Arial Narrow rather than the client default, so a broken
install draws what the addon drew last week.
Portrait set the unit, faced it at half a radian and never touched it
again. Every character sheet in the game since 2004 lets you drag the
figure round, and this one is built on a figure standing in the middle of
the screen with two columns of gear either side of him.

Left drag turns him at Blizzard's own ten thousandths of a radian to the
pixel, so a drag here and a drag in the dressing room move the same
amount. The wheel walks him nearer and further. A mark in his bottom
corner puts the weapons in his hands, which is the only way to look at a
weapon you are wearing, and it is a mark on the figure rather than a row
on a settings page because nobody opens a settings window to look at a
sword. All three are this character's own saved variables, so the sheet
opens the way you left it and a gnome and a tauren are posed apart.

The wheel drives two calls rather than one, and that is the half worth
saying. SetCamDistanceScale on its own brings the camera in on the model's
origin, which on a character is between his feet, so half distance is a
close look at a pair of boots. SetPosition drops him by as much as the
camera comes in. It stops dead at both ends: the sheet is half the screen
and carries no reset, so a figure wound out of that half is a bug the
player can only undo by winding the wheel back, and the clamp is gated on
both stops rather than on the one anybody would think to try.

The turn is a ticker on the model rather than a handler of its own, which
is what the guard scan can walk out from, and hanging it off the figure is
what stops a sheet shut mid drag turning something nobody can see. Every
call on the model is probed and pcalled the way SetUnit already was, so a
client that will not draw one leaves the page working.

The right button stays the camera's as far as this client allows. It gets
UI.PassCamera like every row on the page, and on 2.5.6 that probe fails,
so the honest half is the one this file owns: a right press arms no turn,
and the drag the player is making is not also spinning the figure under
it.

The stub had to learn three of the four calls. It answers any PascalCase
name with a no-op, so a figure that was never walked nearer and never
handed his weapons measured identically to one that was, which is a test
that cannot fail. SetRotation was already there, because the client spells
a model's facing and a texture's angle the same and the map arrow found it
first.

The presses go through H.mouse rather than by name, which the hit test
now insists on and which this item wants anyway: the claim is that a press
on the figure reaches the figure, and a handler called by hand says
nothing about who the client would have given it to. The right press is
the one exception and is delivered outright, because the stub honours
PassCamera and would route it past him, and the guard being asserted is
the one that runs on the client where that call does not exist.
The gear page has no ground under it by design, so nineteen item names are
drawn straight onto whatever the player is standing on. Every string on
that page already takes the rim, and a rim is a patch: it thickens each
glyph, costs a second draw of the whole string, and still leaves a white
name on snow as a name you lean in to read.

So each filled row carries a wash now. It is black, it is solid at the
disc and it is gone by the far end of the text, which makes it a shadow
the size of the thing casting it rather than a panel behind the row. The
size is the whole of the answer and it is measured in the repaint, after
the two strings have their text: as tall as both of them plus eighteen,
and as wide as the longer of them plus forty-eight. An empty slot draws
none, because a shadow under the word trinket is a shadow under nothing
and eight of the nineteen rows are empty on most characters.

The direction is the caller's, and that is the half worth arguing. A right
hand row reads inward from a disc on the right, so a wash that always ran
left to right would be solid where there is no text and clear under every
letter, which is worse than no wash at all. entry.side picks the end, and
the harness reads the gradient back off both columns rather than asserting
that a call was made: a wash running backwards draws perfectly and
measures perfectly, and the readback is the only thing that tells it from
a right one.

UI.Wash is a primitive beside the rest of Draw.lua rather than a private
texture on this page, because every window the addon draws over the world
has the same problem in a smaller form. It is not a file with the ramp
baked in: a row is as wide as its own name, so one file would be stretched
by nineteen different amounts and the fade would start in a different
place on each, and the colour it carried would be a palette decision
living in art. SetGradient draws the ramp per vertex for nothing.

That call is probed in Core.lua with the other shims, and probed harder
than most, because it was rewritten rather than added: the form taking
eight loose numbers was removed. OPie passes plain colour tables and
Narcissus wraps CreateColor round its own, and CreateColor is what is
taken here, since a raw table is a guess at which fields the build reads.
The harness client knew neither name and now has both.
A socket drew as a five pixel dot filled in the gem's quality colour. That
says a hole is occupied and nothing else: two rubies and a topaz are three
red dots, and the only way to find out what is actually in a piece was to
hover it and read the tooltip, which is the thing the row exists to save
you.

The disc is twelve pixels now and carries the gem's own icon, clipped to
the same circle by UI.Clip, with the quality colour left showing as a two
pixel ring round it. A hole is the same disc empty with its ring at half
strength, which is enough to read a place for a gem against a gem without
either needing a label.

An empty socket is drawn neutral, and that is the data rather than a
preference worth defending. A socket is meta, red, yellow or blue, and
that colour is a fact about the item; an item link carries the gem sitting
in each filled hole and says nothing at all about the empty ones. There is
no call that recovers it, and reading it off the slot would be the page
making a colour up. So the ring takes the panel's edge where there is no
gem, and the hover is where the socket gets named, because the client
writes "Yellow Socket" into its own tooltip for the slot and the row
already opens that tooltip.

Placing the disc turned up a second thing. It was pinned by its far edge
to the row and by its top to the item level line, which constrains the
middle in y and the top edge in y both, and anchors beat SetSize: twelve
wide and twenty-two tall is an ellipse. The note is anchored across the
full width, so its far edge is the row's, and one point on the note says
where the disc goes in both directions and leaves the size alone.

The gate reads the gem's icon back off the first disc, checks it carries a
mask and is narrower than the ring round it, checks the empty one is the
edge colour weaker than the filled one, and then aims at the disc rather
than calling a handler: the pointer on a disc has to reach the row, and
the row's hover has to come back with a socket line in it. That last pair
is what fails the day somebody hangs a second tooltip off a disc and takes
the row's away.
Four tabs, and three of them were a press between two halves of one
question. The stats went into the column down the right of the gear page
a while ago for that reason, and the skills belong there for a sharper
version of it: the miss badge at the head of that column is computed from
your weapon skill, and the two had been on separate pages since the sheet
was built. Skills.Groups already hands back what Stats.Groups hands back,
so folding them is one list appended to another and the readout draws it
unchanged.

Reputation could not follow them. A faction is not a number about your
character and nothing on that list moves when you swap a ring, so it gets
a window instead, on /wk reputation, hosting the same readout pane with
the same Rep.Groups filling it. Character/Reputation.lua did not change a
line. Its host is built on the first open, which is what every window in
the addon does except the sheet it came off, and the sheet cannot because
its key runs a snippet.

What is left is a window with one pane in it, so the tab strip, the
opaque cover the two prose pages needed, the PAGE ceiling they were
drawn at and the Select switch all come out. The page starts at the
padding now instead of under a row of words, which moves the four badges
up into the room the strip was using and leaves the grip the whole top
edge rather than the two ends either side of the tabs.

The harness gates the near end of that edge with a press aimed at it,
twenty units in from the corner, which is where the first tab used to
sit and take the click. The fold is asserted where it is drawn, in
52-gear-page, and the standings have a section of their own whose first
claim is that nothing was built before the word was typed. 52-character
is 832 lines against a ceiling of 858, so the ceiling comes down to what
it measures and its reason stops naming the tabs.
The character sheet has no ground by design, so nineteen item names, a
column of numbers and a figure were read against whatever the player
happened to be standing on. That is text scattered over scenery rather
than a page. One wash across the sheet's own half of the monitor fixes
it: solid at the outer edge, gone by the middle where the figure stands,
faded in over a fifth of a second rather than shown, because a rectangle
of shadow that arrives between one frame and the next on a key press
reads as a bug.

It belongs to the window and not to the gear page, so it is drawn behind
the screen flag in UI/Window.lua and the next screen window this addon
grows gets it by handing over one getter. It is pinned to the window's
own two corners and to nothing else, which is what keeps it off the half
of the screen the sheet was cut down to give back.

The fade hangs off the frame's own OnShow rather than off Window:Show,
and that is the half worth naming. The sheet is opened in a fight by a
secure snippet that runs none of this file's Lua, so a fade on the method
would be a fade nobody saw on the key most people open the sheet with.
Character/Window.lua hooks its paint under it instead of setting it.

The wash must not answer the pointer, and that is the one thing here that
could cost a player the world behind the sheet without ever looking
wrong. A screen window turns the mouse off on its own frame so a click
reaches the mob; a shadow over half the monitor on a frame that took the
mouse is half a monitor you can no longer target, loot or turn the camera
in, and it draws and hovers identically either way. So it is a texture on
the frame that already refuses the mouse, and there is no new surface for
anyone to switch it on later. The gate is a hundred and twenty one presses
on a grid over the sheet: none of them may land on the frame itself, and
the only child of it any of them may reach is the grip.

The gate is a section of its own, 52-screen-dark.lua, and not a block at
the foot of the gear page. It went there first and it was in the wrong
file on the merits: everything else in that one is the page, its rows, its
columns, its figure and its readout, and this is the library's window
under all of them. The line ceiling that file went over is only what
noticed. The section opens the sheet itself and shuts it again, under
52-character rather than over it, because that section's first assertion
is that nothing has opened the sheet yet.

The six answers that say what kind of window the sheet is came out of
Character/Window.lua's builder into a function of their own, the way
UI/Window.lua already splits its own chrome out of its own constructor.
Forty five of those lines are prose arguing why this window has no
ground, no line, no title bar and no place in the pile, and none of it is
something a reader following how the sheet is assembled has to step
through.
It said the item level and stopped. On a character at the cap every piece
is enchanted, and an unenchanted pair of boots is the one gap in your gear
that nothing on the page could show you: the item level does not move, the
figure looks the same, and finding it means hovering all nineteen rows.

The name of an enchant costs a tooltip scan, because a link carries the id
and no call in the client turns one into a word. The id is free though, and
a piece with nothing on it says so in its own link, so the scan is asked for
only where there is something to name and only on the repaint that found the
link moved. Redress already compares those nineteen links for the figure and
now marks the row it found, which is the same answer to the same question
and is nineteen scans on the paints that matter rather than on every one.

The three weapon rows carry the other half, which is the stone or the oil
counting down where the level sits. That is a number that goes stale, so it
is on a tick, and the tick is on ns.UI.Forever with a visibility test at the
top rather than on a frame of its own: the gear page's frame is shown from
the moment it is built and never taken down, because it holds nineteen
secure buttons and hiding one of those in a fight is a protected act, so its
own flag says nothing about whether anybody can see it. The write behind the
comparison is on the whole minute, not on the string, which is the shape
item 34 was.

The right hand column reads its three strings in the other order. Both sides
justify to their own disc, so printing them the same way round would put half
the page's item levels out in the middle of it, and Narcissus swaps the same
pair on the same line for the same reason.

The wash under the two strings was the thing this most easily broke. It is
as wide as the longer of them and the line was two digits when that was
written; a string anchored at both ends measures at its full length however
much of it is drawn, so the shadow under a long enchant would have run out
past the edge the letters are cut off at and over the figure. Both strings
are clamped through one function now rather than the name being clamped and
the note not, and the gate measures the clamp on a fixture that overruns its
row.

UI/Scan.lua grew the reader rather than the caller growing a fourth copy of
the prologue. Read, Has and Match ask the client the same question and
differ only in what they do with the answer, and the guard on the second
argument is the one that would have been left out of the copy.

The assertions are a section of their own rather than a block in
52-gear-page.lua, which is a file already at its own ceiling on a subject
that keeps growing. What is asserted there is what the page draws about a
piece; what is asserted here is what it reads out of the client to draw it,
which is a scan, a call nobody has settled the shape of, and a tick.
tvidal manually merged commit 40d8fe40bf into master 2026-07-16 14:29:50 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
northgale/warriorkit!50
No description provided.