feat(runecrafting): twelve altars, and the half of the map no name scan can see #132

Merged
sickday merged 10 commits from feat/runecrafting into main 2026-08-06 15:17:19 +00:00
Owner

Phase 2's sixth skill, end to end. Twelve altars air through death plus astral, each with its rune, level, experience and multiple-rune divisor; entry by talisman or by a worn tiara and the ruins' own Enter; the portal back out; tiara binding at every altar with a talisman; both directions of all six combination pairs with the binding necklace and its sixteen charges; essence pouches with fill, empty, check, degrade and repair; essence mining, which is the resource the whole skill runs on; and the Ourania altar, already walkable through a ladder pair the transport table carried.

Contract.Runecrafting holds the rules, World.Runecrafting the table, World.Content.Runecraft the altar, World.Content.Ruins the doors, World.Content.Pouch the pouches and World.Content.Tiara the eleven varbits.

The finding: a loc that is placed nowhere may still be in the world under another id

Not one Mysterious ruins is placed anywhere, and this cache carries twenty-two definitions of one.

The ruins are a multiloc. What the landscape places is a base — 14989, 14990, … 15612 — whose name is null and whose action list is empty. The client transforms it when it builds the menu, into 14398 (no options) or 14399 (Enter), and which one it picks is a varbit: 607..617, one per tiara, all eleven bits 0..10 of varp 491.

So a scan for "ruins with an Enter option" finds eleven definitions and zero placements, and a scan of the altar sites finds Ruined Pillar and nothing else. Both are true, and both read exactly like the content being absent from this cache.

The transform lives in loc opcodes 77 and 92, which Cache.Loc decodes the length of and throws away — correctly, for collision. Recovering the map meant re-walking config group 2/6 with a parser that keeps them.

The same fact settles the protocol half. The client's scene stores the base id and applies the transform only for the menu text, so an Enter click arrives at the server as loc 14989 and nothing in this engine ever sees 14399. Wiring the transform ids would have produced a door that is drawn, clickable and dead.

Numbers, and where two of them are corrected

Multiple runes are a divisor, one number per altar rather than a threshold list: runes = 1 + div(level, multiple). Two era-adjacent tables state 29 for earth against the published 26, and 200 for law and death where the values are 95 and 99. A divisor is what makes that checkable — 26 and 29 agree at level 1 and at 99 and disagree at four thresholds in between, which is the shape of an error that survives review in two independent codebases. Contract.RunecraftingTest walks all twelve divisors against every published threshold.

A tiara is worth xp * 5 at every altar. One factor reproduces eleven published values (air 25, mind 27.5, … death 50); eleven transcribed numbers would be eleven chances to be wrong.

The combination rate is a flat 50% per essence and does not scale with level. It was a placeholder until the last commit here: the placeholder had it rising to certainty at 99, which is the shape anybody would guess, and the shape is what gives it away — a rate reaching certainty makes the binding necklace worthless above about 80, and the necklace is worn at 99. An item that stays valuable is evidence about the mechanic it insures against. So level reaches the plan only as a refusal.

The roll is per essence rather than per batch, which the client's own message pair proves: "You partially succeed to bind the temple's power into mud runes." is only expressible if each essence is rolled separately. Two choices around it are deliberate — a failed bind consumes both the essence and the catalyst, and experience is paid per rune that survives rather than per attempt, which is the reading that cannot be exploited by deliberately failing.

The cache drew two boundaries for free

The Arceuus Blood and Soul altars are both placed in this cache. Neither carries Craft-rune; both answer Bind. Blood and soul runecrafting is 2019 content and the cache refused it without being asked — the scenery shipped years before the skill did. When era is in doubt, ask what the loc lets you do rather than whether the loc exists.

The second boundary is the game's own. Two Portal definitions sit beside the essence rock — 7472 (Use) and 7473 (Exit) — and neither is placed anywhere. The mine is a one-way trip until Magic has teleports. Filed as #128, and recorded in the data file at the row that causes it.

Per-item state, and a Dialyzer report that named none of it

A pouch is not a container — no slots, one kind of item, never drawn — so it is three numbers per pouch id: which essence, how many, and how many fills it has survived. It could not go on World.Player, which is at the 32-key cliff, so World.Inventory carries it and two new columns persist it. The fill count is persisted deliberately: a wear counter that reset at logout would make world hopping the cheapest pouch repair in the game.

