Closed 2 on the reference pair and 4 on pair 2, regressed 0, with no operator input. Measured, both pairs, never netted. - S00 stamps PvSav from Sav on every live player, before any phase can move it. 0 closed on pair 1 (a no-op there), 2 on pair 2. - T31 recovers the per-player difficulty column by recomputing BnkEl from the colony state the input save was written from and comparing against the BnkEl the save carries. That removes the --ai-player flag as a blocker and turns the phase's self-check into a real one: it used to compare its POST-turn result against the PRE-turn stored value, so its 6-of-8 only ever covered the six players whose limit does not move. The load-time check passes for every live player of all eleven corpus saves. T31 Blocked -> Partial; BnkPr still needs the tuning constant. - BANKRUPTCY_PROTECTION_LIMIT_FACTOR is multiplied in as fmul dword ptr, so it is a float32 in the image; the engine narrows it now. Zero leaves move on this corpus -- all seven of its BnkPr records land where the two constants agree -- and three hand-written test expectations moved (rule 23). docs/PL-players-residual.md carries the decomposition, the predictions written before the build, where they were wrong, and the ranked remainder.
299 lines
21 KiB
Markdown
299 lines
21 KiB
Markdown
# PL — decomposing the `/Sim/players` residual
|
||
|
||
Every lane so far attacked a named subsystem and reported which leaves fell out. This one runs
|
||
the other way: start from the residual the metric reports, group it by **mechanism**, and say
|
||
what the remaining leaves actually are. Closing leaves is secondary to the decomposition.
|
||
|
||
Binary re-measured, not inherited. `sots_turn` built from `main` (`aabd8a3`) in this worktree,
|
||
`tools/standalone_report.py --binary …`:
|
||
|
||
| run | pair 1 residual | pair 2 residual | `/Sim/players` share |
|
||
|---|---:|---:|---:|
|
||
| `main` (`aabd8a3`), no options | **128** | **69** | **54 / 24** |
|
||
| `main`, `--commit-blocked=T31 --ai-player 1` | 126 | 67 | 52 / 22 |
|
||
| **this lane**, no options | **126** | **65** | **52 / 20** |
|
||
| this lane, `--data ROOT --commit-blocked=P11 --ai-player 1 --ai-player 2 --ai-player 3` | **122** | **62** | **48 / 17** |
|
||
|
||
`closed` / `regressed` on the reference pairs, never netted: this lane closes **2 on pair 1 and
|
||
4 on pair 2 with no options at all, and regresses 0**; with a data root, **87 / 0** and **46 /
|
||
0** against the 209 and 108 baselines.
|
||
|
||
Two corrections to the record before anything else:
|
||
|
||
* The stale `status.json` breakdown said `/Sim/players` 55. It is **54** on pair 1 and **24**
|
||
on pair 2.
|
||
* **The published 128 / 69 overstates what is unmodelled.** Seven of those leaves belong to a
|
||
phase that is already written and only wants the game's data directory — see §3. The metric
|
||
harness invokes `sots_turn` with no `--data`, so it measures the engine *plus* that missing
|
||
argument. Handing it a root is a one-line harness change worth 4 + 3 leaves today, and more
|
||
once the tuning table is wired (§6, PL-3).
|
||
|
||
---
|
||
|
||
## 1. The decomposition
|
||
|
||
The first fact, and the one that reframes the rest: **53 of the 54 leaves on pair 1 and 23 of
|
||
the 24 on pair 2 are values our turn writes back unchanged from the input save.** Only one leaf
|
||
in the whole block — player 32's `Sav` — is a number we computed and got wrong. The residual is
|
||
almost entirely *unmodelled*, not *mismodelled*.
|
||
|
||
| # | mechanism | pair 1 | pair 2 | rung | blocked on |
|
||
|---|---|---:|---:|---|---|
|
||
| M1 | **AI research orders** — `ResRate`, `ResTNm`, `ResErrRoll`, and the tech-tree state they move (`St[n]`, `TResDone[n]`, `Tbd[n]`) | 12 | 2 | **B** | `game/ai`: the AI picks a target *during* the turn |
|
||
| M2 | **AI ship construction** — `ShipRecs/*`, `Maint`, `FNG/FNGNum`, `NumDes` + the new `Des[…]` frame, `lboid`, and player 32's `Sav`/`PvSav` | 15 | 9 | **B** | lane B6: no build order exists at load |
|
||
| M3 | **Observation records refreshed by the AI's own new design** — `odes`, `owep`, `otch/*/otnL` (phase T34 `RecordObservedDesigns`, stub) | 9 | 1 | **B** | downstream of M2 — the observed design *is* the design the AI just made |
|
||
| M4 | **Events** — `Events/EvNxID` + the bucket frames | 8 | 6 | A (4/3) + B (4/3) | half is the human's `EVENT_NO_RESEARCH`, implemented in P11 and gated only on a **game-data root**; half is the AI's `EVENT_SHIPS_BUILT`/`EVENT_RESEARCH_OVERBUDGET` (M2/M1) |
|
||
| M5 | **Bankruptcy limits** — `BnkEl`, `BnkPr` | 4 | 4 | **A** | `BnkEl`: **closed here** (PL-2, 2 per pair). `BnkPr`: needs one tuning constant from the data files, and the loader that would fill it is not wired |
|
||
| M6 | **`Status`** — 0 → 4 on four of eight players | 4 | 0 | A? | the **predicate** is not identifiable from this corpus (§4) |
|
||
| M7 | **`PvSav`** — the S00 snapshot, for players M2 does not touch | 0 | 2 | **A** | nothing — **closed here** (PL-1) |
|
||
| M8 | **A monster faction's one-off design** — player 528 `NumDes` 18→19 + `Des[1712 "Refugee Trade Ship"]` | 2 | 0 | B? | unexplained; a non-empire faction creates a design on turn 1→2 and never again |
|
||
| | **total** | **54** | **24** | | |
|
||
|
||
Per-mechanism evidence is in §2–§4; what this lane closed is §5, the ranked remainder §8.
|
||
|
||
### Rung split of the block
|
||
|
||
| | pair 1 | pair 2 |
|
||
|---|---:|---:|
|
||
| Rung B (needs the AI's own orders): M1, M2, M3, half of M4, M8 | 44 | 15 |
|
||
| Rung A, implemented, gated on a **game-data root** (`--data`): half of M4 | 4 | 3 |
|
||
| Rung A, closed by this lane: `BnkEl` ×2, `PvSav` | 2 | 4 |
|
||
| Rung A, blocked on a named unread/unwired thing: `BnkPr` | 2 | 2 |
|
||
| not identifiable from the corpus: `Status` | 2 | 0 |
|
||
|
||
(`Status` is 4 leaves in the block, but two of them — players 496 and 512 — are on shadow
|
||
empires that also have M1 leaves; the row above counts each leaf once, under the *first*
|
||
mechanism that has to land. `Status` for the human and for the AI empire is the irreducible
|
||
pair.)
|
||
|
||
**Four fifths of the `/Sim/players` residual is Rung B**, and it all hangs off two decisions by
|
||
one AI player: pick a research target, and queue one destroyer. The Rung-A part is small, and
|
||
most of what is left of it is *already written* — it needs the harness handed the game's data
|
||
directory, not more reverse engineering.
|
||
|
||
---
|
||
|
||
## 2. M1 / M2 / M3 — the three faces of one AI turn (Rung B)
|
||
|
||
Player 32 "Fane Lao" is the only AI empire that owns colonies, and it does two things in the
|
||
turn that our engine cannot: it assigns research, and it builds a ship. Everything in M1, M2
|
||
and M3 is a consequence.
|
||
|
||
Measured, from the state-checksum tree over the three saves:
|
||
|
||
* `ResTNm` `''` → `IND_Waldo` and `ResRate` `0.25` → `0.800000011920929` — the AI's own order.
|
||
The two "Singularity" shadow empires do the same (`DRV_PlsFiss`, `BIO_GnMod`), and their
|
||
`ResErrRoll` flips `False` → `True`. `St[106]` 2 → 3 and `TResDone[106]` 0 → 2879 → 5768 then
|
||
follow from the allocation.
|
||
* `PvSav` 50,000 → 38,100 and again 92,651 → 80,751: **11,900 leaves the treasury before the
|
||
first spine phase on both turns.** That is lane B6's queue-time deduction of a build order the
|
||
input file does not contain, and it is why player 32's `PvSav` does not close in §5.
|
||
* The new design is `Des[18 "Honor Lance"]`, and the observation records name it exactly:
|
||
`odes/.[1]` holds `odid=18, opid=32` — **the AI observes its own new design**. `owep/.[1]` is
|
||
`bal_gauss`, that design's weapon, and the three `otch` entries whose `otnL` moves from 1 to 2
|
||
are `DRV_Fissn`, `WEP_GsDrvr`, `DRV_Hyper` — that design's technologies. So T34
|
||
`RecordObservedDesigns` is not an intel pass over enemy fleets at all on this workload; it is
|
||
a self-registration triggered by design creation.
|
||
* On pair 2 the same design is built again (`srb[0]` 1→2, another `EVENT_SHIPS_BUILT`), and this
|
||
time `odes/.[1]/otnL` moves 2→3 while the `otch` entries stay at 2. **`odes` is re-stamped on
|
||
build; `otch` is re-stamped on design creation.** One workload, two different refresh rules —
|
||
worth recording before anyone models T34 from the pair-1 numbers alone.
|
||
|
||
None of this is closable without `game/ai`. It is listed here so the next lane does not
|
||
re-derive it: **36 of the 54 leaves on pair 1 are one AI player's research pick and one
|
||
destroyer**, and four more are the events those two decisions post.
|
||
|
||
## 3. M4 — events
|
||
|
||
The corpus posts exactly two events on pair 1 and three on pair 2, and the split is clean:
|
||
|
||
| player | event | rung |
|
||
|---|---|---|
|
||
| 0 (human) | `EVENT_NO_RESEARCH`, every turn | **A** — P11 implements it; blocked on the string table |
|
||
| 1 (AI) | `EVENT_SHIPS_BUILT` (both turns), `EVENT_RESEARCH_OVERBUDGET` (turn 3) | B — M2 and M1 |
|
||
|
||
P11's own note is precise about it: *"4 event(s) NOT posted: no string table."* Lane EV measured
|
||
`--commit-blocked=P11 --data ROOT --ai-player 1 --ai-player 2 --ai-player 3` at **4 closed on
|
||
pair 1, 3 on pair 2, 0 regressed**. Those leaves are in today's residual only because
|
||
`tools/standalone_report.py` invokes the binary with no `--data` root.
|
||
|
||
## 4. M6 — `Status`, and why it is not identifiable here
|
||
|
||
`Status` goes 0 → 4 on turn 1→2 for players 16, 32, 496 and 512, and stays 0 for 528, 544, 560
|
||
and 576. It then stays 4 on turn 2→3, which is why the leaf does not appear in pair 2's residual
|
||
at all.
|
||
|
||
The trouble is that the two sets are **perfectly correlated with species**: the four that take
|
||
the 4 are species 0 and 2, the four that do not are all species 4. Sweeping every scalar field
|
||
of the player record on `turn1-state`, this many of them split the roster exactly the same
|
||
way — `ReqCL`, `AMine`, `CnTrd`, `CnVItl`, `cdp`, `hadvs`, `harcc`, `hgs`, `PvMA`, `CstE`,
|
||
`CstR`, `CstT`, `MinRate`, `MaxOH`, `NPTrk`, `PrGtTrf`, `TerraMod`, `pddm` — eighteen of them.
|
||
Any one of them "fits". Only two corpus saves carry a non-zero `Status` at all, so there is one observation of one
|
||
transition on one roster: **any predicate chosen here is a one-bit fit on eight correlated
|
||
players, which is exactly the shape rule 23 was written for.**
|
||
|
||
`ReqCL` is the most plausible on name and on meaning — `Status = 4` is written by
|
||
`MarkPlayerTurnEnded` from the End-Turn submission paths (W2), and `ReqCL` reads as "a client
|
||
must submit for this player" — but it is a hypothesis with a workload attached, not a finding.
|
||
|
||
**The probe that settles it** (rule 18, rule 20): an entry probe on the writer W2 located at
|
||
`OnMessage+0xa15`, capturing the player index on every call for one turn. It names the set
|
||
directly and costs one VM session. Instrument the *entry*, not the count.
|
||
|
||
## 5. What this lane changed — measured
|
||
|
||
Predictions are in §6, written and committed before the build (`c852965`). All three held; the
|
||
one thing that came out differently is recorded in §7 rather than quietly corrected.
|
||
|
||
| change | pair 1 closed | pair 2 closed | regressed | leaves |
|
||
|---|---:|---:|---:|---|
|
||
| **PL-1** S00 stamps `PvSav` from `Sav` | 0 | 2 | 0 | `Player[16]/PvSav`, `Player[576]/PvSav` |
|
||
| **PL-2** T31 recovers its difficulty column from the input save | 2 | 2 | 0 | `Player[16]/BnkEl`, `Player[32]/BnkEl` on both pairs |
|
||
| **PL-3** the protection factor is narrowed to float32 | 0 | 0 | 0 | none on this corpus, by construction |
|
||
| total | **2** | **4** | **0** | |
|
||
|
||
Independently of the leaf count, PL-2 turns T31's self-check into a real one. It used to report
|
||
"BnkEl reproduced for 6 of 8" on `turn1-state` **regardless of the AI roster**, because it was
|
||
comparing its *post-turn* computation against the *pre-turn* stored value — so the only players
|
||
it could ever "reproduce" were the six whose limit does not move at all. That is thin coverage
|
||
of exactly the shape rule 15 describes, and the phase's own catalog text claimed "8 of 8 with
|
||
the AI flag supplied", which the binary never printed. The check is now taken at load, against
|
||
the colony state the save was written from, and it passes **for every live player of all eleven
|
||
corpus saves** — 7/7 or 8/8 on each. The whole max-income chain is now verified on eleven saves
|
||
instead of two.
|
||
|
||
The column identification resolves to **1 AI and 1 non-AI on every one of the eleven saves**,
|
||
with the rest ambiguous, and needs no `--ai-player`.
|
||
|
||
## 6. Predictions, written before the build (rule 2)
|
||
|
||
### PL-1 — `PvSav` is S00's snapshot of `Sav`
|
||
|
||
`PvSav(turn N+1) == Sav(turn N)` holds exactly for players 16 and 576 across all three saves
|
||
(50,000 → 50,000 → 289,688 against `Sav` 50,000 → 289,688 → 532,369; and 0 → 0 → 98,871 against
|
||
`Sav` 0 → 98,871 → 198,730). For player 32 it is `Sav(turn N) − 11,900`, the M2 deduction.
|
||
|
||
**Predicted:** pair 1 — 0 closed, 0 regressed (every player's `Sav` already equals its `PvSav`
|
||
in `turn1-state`, so the snapshot is a no-op there). Pair 2 — **2 closed** (`Player[16]/PvSav`,
|
||
`Player[576]/PvSav`), **0 regressed**; player 32 stays open, short by exactly 11,900.
|
||
|
||
**Falsification.** If the snapshot were taken later than S00 — say after P02 writes savings —
|
||
player 16's `turn3` `PvSav` would be its *turn 3* `Sav` (532,369), not its turn 2 `Sav`
|
||
(289,688). It is 289,688. If the snapshot were per-player conditional, some player other than 32
|
||
would show an offset; none does. If it regresses anything, the symptom is a player whose
|
||
`PvSav` and `Sav` differ in the input for a reason that is not M2 — there is none in the corpus,
|
||
so a regression here means the field is not a snapshot at all.
|
||
|
||
### PL-2 — T31's difficulty column is recoverable from the input save
|
||
|
||
T31 is blocked on `ServerPlayer+0xf9`, the per-player "is AI" flag, which the save does not
|
||
carry and which is worth ×1.1 on that player's max income. The operator supplies it with
|
||
`--ai-player N`.
|
||
|
||
But the input save **already carries the answer**: its `BnkEl` was written by the original at
|
||
the end of the previous turn from the colony state the save holds. Computing `BnkEl` from that
|
||
state under both columns and comparing against the stored value identifies the column — for
|
||
every player whose max income is non-zero, since the two columns differ by 10%, far more than
|
||
one truncation.
|
||
|
||
**Predicted:** on `turn1-state`, the column resolves to non-AI for player 0 and AI for player 1;
|
||
it is *ambiguous* (both columns give `BnkEl = 0`) for the five players that own nothing, where
|
||
the value is 0 either way. Player 7's column is unknown before measuring. With the column
|
||
identified from the save and `BnkEl` committed, the phase closes **2 leaves per pair, 0
|
||
regressed** — the same as `--commit-blocked=T31 --ai-player 1` measures today, but with no
|
||
operator input.
|
||
|
||
**Falsification.** (a) If the AI flag does more than select a ×1.1 income column, the identified
|
||
column will still reproduce the *stored* value while the *post-turn* value diverges; symptom —
|
||
`BnkEl` closes on pair 1 and regresses on pair 2, or the other way round. (b) Player 576's
|
||
`BnkEl` is **constant** at −665,806 across all three saves although its `Sav` grows; if its
|
||
column is identified and its post-turn max income has moved, committing regresses that leaf.
|
||
Symptom — 2 closed and 1 regressed. (c) If the pre-turn computation reproduces *neither*
|
||
column's value for a player, the phase must abstain for that player, and the leaf stays
|
||
pass-through — no regression, but no close either.
|
||
|
||
### PL-3 — the protection factor is a widened float (rule 23, read the four bytes)
|
||
|
||
`ComputeBankruptcyLimits` multiplies `BANKRUPTCY_PROTECTION_LIMIT_FACTOR` in as a `double`. The
|
||
image does not. In `ServerPlayer::UpdateBankruptcyLimits` the two constants are loaded by
|
||
**different opcodes**, and that is the whole point:
|
||
|
||
```
|
||
DB 45 FC fild dword [ebp-4] ; maxIncome, still on the stack afterwards
|
||
DD 05 30 EC A2 00 fld qword [0x00a2ec30] ; DD /0 = m64fp -- a DOUBLE in .rdata
|
||
D8 F9 fdivr st(0), st(1) ; maxIncome / that double
|
||
E8 … call ftol ; -> BnkEl, then the -2e9 clamp
|
||
8B 0D DC DF AE 00 mov ecx, [0x00aedfdc] ; the tuning pointer slot
|
||
D8 09 fmul dword [ecx] ; D8 /1 = m32fp -- a FLOAT32
|
||
E8 … call ftol
|
||
F7 D8 neg eax ; -> BnkPr
|
||
```
|
||
|
||
The `.rdata` double at `0x00a2ec30` reads `00 00 00 40 33 33 C3 BF` = **−0.15000000596046448**,
|
||
i.e. `(double)(float)-0.15` — already handled. The protection factor is the other case: it is
|
||
read `dword ptr`, so whatever decimal the data file carries is **narrowed to float32 before the
|
||
multiply**, and our `double` multiply is wrong at every boundary.
|
||
|
||
**Predicted:** 0 leaves move on this corpus — all seven non-zero `BnkPr` records invert to a max
|
||
income where `3.3` and `(double)3.3f` truncate to the same integer. Three hand-written test
|
||
expectations move by one or twenty (`-3300 → -3299`, `-330 → -329`,
|
||
`-1320000000 → -1319999980`). The two constants disagree on roughly **1.1% of max-income values
|
||
at the corpus's empire size**, rising with empire size, so this is a fix the corpus cannot see —
|
||
report it as thin coverage, not as verified.
|
||
|
||
**Falsification.** If the factor were a `double` in the image the opcode would be `DC /1`, not
|
||
`D8 /1`. If some *other* call site multiplies the same global as a qword, the storage is a
|
||
double and this is wrong; the check is a cross-reference sweep on `0x00aedfdc`.
|
||
|
||
---
|
||
|
||
## 7. Where the predictions were wrong
|
||
|
||
**"Ambiguous" is not a synonym for "owns nothing."** PL-2 predicted the ambiguous bucket would
|
||
be the five players with no colonies. It is six, and the extra one is `Player[576] "Independent
|
||
Colony"`, which owns a system and carries a non-zero `BnkEl` of −665,806. The reason is in the
|
||
difficulty table itself: the AI row is selected by `isAI && !npc`, and player 576 is an NPC, so
|
||
both columns hand it the same row. The note the phase prints and the header comment in
|
||
`game/sim/player_turn.h` were both corrected to say so — an ambiguous player is one where the
|
||
two columns *agree*, which happens for two different reasons.
|
||
|
||
That also disposed of falsification (b): player 576's limit is constant across all three saves,
|
||
and had the identification been wrong about it the commit would have regressed that leaf.
|
||
It did not; 576's post-turn max income reproduces the same −665,806.
|
||
|
||
Falsification (a) and (c) did not fire: no player of any corpus save came out `Unidentified`,
|
||
and no leaf regressed on either pair.
|
||
|
||
---
|
||
**The float/double disagreement rate is 10%, not "roughly 1.1%".** PL-3 estimated the rate from
|
||
the size of the gap (`3.3 - (double)3.3f` is 4.768e-8, which at a max income of 240,000 is about
|
||
0.011 of a unit, so ~1.1% of values should straddle an integer). Measured over every integer max
|
||
income from 230,000 to 250,000: **2,000 of 20,000 disagree — exactly 10%, and exactly the
|
||
multiples of ten.** The estimate was wrong because it assumed the products are uniformly
|
||
distributed against the integer grid and they are not: `3.3 x 10k` is an exact integer in
|
||
decimal for every k, so the decimal lands *on* the boundary at every tenth value and the
|
||
image's float lands just below it. The first disagreement is at `maxIncome = 10`
|
||
(33.0 against 32.99999952), not somewhere out in the hundreds of thousands.
|
||
|
||
That makes the fix meaningfully larger than predicted and the corpus' silence about it
|
||
correspondingly more suspicious: seven records, none of them a multiple of ten.
|
||
|
||
---
|
||
|
||
## 8. The ranked remainder
|
||
|
||
Ranked by leaves per unit of effort, both pairs summed, with the blocker named. Nothing here is
|
||
"more analysis of `/Sim/players`" — every row is a named thing somewhere else.
|
||
|
||
| rank | item | leaves (p1 + p2) | effort | what it needs |
|
||
|---|---|---:|---|---|
|
||
| 1 | **give the metric harness a data root** | 4 + 3 = **7** | one line in `tools/standalone_report.py` | `--data <extracted gob> --commit-blocked=P11 --ai-player 1 --ai-player 2 --ai-player 3`. Already measured end to end on CT111 against `/srv/re-lab/gob-extract`: **87 closed / 0 regressed** on pair 1 and **46 / 0** on pair 2, residual 122 and 62. It closes nothing new in the engine; it stops the metric reporting an argument as if it were a gap |
|
||
| 2 | **wire `StrategyVars.txt` into a `TuningTable` and set `opt.haveTuning`** | 2 + 2 = **4** | small, contained | `TurnOptions::haveTuning` is **read in three places and never assigned** — the field is dead. `game/config`'s loader exists and the data root already parses; nothing else is missing. Unblocks `BnkPr` in T31 (and, per rule 23, it must be narrowed to float32 on the way in — that is done, PL-3). Also unblocks whatever `RunCivilianGrowth` does with the flag |
|
||
| 3 | **`game/ai`: research target selection** | 12 + 2 = **14** | a module | `ResRate`/`ResTNm` are the AI's own orders, and every tech-tree leaf follows from them. Three players do it per turn (the AI empire and both Singularity shadows) |
|
||
| 4 | **`game/ai`: the build order** | 15 + 9 = **24**, plus M3's 10 | a module | one destroyer, queued during the turn, deducted 11,900 from the treasury *before* spine phase 0. It is the single largest cause in the block and lane B6 already proved it cannot come from the save |
|
||
| 5 | **T34 `RecordObservedDesigns`** | (10, downstream of 4) | small once 4 lands | the mechanism is now readable off the corpus (§2): a design's creator registers the design in its own `odes`, its weapons in `owep`, and re-stamps `otnL` on the design's techs in `otch`; `odes` re-stamps on *build*, `otch` on *creation*. Two different refresh rules from one workload — treat both as hypotheses until a second workload exists |
|
||
| 6 | **`Status`'s predicate** | 4 + 0 = **4** | one VM session | an entry probe on W2's writer at `OnMessage+0xa15`, capturing the player index per call (§4). Not a reading problem — eighteen fields fit the one observation the corpus offers |
|
||
| 7 | **M8: player 528's one-off design** | 2 + 0 = **2** | unknown | a non-empire faction ("Alien Menace") creates `Des[1712 "Refugee Trade Ship"]` on turn 1→2 and never again, and the id it takes is the first of the three `NMnx` issues that turn. Nothing in the corpus explains what triggers it. Lowest rank because two leaves is not worth a hunt until something else brings us into that code |
|
||
|
||
**What is NOT on this list, deliberately:** more decomposition of `/Sim/players`. The block is
|
||
now fully attributed — every one of the 54 and 24 leaves belongs to a row above.
|