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.
889 lines
27 KiB
Go
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
|
|
}
|