From 0e60567a7a0addb9421769ee7be7853bfa8788cb Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sat, 19 Sep 2026 03:15:59 -0400 Subject: refactor: simplifcation, change how rooms are stored in player yaml, remove legacy code --- internal/player/roomset.go | 142 ++++++++++++++++++++++++++++++++------------- 1 file changed, 102 insertions(+), 40 deletions(-) (limited to 'internal/player/roomset.go') diff --git a/internal/player/roomset.go b/internal/player/roomset.go index 3565bb6..d7e1b47 100644 --- a/internal/player/roomset.go +++ b/internal/player/roomset.go @@ -2,75 +2,103 @@ package player import ( "encoding/base64" - "math/bits" + "encoding/binary" + "fmt" + "sort" + "strings" "gopkg.in/yaml.v3" ) +// RoomSet is the set of room IDs a character has visited, used for the map +// discovery view. +// +// It is stored sparsely: room IDs are filename-derived and may be large and +// widely spaced (the shipped example rooms run from 1001 to 99999301), so an +// ID-indexed bitset would grow with the *largest* ID rather than the number of +// visited rooms. A sparse map keeps memory proportional to the visited count, +// and the YAML encoding is a sorted, delta-encoded uvarint blob (see +// MarshalYAML) which is a handful of bytes for a typical character regardless +// of how high the IDs run. +// +// The zero value is a valid empty set. Copies share the underlying map, so a +// RoomSet should not be copied after a concurrent mutation (the game layer only +// touches it on the owner goroutine). type RoomSet struct { - bits []byte + ids map[int]struct{} +} + +// roomSetV1Prefix tags the delta-encoded encoding. The colon is safe inside a +// YAML plain scalar: ':' is only a mapping indicator when followed by a space, +// and ':' is not in the base64 alphabet, so the prefix cannot be confused with +// payload bytes. +const roomSetV1Prefix = "v1:" + +// roomSetMaxID bounds decoded room IDs. IDs are filename-derived and fit +// comfortably in an int32; rejecting anything larger keeps a corrupt delta +// blob from synthesizing absurd IDs. +const roomSetMaxID = 1<<31 - 1 + +func (rs *RoomSet) ensure() { + if rs.ids == nil { + rs.ids = make(map[int]struct{}) + } } func (rs *RoomSet) Add(roomID int) { if roomID < 0 { return } - byteIdx := roomID / 8 - if byteIdx >= len(rs.bits) { - nb := make([]byte, byteIdx+1) - copy(nb, rs.bits) - rs.bits = nb - } - rs.bits[byteIdx] |= 1 << (roomID % 8) + rs.ensure() + rs.ids[roomID] = struct{}{} } func (rs *RoomSet) Has(roomID int) bool { - if roomID < 0 { + if roomID < 0 || rs.ids == nil { return false } - byteIdx := roomID / 8 - return byteIdx < len(rs.bits) && rs.bits[byteIdx]&(1<<(roomID%8)) != 0 + _, ok := rs.ids[roomID] + return ok } func (rs *RoomSet) Len() int { - n := 0 - for _, b := range rs.bits { - n += bits.OnesCount8(b) + if rs.ids == nil { + return 0 } - return n + return len(rs.ids) } func (rs RoomSet) IsZero() bool { - return len(rs.bits) == 0 + return len(rs.ids) == 0 } +// Keys returns the visited room IDs in ascending order. func (rs *RoomSet) Keys() []int { - var keys []int - for byteIdx, b := range rs.bits { - if b == 0 { - continue - } - base := byteIdx * 8 - for bitIdx := 0; bitIdx < 8; bitIdx++ { - if b&(1< 0 { + delta, n := binary.Uvarint(raw) + if n <= 0 { + return fmt.Errorf("rooms_visited: corrupt delta encoding") + } + raw = raw[n:] + if delta > roomSetMaxID || prev > roomSetMaxID-int(delta) { + return fmt.Errorf("rooms_visited: room ID out of range") + } + prev += int(delta) + rs.Add(prev) } - rs.bits = b return nil } -- cgit v1.2.3