A feed folds a repeat into the row it is already on #52

Manually merged
atanabe merged 9 commits from feat/052-a-feed-folds-a-repeat-into-the-row-it-is-alrea into master 2026-07-30 06:17:16 +02:00
Owner

A feed folds a repeat into the row it is already on

Along the way:

  • A reagent says whether it is still worth a point
  • An objective is a name and two numbers
  • The loot feed folds twelve bandages onto one row
  • An item says why it matters to you
  • A feed row is read on a gradient, not on a panel
  • A tree may not reach into another tree
  • The switch list is Core's, not the unit frames'
  • Why an item matters is a tree, not part of the floor

Touches 26 files: docs/README.md, scripts/check.sh, scripts/harness/client/12-questlog.lua, scripts/harness/client/20-tradeskill.lua, scripts/harness/runner.lua, scripts/harness/sections/05-bars-tick.lua and 20 more.

A feed folds a repeat into the row it is already on Along the way: - A reagent says whether it is still worth a point - An objective is a name and two numbers - The loot feed folds twelve bandages onto one row - An item says why it matters to you - A feed row is read on a gradient, not on a panel - A tree may not reach into another tree - The switch list is Core's, not the unit frames' - Why an item matters is a tree, not part of the floor Touches 26 files: `docs/README.md`, `scripts/check.sh`, `scripts/harness/client/12-questlog.lua`, `scripts/harness/client/20-tradeskill.lua`, `scripts/harness/runner.lua`, `scripts/harness/sections/05-bars-tick.lua` and 20 more.
atanabe approved these changes 2026-07-28 10:30:10 +02:00
atanabe left a comment

Approving. The fold pushing nothing is the decision that makes this safe: the number on the bandage row climbs where the row already is, and somebody scrolled back through their own history does not have the column move under them.

Approving. The fold pushing nothing is the decision that makes this safe: the number on the bandage row climbs where the row already is, and somebody scrolled back through their own history does not have the column move under them.
@ -79,0 +87,4 @@
-- list was written by the same walk, so the first one answers for the rest and
-- this stays one lookup on a path the loot filter takes for every slot on a
-- corpse.
local function Stale(list)
Owner

next(list) deciding the shape of the whole list, on a path the loot filter takes for every slot on a corpse. That holds while one walk writes every entry. The day a second writer appends in a different shape, the first entry answers for a list that is now mixed and every reader takes the wrong branch silently. Is there one writer, and can the file say so out loud?

`next(list)` deciding the shape of the whole list, on a path the loot filter takes for every slot on a corpse. That holds while one walk writes every entry. The day a second writer appends in a different shape, the first entry answers for a list that is now mixed and every reader takes the wrong branch silently. Is there one writer, and can the file say so out loud?
@ -878,0 +936,4 @@
-- with the comparison Feed:Push makes. Without that a feed that folds at a full
-- ring loses one from the count per fold, and the oldest rows go out of reach
-- of a scrollbar measuring against it.
function Feed:Fold(match)
Owner

The matching fix-up only runs once self.written >= self.cap. Before the ring has filled, Feed:Entry has not taken anything off the count, so there is nothing to put back. That is correct and I drew it on paper twice to be sure. Give the condition a name so the next reader does not have to.

The `matching` fix-up only runs once `self.written >= self.cap`. Before the ring has filled, `Feed:Entry` has not taken anything off the count, so there is nothing to put back. That is correct and I drew it on paper twice to be sure. Give the condition a name so the next reader does not have to.
Author
Owner

There is a local called wrapped now, with your sentence on it: before the ring has been round once, Feed:Entry took nothing off the count, so nothing goes back on.

There is a local called `wrapped` now, with your sentence on it: before the ring has been round once, Feed:Entry took nothing off the count, so nothing goes back on.
Author
Owner

One writer, and it is the bake in scripts/words.lua. I have put that in the header and added an assert in the walk itself, so a second writer in a different shape fails at load rather than quietly reading every list the wrong way.

