454 lines
15 KiB
Go
454 lines
15 KiB
Go
package player
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"bd2server/internal/server/stateio"
|
|
"bd2server/internal/server/wire"
|
|
)
|
|
|
|
const equipmentSlotCount = 5
|
|
|
|
type equipmentBatchUse struct {
|
|
CharacterIndex uint64
|
|
Equipment []uint64
|
|
}
|
|
|
|
type equipmentPresetKey struct {
|
|
CharacterIndex uint64
|
|
Slot uint64
|
|
}
|
|
|
|
type equipmentPresetItem struct {
|
|
Type uint64 `json:"type"`
|
|
EquipmentIndex uint64 `json:"equipment_index"`
|
|
}
|
|
|
|
type equipmentPreset struct {
|
|
CharacterIndex uint64 `json:"character_index"`
|
|
Slot uint64 `json:"slot"`
|
|
Name string `json:"name"`
|
|
ResourceID uint64 `json:"resource_id"`
|
|
ResourceColor uint64 `json:"resource_color"`
|
|
Items []equipmentPresetItem `json:"items"`
|
|
}
|
|
|
|
func (s *EquipmentInventory) batchUse(request []byte) (int, []byte, bool, error) {
|
|
entries, err := decodeEquipmentBatchUse(request)
|
|
if err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
if s.characters == nil {
|
|
return 0, nil, true, errors.New("player: EquipBatchUse character store unavailable")
|
|
}
|
|
requestedCharacters := make(map[uint64]bool, len(entries))
|
|
for _, entry := range entries {
|
|
if requestedCharacters[entry.CharacterIndex] {
|
|
return 0, nil, true, fmt.Errorf("player: EquipBatchUse repeats character %d", entry.CharacterIndex)
|
|
}
|
|
requestedCharacters[entry.CharacterIndex] = true
|
|
if _, found := s.characters.Find(entry.CharacterIndex); !found {
|
|
return 0, nil, true, fmt.Errorf("player: EquipBatchUse unknown character %d", entry.CharacterIndex)
|
|
}
|
|
}
|
|
|
|
s.mu.Lock()
|
|
if len(s.slots) == 0 {
|
|
s.mu.Unlock()
|
|
return 0, nil, true, errors.New("player: EquipBatchUse slot design unavailable")
|
|
}
|
|
desired := make(map[uint64]uint64)
|
|
for _, entry := range entries {
|
|
for slot, index := range entry.Equipment {
|
|
if index == 0 {
|
|
continue
|
|
}
|
|
if desired[index] != 0 {
|
|
s.mu.Unlock()
|
|
return 0, nil, true, fmt.Errorf("player: EquipBatchUse repeats equipment %d", index)
|
|
}
|
|
position := s.equipmentPositionLocked(index)
|
|
if position < 0 {
|
|
s.mu.Unlock()
|
|
return 0, nil, true, fmt.Errorf("player: EquipBatchUse unknown equipment %d", index)
|
|
}
|
|
item := s.owned.Equipment[position]
|
|
if designedSlot, ok := s.slots[item.ID]; !ok || designedSlot != uint64(slot) {
|
|
s.mu.Unlock()
|
|
return 0, nil, true, fmt.Errorf("player: EquipBatchUse equipment %d does not belong in slot %d", index, slot)
|
|
}
|
|
if item.UseChar != 0 && item.UseChar != entry.CharacterIndex && !requestedCharacters[item.UseChar] {
|
|
s.mu.Unlock()
|
|
return 0, nil, true, fmt.Errorf("player: EquipBatchUse equipment %d is still used by character %d", index, item.UseChar)
|
|
}
|
|
desired[index] = entry.CharacterIndex
|
|
}
|
|
}
|
|
|
|
next := cloneEquipmentSnapshot(s.owned)
|
|
affected := make(map[uint64]bool, len(entries))
|
|
for character := range requestedCharacters {
|
|
affected[character] = true
|
|
}
|
|
for i := range next.Equipment {
|
|
item := &next.Equipment[i]
|
|
if requestedCharacters[item.UseChar] {
|
|
item.UseChar = 0
|
|
}
|
|
}
|
|
for i := range next.Equipment {
|
|
item := &next.Equipment[i]
|
|
character := desired[item.InvenIndex]
|
|
if character == 0 {
|
|
continue
|
|
}
|
|
if item.UseChar != 0 && item.UseChar != character {
|
|
affected[item.UseChar] = true
|
|
}
|
|
item.UseChar = character
|
|
}
|
|
if err := s.commitLocked(next, "batch use"); err != nil {
|
|
s.mu.Unlock()
|
|
return 0, nil, true, err
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
characterIndices := make([]uint64, 0, len(affected))
|
|
for index := range affected {
|
|
characterIndices = append(characterIndices, index)
|
|
}
|
|
sort.Slice(characterIndices, func(i, j int) bool { return characterIndices[i] < characterIndices[j] })
|
|
var response []byte
|
|
for _, index := range characterIndices {
|
|
if character, found := s.characters.Find(index); found {
|
|
response = wire.AppendBytes(response, 1, CharacterWire(character))
|
|
}
|
|
}
|
|
return 276, response, true, nil
|
|
}
|
|
|
|
func decodeEquipmentBatchUse(request []byte) ([]equipmentBatchUse, error) {
|
|
var result []equipmentBatchUse
|
|
err := wire.Walk(request, func(field wire.Field) error {
|
|
if field.Number != 2 {
|
|
return nil
|
|
}
|
|
if field.Type != 2 {
|
|
return errors.New("player: EquipBatchUse invalid batch entry")
|
|
}
|
|
entry := equipmentBatchUse{}
|
|
var characterSeen bool
|
|
if err := wire.Walk(field.Value, func(nested wire.Field) error {
|
|
switch nested.Number {
|
|
case 1:
|
|
if characterSeen || nested.Type != 0 {
|
|
return errors.New("player: EquipBatchUse invalid character")
|
|
}
|
|
entry.CharacterIndex, _ = binary.Uvarint(nested.Value)
|
|
characterSeen = true
|
|
case 2:
|
|
values, err := decodeRepeatedUint64(nested)
|
|
if err != nil {
|
|
return errors.New("player: EquipBatchUse invalid equipment list")
|
|
}
|
|
entry.Equipment = append(entry.Equipment, values...)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if !characterSeen || entry.CharacterIndex == 0 || len(entry.Equipment) != equipmentSlotCount {
|
|
return errors.New("player: EquipBatchUse requires one character and five equipment slots")
|
|
}
|
|
result = append(result, entry)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(result) == 0 {
|
|
return nil, errors.New("player: EquipBatchUse has no batch entries")
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func decodeRepeatedUint64(field wire.Field) ([]uint64, error) {
|
|
switch field.Type {
|
|
case 0:
|
|
value, count := binary.Uvarint(field.Value)
|
|
if count <= 0 {
|
|
return nil, wire.ErrMalformed
|
|
}
|
|
return []uint64{value}, nil
|
|
case 2:
|
|
var result []uint64
|
|
for offset := 0; offset < len(field.Value); {
|
|
value, count := binary.Uvarint(field.Value[offset:])
|
|
if count <= 0 {
|
|
return nil, wire.ErrMalformed
|
|
}
|
|
result = append(result, value)
|
|
offset += count
|
|
}
|
|
return result, nil
|
|
default:
|
|
return nil, wire.ErrMalformed
|
|
}
|
|
}
|
|
|
|
func (s *EquipmentInventory) loadEquipmentPresets(entries stateio.EntryStore) error {
|
|
raw, err := entries.ListEntries("equipment", "presets")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for key, payload := range raw {
|
|
parsed, err := parseEquipmentPresetKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := stateio.RequireExactJSONObject(payload, "character_index", "slot", "name", "resource_id", "resource_color", "items"); err != nil {
|
|
return fmt.Errorf("player: incompatible equipment preset %q: %w", key, err)
|
|
}
|
|
var preset equipmentPreset
|
|
if err := json.Unmarshal(payload, &preset); err != nil || preset.CharacterIndex != parsed.CharacterIndex || preset.Slot != parsed.Slot {
|
|
return fmt.Errorf("player: invalid equipment preset %q", key)
|
|
}
|
|
if err := validateEquipmentPresetShape(preset); err != nil {
|
|
return fmt.Errorf("player: invalid equipment preset %q: %w", key, err)
|
|
}
|
|
s.presets[parsed] = preset
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseEquipmentPresetKey(value string) (equipmentPresetKey, error) {
|
|
left, right, found := strings.Cut(value, ":")
|
|
if !found {
|
|
return equipmentPresetKey{}, fmt.Errorf("player: invalid equipment preset key %q", value)
|
|
}
|
|
character, characterErr := strconv.ParseUint(left, 10, 64)
|
|
slot, slotErr := strconv.ParseUint(right, 10, 64)
|
|
if characterErr != nil || slotErr != nil || character == 0 || slot < 1 || slot > 5 || value != equipmentPresetStorageKey(equipmentPresetKey{CharacterIndex: character, Slot: slot}) {
|
|
return equipmentPresetKey{}, fmt.Errorf("player: invalid equipment preset key %q", value)
|
|
}
|
|
return equipmentPresetKey{CharacterIndex: character, Slot: slot}, nil
|
|
}
|
|
|
|
func equipmentPresetStorageKey(key equipmentPresetKey) string {
|
|
return strconv.FormatUint(key.CharacterIndex, 10) + ":" + strconv.FormatUint(key.Slot, 10)
|
|
}
|
|
|
|
func validateEquipmentPresetShape(preset equipmentPreset) error {
|
|
if preset.CharacterIndex == 0 || preset.Slot < 1 || preset.Slot > 5 || !utf8.ValidString(preset.Name) ||
|
|
utf8.RuneCountInString(preset.Name) < 1 || utf8.RuneCountInString(preset.Name) > 16 ||
|
|
preset.ResourceID < 1 || preset.ResourceID > 21 || preset.ResourceColor > 5 || len(preset.Items) != equipmentSlotCount {
|
|
return errors.New("invalid fields")
|
|
}
|
|
seen := make(map[uint64]bool, equipmentSlotCount)
|
|
for _, item := range preset.Items {
|
|
if item.Type >= equipmentSlotCount || seen[item.Type] {
|
|
return errors.New("invalid equipment slots")
|
|
}
|
|
seen[item.Type] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *EquipmentInventory) presetInfo() (int, []byte, bool, error) {
|
|
s.mu.Lock()
|
|
presets := make([]equipmentPreset, 0, len(s.presets))
|
|
for _, preset := range s.presets {
|
|
presets = append(presets, cloneEquipmentPreset(preset))
|
|
}
|
|
s.mu.Unlock()
|
|
sort.Slice(presets, func(i, j int) bool {
|
|
if presets[i].CharacterIndex != presets[j].CharacterIndex {
|
|
return presets[i].CharacterIndex < presets[j].CharacterIndex
|
|
}
|
|
return presets[i].Slot < presets[j].Slot
|
|
})
|
|
var response []byte
|
|
for i := 0; i < len(presets); {
|
|
character := presets[i].CharacterIndex
|
|
var encoded []byte
|
|
encoded = wire.AppendVarint(encoded, 1, character)
|
|
for i < len(presets) && presets[i].CharacterIndex == character {
|
|
encoded = wire.AppendBytes(encoded, 2, equipmentPresetWire(presets[i]))
|
|
i++
|
|
}
|
|
response = wire.AppendBytes(response, 1, encoded)
|
|
}
|
|
return 253, response, true, nil
|
|
}
|
|
|
|
func equipmentPresetWire(preset equipmentPreset) []byte {
|
|
var out []byte
|
|
if preset.Name != "" {
|
|
out = wire.AppendString(out, 1, preset.Name)
|
|
}
|
|
out = wire.AppendVarint(out, 2, preset.Slot)
|
|
if preset.ResourceID != 0 {
|
|
out = wire.AppendVarint(out, 3, preset.ResourceID)
|
|
}
|
|
if preset.ResourceColor != 0 {
|
|
out = wire.AppendVarint(out, 4, preset.ResourceColor)
|
|
}
|
|
for _, item := range preset.Items {
|
|
var encoded []byte
|
|
if item.Type != 0 {
|
|
encoded = wire.AppendVarint(encoded, 1, item.Type)
|
|
}
|
|
if item.EquipmentIndex != 0 {
|
|
encoded = wire.AppendVarint(encoded, 2, item.EquipmentIndex)
|
|
}
|
|
out = wire.AppendBytes(out, 5, encoded)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *EquipmentInventory) presetSave(request []byte) (int, []byte, bool, error) {
|
|
preset, err := decodeEquipmentPresetRequest(request, true)
|
|
if err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
if s.characters == nil {
|
|
return 0, nil, true, errors.New("player: EquipPresetSave character store unavailable")
|
|
}
|
|
if _, found := s.characters.Find(preset.CharacterIndex); !found {
|
|
return 0, nil, true, fmt.Errorf("player: EquipPresetSave unknown character %d", preset.CharacterIndex)
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if len(s.slots) == 0 {
|
|
return 0, nil, true, errors.New("player: EquipPresetSave slot design unavailable")
|
|
}
|
|
for _, item := range preset.Items {
|
|
if item.EquipmentIndex == 0 {
|
|
continue
|
|
}
|
|
position := s.equipmentPositionLocked(item.EquipmentIndex)
|
|
if position < 0 {
|
|
return 0, nil, true, fmt.Errorf("player: EquipPresetSave unknown equipment %d", item.EquipmentIndex)
|
|
}
|
|
equipment := s.owned.Equipment[position]
|
|
if s.slots[equipment.ID] != item.Type {
|
|
return 0, nil, true, fmt.Errorf("player: EquipPresetSave equipment %d does not match character slot", item.EquipmentIndex)
|
|
}
|
|
}
|
|
if err := s.persistEquipmentPreset(preset, "save"); err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
s.presets[equipmentPresetKey{CharacterIndex: preset.CharacterIndex, Slot: preset.Slot}] = cloneEquipmentPreset(preset)
|
|
return 254, nil, true, nil
|
|
}
|
|
|
|
func (s *EquipmentInventory) presetNameChange(request []byte) (int, []byte, bool, error) {
|
|
metadata, err := decodeEquipmentPresetRequest(request, false)
|
|
if err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
key := equipmentPresetKey{CharacterIndex: metadata.CharacterIndex, Slot: metadata.Slot}
|
|
if s.characters == nil {
|
|
return 0, nil, true, errors.New("player: EquipPresetNameChange character store unavailable")
|
|
}
|
|
if _, found := s.characters.Find(metadata.CharacterIndex); !found {
|
|
return 0, nil, true, fmt.Errorf("player: EquipPresetNameChange unknown character %d", metadata.CharacterIndex)
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
preset, found := s.presets[key]
|
|
if !found {
|
|
preset = metadata
|
|
}
|
|
preset.Name = metadata.Name
|
|
preset.ResourceID = metadata.ResourceID
|
|
preset.ResourceColor = metadata.ResourceColor
|
|
if err := s.persistEquipmentPreset(preset, "name change"); err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
s.presets[key] = cloneEquipmentPreset(preset)
|
|
return 259, nil, true, nil
|
|
}
|
|
|
|
func decodeEquipmentPresetRequest(request []byte, withItems bool) (equipmentPreset, error) {
|
|
character, found, err := wire.Varint(request, 2)
|
|
if err != nil || !found || character == 0 {
|
|
return equipmentPreset{}, errors.New("player: equipment preset request missing character")
|
|
}
|
|
slot, found, err := wire.Varint(request, 3)
|
|
if err != nil || !found || slot < 1 || slot > 5 {
|
|
return equipmentPreset{}, errors.New("player: equipment preset request invalid slot")
|
|
}
|
|
nameBytes, _, err := wire.Bytes(request, 4)
|
|
if err != nil || !utf8.Valid(nameBytes) {
|
|
return equipmentPreset{}, errors.New("player: equipment preset request invalid name")
|
|
}
|
|
resourceID, _, err := wire.Varint(request, 5)
|
|
if err != nil || resourceID < 1 || resourceID > 21 {
|
|
return equipmentPreset{}, errors.New("player: equipment preset request invalid resource")
|
|
}
|
|
color, _, err := wire.Varint(request, 6)
|
|
if err != nil || color > 5 {
|
|
return equipmentPreset{}, errors.New("player: equipment preset request invalid color")
|
|
}
|
|
preset := equipmentPreset{CharacterIndex: character, Slot: slot, Name: string(nameBytes), ResourceID: resourceID, ResourceColor: color}
|
|
if withItems {
|
|
err = wire.Walk(request, func(field wire.Field) error {
|
|
if field.Number != 7 {
|
|
return nil
|
|
}
|
|
if field.Type != 2 {
|
|
return errors.New("player: EquipPresetSave invalid equipment entry")
|
|
}
|
|
typeID, _, err := wire.Varint(field.Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
index, _, err := wire.Varint(field.Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
preset.Items = append(preset.Items, equipmentPresetItem{Type: typeID, EquipmentIndex: index})
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return equipmentPreset{}, err
|
|
}
|
|
} else {
|
|
preset.Items = make([]equipmentPresetItem, equipmentSlotCount)
|
|
for i := range preset.Items {
|
|
preset.Items[i].Type = uint64(i)
|
|
}
|
|
}
|
|
if err := validateEquipmentPresetShape(preset); err != nil {
|
|
return equipmentPreset{}, fmt.Errorf("player: equipment preset request: %w", err)
|
|
}
|
|
sort.Slice(preset.Items, func(i, j int) bool { return preset.Items[i].Type < preset.Items[j].Type })
|
|
return preset, nil
|
|
}
|
|
|
|
func (s *EquipmentInventory) persistEquipmentPreset(preset equipmentPreset, operation string) error {
|
|
payload, err := json.Marshal(preset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
key := equipmentPresetKey{CharacterIndex: preset.CharacterIndex, Slot: preset.Slot}
|
|
change := stateio.EntryMutation{Bucket: "presets", Key: equipmentPresetStorageKey(key), Payload: payload}
|
|
if err := s.store.SaveWithEntries("equipment", nil, []stateio.EntryMutation{change}); err != nil {
|
|
return fmt.Errorf("player: persist equipment preset %s: %w", operation, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cloneEquipmentPreset(preset equipmentPreset) equipmentPreset {
|
|
preset.Items = append([]equipmentPresetItem(nil), preset.Items...)
|
|
return preset
|
|
}
|