Widening hydrate_inventory/1 from a three-tuple to a four-tuple left Database.Players.loaded() declaring the old shape, and Dialyzer answered with four unrelated "will never be called" warnings in Network.Session.Game plus an unmatchable {:error, :world_full} — naming none of the cause. One stale field in one type, four warnings in another module. Third recorded sighting of the closed-map-type invariant and the first outside a struct; measured by stashing the tree and re-running rather than guessed.

Ids, and what they say about porting

This is 2004 content and its altar, ruins, portal, talisman, tiara and rune ids are all identical here and in an era-adjacent table — a counterexample to recency predicts whether an id ports, and not to the measurement behind it. Renumbering happened in ranges rather than globally: 7453–7495, the ore rocks here, is Agility obstacles there, while 14897+ and 5527+ are untouched in both. The narrower rule: an id that ports is not evidence that its neighbours do. Every id in priv/data/runecrafting.exs is re-checked on every suite run by World.RunecraftingTest, which is the generator this hand-written table does not have.

Verification

mix revenant.ci --no-postgres on the SQLite leg: 1407 doctests, 55 properties, 3279 tests. The only failure across two full runs was World.Content.LightTest's seed-deterministic one (#119), which passed on the other seed.

Format, Credo, Dialyzer and check_doc_comments.sh all match clean origin/main exactly — measured by stashing every working change and re-running, not inferred. Three gates are red on that baseline and this branch adds nothing to them; filed as #131.

The Postgres leg has not been run against this branch on this machine, and it matters because of the migration. It was green on the machine the work started on.

What is left, all filed

#126 the Ourania yield table (its distribution is recovered and cross-verified; the experience is what blocks it), #127 the Abyss obstacle ring, #128 the one-way essence mine, #129 Magic Imbue, #130 pouches and the astral tiara having no in-game source until quests.