One writer, and it is the bake in scripts/words.lua. I have put that in the header and added an assert in the walk itself, so a second writer in a different shape fails at load rather than quietly reading every list the wrong way.
A feed had one way to record an arrival and it was a new row, so twelve
bandages off one corpse are twelve rows saying the same thing and the column
that is meant to be a glance is a list. Feed:Fold takes a match function and
the slot the caller has already filled from Feed:Entry, walks back from the
newest through sixteen held entries, hands each of them and that slot to the
function, and on the first yes marks the column and gives the entry back for
the caller to add to. Nil is nobody wanting it and the caller pushes as it
always did. Nothing calls it yet. Feeds/Loot.lua is items 74 and 75, and the
header on the function is written for them: the fields on the entry it hands
back are the caller's to write, at included, and a field the filter reads is
not, because matching was counted with the old answer.

Sixteen rather than the ring, because a walk of four hundred entries per
arrival is what Feed:Window exists to avoid and a fold sits on the path a pull
drives. Sixteen back is a corpse and the two before it. In place rather than
to the top, because an entry that folded and then jumped to the newest row
would reorder the column under somebody reading history, which is what
Feed:Push's offset arithmetic is written to prevent. written does not move and
neither does the offset.

The count is the half worth reading. Feed:Entry takes the oldest entry off
matching on its way to handing its slot over, because a push was about to
carry that entry out of the ring, and on a fold no push comes. Left alone,
a feed that folds at a full ring loses one from the count every time, and the
oldest rows go out of reach of a scrollbar measuring against it. What the ring
holds at that end now is the filled slot itself, so Fold counts it there with
the comparison Feed:Push makes. 31-feeds.lua states it as a fold and then a
recount, which is 400 both ways with the line and 399 against 400 without it,
and asserts the walk stopped after sixteen rather than after four hundred.
Comfort/Reagents.lua read the difficulty of every recipe it walked and
threw it away: the walk used it to tell a category row from a recipe row
and for nothing else. So the list could say tailoring uses linen and could
not say tailoring has not gained a point from linen in twelve levels, and a
feed built on it is a feed telling you to keep six stacks of something you
should be selling. An entry is now the profession and the best difficulty
any recipe wanting that id has, and Reagents.Skill answers both. Has is
untouched and answers exactly what it answered. Item 83 is the caller.

The best of, rather than the last written, is the half worth reading.
Copper bar is wanted by an orange belt and by a grey breastplate, and one
orange recipe is a point still there however many grey ones sit beside it,
so a walk keeps the highest word it saw for an id and the four rank
trivial, easy, medium, optimal. A word that ranking does not know ranks
under all four and is still written down, because an unranked difficulty is
a client saying something new and taking it for trivial drops a reagent off
the feed on the strength of a word nobody here has read. The saved shape
changed, so a list whose values are profession names is dropped whole on
the first read: it costs one profession window per profession, which is the
recovery the throttle already assumes, and reading it across would mean
inventing a difficulty for every id on it. The trade skill stub answers
optimal unless a fixture row names a difficulty, and 70-reagents.lua turns
two blacksmithing recipes grey to say that copper bar keeps its point and
rough stone loses one.
Client.Objectives hands over the client's own sentence, so "Red Silk
Bandana: 0/6" arrives as one string with the name, the nought and the six
inside it and nothing in the addon could get at any of the three.
Client.Counted is a second reading of the same line and answers those three.
Client.Objectives is untouched: the tracker and the quest window both draw
the sentence whole and neither of them wants it taken apart, so the reading
sits beside it rather than inside it.

The sentence is not typed out. QUEST_ITEMS_NEEDED, QUEST_MONSTERS_KILLED and
QUEST_OBJECTS_FOUND come off the client and become patterns, which is what
Core/Loot.lua does with the loot messages and for the same reason: a colon
and a slash written into the addon is a reading that captures nothing
outside English. The kind the client puts beside the line picks which of the
three to use. One detail is not optional on this client. Its deDE writes
QUEST_ITEMS_NEEDED as "%1$s: %2$d/%3$d", so the capture indexes come off
before the magic characters are escaped, and a pattern with the $ left in
matches nothing at all for a whole locale. Questie strips them the same way
in QuestieLib:SanitizePattern.

Which capture holds the name is worked out rather than assumed, and that is
the half worth reading. Questie's QuestieLib.TrimObjectiveText takes the
same three captures and then asks whether the name came back a number, under
the comment "SOME objectives are reversed in TBC, why blizzard?". The
client's own retail build is that layout: it writes the same objective as
"5/12 Defias Trapper slain". So the capture that is not a number is the name
and the other two are the count in the order they were written, which reads
both ways round. The item named QuestieQuest.lua at 1191 and 1204 for this;
the live 11.37.1 copy carries it in Modules/Libs/QuestieLib.lua at 548, 560
and 567.

