fix(ironman): publish the mode, or only the tick can see it #229

Merged
sickday merged 1 commit from feat/ironman-modes into main 2026-08-16 03:10:50 +00:00
Owner

Found in the first live sitting: a Hardcore Iron Man could still send a
trade offer.

World.tradeable_pair/2 reads state.roster[index], and a roster entry is
written once at join and never updated. The mode was set mid-session, so
the gate saw the account as it was at login: unrestricted. The tick's
player.account was correct throughout, which is exactly what made it
invisible.

Two more readers of the same stale copy are fixed by the same push -- the
crown on a private message and the crown on a Friends Chat line. Public
chat was never affected; it is stamped from player.account in the tick.

The mechanism already existed. Requests.account_update/3 was written for
::grant and its own moduledoc already names this failure -- "or the crown
the roster puts on chat messages would be the one the player logged in
with forever". It merged only granted and root?. So: add ironman to that
merge, and cast {:account_update, account} from Ironman.record/3, which is
the single write point both pick/2 and the Hardcore death downgrade go
through. A cast, so it is safe from inside the tick.

Why seven passing gate tests missed it: all of them built an account that
was already restricted and asked the gate about it, which is not the path
a player takes. Worse, the omission cannot be reached by a unit test at
all -- World.request/2 is a cast to a named process, and a cast to an
unregistered name is a silent :ok. The regression test is therefore a
world test, verified to fail 3/3 with publish/1 removed.

Found in the first live sitting: a Hardcore Iron Man could still send a trade offer. World.tradeable_pair/2 reads state.roster[index], and a roster entry is written once at join and never updated. The mode was set mid-session, so the gate saw the account as it was at login: unrestricted. The tick's player.account was correct throughout, which is exactly what made it invisible. Two more readers of the same stale copy are fixed by the same push -- the crown on a private message and the crown on a Friends Chat line. Public chat was never affected; it is stamped from player.account in the tick. The mechanism already existed. Requests.account_update/3 was written for ::grant and its own moduledoc already names this failure -- "or the crown the roster puts on chat messages would be the one the player logged in with forever". It merged only granted and root?. So: add ironman to that merge, and cast {:account_update, account} from Ironman.record/3, which is the single write point both pick/2 and the Hardcore death downgrade go through. A cast, so it is safe from inside the tick. Why seven passing gate tests missed it: all of them built an account that was already restricted and asked the gate about it, which is not the path a player takes. Worse, the omission cannot be reached by a unit test at all -- World.request/2 is a cast to a named process, and a cast to an unregistered name is a silent :ok. The regression test is therefore a world test, verified to fail 3/3 with publish/1 removed.
fix(ironman): publish the mode, or only the tick can see it
All checks were successful
ci / gates (pull_request) Successful in 3m18s
build / image (push) Successful in 37s
ci / gates (push) Successful in 3m45s
44ba250318
Found in the first live sitting: a Hardcore Iron Man could still send a
trade offer.

World.tradeable_pair/2 reads state.roster[index], and a roster entry is
written once at join and never updated. The mode was set mid-session, so
the gate saw the account as it was at login: unrestricted. The tick's
player.account was correct throughout, which is exactly what made it
invisible.

Two more readers of the same stale copy are fixed by the same push -- the
crown on a private message and the crown on a Friends Chat line. Public
chat was never affected; it is stamped from player.account in the tick.

The mechanism already existed. Requests.account_update/3 was written for
::grant and its own moduledoc already names this failure -- "or the crown
the roster puts on chat messages would be the one the player logged in
with forever". It merged only granted and root?. So: add ironman to that
merge, and cast {:account_update, account} from Ironman.record/3, which is
the single write point both pick/2 and the Hardcore death downgrade go
through. A cast, so it is safe from inside the tick.

Why seven passing gate tests missed it: all of them built an account that
was already restricted and asked the gate about it, which is not the path
a player takes. Worse, the omission cannot be reached by a unit test at
all -- World.request/2 is a cast to a named process, and a cast to an
unregistered name is a silent :ok. The regression test is therefore a
world test, verified to fail 3/3 with publish/1 removed.
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!229
No description provided.