Files
bd2/go/internal/server/gacha/first_gacha.go
T

171 lines
5.9 KiB
Go

package gacha
import (
"errors"
"fmt"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/player"
"bd2server/internal/server/wire"
)
const firstGachaResponseDigest = "first-gacha-confirm-v1"
type firstGachaPreview struct {
sequence uint64
rewards []gamedata.FirstGachaReward
equipment map[int]player.Equipment
response []byte
}
func (s *Service) previewFirstGacha(request []byte) (int, []byte, bool, error) {
if s.first == nil {
return 175, nil, true, errors.New("gacha: first gacha is not configured")
}
if s.FirstGachaCompleted() {
return 175, nil, true, errors.New("gacha: first gacha already completed")
}
for _, field := range []int{3, 4} {
value, _, err := wire.Varint(request, field)
if err != nil || value != 0 {
return 175, nil, true, errors.New("gacha: first gacha preview cannot use a cash product")
}
}
seq, found, err := wire.Varint(request, 1)
if err != nil || !found || seq == 0 {
return 175, nil, true, errors.New("gacha: invalid first gacha preview sequence")
}
session := s.loginIdentity()
s.firstMu.Lock()
defer s.firstMu.Unlock()
if previous, found := s.firstPreviews[session]; found && previous.sequence == seq {
return 175, append([]byte(nil), previous.response...), true, nil
}
rewards, err := s.first.Roll()
if err != nil {
return 175, nil, true, err
}
preview := firstGachaPreview{sequence: seq, rewards: append([]gamedata.FirstGachaReward(nil), rewards...), equipment: make(map[int]player.Equipment)}
var bundle []byte
for sortID, reward := range rewards {
switch reward.Type {
case 11:
costume := wire.AppendVarint(nil, 2, reward.ID)
if sortID != 0 {
costume = wire.AppendVarint(costume, 6, uint64(sortID))
}
bundle = wire.AppendBytes(bundle, 3, costume)
case 10:
entry, err := firstGachaEquipment(s.first.EquipmentCatalog(), reward.ID, uint64(sortID))
if err != nil {
return 175, nil, true, err
}
preview.equipment[sortID] = entry
bundle = wire.AppendBytes(bundle, 4, player.EquipmentWire(entry))
default:
return 175, nil, true, fmt.Errorf("gacha: unsupported first reward type %d", reward.Type)
}
}
preview.response = wire.AppendBytes(nil, 1, bundle)
s.firstPreviews[session] = preview
if s.onPreview != nil {
if err := s.onPreview(); err != nil {
delete(s.firstPreviews, session)
return 175, nil, true, fmt.Errorf("gacha: update first preview mission: %w", err)
}
}
return 175, append([]byte(nil), preview.response...), true, nil
}
func firstGachaEquipment(catalog *gamedata.EquipmentGachaCatalog, id, sortID uint64) (player.Equipment, error) {
if catalog == nil {
return player.Equipment{}, errors.New("gacha: first gacha equipment catalog missing")
}
main, sub, private, err := catalog.RollOptions(id)
if err != nil {
return player.Equipment{}, err
}
entry := player.Equipment{ID: id, SortID: sortID, 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}
}
return entry, nil
}
func (s *Service) confirmFirstGacha(_ []byte, seq, buyType uint64, tickets []player.Item, explicitIdentity string) (int, []byte, bool, error) {
if s.first == nil || s.equipmentInventory == nil {
return 146, nil, true, errors.New("gacha: first gacha runtime is not attached")
}
if buyType != 1 || len(tickets) != 0 {
return 146, nil, true, errors.New("gacha: first gacha requires normal free confirmation")
}
identity := explicitIdentity
if identity == "" {
identity = s.requestIdentity(s.first.GachaID, seq)
}
responseIdentity := identity + ":response"
if response, found, err := s.collection.GachaBatchResponse(responseIdentity, firstGachaResponseDigest); found || err != nil {
return 146, response, true, err
}
if s.FirstGachaCompleted() {
return 146, nil, true, errors.New("gacha: first gacha already completed")
}
session := s.loginIdentity()
s.firstMu.Lock()
preview, found := s.firstPreviews[session]
s.firstMu.Unlock()
if !found || len(preview.rewards) != s.first.Count {
return 146, nil, true, errors.New("gacha: first gacha confirmation requires a preview")
}
var costumeIDs, costumeSortIDs []uint64
var equipment []player.Equipment
for sortID, reward := range preview.rewards {
switch reward.Type {
case 11:
costumeIDs = append(costumeIDs, reward.ID)
costumeSortIDs = append(costumeSortIDs, uint64(sortID))
case 10:
candidate, ok := preview.equipment[sortID]
if !ok || candidate.ID != reward.ID {
return 146, nil, true, fmt.Errorf("gacha: first preview missing equipment slot %d", sortID)
}
saved, err := s.equipmentInventory.GrantGeneratedOnce(fmt.Sprintf("%s:first-equip:%d", identity, sortID), candidate)
if err != nil {
return 146, nil, true, err
}
equipment = append(equipment, saved)
default:
return 146, nil, true, fmt.Errorf("gacha: unsupported first reward type %d", reward.Type)
}
}
grant, err := s.collection.GrantRegularPurchase(identity, costumeIDs, s.first.CostumeCatalog(), player.GachaPurchase{
Group: s.first.Group, BuyType: 1, RewardCount: uint64(s.first.Count), RewardSortIDs: costumeSortIDs,
})
if err != nil {
return 146, nil, true, err
}
if err := s.creditOverflow(identity, grant); err != nil {
return 146, nil, true, err
}
bundle := s.rewardBundle(grant)
for _, entry := range equipment {
bundle = wire.AppendBytes(bundle, 4, player.EquipmentWire(entry))
}
response := wire.AppendBytes(nil, 1, bundle)
if err := s.collection.RecordGachaBatch(responseIdentity, firstGachaResponseDigest, response); err != nil {
return 146, nil, true, err
}
if s.onPreview != nil {
if err := s.onPreview(); err != nil {
return 146, nil, true, fmt.Errorf("gacha: update first gacha mission: %w", err)
}
}
return 146, response, true, nil
}