The quest log stub grows the three sentences at their real enUS 2.5.6
values, because a stub holding something convenient would agree with a
reader that had typed the punctuation out. Section 47 reads the fixture's
two counted objectives through the model, checks the sentence the tracker
gets is still whole, then puts eight lines through the reading directly: an
object, a name with the sentence's own colon in it, a line the client counts
nothing on, a kill with no numbers in it, the German layout, the reversed
layout, a client carrying no sentence for that kind at all, and the client's
own sentence again afterwards. That took the section past the 800 line
ceiling, so it is on HARNESS_LINE_ALLOWED at 823 with the reason every other
entry there carries.
A craft hands you one bandage a second and the client says so every time, so a
column meant to be a glance was twelve rows of the same sentence. AddItem asks
Feed:Fold before it pushes and pushes only when nothing took it. Two arrivals
are one pickup when the link is the same, the looter is the same, and the row
they would fold into arrived inside sixty seconds, read off entry.at. The count
becomes the running total, the number on the row is written from it and reads
x12 where it read x1 twelve times, and at moves to the newest of them, because
a row that folded is a row about the last one you picked up. A fold writes the
count, that number, the tally of pickups and at. The three the filter reads are
this same item's already, and matching was counted with the old answer.

The looter is in the match though item 74 is silent on it. who is nil on your
own pickup and a name on somebody else's, and the hover draws it as "Went to",
so a fold on the link alone puts your linen and a party member's on one row and
that row counts three of them while naming one player. It is the defect item 75
refuses for coin, a column of arithmetic saying something untrue, and it is
worse here because the group column exists to answer who got it. Your linen
folds onto yours and Bram's onto his.

Sixty seconds is a number rather than the whole ring, for the reason Feed:Fold
is bounded at sixteen entries. A vendor run an hour ago is a different
afternoon. It is not a setting: a slider on it would be the third control over
what this column shows, and the header over the quality chips is an argument
against the second. The match is one function with the moment beside it rather
than a closure per arrival, because a corpse full of loot drives this path.

The tooltip is the other half of the fold and the half that is easy to lose. A
row reading x12 whose hover says 12 has thrown it away, because that number is
on the row already. The Stack line says twelve in three pickups, and the Looted
line is dated by the last of the three rather than by the arrival that opened
the row. 40-loot-feed.lua covers the fold, an item in between that does not end
the run, the window expiring at sixty one seconds, both of those lines, and one
item off two looters coming to two rows of three and two.

31-feeds.lua fills a column by dropping one item over and over, which under a
fold is a column with one row in it, and three of its scenes broke on that. Its
drops carry an enchant per arrival now, so every link is its own item and the
ring test is about the ring rather than about the fold. That file sits one line
under its own ceiling of 800, which is why the reason is written once at the
first of the three and not at all three.
Three parts outside src/Feeds/ are going to draw the same fact about an item
and none of them could get at it. The quest log knows an objective is counting
it, the reagent list knows a profession can still gain a point off it, and the
loot filter knows it would have left it on the corpse. All three readings sit
in three different trees and a part may not name a file outside its own, so
ns.Need is the one call that asks them and it is in Core, in the shape
Core/QuestItems.lua has: it wraps readings other parts own, holds no setting
and draws nothing.

The order is in the function and not in the caller, which is the whole reason
this is one call rather than three. A wolf liver that is also a leatherworking
reagent is a wolf liver you need eight of, so it goes quest, then skill, then
trash, then nothing, which is what most items get. Two callers ranking the same
item differently is what this exists to stop.

The trash source wanted a second argument and the item did not say so.
Wanted.Take answers about a loot slot rather than about an item, and the loot
feed reads CHAT_MSG_LOOT, where there is no slot to hand over. So ns.Need takes
the slot as an optional second argument and asks the filter only when it is
given. A bag square has no slot and never will, and gets the two answers that
are facts about the item.

