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

Open
sickday wants to merge 5 commits from feat/runecrafting into main
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: 74537495, 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
All checks were successful
ci / gates (pull_request) Successful in 55s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/runecrafting:feat/runecrafting
git switch feat/runecrafting

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff feat/runecrafting
git switch feat/runecrafting
git rebase main
git switch main
git merge --ff-only feat/runecrafting
git switch feat/runecrafting
git rebase main
git switch main
git merge --no-ff feat/runecrafting
git switch main
git merge --squash feat/runecrafting
git switch main
git merge --ff-only feat/runecrafting
git switch main
git merge feat/runecrafting
git push origin main
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.