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
|
// 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.
|
// than draw a roster with a single chair in it.
|
||||||
Party []PartySeatView `json:"party,omitempty"`
|
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
|
// 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,
|
ArmorClass: c.ArmorClass,
|
||||||
Abilities: [6]int{c.STR, c.DEX, c.CON, c.INT, c.WIS, c.CHA},
|
Abilities: [6]int{c.STR, c.DEX, c.CON, c.INT, c.WIS, c.CHA},
|
||||||
Modifiers: c.Modifiers(),
|
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 {
|
if equip, err := loadAdvEquipment(uid); err == nil {
|
||||||
for _, slot := range allSlots {
|
for _, slot := range allSlots {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package plugin
|
package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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
|
// 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
|
// snapshot, which is the only way to reach it — Party rides RosterDetail, so this
|
||||||
// also proves the wiring in buildRosterSnapshot and not just partySeatViews.
|
// also proves the wiring in buildRosterSnapshot and not just partySeatViews.
|
||||||
|
|||||||
Reference in New Issue
Block a user