Cached per item id, because AddItem in Feeds/Loot.lua is on a path a pull
drives. What is cached is the quest and the skill answer; the trash answer is
about a corpse in front of you and is asked live for a handful of table reads.
The whole cache goes on the quest log's two events, on SKILL_LINES_CHANGED and
on the trade window's four. The log itself is walked once per change to it
rather than once per item: one item looked up against the whole log is a walk
per drop, and the whole log looked up once is a walk per change, which is far
rarer than a corpse being emptied. An objective already at its total is not a
reason, because a line reading 8 of 8 wants no more of the thing, and a reagent
whose recipes have all gone grey is not one either.

The three reason colours go in UI.Color keyed by the word they stand for, so a
fourth reason is a fourth entry and nothing else. Section 82 stands one wolf
liver up as a quest objective and a blacksmithing reagent at once and reads
quest back off it, then moves the count with nothing fired and reads the stale
answer, which is the only way to tell a cache from a walk.
Both feeds painted the window colour across the whole frame at whatever the
background slider said, and every string on them carried a rim. The loot feed
shipped that panel at 15 percent because a surface over open world was
covering scenery to hold up text that did not need holding up, and the rim was
the other half of the same admission: a feed has no ground it can promise, so
each glyph spent a pixel of every stroke saying so.

The panel is gone and each row carries a wash. It is ns.UI.Color.shadow, solid
at the stripe and gone by the end of the text, which makes it a shadow the size
of the row rather than a surface behind the column. Where it ends is the half
worth arguing. A wash as wide as the row is a rectangle with one soft edge and
that is the panel again, so it stops at the right edge of the number column,
three units short of the row, and Resize works that out with the other four
widths and hands it over as geom.wash. The background slider is what says how
strongly it is painted, on the texture rather than in the ramp, because a
gradient rebuilt per drag is twenty four of them made for a multiply the client
was going to do anyway. Full strength is therefore the palette's shadow at 0.55
and not black: a slider that reached opaque would be the panel back at the top
of its range. Every stream gets it, because the combat feed is over the world
by the same argument and Feeds/Combat.lua never asked for a different answer.
The panel page says ground under the rows where it said background, and the
hint under it no longer promises an outline.

The status strip gets the same thing and takes its ramp the other way. The
three readings at the foot of the loot feed sit left, centre and right, so a
wash along the strip's length is solid under one of them and gone by another:
from the left it grounds what you are holding and abandons the rate, and from
the right it does the reverse. Up the strip's twenty units the ramp crosses all
three at the same height, so what darkens is the foot of the window rather than
a bar laid across it under one number. Three washes cut to the three readings
is the other answer and it draws three shadows with daylight between them along
a strip that is one strip. It is the same slider as the rows above it, so the
strip and the column never read as two surfaces.

The rim comes off in the same commit, at seven sites in UI/Feed.lua and three
in the status strip, and the plan said ten for the first of those. One commit
is not a preference here. Text with no rim and no wash under it is the worst of
the three states and it is exactly what splitting this would ship, which is
also why the strip is washed in this commit rather than left to the one that
turns its readings into badges. A badge draws a number, a word and a tone; that
is not a promise about what is behind it. UI.SHADOW is what the character sheet
draws in over the same world for the same reason, and a shadow holds a glyph
off what is behind it without spending any of the glyph.

UI.FEED_ICON_LOW stays at 16. The floor that put it there was the rim's, so the
number is free to come down now, and it comes down when somebody measures a
shorter row with the wash actually under it rather than in the commit that
removed the reason.

The row section went into 40-loot-feed.lua because 31-feeds.lua is one line off
its own ceiling, and every check in it is one that fails silently. A wash the
width of the row draws a rectangle and measures fine. A wash made after the
hover glow takes the light out of the row under the cursor, so both are found
in the row's own region list and the order is read back. A wash running the
other way is solid where the number is and clear under the name, so the ramp is
read off the texture rather than inferred from the call being made. A wash that
ignored the slider would leave the shipped 15 as a black bar under thirteen
rows. The strip's is in 35-purse.lua, which asserted that these three carried a
rim: that was the fix that got reported and it is not the answer any more, so
it asserts a shadow, the 14 the strip is 20 units tall to hold, and a ramp that
runs up the strip rather than along it.
The addon is one namespace, so nothing in the language stopped Mail from
calling into Bags or Character from calling into Buttons. The rule that says
it should not was a sentence in a comment in Core/Piles.lua, and by the time
anyone counted there were thirty-six edges crossing trees that nobody had
decided on.

