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.
246 lines
7.6 KiB
Go
246 lines
7.6 KiB
Go
package world
|
|
|
|
import (
|
|
"bd2server/internal/server/gamedata"
|
|
"bd2server/internal/server/player"
|
|
"bd2server/internal/server/wire"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
func (s *Service) WithFieldObjects(designs map[int]gamedata.FieldObjectDesign) *Service {
|
|
s.fieldObjects = designs
|
|
return s
|
|
}
|
|
func (s *Service) AttachFieldObjectRuntime(root, version string) error {
|
|
schedule, err := gamedata.LoadFieldResetSchedule(root, version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.fieldReset = schedule
|
|
s.fieldObjectLoader = func(pack int) (gamedata.FieldObjectDesign, error) {
|
|
return gamedata.LoadFieldObjects(root, version, pack)
|
|
}
|
|
s.fieldObjects = map[int]gamedata.FieldObjectDesign{}
|
|
return nil
|
|
}
|
|
func (s *Service) fieldObjectDesign(pack int) (gamedata.FieldObjectDesign, error) {
|
|
if design, ok := s.fieldObjects[pack]; ok {
|
|
return design, nil
|
|
}
|
|
if s.fieldObjectLoader == nil {
|
|
return gamedata.FieldObjectDesign{}, fmt.Errorf("world: field object design unavailable")
|
|
}
|
|
if _, story := s.packs[pack]; !story {
|
|
if _, field := s.fieldPacks[pack]; !field {
|
|
return gamedata.FieldObjectDesign{}, fmt.Errorf("%w: unknown field pack", ErrInvalidRequest)
|
|
}
|
|
}
|
|
design, err := s.fieldObjectLoader(pack)
|
|
if err != nil {
|
|
return design, err
|
|
}
|
|
s.fieldObjects[pack] = design
|
|
return design, nil
|
|
}
|
|
func (s *Service) openedFieldObjects(pack int) ([]int, error) {
|
|
ids, err := s.state.OpenedFieldRewards(pack)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ids) == 0 {
|
|
return []int{}, nil
|
|
}
|
|
design, err := s.fieldObjectDesign(pack)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var active []int
|
|
for _, id := range ids {
|
|
obj, ok := design.Objects[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("world: saved field object absent from design")
|
|
}
|
|
period, e := s.fieldObjectPeriod(obj)
|
|
if e != nil {
|
|
continue
|
|
}
|
|
opened, e := s.state.FieldRewardOpened(pack, id, period)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if opened {
|
|
active = append(active, id)
|
|
}
|
|
}
|
|
return active, nil
|
|
}
|
|
func (s *Service) WithFieldResetSchedule(schedule gamedata.FieldResetSchedule) *Service {
|
|
s.fieldReset = schedule
|
|
return s
|
|
}
|
|
func (s *Service) fieldObjectPeriod(obj gamedata.FieldRewardObject) (string, error) {
|
|
return s.fieldReset.Period(obj.ResetType, time.Now())
|
|
}
|
|
func (s *Service) handleFieldObjectInfo(request []byte) (int, []byte, bool, error) {
|
|
pack, err := requestPack(request)
|
|
if err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
if !s.packUnlocked(pack) {
|
|
return 0, nil, true, fmt.Errorf("%w: unavailable field pack", ErrInvalidRequest)
|
|
}
|
|
ids, err := s.openedFieldObjects(pack)
|
|
if err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
var response []byte
|
|
for _, id := range ids {
|
|
response = wire.AppendBytes(response, 1, wire.AppendVarint(nil, 1, uint64(id)))
|
|
}
|
|
return 28, response, true, nil
|
|
}
|
|
func (s *Service) handleFieldObjectReward(request []byte) (int, []byte, bool, error) {
|
|
pack, err := requestPack(request)
|
|
if err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
group, _, err := wire.Varint(request, 3)
|
|
if err != nil || group == 0 || group > uint64(^uint32(0)>>1) {
|
|
return 0, nil, true, ErrInvalidRequest
|
|
}
|
|
id, _, err := wire.Varint(request, 4)
|
|
if err != nil || id == 0 || id > uint64(^uint32(0)>>1) {
|
|
return 0, nil, true, ErrInvalidRequest
|
|
}
|
|
bundle, err := s.openFieldObject(pack, int(group), int(id))
|
|
if err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
return 29, wire.AppendBytes(nil, 1, bundle), true, nil
|
|
}
|
|
func (s *Service) openFieldObject(pack, group, id int) ([]byte, error) {
|
|
design, err := s.fieldObjectDesign(pack)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
obj, exists := design.Objects[id]
|
|
if !exists || obj.GroupID != group || !s.packUnlocked(pack) || s.state.ActivePackID() != pack {
|
|
return nil, fmt.Errorf("%w: unavailable field object", ErrInvalidRequest)
|
|
}
|
|
if position, ok := s.state.Position(); ok && (position.PackID != pack || position.Position.MapID != obj.MapID) {
|
|
return nil, fmt.Errorf("%w: field object outside current map", ErrInvalidRequest)
|
|
}
|
|
if obj.BuffID != 0 || obj.MonsterID != 0 || obj.QuestID != 0 || len(obj.Rewards) == 0 {
|
|
return nil, fmt.Errorf("%w: unsupported field object reward graph/reset", ErrInvalidRequest)
|
|
}
|
|
|
|
period, err := s.fieldObjectPeriod(obj)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opened, err := s.state.FieldRewardOpened(pack, id, period)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if opened {
|
|
return []byte{}, nil
|
|
}
|
|
if s.wallet == nil || s.inventory == nil {
|
|
return nil, fmt.Errorf("world: field reward stores unavailable")
|
|
}
|
|
var rewards []gamedata.Reward
|
|
var itemRewards []gamedata.BattleReward
|
|
var equipmentRewards []player.Equipment
|
|
// Validate every branch's type and quantity before drawing or writing a
|
|
// receipt. LoadFieldObjects validates every equipment option tree, including
|
|
// branches with zero weight, before installing the catalog.
|
|
for _, r := range obj.Rewards {
|
|
if r.Count == 0 || r.Count > uint64(^uint32(0)>>1) {
|
|
return nil, fmt.Errorf("world: invalid field reward count")
|
|
}
|
|
switch r.Type {
|
|
case 2, 3, 4, 12, 20:
|
|
case 5, 7, 8, 9, 13, 14, 17, 19, 27, 29:
|
|
if r.ID == 0 {
|
|
return nil, fmt.Errorf("world: invalid field item")
|
|
}
|
|
case 10:
|
|
if r.ID == 0 || r.Count > 100 || s.equipment == nil || design.Equipment == nil {
|
|
return nil, fmt.Errorf("world: invalid field equipment")
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("%w: unsupported field reward type %d", ErrInvalidRequest, r.Type)
|
|
}
|
|
}
|
|
selected, err := obj.Draw()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, r := range selected {
|
|
if r.Count == 0 {
|
|
return nil, fmt.Errorf("world: empty field reward")
|
|
}
|
|
switch r.Type {
|
|
case 2, 3, 4, 12, 20:
|
|
rewards = append(rewards, gamedata.Reward{Type: r.Type, ID: r.ID, Count: r.Count})
|
|
case 5, 7, 8, 9, 13, 14, 17, 19, 27, 29:
|
|
if r.ID == 0 {
|
|
return nil, fmt.Errorf("world: invalid field item")
|
|
}
|
|
itemRewards = append(itemRewards, r)
|
|
case 10:
|
|
if s.equipment == nil || design.Equipment == nil || r.Count > 100 || len(equipmentRewards)+int(r.Count) > 100 {
|
|
return nil, fmt.Errorf("world: field equipment reward unavailable")
|
|
}
|
|
for n := uint64(0); n < r.Count; n++ {
|
|
main, sub, private, e := design.Equipment.RollOptions(r.ID)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
entry := player.Equipment{ID: r.ID, Rank: []uint64{0, 0, 0}}
|
|
for _, option := range main {
|
|
entry.MainOption = append(entry.MainOption, player.EquipmentOption{GroupID: option.GroupID, ID: option.ID})
|
|
}
|
|
for _, option := range sub {
|
|
entry.SubOption = append(entry.SubOption, player.EquipmentOption{GroupID: option.GroupID, ID: option.ID})
|
|
}
|
|
if private != nil {
|
|
entry.PrivateOption = &player.EquipmentOption{GroupID: private.GroupID, ID: private.ID}
|
|
}
|
|
equipmentRewards = append(equipmentRewards, entry)
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("%w: unsupported field reward type %d", ErrInvalidRequest, r.Type)
|
|
}
|
|
}
|
|
identity := fmt.Sprintf("field-reward:%d:%d:%s", pack, id, period)
|
|
if _, err = s.wallet.GrantQuestOnce(identity, rewards); err != nil {
|
|
return nil, err
|
|
}
|
|
items, err := s.inventory.GrantOnce(identity, itemRewards)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i, entry := range equipmentRewards {
|
|
equipmentRewards[i], err = s.equipment.GrantGeneratedOnce(fmt.Sprintf("%s:equipment:%d", identity, i), entry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err = s.state.MarkFieldRewardOpened(pack, id, period); err != nil {
|
|
return nil, err
|
|
}
|
|
var bundle []byte
|
|
for _, r := range rewards {
|
|
bundle = wire.AppendBytes(bundle, 1, player.ItemWire(player.Item{ID: r.ID, Type: r.Type, Count: r.Count}))
|
|
}
|
|
for _, item := range items {
|
|
bundle = wire.AppendBytes(bundle, 1, player.ItemWire(item))
|
|
}
|
|
for _, entry := range equipmentRewards {
|
|
bundle = wire.AppendBytes(bundle, 4, player.EquipmentWire(entry))
|
|
}
|
|
return bundle, nil
|
|
}
|