Phase 2's sixth skill, end to end. Twelve altars air through death plus astral, each with its rune, level, experience and multiple-rune divisor; entry by talisman or by a worn tiara and the ruins' own `Enter`; the portal back out; tiara binding at every altar with a talisman; both directions of all six combination pairs with the binding necklace and its sixteen charges; essence pouches with fill, empty, check, degrade and repair; essence mining, which is the resource the whole skill runs on; and the Ourania altar, already walkable through a ladder pair the transport table carried. `Contract.Runecrafting` holds the rules, `World.Runecrafting` the table, `World.Content.Runecraft` the altar, `World.Content.Ruins` the doors, `World.Content.Pouch` the pouches and `World.Content.Tiara` the eleven varbits. ## The finding: a loc that is placed nowhere may still be in the world under another id **Not one `Mysterious ruins` is placed anywhere**, and this cache carries twenty-two definitions of one. The ruins are a multiloc. What the landscape places is a base — `14989`, `14990`, … `15612` — whose name is `null` and whose action list is empty. The client transforms it when it builds the menu, into `14398` (no options) or `14399` (`Enter`), and which one it picks is a varbit: `607..617`, one per tiara, all eleven bits 0..10 of varp 491. So a scan for "ruins with an `Enter` option" finds eleven definitions and zero placements, and a scan of the altar sites finds `Ruined Pillar` and nothing else. Both are true, and both read exactly like the content being absent from this cache. The transform lives in loc opcodes `77` and `92`, which `Cache.Loc` decodes the length of and throws away — correctly, for collision. Recovering the map meant re-walking config group 2/6 with a parser that keeps them. The same fact settles the protocol half. The client's scene stores the **base** id and applies the transform only for the menu text, so an `Enter` click arrives at the server as loc `14989` and nothing in this engine ever sees `14399`. Wiring the transform ids would have produced a door that is drawn, clickable and dead. ## Numbers, and where two of them are corrected **Multiple runes are a divisor**, one number per altar rather than a threshold list: `runes = 1 + div(level, multiple)`. Two era-adjacent tables state `29` for earth against the published `26`, and `200` for law and death where the values are `95` and `99`. A divisor is what makes that checkable — 26 and 29 agree at level 1 and at 99 and disagree at four thresholds in between, which is the shape of an error that survives review in two independent codebases. `Contract.RunecraftingTest` walks all twelve divisors against every published threshold. **A tiara is worth `xp * 5`** at every altar. One factor reproduces eleven published values (air 25, mind 27.5, … death 50); eleven transcribed numbers would be eleven chances to be wrong. **The combination rate is a flat 50% per essence and does not scale with level.** It was a placeholder until the last commit here: the placeholder had it rising to certainty at 99, which is the shape anybody would guess, and the shape is what gives it away — a rate reaching certainty makes the binding necklace worthless above about 80, and the necklace is worn at 99. An item that stays valuable is evidence about the mechanic it insures against. So `level` reaches the plan only as a refusal. The roll is per essence rather than per batch, which the client's own message pair proves: *"You partially succeed to bind the temple's power into mud runes."* is only expressible if each essence is rolled separately. Two choices around it are deliberate — a failed bind consumes both the essence and the catalyst, and experience is paid per rune that survives rather than per attempt, which is the reading that cannot be exploited by deliberately failing. ## The cache drew two boundaries for free The Arceuus **Blood** and **Soul** altars are both placed in this cache. Neither carries `Craft-rune`; both answer `Bind`. Blood and soul runecrafting is 2019 content and the cache refused it without being asked — the scenery shipped years before the skill did. When era is in doubt, ask what the loc lets you do rather than whether the loc exists. The second boundary is the game's own. Two `Portal` definitions sit beside the essence rock — `7472` (`Use`) and `7473` (`Exit`) — and **neither is placed anywhere**. The mine is a one-way trip until Magic has teleports. Filed as #128, and recorded in the data file at the row that causes it. ## Per-item state, and a Dialyzer report that named none of it A pouch is not a container — no slots, one kind of item, never drawn — so it is three numbers per pouch id: which essence, how many, and how many fills it has survived. It could not go on `World.Player`, which is at the 32-key cliff, so `World.Inventory` carries it and two new columns persist it. The fill count is persisted deliberately: a wear counter that reset at logout would make world hopping the cheapest pouch repair in the game. Widening `hydrate_inventory/1` from a three-tuple to a four-tuple left `Database.Players.loaded()` declaring the old shape, and Dialyzer answered with four unrelated *"will never be called"* warnings in `Network.Session.Game` plus an unmatchable `{:error, :world_full}` — naming none of the cause. One stale field in one type, four warnings in another module. Third recorded sighting of the closed-map-type invariant and the first outside a struct; measured by stashing the tree and re-running rather than guessed. ## Ids, and what they say about porting This is 2004 content and its altar, ruins, portal, talisman, tiara and rune ids are all identical here and in an era-adjacent table — a counterexample to *recency predicts whether an id ports*, and not to the measurement behind it. Renumbering happened in **ranges** rather than globally: `7453`–`7495`, the ore rocks here, is Agility obstacles there, while `14897`+ and `5527`+ are untouched in both. The narrower rule: **an id that ports is not evidence that its neighbours do.** Every id in `priv/data/runecrafting.exs` is re-checked on every suite run by `World.RunecraftingTest`, which is the generator this hand-written table does not have. ## Verification `mix revenant.ci --no-postgres` on the SQLite leg: 1407 doctests, 55 properties, 3279 tests. The only failure across two full runs was `World.Content.LightTest`'s seed-deterministic one (#119), which passed on the other seed. Format, Credo, Dialyzer and `check_doc_comments.sh` all match clean `origin/main` **exactly** — measured by stashing every working change and re-running, not inferred. Three gates are red on that baseline and this branch adds nothing to them; filed as #131. **The Postgres leg has not been run against this branch on this machine**, and it matters because of the migration. It was green on the machine the work started on. ## What is left, all filed #126 the Ourania yield table (its distribution is recovered and cross-verified; the *experience* is what blocks it), #127 the Abyss obstacle ring, #128 the one-way essence mine, #129 Magic Imbue, #130 pouches and the astral tiara having no in-game source until quests.
`7471 Rune Essence` is none of the things an ore rock is: 5x5 where every
other is 1x1, named for what it gives where every other is called `Rocks`,
with no empty twin, and yielding a different item depending on the miner's
level -- rune essence below Mining 30 and pure essence at or above it.

So the extractor grew a second stated list with its own validator, the
generated row grew an optional `upgrade` field that `Mine.product/2` reads,
and `deplete: 0` is what "never empties" looks like in a column that already
expressed "always empties" as `8`. The roll is still taken and can never
succeed, which keeps it a data value rather than a branch at the caller.
A pouch is not a container -- no slots, one kind of item, never drawn -- so
it is three numbers per pouch id: which essence, how many, and how many
fills it has survived. `World.Inventory` carries them and two new columns
persist them. The fill count is persisted deliberately: a wear counter that
reset at logout would make world hopping the cheapest pouch repair in the
game.

