Files
bd2/go/internal/server/deck/preset.go
T
Flechazo 5b5a219dc2 feat(server): add special loot and data-driven gameplay rules
Generate equipment loot atomically, grant random boxes and prevent repeat claims.
Read gacha pools, pity, products, tickets, missions, presets, chapter rewards
and inventory costs from GameData; extend both collaboration UR ticket pools.
Sync owned recipes, configure startup state and calendars in versioned seeds,
and validate unlock tickets and revival talents.
2026-10-04 19:15:59 +08:00

889 lines
27 KiB
Go

package deck
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"bd2server/internal/server/player"
"bd2server/internal/server/stateio"
"bd2server/internal/server/wire"
)
type Preset struct {
Name string `json:"name"`
ResourceID uint64 `json:"resource_id"`
ResourceColor uint64 `json:"resource_color"`
Slot uint64 `json:"slot"`
Decks []PresetDeck `json:"decks"`
Blesses []PresetBless `json:"blesses"`
}
type PresetDeck struct {
Deck DeckEntry `json:"deck"`
CostumeIndex uint64 `json:"costume_index"`
Equipment []PresetEquipmentItem `json:"equipment"`
Team uint64 `json:"team"`
}
type PresetEquipmentItem struct {
Type uint64 `json:"type"`
Index uint64 `json:"index"`
}
type PresetBless struct {
DeckType uint64 `json:"deck_type"`
IDs []uint64 `json:"ids"`
}
type CostumeSetting struct {
CharacterIndex uint64 `json:"character_index"`
Sequence []CostumeSettingItem `json:"sequence"`
BattleMode uint64 `json:"battle_mode"`
MonsterID uint64 `json:"monster_id"`
}
type CostumeSettingItem struct {
CostumeIndex int64 `json:"costume_index"`
BurstLevel uint64 `json:"burst_level"`
}
func (s *Store) AttachPresetRuntime(wallet *player.Wallet, characters *player.CharacterStore, equipment *player.EquipmentInventory, collection *player.CollectionStore) error {
if wallet == nil || characters == nil || equipment == nil || collection == nil {
return errors.New("deck: incomplete preset runtime")
}
s.mu.Lock()
defer s.mu.Unlock()
s.wallet, s.characters, s.equipment, s.collection = wallet, characters, equipment, collection
return s.validatePresetOwnershipLocked()
}
func (s *Store) BeginSession(id string) {
s.mu.Lock()
defer s.mu.Unlock()
if id == "" || id == s.sessionID {
return
}
s.sessionID = id
s.replies = map[string]deckReply{}
}
func (s *Store) PresetSlotCount() uint64 {
s.mu.RLock()
defer s.mu.RUnlock()
return s.presetSlots
}
func (s *Store) loadPresetEntries() error {
rawConfig, found, err := s.storage.LoadEntry("deck", "preset_config", "slots")
if err != nil {
return err
}
if found {
if err := json.Unmarshal(rawConfig, &s.presetSlots); err != nil || s.presetSlots < s.presetDesign.BaseCount || s.presetSlots > s.presetDesign.Maximum {
return errors.New("deck: invalid preset slot configuration")
}
}
raw, err := s.storage.ListEntries("deck", "presets")
if err != nil {
return err
}
for key, payload := range raw {
slot, err := strconv.ParseUint(key, 10, 64)
if err != nil || key != strconv.FormatUint(slot, 10) {
return fmt.Errorf("deck: invalid preset key %q", key)
}
var preset Preset
if err := json.Unmarshal(payload, &preset); err != nil || preset.Slot != slot {
return fmt.Errorf("deck: invalid preset %q", key)
}
if err := s.validatePresetShape(preset, s.presetSlots); err != nil {
return fmt.Errorf("deck: invalid preset %q: %w", key, err)
}
s.presets[slot] = preset
}
rawSettings, err := s.storage.ListEntries("deck", "costume_settings")
if err != nil {
return err
}
for key, payload := range rawSettings {
index, err := strconv.ParseUint(key, 10, 64)
if err != nil || index == 0 || key != strconv.FormatUint(index, 10) {
return fmt.Errorf("deck: invalid costume setting key %q", key)
}
var setting CostumeSetting
if err := json.Unmarshal(payload, &setting); err != nil || setting.CharacterIndex != index {
return fmt.Errorf("deck: invalid costume setting %q", key)
}
s.costumeSettings[index] = setting
}
return nil
}
func (s *Store) validatePresetOwnershipLocked() error {
if s.characters == nil || s.collection == nil || s.equipment == nil {
return nil
}
for _, preset := range s.presets {
if err := s.validatePresetOwnedLocked(preset); err != nil {
return fmt.Errorf("deck: saved preset %d: %w", preset.Slot, err)
}
}
for _, setting := range s.costumeSettings {
if _, found := s.characters.Find(setting.CharacterIndex); !found {
return fmt.Errorf("deck: costume setting references unknown character %d", setting.CharacterIndex)
}
for _, item := range setting.Sequence {
if item.CostumeIndex > 0 {
if _, found := s.collection.CostumeByIndex(uint64(item.CostumeIndex)); !found {
return fmt.Errorf("deck: costume setting references unknown costume %d", item.CostumeIndex)
}
}
}
}
return nil
}
func (s *Store) validatePresetShape(p Preset, slotCount uint64) error {
if p.Slot >= slotCount || (p.ResourceID != 0 && !s.presetDesign.Icons[p.ResourceID]) || p.ResourceColor > 5 || !validPresetName(p.Name) || len(p.Decks) > 5 {
return errors.New("invalid metadata or deck count")
}
characters, positions, sequences := map[uint64]bool{}, map[uint64]bool{}, map[uint64]bool{}
for _, deck := range p.Decks {
if deck.Deck.CharacterInvenIndex == 0 || deck.Deck.CostumeInvenIndex > 11 || deck.Deck.Slot == 0 || deck.Deck.Slot > 5 || deck.Team != 0 ||
characters[deck.Deck.CharacterInvenIndex] || positions[deck.Deck.CostumeInvenIndex] || sequences[deck.Deck.Slot] {
return errors.New("invalid deck entry")
}
characters[deck.Deck.CharacterInvenIndex] = true
positions[deck.Deck.CostumeInvenIndex] = true
sequences[deck.Deck.Slot] = true
if len(deck.Equipment) != 5 {
return errors.New("preset deck requires five equipment slots")
}
seen := map[uint64]bool{}
for _, item := range deck.Equipment {
if item.Type >= 5 || seen[item.Type] {
return errors.New("invalid preset equipment slot")
}
seen[item.Type] = true
}
}
return nil
}
func validPresetName(value string) bool {
if !utf8.ValidString(value) || utf8.RuneCountInString(value) < 1 || utf8.RuneCountInString(value) > 16 || strings.Contains(value, "<") || strings.Contains(value, ">") {
return false
}
for _, r := range value {
if unicode.IsControl(r) {
return false
}
}
return true
}
func (s *Store) validatePresetOwnedLocked(p Preset) error {
if err := s.validatePresetShape(p, s.presetSlots); err != nil {
return err
}
ownedEquipment := make(map[uint64]player.Equipment)
for _, item := range s.equipment.All() {
ownedEquipment[item.InvenIndex] = item
}
seenEquipment := map[uint64]bool{}
for _, deck := range p.Decks {
if _, found := s.characters.Find(deck.Deck.CharacterInvenIndex); !found {
return fmt.Errorf("unknown character %d", deck.Deck.CharacterInvenIndex)
}
if deck.CostumeIndex != 0 {
costume, found := s.collection.CostumeByIndex(deck.CostumeIndex)
if !found || costume.UseChar != deck.Deck.CharacterInvenIndex {
return fmt.Errorf("unknown costume %d", deck.CostumeIndex)
}
}
for _, reference := range deck.Equipment {
if reference.Index == 0 {
continue
}
if seenEquipment[reference.Index] || ownedEquipment[reference.Index].InvenIndex == 0 {
return fmt.Errorf("invalid or repeated equipment %d", reference.Index)
}
seenEquipment[reference.Index] = true
}
binding := player.PresetEquipmentBinding{CharacterIndex: deck.Deck.CharacterInvenIndex, Equipment: make([]uint64, 5)}
for _, reference := range deck.Equipment {
binding.Equipment[reference.Type] = reference.Index
}
if err := s.equipment.ValidatePresetEquipment([]player.PresetEquipmentBinding{binding}); err != nil {
return err
}
}
return nil
}
func decodePreset(data []byte) (Preset, error) {
var p Preset
name, _, err := wire.Bytes(data, 1)
if err != nil {
return p, err
}
p.Name = string(name)
p.ResourceID, _, err = wire.Varint(data, 2)
if err != nil {
return p, err
}
p.ResourceColor, _, err = wire.Varint(data, 3)
if err != nil {
return p, err
}
p.Slot, _, err = wire.Varint(data, 4)
if err != nil {
return p, err
}
err = wire.Walk(data, func(field wire.Field) error {
if field.Type != 2 {
return nil
}
switch field.Number {
case 5:
deck, err := decodePresetDeck(field.Value)
if err != nil {
return err
}
p.Decks = append(p.Decks, deck)
case 6:
bless, err := decodePresetBless(field.Value)
if err != nil {
return err
}
p.Blesses = append(p.Blesses, bless)
}
return nil
})
return p, err
}
func decodePresetDeck(data []byte) (PresetDeck, error) {
var result PresetDeck
base, found, err := wire.Bytes(data, 1)
if err != nil || !found {
return result, errors.New("deck: preset missing deck base")
}
character, ok, err := wire.Varint(base, 1)
if err != nil || !ok || character == 0 {
return result, errors.New("deck: invalid preset character")
}
position, _, err := wire.Varint(base, 2)
if err != nil {
return result, err
}
sequence, ok, err := wire.Varint(base, 3)
if err != nil || !ok || sequence == 0 {
return result, errors.New("deck: invalid preset sequence")
}
result.Deck = DeckEntry{CharacterInvenIndex: character, CostumeInvenIndex: position, Slot: sequence}
result.CostumeIndex, _, err = wire.Varint(data, 2)
if err != nil {
return result, err
}
result.Team, _, err = wire.Varint(data, 4)
if err != nil {
return result, err
}
err = wire.Walk(data, func(field wire.Field) error {
if field.Number != 3 {
return nil
}
if field.Type != 2 {
return errors.New("deck: invalid preset equipment")
}
typeID, _, err := wire.Varint(field.Value, 1)
if err != nil {
return err
}
index, _, err := wire.Varint(field.Value, 2)
if err != nil {
return err
}
result.Equipment = append(result.Equipment, PresetEquipmentItem{Type: typeID, Index: index})
return nil
})
sort.Slice(result.Equipment, func(i, j int) bool { return result.Equipment[i].Type < result.Equipment[j].Type })
return result, err
}
func decodePresetBless(data []byte) (PresetBless, error) {
var result PresetBless
result.DeckType, _, _ = wire.Varint(data, 1)
err := wire.Walk(data, func(field wire.Field) error {
if field.Number != 2 {
return nil
}
values, err := repeatedUint64(field)
if err != nil {
return err
}
result.IDs = append(result.IDs, values...)
return nil
})
return result, err
}
func repeatedUint64(field wire.Field) ([]uint64, error) {
if field.Type == 0 {
value, count := binary.Uvarint(field.Value)
if count <= 0 {
return nil, wire.ErrMalformed
}
return []uint64{value}, nil
}
if field.Type != 2 {
return nil, wire.ErrMalformed
}
var out []uint64
for offset := 0; offset < len(field.Value); {
value, count := binary.Uvarint(field.Value[offset:])
if count <= 0 {
return nil, wire.ErrMalformed
}
out = append(out, value)
offset += count
}
return out, nil
}
func presetWire(p Preset) []byte {
var out []byte
if p.Name != "" {
out = wire.AppendString(out, 1, p.Name)
}
if p.ResourceID != 0 {
out = wire.AppendVarint(out, 2, p.ResourceID)
}
if p.ResourceColor != 0 {
out = wire.AppendVarint(out, 3, p.ResourceColor)
}
if p.Slot != 0 {
out = wire.AppendVarint(out, 4, p.Slot)
}
for _, deck := range p.Decks {
out = wire.AppendBytes(out, 5, presetDeckWire(deck))
}
for _, bless := range p.Blesses {
var b []byte
if bless.DeckType != 0 {
b = wire.AppendVarint(b, 1, bless.DeckType)
}
for _, id := range bless.IDs {
b = wire.AppendVarint(b, 2, id)
}
out = wire.AppendBytes(out, 6, b)
}
return out
}
func presetDeckWire(deck PresetDeck) []byte {
base := wire.AppendVarint(nil, 1, deck.Deck.CharacterInvenIndex)
if deck.Deck.CostumeInvenIndex != 0 {
base = wire.AppendVarint(base, 2, deck.Deck.CostumeInvenIndex)
}
base = wire.AppendVarint(base, 3, deck.Deck.Slot)
out := wire.AppendBytes(nil, 1, base)
if deck.CostumeIndex != 0 {
out = wire.AppendVarint(out, 2, deck.CostumeIndex)
}
for _, item := range deck.Equipment {
var b []byte
if item.Type != 0 {
b = wire.AppendVarint(b, 1, item.Type)
}
if item.Index != 0 {
b = wire.AppendVarint(b, 2, item.Index)
}
out = wire.AppendBytes(out, 3, b)
}
if deck.Team != 0 {
out = wire.AppendVarint(out, 4, deck.Team)
}
return out
}
func (s *Store) presetCacheKey(kind string, seq uint64) string {
return kind + ":" + s.sessionID + ":" + strconv.FormatUint(seq, 10)
}
func (s *Store) persistPresetLocked(p Preset) error {
payload, err := json.Marshal(p)
if err != nil {
return err
}
core, err := s.corePayloadLocked()
if err != nil {
return err
}
return s.storage.SaveWithEntries("deck", core, []stateio.EntryMutation{{Bucket: "presets", Key: strconv.FormatUint(p.Slot, 10), Payload: payload}})
}
func (s *Store) corePayloadLocked() ([]byte, error) {
payload, err := json.MarshalIndent(s.state, "", " ")
if err != nil {
return nil, err
}
return append(payload, '\n'), nil
}
func (s *Store) cachedReplyLocked(kind string, seq uint64) (deckReply, bool) {
reply, found := s.replies[s.presetCacheKey(kind, seq)]
if found {
reply.body = append([]byte(nil), reply.body...)
}
return reply, found
}
func (s *Store) rememberReplyLocked(kind string, seq uint64, code int, body []byte) {
s.replies[s.presetCacheKey(kind, seq)] = deckReply{code: code, body: append([]byte(nil), body...)}
}
func requestSequence(request []byte) (uint64, error) {
seq, found, err := wire.Varint(request, 1)
if err != nil || !found || seq == 0 {
return 0, errors.New("deck: invalid request sequence")
}
return seq, nil
}
func (s *Store) handlePresetInfo(request []byte) (int, []byte, bool, error) {
if _, err := requestSequence(request); err != nil {
return 0, nil, true, err
}
s.mu.RLock()
defer s.mu.RUnlock()
slots := make([]uint64, 0, len(s.presets))
for slot := range s.presets {
if slot < s.presetSlots {
slots = append(slots, slot)
}
}
sort.Slice(slots, func(i, j int) bool { return slots[i] < slots[j] })
var response []byte
for _, slot := range slots {
response = wire.AppendBytes(response, 1, presetWire(s.presets[slot]))
}
return 178, response, true, nil
}
func (s *Store) handlePresetSave(request []byte) (int, []byte, bool, error) {
seq, err := requestSequence(request)
if err != nil {
return 0, nil, true, err
}
raw, found, err := wire.Bytes(request, 2)
if err != nil || !found {
return 0, nil, true, errors.New("deck: PresetSave missing preset")
}
preset, err := decodePreset(raw)
if err != nil {
return 0, nil, true, fmt.Errorf("deck: decode preset: %w", err)
}
s.mu.Lock()
defer s.mu.Unlock()
if reply, found := s.cachedReplyLocked("save", seq); found {
return reply.code, reply.body, true, nil
}
if s.characters == nil || s.collection == nil || s.equipment == nil {
return 0, nil, true, errors.New("deck: preset runtime unavailable")
}
if err := s.validatePresetOwnedLocked(preset); err != nil {
return 0, nil, true, fmt.Errorf("deck: invalid preset: %w", err)
}
if err := s.persistPresetLocked(preset); err != nil {
return 0, nil, true, fmt.Errorf("deck: persist preset: %w", err)
}
s.presets[preset.Slot] = preset
s.rememberReplyLocked("save", seq, 179, nil)
return 179, nil, true, nil
}
func (s *Store) handlePresetAddSlot(request []byte) (int, []byte, bool, error) {
seq, err := requestSequence(request)
if err != nil {
return 0, nil, true, err
}
count, found, err := wire.Varint(request, 2)
if err != nil || !found || count == 0 {
return 0, nil, true, errors.New("deck: PresetAddSlot invalid count")
}
s.mu.Lock()
defer s.mu.Unlock()
if reply, found := s.cachedReplyLocked("add-slot", seq); found {
return reply.code, reply.body, true, nil
}
if s.wallet == nil || s.presetDesign.Validate() != nil {
return 0, nil, true, errors.New("deck: preset wallet/design unavailable")
}
if s.presetSlots > s.presetDesign.Maximum || count > s.presetDesign.Maximum-s.presetSlots {
return 0, nil, true, errors.New("deck: preset slot limit exceeded")
}
if count > ^uint64(0)/s.presetDesign.Price {
return 0, nil, true, errors.New("deck: preset slot price overflow")
}
identity := "preset-slot:" + s.sessionID + ":" + strconv.FormatUint(seq, 10)
var spendErr error
switch s.presetDesign.PriceType {
case 4:
_, spendErr = s.wallet.SpendGoldOnce(identity, count*s.presetDesign.Price)
case 3:
_, spendErr = s.wallet.SpendFreeJewelryOnce(identity, count*s.presetDesign.Price)
case 2:
_, spendErr = s.wallet.SpendJewelryOnce(identity, count*s.presetDesign.Price)
case 12:
_, spendErr = s.wallet.SpendCatalystOnce(identity, count*s.presetDesign.Price)
}
if err := spendErr; err != nil {
return 0, nil, true, fmt.Errorf("deck: buy preset slot: %w", err)
}
next := s.presetSlots + count
payload, err := json.Marshal(next)
if err != nil {
return 0, nil, true, err
}
core, err := s.corePayloadLocked()
if err != nil {
return 0, nil, true, err
}
if err := s.storage.SaveWithEntries("deck", core, []stateio.EntryMutation{{Bucket: "preset_config", Key: "slots", Payload: payload}}); err != nil {
return 0, nil, true, fmt.Errorf("deck: persist preset slots: %w", err)
}
s.presetSlots = next
s.rememberReplyLocked("add-slot", seq, 180, nil)
return 180, nil, true, nil
}
func (s *Store) handlePresetInfoChange(request []byte) (int, []byte, bool, error) {
seq, err := requestSequence(request)
if err != nil {
return 0, nil, true, err
}
nameBytes, _, err := wire.Bytes(request, 2)
if err != nil {
return 0, nil, true, errors.New("deck: invalid preset name")
}
resourceID, _, err := wire.Varint(request, 3)
if err != nil {
return 0, nil, true, errors.New("deck: invalid preset icon")
}
color, _, err := wire.Varint(request, 4)
if err != nil {
return 0, nil, true, errors.New("deck: invalid preset color")
}
// Ordinary preset slots are zero-based. Proto3 omits slot=0, so an absent
// field 5 is the first slot rather than a malformed request.
slot, _, err := wire.Varint(request, 5)
if err != nil {
return 0, nil, true, errors.New("deck: invalid preset slot")
}
s.mu.Lock()
defer s.mu.Unlock()
if reply, found := s.cachedReplyLocked("info-change", seq); found {
return reply.code, reply.body, true, nil
}
preset, exists := s.presets[slot]
if !exists {
preset = Preset{Slot: slot, Decks: []PresetDeck{}, Blesses: []PresetBless{}}
}
preset.Name, preset.ResourceID, preset.ResourceColor = string(nameBytes), resourceID, color
if err := s.validatePresetShape(preset, s.presetSlots); err != nil {
return 0, nil, true, fmt.Errorf("deck: invalid preset metadata: %w", err)
}
if err := s.persistPresetLocked(preset); err != nil {
return 0, nil, true, err
}
s.presets[slot] = preset
s.rememberReplyLocked("info-change", seq, 278, nil)
return 278, nil, true, nil
}
func (s *Store) handlePresetDelete(request []byte) (int, []byte, bool, error) {
seq, err := requestSequence(request)
if err != nil {
return 0, nil, true, err
}
var slots []uint64
if err := wire.Walk(request, func(field wire.Field) error {
if field.Number != 2 {
return nil
}
values, err := repeatedUint64(field)
if err != nil {
return err
}
slots = append(slots, values...)
return nil
}); err != nil || len(slots) == 0 {
return 0, nil, true, errors.New("deck: invalid preset delete slots")
}
s.mu.Lock()
defer s.mu.Unlock()
if reply, found := s.cachedReplyLocked("delete", seq); found {
return reply.code, reply.body, true, nil
}
seen := make(map[uint64]bool, len(slots))
changes := make([]stateio.EntryMutation, 0, len(slots))
for _, slot := range slots {
if slot >= s.presetSlots || seen[slot] {
return 0, nil, true, errors.New("deck: invalid or duplicate preset delete slot")
}
seen[slot] = true
changes = append(changes, stateio.EntryMutation{Bucket: "presets", Key: strconv.FormatUint(slot, 10), Delete: true})
}
core, err := s.corePayloadLocked()
if err != nil {
return 0, nil, true, err
}
if err := s.storage.SaveWithEntries("deck", core, changes); err != nil {
return 0, nil, true, err
}
for slot := range seen {
delete(s.presets, slot)
}
// 2.35.10 contains the request/response classes but no PacketCode enum
// member. Code zero is the same compatibility fallback used for other
// unnumbered local endpoints; it must not be treated as an official value.
s.rememberReplyLocked("delete", seq, 0, nil)
return 0, nil, true, nil
}
func (s *Store) handlePresetUse(request []byte) (int, []byte, bool, error) {
seq, err := requestSequence(request)
if err != nil {
return 0, nil, true, err
}
// PresetUse is also zero-based and slot=0 is omitted by proto3.
slot, _, err := wire.Varint(request, 2)
if err != nil {
return 0, nil, true, errors.New("deck: PresetUse missing slot")
}
s.mu.Lock()
defer s.mu.Unlock()
if reply, found := s.cachedReplyLocked("use", seq); found {
return reply.code, reply.body, true, nil
}
preset, found := s.presets[slot]
if !found || len(preset.Decks) == 0 {
return 0, nil, true, errors.New("deck: PresetUse references an empty slot")
}
if s.characters == nil || s.collection == nil || s.equipment == nil {
return 0, nil, true, errors.New("deck: preset runtime unavailable")
}
if err := s.validatePresetOwnedLocked(preset); err != nil {
return 0, nil, true, fmt.Errorf("deck: stale preset: %w", err)
}
deckEntries := make([]DeckEntry, 0, len(preset.Decks))
assignments := make(map[uint64]uint64, len(preset.Decks))
bindings := make([]player.PresetEquipmentBinding, 0, len(preset.Decks))
for _, entry := range preset.Decks {
deckEntries = append(deckEntries, entry.Deck)
assignments[entry.Deck.CharacterInvenIndex] = entry.CostumeIndex
equipment := make([]uint64, 5)
for _, item := range entry.Equipment {
equipment[item.Type] = item.Index
}
bindings = append(bindings, player.PresetEquipmentBinding{CharacterIndex: entry.Deck.CharacterInvenIndex, Equipment: equipment})
}
if _, err := s.characters.ApplyPresetCostumes(assignments); err != nil {
return 0, nil, true, fmt.Errorf("deck: apply preset costumes: %w", err)
}
affected, err := s.equipment.ApplyPresetEquipment(bindings)
if err != nil {
return 0, nil, true, fmt.Errorf("deck: apply preset equipment: %w", err)
}
next := clone(s.state)
next.Deck = append([]DeckEntry(nil), deckEntries...)
for character, costume := range assignments {
next.Costumes[character] = costume
}
if err := s.commit(next); err != nil {
return 0, nil, true, fmt.Errorf("deck: persist applied preset: %w", err)
}
var response []byte
for _, entry := range deckEntries {
base := wire.AppendVarint(nil, 1, entry.CharacterInvenIndex)
if entry.CostumeInvenIndex != 0 {
base = wire.AppendVarint(base, 2, entry.CostumeInvenIndex)
}
base = wire.AppendVarint(base, 3, entry.Slot)
response = wire.AppendBytes(response, 1, base)
}
characterSet := make(map[uint64]bool, len(assignments)+len(affected))
for index := range assignments {
characterSet[index] = true
}
for _, character := range affected {
characterSet[character.InvenIndex] = true
}
indices := make([]uint64, 0, len(characterSet))
for index := range characterSet {
indices = append(indices, index)
}
sort.Slice(indices, func(i, j int) bool { return indices[i] < indices[j] })
for _, index := range indices {
if character, found := s.characters.Find(index); found {
response = wire.AppendBytes(response, 2, player.CharacterWire(character))
}
}
for _, binding := range bindings {
info := wire.AppendVarint(nil, 1, binding.CharacterIndex)
for _, index := range binding.Equipment {
info = wire.AppendVarint(info, 2, index)
}
response = wire.AppendBytes(response, 3, info)
}
s.rememberReplyLocked("use", seq, 409, response)
return 409, response, true, nil
}
func costumeSettingWire(setting CostumeSetting) []byte {
out := wire.AppendVarint(nil, 1, setting.CharacterIndex)
for _, item := range setting.Sequence {
entry := wire.AppendVarint(nil, 1, uint64(item.CostumeIndex))
if item.BurstLevel != 0 {
entry = wire.AppendVarint(entry, 2, item.BurstLevel)
}
out = wire.AppendBytes(out, 2, entry)
}
if setting.BattleMode != 0 {
out = wire.AppendVarint(out, 3, setting.BattleMode)
}
if setting.MonsterID != 0 {
out = wire.AppendVarint(out, 4, setting.MonsterID)
}
return out
}
func decodeCostumeSetting(data []byte) (CostumeSetting, error) {
var result CostumeSetting
character, found, err := wire.Varint(data, 1)
if err != nil || !found || character == 0 {
return result, errors.New("deck: costume setting missing character")
}
result.CharacterIndex = character
result.BattleMode, _, err = wire.Varint(data, 3)
if err != nil {
return result, err
}
result.MonsterID, _, err = wire.Varint(data, 4)
if err != nil {
return result, err
}
err = wire.Walk(data, func(field wire.Field) error {
if field.Number != 2 {
return nil
}
if field.Type != 2 {
return errors.New("deck: invalid costume setting sequence")
}
raw, found, err := wire.Varint(field.Value, 1)
if err != nil || !found {
return errors.New("deck: costume setting entry missing costume")
}
burst, _, err := wire.Varint(field.Value, 2)
if err != nil {
return err
}
result.Sequence = append(result.Sequence, CostumeSettingItem{CostumeIndex: int64(raw), BurstLevel: burst})
return nil
})
return result, err
}
func (s *Store) validateCostumeSettingLocked(setting CostumeSetting) error {
if s.characters == nil || s.collection == nil {
return errors.New("deck: costume setting runtime unavailable")
}
if setting.BattleMode != 0 || setting.MonsterID != 0 {
return errors.New("deck: ordinary costume setting requires normal battle mode")
}
if _, found := s.characters.Find(setting.CharacterIndex); !found {
return fmt.Errorf("deck: unknown costume setting character %d", setting.CharacterIndex)
}
for _, item := range setting.Sequence {
if item.CostumeIndex <= 0 {
if item.CostumeIndex != 0 && item.CostumeIndex != -1 {
return fmt.Errorf("deck: invalid costume setting sentinel %d", item.CostumeIndex)
}
continue
}
costume, found := s.collection.CostumeByIndex(uint64(item.CostumeIndex))
if !found || costume.UseChar != setting.CharacterIndex {
return fmt.Errorf("deck: costume %d does not belong to character %d", item.CostumeIndex, setting.CharacterIndex)
}
if item.BurstLevel > costume.BurstLevel {
return fmt.Errorf("deck: costume %d burst level exceeds owned level", item.CostumeIndex)
}
}
return nil
}
func (s *Store) handleCostumeSettingInfo(request []byte) (int, []byte, bool, error) {
if _, err := requestSequence(request); err != nil {
return 0, nil, true, err
}
s.mu.RLock()
defer s.mu.RUnlock()
indices := make([]uint64, 0, len(s.costumeSettings))
for index := range s.costumeSettings {
indices = append(indices, index)
}
sort.Slice(indices, func(i, j int) bool { return indices[i] < indices[j] })
var response []byte
for _, index := range indices {
response = wire.AppendBytes(response, 1, costumeSettingWire(s.costumeSettings[index]))
}
return 397, response, true, nil
}
func (s *Store) handleCostumeSettingSave(request []byte) (int, []byte, bool, error) {
seq, err := requestSequence(request)
if err != nil {
return 0, nil, true, err
}
raw, found, err := wire.Bytes(request, 2)
if err != nil || !found {
return 0, nil, true, errors.New("deck: DeckCostumeSettingSave missing setting")
}
setting, err := decodeCostumeSetting(raw)
if err != nil {
return 0, nil, true, err
}
s.mu.Lock()
defer s.mu.Unlock()
if reply, found := s.cachedReplyLocked("costume-setting-save", seq); found {
return reply.code, reply.body, true, nil
}
if err := s.validateCostumeSettingLocked(setting); err != nil {
return 0, nil, true, err
}
payload, err := json.Marshal(setting)
if err != nil {
return 0, nil, true, err
}
change := stateio.EntryMutation{Bucket: "costume_settings", Key: strconv.FormatUint(setting.CharacterIndex, 10), Payload: payload}
core, err := s.corePayloadLocked()
if err != nil {
return 0, nil, true, err
}
if err := s.storage.SaveWithEntries("deck", core, []stateio.EntryMutation{change}); err != nil {
return 0, nil, true, err
}
s.costumeSettings[setting.CharacterIndex] = setting
s.rememberReplyLocked("costume-setting-save", seq, 398, nil)
return 398, nil, true, nil
}