scripts/trees.lua is the gate. It derives who owns what from the source rather
than from a list somebody maintains: every top-level `ns.X = X` says the tree
it sits in owns X, so a file moved between trees takes its symbols with it.
Four rules run over that.

The base is closed. Six trees may be named freely and they were picked by
measurement, not preference: Core, UI, Ck, Class, Unit and Perf are each named
by four or more trees and each names nothing outside the base. Run with
TREES_REPORT=1 for the table. UnitFrames looks like base on in-degree alone and
is not, because eight of its nine callers want ns.BlizzHide, which is a
switchboard over Core/Attic.lua that landed in UnitFrames because unit frames
asked first. A tree that reaches into half the addon is a hub, not a floor.

A feature may name its own tree and the base, and every other crossing needs an
entry carrying the count and a written reason. The list ratchets both ways: an
edge named fewer times than its entry claims fails until the number comes down,
a raise fails in scripts/ratchet.lua against the committed copy, and an entry
that matches nothing fails so the list stays a work list rather than a pile of
permissions for code that is gone.

A file-scope capture of a symbol whose file loads later is nil, and no entry
excuses it. This one is per file rather than per tree, which is the version
that is right: the toc names Core three times, and ranking a tree by its first
appearance called Core/Panel.lua and Core/MenuSkin.lua load-time nils when the
toc's own comment says they load after UI/Widgets.lua on purpose.

The tokeniser moves to scripts/words.lua because two gates now read Lua the
same way. Skipping comments is the whole reliability of this one: the headers
in this codebase name other files' symbols in prose, and ns.CharReadout appears
in three sentences no code runs. A grep counts those.

Ownership is claimed two ways and both count. `ns.X = X` is what nearly every
file writes and `function ns.X()` is what Core writes ninety-seven times and
Feeds/Loot.lua writes once; missing the second left ns.QualityWord owned by
nobody and every call to it unchecked.

The block counter that decides file scope is the part that had to be got right
and took three tries, so it now asserts: a file whose walk does not come back
to level fails outright rather than reporting guesses. Both bugs it was written
against would have failed there on the first run.
UnitFrames/Blizzard.lua owned the one question every window in this addon has
to answer: this addon draws X, so should the client's X still be on screen? It
lived there because unit frames wanted it first and nothing since had a reason
to move it. Then the character sheet, the talent window, the spell book, the
bags, the chat, the merchant, the map and the level bar each had to name
UnitFrames to take the client's copy of themselves off the screen, and not one
of them knows anything about a unit frame.

scripts/trees.lua counted those eight edges the day it landed and they were
eight of the thirty-six it found. The answer to eight edges into one file is to
move the file, not to write eight entries excusing the callers, so it is
Core/BlizzHide.lua now, beside Core/Attic.lua where the frames it takes down
actually go, and after UI/Ticker.lua for the once-a-second pass it arms. The
gate reads twenty-eight edges and its allow-list is eight entries shorter.

Two lists named the old path and both were wrong the moment it moved. check.sh
exempted it from the tooltip hint rule, and the harness ticker check kept it on
the list of files allowed to carry a permanent tick. Neither would have failed
on its own: the hint rule only reads the path it is given and the ticker check
asserts a count that a dead key cannot change. That is what a stale allow-list
entry looks like from the inside.
Core/Need.lua reads Quests/Client.lua, Comfort/Reagents.lua and
Comfort/Wanted.lua, and its header gives the right reason for the wrong place:
a part may not name a file outside its own tree, so it went into the one tree
every part is allowed to name. That inverts the dependency. Core is the floor
thirty-two trees stand on, and a floor that reaches back into the quest log
means every window wanting a colour on a row depends on the quest log.

scripts/trees.lua caught it on the first run after both landed, which is the
rule it enforces with no allow-list at all: a base tree may name only base
trees. So the file is src/Need/Need.lua, loading after all three parts it
reads, and the three readings are three entries with a line each saying what
they are for. Nothing about the code changed.

A window that comes to ask will need an entry of its own, and that is the gate
working rather than getting in the way: one line naming why a loot row wants to
know what an item is for.
atanabe manually merged commit de3540c814 into master 2026-07-30 06:17:16 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 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!52
No description provided.