It could not go on `World.Player`, which is at the 32-key cliff. That is now
load-bearing rather than theoretical, and `World.Inventory` is the right side
of the wall for anything that is about items.

Widening `hydrate_inventory/1` from a three-tuple to a four-tuple left
`Database.Players.loaded()` declaring the old shape, and Dialyzer answered
with four unrelated "will never be called" warnings in `Network.Session.Game`
and named none of the cause. The closed-map-type invariant, third sighting
and first outside a struct.
Phase 2's sixth skill. Twelve altars air through death plus astral, each with
its rune, level, experience and multiple-rune divisor; entry by talisman or
by a worn tiara and the ruins' own `Enter`; the portal back out; tiara
binding; both directions of all six combination pairs with the binding
necklace and its sixteen charges; essence pouches with fill, empty, check,
degrade and repair; and the Ourania altar, already walkable through a ladder
pair the transport table carried.

The half of the map no name scan can see: not one `Mysterious ruins` is
placed anywhere, and this cache carries twenty-two definitions of one. The
ruins are a multiloc -- the landscape places a nameless base with an empty
action list and the client transforms it into `14398` or `14399` against one
of eleven varbits, `607..617`, bits 0..10 of varp 491. So a scan for "ruins
with an Enter option" finds eleven definitions and zero placements, and both
readings look exactly like the content being absent. The transform lives in
loc opcodes 77 and 92, which the loc decoder measures and discards --
correctly, for collision -- so recovering the map meant re-walking config
group 2/6 with a parser that keeps them. The same fact settles the protocol
half: the client's scene stores the base id, so an `Enter` click arrives as
`14989` and nothing here ever sees `14399`.

Multiple runes are one number per altar rather than a threshold list:
`1 + div(level, multiple)`. Two era-adjacent tables state 29 for earth
against the published 26, and 200 for law and death where the values are 95
and 99 -- 26 and 29 agree at 1 and at 99 and disagree at four thresholds
between, which is the shape of an error that survives review twice. A tiara
is worth `xp * 5` at every altar, one factor reproducing eleven published
values.

The combination roll is per essence rather than per batch, which the client's
own message pair proves: a partial success is only expressible if each
essence is rolled separately. A failed bind consumes both the essence and the
catalyst, and experience is paid per rune that survives rather than per
attempt -- the reading that cannot be exploited by deliberately failing.

The cache drew the era boundary for free again: the Blood and Soul altars are
both placed and neither carries `Craft-rune`. And it drew a second one that
is the game's own -- both essence-mine portals are defined and neither is
placed, so the mine is a one-way trip until Magic has teleports.

Two numbers carry `sourced: :placeholder` and are flagged at the row they sit
on: the combination success rate and the Ourania yield table.
fix(runecrafting): the combination rate is a flat 50%, not a level curve
All checks were successful
ci / gates (pull_request) Successful in 1m4s
b8dd240ba0
The first of the file's two placeholders, and it turned out to have a source
after all: the wiki states the rate as a property of the craft rather than of
the crafter -- "the chance of successfully binding each individual rune
together is only 50%" -- with no level term at all. The placeholder read 50%
at level 1 rising to certainty at 99, which is the shape somebody would guess
and it makes the binding necklace worthless above about 80. That the necklace
is still worth wearing at 99 is the observable the flat rate explains.

So `combination_chance` is one number rather than two anchors, `level`
reaches the plan only as a refusal, and the contract test that asserted the
chance rises now asserts it does not move across five levels. The changes
section carries one entry for combination runes -- 2025-05-14, on what they
may be spent on -- which is post-era and does not touch the craft.
Merge branch 'main' into feat/runecrafting
All checks were successful
ci / gates (pull_request) Successful in 55s
baff26e0ac
`World.RunecraftWorldTest` mined the essence rock twice — once at Mining 1
for rune essence, once at 30 for pure — and each waited 12s for the yield.
That is 20 ticks, and a pickaxe legal at Mining 1 is bronze (period 8) or
iron (7), so the deadline bought **two rolls**; the rock is `low: 129` out of
256 at level 1, and one required success failed 24.6% of the time. Measured,
not inferred: it failed once in three runs of the file alone, and it is what
made the Postgres leg red while SQLite was green — the two legs differ only
in which side of a coin flip they landed on.

The branch those tests were really asserting is `Mine.product/2`, which is
pure and public. It is now pinned in `World.MiningTest` at levels 1, 29, 30
and 99, plus a sweep proving every rock without an `upgrade` answers the same
at every level. Nothing there rolls anything.

