adventure: work the five review findings the last pass left open
The extract pre-check is gone. It read a snapshot up to two minutes behind and still got the last word, so somebody who set out over Matrix during a lagging roster push was told they weren't on an expedition for a run gogobee would happily have ended. Same call abandon and leave already made: let it through and let rejected_not_running be the answer. The siege_join check stays, because whether a boss is camped outside town is town-wide and runs on a day-or-longer clock, but it now reads one column through SiegeIsCamped instead of loading every defender row and the whole history to look at one flag. The war-room history insert is OR REPLACE. boss_id is the primary key and it was never settled whether gogobee means the siege instance or the boss type by it, so a duplicate pair used to fail the transaction carrying the live boss and the muster too and freeze the war room on the last good snapshot. A dropped history row is the smaller failure; the open question is noted in the schema. offersToUndo's guard didn't cover the case its comment claimed. A gogobee too old to push seats sends a valid blob with no party key, which decodes to the same empty slice as a solo run, and a party member got shown the button that throws away everyone's day. That needs a new field, so whoDetail gains party_known and the flag gates the empty-list branch alone; the branch that reads the viewer's own seat is self-evidencing and keeps working against any sender. gogobee's half is written up in adventure_party_known_flag.md. And an empty offer list no longer claims "you're already out there", which Pete can't actually know from a game box too old to push offers at all.
This commit is contained in:
+24
-14
@@ -111,10 +111,11 @@ func (s *Server) handleAdvOrder(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Per-verb pre-checks, all courtesy only. Both read Pete's snapshot copy, which
|
||||
// is up to two minutes behind the game box, so neither is authoritative and
|
||||
// neither is allowed to be the last word — a run that ended in that window comes
|
||||
// back from gogobee as rejected_not_running, which is the honest answer.
|
||||
// The roster lookup is for the character name the order carries; the one
|
||||
// surviving pre-check below is courtesy only. Anything read here is Pete's
|
||||
// snapshot copy, up to two minutes behind the game box, so it is never
|
||||
// authoritative and is only allowed the last word where being two minutes late
|
||||
// cannot make it wrong.
|
||||
characterName := ""
|
||||
entry, haveEntry, err := storage.RosterEntryByToken(token)
|
||||
if err != nil {
|
||||
@@ -125,20 +126,25 @@ func (s *Server) handleAdvOrder(w http.ResponseWriter, r *http.Request) {
|
||||
if haveEntry {
|
||||
characterName = entry.Name
|
||||
}
|
||||
switch req.Action {
|
||||
case storage.AdvActionExtract:
|
||||
if haveEntry && entry.Status != "expedition" {
|
||||
writeAdvOrderError(w, http.StatusConflict, "you're not on an expedition")
|
||||
return
|
||||
}
|
||||
case storage.AdvActionSiegeJoin:
|
||||
snap, known, err := storage.LoadSiege()
|
||||
// No pre-check on extract, deliberately, and it is the same call abandon and
|
||||
// leave make in resolveAdvOrderParams: the mark's status is up to two minutes
|
||||
// stale here and a Matrix departure can outrun the roster push, so "reads idle"
|
||||
// would refuse a run gogobee would happily have ended. The cost accepted is
|
||||
// that a genuine mistake comes back as rejected_not_running rather than as an
|
||||
// instant refusal, which is the honest answer anyway.
|
||||
if req.Action == storage.AdvActionSiegeJoin {
|
||||
// This one stays, because it is not a personal status: whether a boss is
|
||||
// camped outside town is a town-wide fact on a day-or-longer clock, so a
|
||||
// two-minute-old copy is almost never wrong about it. Note the known/active
|
||||
// split — no snapshot at all must queue the order (a fresh deploy must not
|
||||
// have a dead button); only a snapshot that positively says active=0 refuses.
|
||||
active, known, err := storage.SiegeIsCamped()
|
||||
if err != nil {
|
||||
slog.Error("orders: siege lookup", "err", err)
|
||||
writeAdvOrderError(w, http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
if known && !snap.Active {
|
||||
if known && !active {
|
||||
writeAdvOrderError(w, http.StatusConflict, "no Siege is camped outside town")
|
||||
return
|
||||
}
|
||||
@@ -207,7 +213,11 @@ func resolveAdvOrderParams(owner, token string, req advOrderReq) (*storage.AdvOr
|
||||
return nil, "pick somewhere to go first"
|
||||
}
|
||||
if len(detail.Zones) == 0 {
|
||||
return nil, "you're already out there"
|
||||
// An empty offer list usually means they are already out there, but Pete
|
||||
// cannot tell that from a game box too old to push offers at all, so say
|
||||
// only what was actually seen. Unreachable from the page either way — with
|
||||
// no offers the picker doesn't render — so this is a hand-crafted request.
|
||||
return nil, "nowhere is on offer for you right now"
|
||||
}
|
||||
for _, z := range detail.Zones {
|
||||
if z.ID != req.Zone {
|
||||
|
||||
Reference in New Issue
Block a user