Files
bd2/go/internal/server/player/collection_entries.go
T

264 lines
9.0 KiB
Go

package player
import (
"encoding/json"
"fmt"
"reflect"
"sort"
"strconv"
"bd2server/internal/server/stateio"
)
const collectionDomain = "collection"
var collectionEntryBuckets = [...]string{
"characters", "costumes", "grants", "gacha_applied", "gacha_users",
"gacha_fixed", "step_up_progress", "gacha_point_exchanges",
"gacha_selections", "gacha_selection_changes", "costume_potential",
"costume_burst_levels", "costume_burst_upgrades", "char_awake", "friendships",
}
func rejectInlineCollectionEntries(raw []byte) error {
if err := stateio.RequireExactJSONObject(raw, "version", "next_character_index", "next_costume_index", "latest_preview", "preview_event_index", "preview_locked", "base_costume_levels", "gacha_count_corrected"); err != nil {
return fmt.Errorf("player: incompatible collection core: %w", err)
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(raw, &fields); err != nil {
return fmt.Errorf("player: decode collection core: %w", err)
}
for _, bucket := range collectionEntryBuckets {
if _, found := fields[bucket]; found {
return fmt.Errorf("player: collection core contains entry bucket %q", bucket)
}
}
return nil
}
func loadEntryMap[T any](store stateio.EntryStore, bucket string) (map[string]T, error) {
entries, err := store.ListEntries(collectionDomain, bucket)
if err != nil {
return nil, fmt.Errorf("player: load collection %s: %w", bucket, err)
}
result := make(map[string]T, len(entries))
for key, payload := range entries {
if key == "" {
return nil, fmt.Errorf("player: empty collection %s key", bucket)
}
var value T
if err := json.Unmarshal(payload, &value); err != nil {
return nil, fmt.Errorf("player: decode collection %s entry %q: %w", bucket, key, err)
}
result[key] = value
}
return result, nil
}
func loadCollectionEntries(store stateio.EntryStore, data *collectionSnapshot) error {
var err error
if data.Friendships, err = loadEntryMap[FriendshipEntry](store, "friendships"); err != nil {
return err
}
if data.Characters, err = loadIndexedEntries[Character](store, "characters", func(c Character) uint64 { return c.InvenIndex }); err != nil {
return err
}
if data.Costumes, err = loadIndexedEntries[Costume](store, "costumes", func(c Costume) uint64 { return c.InvenIndex }); err != nil {
return err
}
if data.Grants, err = loadEntryMap[CollectionGrant](store, "grants"); err != nil {
return err
}
if data.GachaApplied, err = loadEntryMap[bool](store, "gacha_applied"); err != nil {
return err
}
if data.GachaUsers, err = loadEntryMap[GachaUserState](store, "gacha_users"); err != nil {
return err
}
if data.GachaFixed, err = loadEntryMap[GachaFixedState](store, "gacha_fixed"); err != nil {
return err
}
if data.StepUpProgress, err = loadEntryMap[uint64](store, "step_up_progress"); err != nil {
return err
}
if data.GachaPointExchange, err = loadEntryMap[GachaPointExchange](store, "gacha_point_exchanges"); err != nil {
return err
}
if data.GachaSelections, err = loadEntryMap[[]GachaSelection](store, "gacha_selections"); err != nil {
return err
}
if data.GachaSelectionChanges, err = loadEntryMap[uint64](store, "gacha_selection_changes"); err != nil {
return err
}
if data.CostumePotential, err = loadEntryMap[[]uint64](store, "costume_potential"); err != nil {
return err
}
if data.CostumeBurstLevels, err = loadEntryMap[uint64](store, "costume_burst_levels"); err != nil {
return err
}
if data.CostumeBurstUpgrades, err = loadEntryMap[CostumeBurstUpgradeRecord](store, "costume_burst_upgrades"); err != nil {
return err
}
if data.CharAwake, err = loadEntryMap[CharAwakeProgress](store, "char_awake"); err != nil {
return err
}
return nil
}
func loadIndexedEntries[T any](store stateio.EntryStore, bucket string, index func(T) uint64) ([]T, error) {
values, err := loadEntryMap[T](store, bucket)
if err != nil {
return nil, err
}
indices := make([]uint64, 0, len(values))
for key, value := range values {
parsed, parseErr := strconv.ParseUint(key, 10, 64)
if parseErr != nil || parsed == 0 || key != strconv.FormatUint(parsed, 10) || index(value) != parsed {
return nil, fmt.Errorf("player: invalid collection %s entry key %q", bucket, key)
}
indices = append(indices, parsed)
}
sort.Slice(indices, func(i, j int) bool { return indices[i] < indices[j] })
result := make([]T, 0, len(indices))
for _, id := range indices {
result = append(result, values[strconv.FormatUint(id, 10)])
}
return result, nil
}
func collectionCore(in collectionSnapshot) collectionSnapshot {
in.Friendships = nil
in.Characters = nil
in.Costumes = nil
in.Grants = nil
in.GachaApplied = nil
in.GachaUsers = nil
in.GachaFixed = nil
in.StepUpProgress = nil
in.GachaPointExchange = nil
in.GachaSelections = nil
in.GachaSelectionChanges = nil
in.CostumePotential = nil
in.CostumeBurstLevels = nil
in.CostumeBurstUpgrades = nil
in.CharAwake = nil
return in
}
func sameCollectionCore(a, b collectionSnapshot) bool {
return reflect.DeepEqual(collectionCore(a), collectionCore(b))
}
func diffEntryMap[T any](bucket string, before, after map[string]T, changes *[]stateio.EntryMutation) error {
for key, value := range after {
old, found := before[key]
if found && reflect.DeepEqual(old, value) {
continue
}
if key == "" {
return fmt.Errorf("player: empty collection %s key", bucket)
}
payload, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("player: encode collection %s entry %q: %w", bucket, key, err)
}
*changes = append(*changes, stateio.EntryMutation{Bucket: bucket, Key: key, Payload: payload})
}
for key := range before {
if _, found := after[key]; !found {
*changes = append(*changes, stateio.EntryMutation{Bucket: bucket, Key: key, Delete: true})
}
}
return nil
}
func indexedMap[T any](bucket string, values []T, index func(T) uint64) (map[string]T, error) {
result := make(map[string]T, len(values))
for _, value := range values {
id := index(value)
if id == 0 {
return nil, fmt.Errorf("player: zero collection %s index", bucket)
}
key := strconv.FormatUint(id, 10)
if _, found := result[key]; found {
return nil, fmt.Errorf("player: duplicate collection %s index %d", bucket, id)
}
result[key] = value
}
return result, nil
}
func diffIndexedEntries[T any](bucket string, before, after []T, index func(T) uint64, changes *[]stateio.EntryMutation) error {
old, err := indexedMap(bucket, before, index)
if err != nil {
return err
}
next, err := indexedMap(bucket, after, index)
if err != nil {
return err
}
return diffEntryMap(bucket, old, next, changes)
}
func diffCollectionEntries(before, after collectionSnapshot) ([]stateio.EntryMutation, error) {
changes := make([]stateio.EntryMutation, 0)
if err := diffEntryMap("friendships", before.Friendships, after.Friendships, &changes); err != nil {
return nil, err
}
for _, item := range []struct {
bucket string
old, next map[string]bool
}{{"gacha_applied", before.GachaApplied, after.GachaApplied}} {
if err := diffEntryMap(item.bucket, item.old, item.next, &changes); err != nil {
return nil, err
}
}
if err := diffIndexedEntries("characters", before.Characters, after.Characters, func(c Character) uint64 { return c.InvenIndex }, &changes); err != nil {
return nil, err
}
if err := diffIndexedEntries("costumes", before.Costumes, after.Costumes, func(c Costume) uint64 { return c.InvenIndex }, &changes); err != nil {
return nil, err
}
if err := diffEntryMap("grants", before.Grants, after.Grants, &changes); err != nil {
return nil, err
}
if err := diffEntryMap("gacha_users", before.GachaUsers, after.GachaUsers, &changes); err != nil {
return nil, err
}
if err := diffEntryMap("gacha_fixed", before.GachaFixed, after.GachaFixed, &changes); err != nil {
return nil, err
}
if err := diffEntryMap("step_up_progress", before.StepUpProgress, after.StepUpProgress, &changes); err != nil {
return nil, err
}
if err := diffEntryMap("gacha_point_exchanges", before.GachaPointExchange, after.GachaPointExchange, &changes); err != nil {
return nil, err
}
if err := diffEntryMap("gacha_selections", before.GachaSelections, after.GachaSelections, &changes); err != nil {
return nil, err
}
if err := diffEntryMap("gacha_selection_changes", before.GachaSelectionChanges, after.GachaSelectionChanges, &changes); err != nil {
return nil, err
}
if err := diffEntryMap("costume_potential", before.CostumePotential, after.CostumePotential, &changes); err != nil {
return nil, err
}
if err := diffEntryMap("costume_burst_levels", before.CostumeBurstLevels, after.CostumeBurstLevels, &changes); err != nil {
return nil, err
}
if err := diffEntryMap("costume_burst_upgrades", before.CostumeBurstUpgrades, after.CostumeBurstUpgrades, &changes); err != nil {
return nil, err
}
if err := diffEntryMap("char_awake", before.CharAwake, after.CharAwake, &changes); err != nil {
return nil, err
}
if len(changes) > 1 {
sort.Slice(changes, func(i, j int) bool {
if changes[i].Bucket != changes[j].Bucket {
return changes[i].Bucket < changes[j].Bucket
}
return changes[i].Key < changes[j].Key
})
}
return changes, nil
}