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

243 lines
7.9 KiB
Go

package player
import (
"errors"
"fmt"
"sort"
"bd2server/internal/server/wire"
)
// PresetEquipmentBinding is one character's complete five-slot equipment
// target used by the ordinary party preset service.
type PresetEquipmentBinding struct {
CharacterIndex uint64
Equipment []uint64
}
// ValidatePresetEquipment checks the complete fixed-width target without
// mutating ownership. It lets a cross-domain PresetUse fail before any
// character or deck state has been changed.
func (s *EquipmentInventory) ValidatePresetEquipment(bindings []PresetEquipmentBinding) error {
if len(bindings) == 0 {
return errors.New("player: empty preset equipment bindings")
}
seenCharacters := make(map[uint64]bool, len(bindings))
for _, binding := range bindings {
if binding.CharacterIndex == 0 || seenCharacters[binding.CharacterIndex] || len(binding.Equipment) != equipmentSlotCount {
return errors.New("player: invalid preset equipment binding")
}
seenCharacters[binding.CharacterIndex] = true
if s.characters == nil {
return errors.New("player: preset character store unavailable")
}
if _, found := s.characters.Find(binding.CharacterIndex); !found {
return fmt.Errorf("player: preset references unknown character %d", binding.CharacterIndex)
}
}
// CharacterStore.Find may calculate maximum HP, and the production stat
// calculator reads equipped items. It must run before taking this mutex or
// preset validation deadlocks by trying to re-enter EquipmentInventory.
s.mu.Lock()
defer s.mu.Unlock()
if len(s.slots) == 0 {
return errors.New("player: preset equipment slot design unavailable")
}
seenEquipment := make(map[uint64]bool)
for _, binding := range bindings {
for slot, index := range binding.Equipment {
if index == 0 {
continue
}
if seenEquipment[index] {
return fmt.Errorf("player: preset repeats equipment %d", index)
}
seenEquipment[index] = true
position := s.equipmentPositionLocked(index)
if position < 0 {
return fmt.Errorf("player: preset references unknown equipment %d", index)
}
item := s.owned.Equipment[position]
if designedSlot, found := s.slots[item.ID]; !found || designedSlot != uint64(slot) {
return fmt.Errorf("player: preset equipment %d does not belong in slot %d", index, slot)
}
}
}
return nil
}
// ApplyPresetEquipment applies the same authoritative state transition as
// /EquipBatchUse without fabricating a second wire protocol implementation.
func (s *EquipmentInventory) ApplyPresetEquipment(bindings []PresetEquipmentBinding) ([]Character, error) {
if err := s.ValidatePresetEquipment(bindings); err != nil {
return nil, err
}
// PresetUse is a server-side atomic operation. Unlike the client-orchestrated
// EquipPreset UI, it must also clear any previous owner of an equipment
// instance in the same batch.
s.mu.Lock()
requested := make(map[uint64]bool, len(bindings))
desired := make(map[uint64]bool)
for _, binding := range bindings {
requested[binding.CharacterIndex] = true
for _, index := range binding.Equipment {
desired[index] = index != 0
}
}
additional := make(map[uint64][]uint64)
for _, item := range s.owned.Equipment {
if item.UseChar == 0 || requested[item.UseChar] || !desired[item.InvenIndex] {
continue
}
if additional[item.UseChar] == nil {
additional[item.UseChar] = make([]uint64, equipmentSlotCount)
for _, equipped := range s.owned.Equipment {
if equipped.UseChar == item.UseChar {
if slot, ok := s.slots[equipped.ID]; ok && slot < equipmentSlotCount {
additional[item.UseChar][slot] = equipped.InvenIndex
}
}
}
}
if slot, ok := s.slots[item.ID]; ok && slot < equipmentSlotCount {
additional[item.UseChar][slot] = 0
}
}
s.mu.Unlock()
for character, equipment := range additional {
bindings = append(bindings, PresetEquipmentBinding{CharacterIndex: character, Equipment: equipment})
}
sort.Slice(bindings, func(i, j int) bool { return bindings[i].CharacterIndex < bindings[j].CharacterIndex })
request := wire.AppendVarint(nil, 1, 1)
for _, binding := range bindings {
entry := wire.AppendVarint(nil, 1, binding.CharacterIndex)
for _, index := range binding.Equipment {
entry = wire.AppendVarint(entry, 2, index)
}
request = wire.AppendBytes(request, 2, entry)
}
_, response, handled, err := s.batchUse(request)
if err != nil {
return nil, err
}
if !handled {
return nil, errors.New("player: preset equipment batch was not handled")
}
var result []Character
if err := wire.Walk(response, func(field wire.Field) error {
if field.Number != 1 || field.Type != 2 {
return nil
}
index, found, err := wire.Varint(field.Value, 1)
if err != nil || !found || index == 0 {
return errors.New("player: malformed preset equipment character response")
}
character, found := s.characters.Find(index)
if !found {
return fmt.Errorf("player: preset equipment character %d disappeared", index)
}
result = append(result, character)
return nil
}); err != nil {
return nil, err
}
return result, nil
}
// ApplyPresetCostumes changes the currently selected costume for each listed
// character and persists both seeded and collection-owned character records.
func (s *CharacterStore) ApplyPresetCostumes(assignments map[uint64]uint64) ([]Character, error) {
if len(assignments) == 0 {
return nil, errors.New("player: empty preset costume assignments")
}
s.mu.Lock()
base := append([]Character(nil), s.characters...)
collection := s.collection
s.mu.Unlock()
if collection == nil {
return nil, errors.New("player: preset costumes require collection store")
}
collectionCharacters := collection.Characters()
seenCostumes := make(map[uint64]bool)
updated := make(map[uint64]Character, len(assignments))
for characterIndex, costumeIndex := range assignments {
var current Character
found := false
for _, character := range base {
if character.InvenIndex == characterIndex {
current, found = character, true
break
}
}
if !found {
for _, character := range collectionCharacters {
if character.InvenIndex == characterIndex {
current, found = character, true
break
}
}
}
if !found {
return nil, fmt.Errorf("player: preset references unknown character %d", characterIndex)
}
if costumeIndex == 0 {
current.CostumeID = 0
current.UseCostume = 0
} else {
if seenCostumes[costumeIndex] {
return nil, fmt.Errorf("player: preset repeats costume %d", costumeIndex)
}
costume, found := collection.CostumeByIndex(costumeIndex)
if !found {
return nil, fmt.Errorf("player: preset references unknown costume %d", costumeIndex)
}
seenCostumes[costumeIndex] = true
current.CostumeID = costume.ID
current.UseCostume = costumeIndex
}
updated[characterIndex] = current
}
nextBase := append([]Character(nil), base...)
baseChanged := false
for i := range nextBase {
if character, ok := updated[nextBase[i].InvenIndex]; ok {
nextBase[i] = character
delete(updated, character.InvenIndex)
baseChanged = true
}
}
if baseChanged {
s.mu.Lock()
if err := s.persist(nextBase); err != nil {
s.mu.Unlock()
return nil, err
}
s.characters = nextBase
s.mu.Unlock()
}
for index, character := range updated {
old, found := collection.FindCharacter(index)
if !found {
return nil, fmt.Errorf("player: preset collection character %d disappeared", index)
}
if err := collection.UpdateCharacter(old.ID, character); err != nil {
return nil, err
}
}
indices := make([]uint64, 0, len(assignments))
for index := range assignments {
indices = append(indices, index)
}
sort.Slice(indices, func(i, j int) bool { return indices[i] < indices[j] })
result := make([]Character, 0, len(indices))
for _, index := range indices {
character, found := s.Find(index)
if !found {
return nil, fmt.Errorf("player: preset character %d disappeared after update", index)
}
result = append(result, character)
}
return result, nil
}