mirror of
https://github.com/prosolis/gogobee.git
synced 2026-09-14 10:51:09 +00:00
Merge party-known-flag: tell Pete this build knows what a party seat is
This commit is contained in:
@@ -288,6 +288,18 @@ type RosterDetail struct {
|
||||
// run — a party of one is not a party, and the page should say nothing rather
|
||||
// than draw a roster with a single chair in it.
|
||||
Party []PartySeatView `json:"party,omitempty"`
|
||||
// PartyKnown says this sender knows what a party seat is. It is a fact about
|
||||
// the build, never about the character, so it is set unconditionally on every
|
||||
// sheet — in town, solo, or seated with three others — and must never be
|
||||
// computed from whether Party has anything in it.
|
||||
//
|
||||
// Party being omitempty is why it exists: a solo run and a game box too old to
|
||||
// push seats both arrive at Pete as an empty slice, and Pete's page has to
|
||||
// decide from that whether the viewer may throw away everybody else's day. An
|
||||
// old build sends no key, which lands as false, and Pete withholds the button.
|
||||
// No omitempty here for the same reason — an absent field is the fail-closed
|
||||
// answer and this one is never absent on purpose.
|
||||
PartyKnown bool `json:"party_known"`
|
||||
}
|
||||
|
||||
// PartySeatView is one body on a shared expedition, as the public page may see
|
||||
|
||||
@@ -144,6 +144,10 @@ func rosterDetail(uid id.UserID, c *DnDCharacter) *peteclient.RosterDetail {
|
||||
ArmorClass: c.ArmorClass,
|
||||
Abilities: [6]int{c.STR, c.DEX, c.CON, c.INT, c.WIS, c.CHA},
|
||||
Modifiers: c.Modifiers(),
|
||||
// Unconditional, and this is the whole point of the field: it says this
|
||||
// build knows about party seats, not that this character has any. Making
|
||||
// it conditional would put it straight back in the hole it closes.
|
||||
PartyKnown: true,
|
||||
}
|
||||
if equip, err := loadAdvEquipment(uid); err == nil {
|
||||
for _, slot := range allSlots {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -178,6 +180,71 @@ func TestOptedOutSeatIsAnonymisedNotDropped(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestPartyKnownIsSetOnEverySheet pins the capability flag Pete's abandon button
|
||||
// hangs off. Party is omitempty, so a solo run and a game box too old to know what
|
||||
// a seat is both reach Pete as an empty slice; the flag is what tells them apart.
|
||||
// It is a fact about this build, so it must be true on a sheet with no party on it
|
||||
// at all — a conditional party_known reads as "this player is solo" and puts the
|
||||
// flag back in the hole it was added to close.
|
||||
func TestPartyKnownIsSetOnEverySheet(t *testing.T) {
|
||||
newBoredomTestDB(t)
|
||||
now := time.Now().UTC()
|
||||
old := now.Add(-30 * time.Hour)
|
||||
|
||||
intown := id.UserID("@intown:test")
|
||||
solo := id.UserID("@solo:test")
|
||||
leader := id.UserID("@leader:test")
|
||||
member := id.UserID("@member:test")
|
||||
seedRosterPlayer(t, intown, "Nonk", &old, &old)
|
||||
seedRosterPlayer(t, solo, "Quack", &old, &old)
|
||||
seedRosterPlayer(t, leader, "Josie", &old, &old)
|
||||
seedRosterPlayer(t, member, "Camcast", &old, &old)
|
||||
|
||||
seedExpedition(t, "exp-solo", solo, "active")
|
||||
seedExpedition(t, "exp-shared", leader, "active")
|
||||
seatLeaderFixture(t, "exp-shared")
|
||||
if err := joinParty("exp-shared", member); err != nil {
|
||||
t.Fatalf("joinParty: %v", err)
|
||||
}
|
||||
|
||||
snap, err := buildRosterSnapshot(now, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildRosterSnapshot: %v", err)
|
||||
}
|
||||
var seen int
|
||||
for _, a := range snap.Adventurers {
|
||||
if a.Detail == nil {
|
||||
t.Fatalf("%s has no detail sheet to carry the flag", a.Name)
|
||||
}
|
||||
seen++
|
||||
if !a.Detail.PartyKnown {
|
||||
t.Errorf("%s (%s, %d seats) published party_known=false; this build knows what a seat is",
|
||||
a.Name, a.Status, len(a.Detail.Party))
|
||||
}
|
||||
}
|
||||
if seen != 4 {
|
||||
t.Fatalf("checked %d sheets, want 4 — a case went missing", seen)
|
||||
}
|
||||
|
||||
// The wire name is the contract: Pete decodes party_known and withholds the
|
||||
// abandon button when it is absent, so a rename here fails silently and only
|
||||
// on the far side.
|
||||
blob, err := json.Marshal(&peteclient.RosterDetail{PartyKnown: true})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(blob), `"party_known":true`) {
|
||||
t.Errorf("detail sheet serialised without party_known: %s", blob)
|
||||
}
|
||||
// And it must not be omitempty: an absent key is Pete's fail-closed answer,
|
||||
// which a false flag has to keep meaning.
|
||||
if blob, err = json.Marshal(&peteclient.RosterDetail{}); err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
} else if !strings.Contains(string(blob), `"party_known":false`) {
|
||||
t.Errorf("party_known is omitempty; false must stay on the wire: %s", blob)
|
||||
}
|
||||
}
|
||||
|
||||
// seatsForOwner pulls one named adventurer's published party out of a whole
|
||||
// snapshot, which is the only way to reach it — Party rides RosterDetail, so this
|
||||
// also proves the wiring in buildRosterSnapshot and not just partySeatViews.
|
||||
|
||||
Reference in New Issue
Block a user