What is left in the world test is the part only the world can prove: a real
click on the real 5x5 rock, through a footprint no other rock has, produces
essence. One test rather than two, an iron pickaxe, and patience sized at
eight rolls, which puts a miss under 1%. Four consecutive runs green, and the
file is faster than it was.

The roll cannot simply be pinned: `Contract.Skilling.dice/0` reads `:rand` in
the world's own process rather than the test's. That is #119's structural
problem and wants a seam this test should not invent.
The mine was recorded as one-way, gated on Magic teleports that do not exist.
That was wrong, and wrong in a way this file had already described three
sections earlier.

The two `Portal` definitions beside the essence rock — 7472 `Use` and 7473
`Exit` — really are placed nowhere. They are transforms. The landscape places
the nameless base `7479` four times, one per corner, and opcode 77 on varbit
2313 turns it into

    [7472, 7473, nil, 7472, 7473, nil, nil, 7472]

so slots 1 and 4 draw `Exit`, slots 0, 3 and 7 draw `Use`, and the loc is
clickable at every value the varbit can hold. A working exit needs no varbit
management at all; it just reads `Use`.

The scan that found it carried a known-positive control — base 14989 ->
[14398, 14399] on varbit 607, reproducing the ruins — because a walk that
decodes nothing finds nothing everywhere, and that is indistinguishable from
an absence.

Wired the way the ruins already are: the dispatch matches the base, since the
base is the only id a client ever sends, and guards on standing at one of the
four listed tiles rather than on the id alone — a nameless base with no action
of its own is a weak claim, and anything else placed under it would otherwise
teleport people.

`destination` is the one invented value and is flagged `sourced: :placeholder`
at its row. In era each portal returns the player to the wizard who sent them,
which needs somewhere to remember the origin, and `World.Player` is at the
32-key cliff. Wizard Cromperty's tile is on the surface, beside a bank, and
one of the three wizards this world actually spawns — Aubury carries the
`Teleport` option and has no spawn anywhere.

Tests pin the half that was got wrong: that the base is placed four times
inside the mine, that it is nameless with no actions while the two portals are
named with actions, that only the base answers the dispatch, and that the
destination is neither blocked nor back inside the mine.
`Bot.Goals.Runecraft` plus a `:runecrafter` kind. Nine phases, and the loop
closes on itself because the mine's exit lands the bot beside the wizard who
sends it back in — `mine.destination` names Cromperty's tile and Cromperty
carries the `Teleport` option the cache states.

Two things about clicking that are not obvious. `Act.object_action/4` takes a
menu slot 1..5, not a verb, so every click resolves its verb against
`Cache.Loc.actions/1` at the call site rather than hardcoding a slot a
reworked definition would move. And a **nameless multiloc base has no action
list to search**, so those fall back to op 1 — the option a player sees
belongs to the transform, and both the mine's portals and both ruins
definitions put theirs first.

`in_mine?/1` is derived from the table rather than typed. It began as the
bounds of the *scan window* that found the portals, which is a rectangle
nobody measured — it would call a bot that had walked out of an invented box
"out of the mine" and send it to craft with nothing. It is now "the same map
square as a listed portal", which cannot drift.

The world test asserts the portal leg and stops there, because the legs past
it are blocked and the trace says why:

     10s  {0, 2886, 4850}  in the mine, at the portal
     12s  {0, 2683, 3326}  out, on the surface, as designed
     16s  {0, 2686, 3324}  three tiles toward the ruins, 297 to go
     20s-68s               stationary — the long leg, #123
     70s  {0, 2797, 5171}  recovery fires and teleports it underground

The second half is #138 and was not predicted. The failure ladder calls
`Bot.Travel.recover/2`, which relocates to the bank nearest the bot's anchor,
and a runecrafter naturally anchors at an essence rock. `nearest_bank/1`
filters by plane, and caves are plane 0 at high y, so the nearest bank to a
tile in the mine is an underground one 1878 tiles away with no walked route
out — a recovery strictly worse than the failure it answers.

This kind now anchors at the mine's exit rather than at the rock, which makes
it safe and leaves the general hazard filed rather than papered over.
Merge remote-tracking branch 'origin/feat/runecrafting' into feat/runecrafting
Some checks failed
ci / gates (pull_request) Successful in 58s
build / image (push) Failing after 43s
ci / gates (push) Successful in 1m3s
923fb6abb5
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
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
Revenant/Server!132
No description provided.