Compare commits
1 Commits
0dc4c70f18
...
3bc2e09e24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bc2e09e24 |
@@ -1541,16 +1541,28 @@ failure, including the deadline expiring with nothing written.
|
|||||||
|
|
||||||
What that guarantees precisely, and what it does not: the encode runs on the one
|
What that guarantees precisely, and what it does not: the encode runs on the one
|
||||||
goroutine that owns the state, so the snapshot is internally consistent and
|
goroutine that owns the state, so the snapshot is internally consistent and
|
||||||
always restorable. It is not guaranteed to be a between-commands snapshot.
|
always restorable. It is not guaranteed to be a between-commands snapshot. Only
|
||||||
`readchar` is also reached from prompts raised part-way through a command
|
one of the three service points gives that: the check at the top of `command`,
|
||||||
(`--More--`, `askOverwrite`, `getStr`, the direction and pack prompts), and the
|
which runs after the previous command returned and before this turn's
|
||||||
command has already mutated state by then — `fight` sets `Count`/`Quiet` and
|
`DoDaemons(Before)`/`DoFuses(Before)`. The other two are both reached from
|
||||||
runs `runTo` before any message, `revealXeroc` writes `Disguise` before emitting
|
inside a `command` call already under way. `readchar` is reached from prompts
|
||||||
one. A save serviced at such a prompt freezes that command half applied;
|
raised part-way through a command (`--More--`, `askOverwrite`, `getStr`, the
|
||||||
restoring re-enters `playit` at the top of `command`, so the rest of that
|
direction and pack prompts), and the command has already mutated state by then —
|
||||||
command never runs. The result is a coherent state one turn's worth of effects
|
`fight` sets `Count`/`Quiet` and runs `runTo` before any message, `revealXeroc`
|
||||||
short, which is the price of being able to save at all for a player whose line
|
writes `Disguise` before emitting one; the ordinary top-of-turn key read is
|
||||||
dropped mid-prompt.
|
inside `command` too, after that turn's BEFORE daemons and `turnUpkeep`.
|
||||||
|
`runShellEscape` is no safer: `shell` is an ordinary command handler (`'!'` in
|
||||||
|
the dispatch table), reached through `executeCommand`, so a goroutine parked in
|
||||||
|
the shell escape has already run this turn's `DoDaemons(Before)`,
|
||||||
|
`DoFuses(Before)`, `turnUpkeep` and the last-command bookkeeping, and has not
|
||||||
|
yet run `DoDaemons(After)`, `DoFuses(After)` or `ringTurnEffects`. Restoring
|
||||||
|
re-enters `playit` at the top of `command` in either case, so the rest of that
|
||||||
|
command never runs — its AFTER daemons and fuses and its ring effects are lost —
|
||||||
|
and the restored game opens with a fresh BEFORE pass on top of the one already
|
||||||
|
in the snapshot: `rollwand` ticks again, any BEFORE fuse is decremented again.
|
||||||
|
The result is a coherent state one turn's worth of effects off, which is the
|
||||||
|
price of being able to save at all for a player whose line dropped mid-prompt or
|
||||||
|
who is away in a shell.
|
||||||
|
|
||||||
The shell escape runs the shell on a helper goroutine so that the game goroutine
|
The shell escape runs the shell on a helper goroutine so that the game goroutine
|
||||||
stays free to answer, but a panic out of `Terminal.ShellEscape` (which is how a
|
stays free to answer, but a panic out of `Terminal.ShellEscape` (which is how a
|
||||||
|
|||||||
16
MEMORY.md
16
MEMORY.md
@@ -27,12 +27,18 @@ encode game state from any goroutine but the game's.
|
|||||||
|
|
||||||
Do not upgrade that into "the snapshot is always taken between commands" — it is
|
Do not upgrade that into "the snapshot is always taken between commands" — it is
|
||||||
not. What is true is that the encode runs on the state-owning goroutine, so the
|
not. What is true is that the encode runs on the state-owning goroutine, so the
|
||||||
snapshot is internally consistent and restorable. `readchar` is reached from
|
snapshot is internally consistent and restorable. Only the check at the top of
|
||||||
|
`command` is a between-commands snapshot; the other two service points both sit
|
||||||
|
inside a `command` call already under way. `readchar` is reached from
|
||||||
mid-command prompts (`--More--`, `askOverwrite`, `getStr`, direction and pack
|
mid-command prompts (`--More--`, `askOverwrite`, `getStr`, direction and pack
|
||||||
prompts) and the command has already mutated state by then, so a save taken
|
prompts) with the command's mutations already applied, and `runShellEscape` is
|
||||||
there freezes that command half applied and the player loses the rest of it on
|
reached from `shell`, an ordinary `'!'` command handler, with that turn's
|
||||||
restore. That is acceptable and documented; the false stronger claim was caught
|
`DoDaemons(Before)`/`DoFuses(Before)` already fired and its AFTER pass not yet.
|
||||||
in review of PR #26 and must not come back.
|
Restoring re-enters `playit` at the top of `command`, so either way the rest of
|
||||||
|
that command is lost and a fresh BEFORE pass runs on top of the one already in
|
||||||
|
the snapshot. That is acceptable and documented; two successive false claims —
|
||||||
|
first that `readchar` was safe, then that two of the three service points were
|
||||||
|
between-commands — were caught in review of PR #26, and neither may come back.
|
||||||
|
|
||||||
Related, and easy to reintroduce: work moved onto a helper goroutine must not be
|
Related, and easy to reintroduce: work moved onto a helper goroutine must not be
|
||||||
allowed to panic there. A panic at the top of any goroutine kills the process
|
allowed to panic there. A panic at the top of any goroutine kills the process
|
||||||
|
|||||||
17
TODO.md
17
TODO.md
@@ -89,12 +89,17 @@ wizard commands).
|
|||||||
exactly the path where the terminal is already broken (issue #12's failure,
|
exactly the path where the terminal is already broken (issue #12's failure,
|
||||||
reintroduced on a new path). `runShellEscape` recovers the helper's panic and
|
reintroduced on a new path). `runShellEscape` recovers the helper's panic and
|
||||||
re-raises it on the game goroutine, pinned by
|
re-raises it on the game goroutine, pinned by
|
||||||
`TestShellEscapePanicUnwindsTheGameGoroutine`. And the first version of this
|
`TestShellEscapePanicUnwindsTheGameGoroutine`. And the doc comment took two
|
||||||
work claimed in four places that nothing is half-mutated at the `readchar`
|
rounds to get right: the first version claimed in four places that nothing is
|
||||||
service point; that is false, since `readchar` is reached from mid-command
|
half-mutated at the `readchar` service point, and the revision that fixed that
|
||||||
prompts. What is actually guaranteed is that the encode runs on the
|
claimed two of the three service points were between-commands. Both are false.
|
||||||
state-owning goroutine, so the snapshot is internally consistent and
|
Only the check at the top of `command` is between commands — `readchar` is
|
||||||
restorable, though it may freeze a command half applied. `Next Step`
|
reached from mid-command prompts, and `runShellEscape` is reached from
|
||||||
|
`shell`, an ordinary `'!'` command handler dispatched inside `command`, with
|
||||||
|
that turn's `DoDaemons(Before)`/`DoFuses(Before)` already fired and its AFTER
|
||||||
|
pass and ring effects not yet. What is actually guaranteed is that the encode
|
||||||
|
runs on the state-owning goroutine, so the snapshot is internally consistent
|
||||||
|
and restorable, though it may freeze a command half applied. `Next Step`
|
||||||
deliberately not rotated: out-of-band issue work.
|
deliberately not rotated: out-of-band issue work.
|
||||||
|
|
||||||
- 2026-08-09 Signal-time terminal restore (`sig-leave`, closes #12): the port
|
- 2026-08-09 Signal-time terminal restore (`sig-leave`, closes #12): the port
|
||||||
|
|||||||
44
game/save.go
44
game/save.go
@@ -774,18 +774,38 @@ func (g *RogueGame) AutoSaveOnSignal(timeout time.Duration) bool {
|
|||||||
// What is guaranteed, exactly: the encode runs on the one goroutine that
|
// What is guaranteed, exactly: the encode runs on the one goroutine that
|
||||||
// owns the state, so the snapshot is internally consistent and always
|
// owns the state, so the snapshot is internally consistent and always
|
||||||
// restorable. It is *not* guaranteed to be a between-commands snapshot.
|
// restorable. It is *not* guaranteed to be a between-commands snapshot.
|
||||||
// Two of the three service points are, but readchar is reached from
|
// Only one of the three service points gives that: the check at the top
|
||||||
// prompts raised part-way through a command — --More-- on the second
|
// of command, which runs after the previous command returned and before
|
||||||
// message of a turn, askOverwrite, getStr, the direction and pack prompts
|
// this turn's DoDaemons(Before)/DoFuses(Before). The other two are both
|
||||||
// — and by then the command has already mutated state: fight sets
|
// reached from inside a command call already under way, and both cost
|
||||||
// g.Count and g.Quiet and runs runTo before any message, revealXeroc
|
// the same on restore.
|
||||||
// writes tp.Disguise before emitting one. A save serviced at such a
|
//
|
||||||
// prompt therefore freezes that command half applied; restoring re-enters
|
// readchar is reached from prompts raised part-way through a command —
|
||||||
// playit at the top of command, so the rest of that command never runs
|
// --More-- on the second message of a turn, askOverwrite, getStr, the
|
||||||
// and the player loses its remaining effects. That is a coherent game
|
// direction and pack prompts — and by then the command has already
|
||||||
// state, one turn's worth of effects short — strictly better than the
|
// mutated state: fight sets g.Count and g.Quiet and runs runTo before
|
||||||
// torn encode this replaced, and the cost of being able to save a player
|
// any message, revealXeroc writes tp.Disguise before emitting one. The
|
||||||
// whose line dropped mid-prompt at all.
|
// ordinary top-of-turn key read in readCommand is inside command too,
|
||||||
|
// after that turn's BEFORE daemons and turnUpkeep.
|
||||||
|
//
|
||||||
|
// runShellEscape is no safer. shell is an ordinary command handler ('!'
|
||||||
|
// in the tables.go dispatch table), reached through executeCommand, so a
|
||||||
|
// goroutine parked in the shell escape has already run this turn's
|
||||||
|
// DoDaemons(Before), DoFuses(Before), turnUpkeep and the last-command
|
||||||
|
// bookkeeping, and has not yet run DoDaemons(After), DoFuses(After) or
|
||||||
|
// ringTurnEffects.
|
||||||
|
//
|
||||||
|
// The cost, at both: restoring re-enters playit at the top of command,
|
||||||
|
// so the rest of that command never runs — its AFTER daemons and fuses
|
||||||
|
// and its ring effects are lost — and the restored game opens with a
|
||||||
|
// fresh BEFORE pass on top of the one already in the snapshot. That
|
||||||
|
// second BEFORE pass is not free: rollwand, a live Before daemon once
|
||||||
|
// swander has fired, ticks again and draws from the RNG every fourth
|
||||||
|
// tick, and any Before fuse is decremented again. The result is still a
|
||||||
|
// coherent game state, one turn's worth of effects off — strictly better
|
||||||
|
// than the torn encode this replaced, and the cost of being able to save
|
||||||
|
// a player whose line dropped mid-prompt, or who is away in a shell, at
|
||||||
|
// all.
|
||||||
func (g *RogueGame) serviceAutoSaveRequest() {
|
func (g *RogueGame) serviceAutoSaveRequest() {
|
||||||
select {
|
select {
|
||||||
case req := <-g.sigSave:
|
case req := <-g.sigSave:
|
||||||
|
|||||||
Reference in New Issue
Block a user