Files
bd2/go/internal/server/domain/battle/hunting/protocol_adapter.go
T

849 lines
24 KiB
Go

package hunting
import (
"bd2server/internal/server/design/gamedata"
"bd2server/internal/server/domain/command"
assets "bd2server/internal/server/domain/inventory"
"bd2server/internal/server/platform/versionconfig"
"bd2server/internal/server/protocol/wire"
"bd2server/internal/server/storage/stateio"
"bytes"
"crypto/rand"
"encoding/binary"
"encoding/json"
"fmt"
"maps"
"math"
"math/big"
"sort"
"strconv"
"strings"
"time"
)
func (s *Service) EnsureForPack(ctx command.Context, pack int) ([]byte, error) {
d, e := s.load(pack)
if e != nil {
return nil, e
}
if len(d.Grounds) == 0 {
return nil, nil
}
st := s.state.Packs[strconv.Itoa(pack)]
if st.Current == 0 {
st.Current = d.Grounds[0].ID
next := s.clone()
next.Packs[strconv.Itoa(pack)] = st
if e = s.persist(ctx, next); e != nil {
return nil, e
}
}
return s.info(pack, d)
}
func (s *Service) SnapshotForPack(ctx command.Context, pack int) ([]byte, error) {
d, e := s.load(pack)
if e != nil {
return nil, e
}
if len(d.Grounds) == 0 {
return nil, nil
}
return s.info(pack, d)
}
func (s *Service) handleGroundSession(ctx command.Context, path string, req []byte, session string) (int, []byte, bool, error) {
if path != "/HuntingGroundEnter" {
return s.handleGroundBase(ctx, path, req)
}
seq, found, e := wire.Varint(req, 1)
if e != nil || !found || seq == 0 {
return 110, nil, true, fmt.Errorf("hunting: missing sequence")
}
key := "enter:" + session + ":" + strconv.FormatUint(seq, 10)
rs, e := s.loadBattleReceipts(ctx)
if e != nil {
return 110, nil, true, e
}
fp := fmt.Sprintf("%x", req)
if r, ok := rs[key]; ok {
if r.Fingerprint != fp {
return 110, nil, true, fmt.Errorf("hunting: enter sequence conflict")
}
return 110, r.Bundle, true, nil
}
code, out, ok, e := s.handleGroundBase(ctx, path, req)
if e != nil || !ok {
return code, out, ok, e
}
rs[key] = battleReceipt{Fingerprint: fp, Bundle: out}
if e = s.saveBattleReceipts(ctx, rs); e != nil {
return code, nil, true, e
}
return code, out, ok, nil
}
// A catalog entry is not an account's active hunting run. The detail
// endpoint preserves an explicitly present empty message until entry.
func (s *Service) info(pack int, d *gamedata.HuntingPack) ([]byte, error) {
st := s.state.Packs[strconv.Itoa(pack)]
initial := st.Current == 0
if st.Current == 0 {
st.Current = d.Grounds[0].ID
}
g, ok := ground(d, st.Current)
if !ok {
return nil, fmt.Errorf("hunting: saved ground missing from GameData")
}
b := wire.AppendVarint(nil, 2, st.Current)
if st.Highest != 0 {
b = wire.AppendVarint(b, 3, st.Highest)
}
b = wire.AppendVarint(b, 5, uint64(pack))
if st.Auto {
b = wire.AppendVarint(b, 1, 1)
}
var monsterInfos [][]byte
if initial {
// The initial catalog lists ordinary monsters only. A boss is not an
// active account encounter simply because its design row exists.
for _, id := range g.Monsters {
monsterInfos = append(monsterInfos, monsterWire(d.Monsters[id], true))
}
} else {
monsterInfos = monstersWithState(d, g, st.Defeated)
}
for _, m := range monsterInfos {
b = wire.AppendBytes(b, 4, m)
}
return b, nil
}
func monsterWire(m gamedata.HuntingMonster, active bool) []byte {
b := wire.AppendVarint(nil, 1, m.ID)
b = wire.AppendVarint(b, 2, m.Decks[0])
if active {
b = wire.AppendVarint(b, 6, 1)
}
return b
}
func monstersWithState(d *gamedata.HuntingPack, g gamedata.HuntingGround, defeated []uint64) [][]byte {
dead := map[uint64]bool{}
for _, id := range defeated {
dead[id] = true
}
boss := len(g.Monsters) == 0
all := true
for _, id := range g.Monsters {
all = all && dead[id]
}
boss = boss || all
out := make([][]byte, 0, len(g.Monsters)+1)
for _, id := range g.Monsters {
out = append(out, monsterWire(d.Monsters[id], !dead[id]))
}
if boss {
out = append(out, monsterWire(d.Monsters[g.BossID], true))
}
return out
}
func (s *Service) CompleteBattle(ctx command.Context, pack int, mode, monster, deck uint64, receipt string) ([]byte, [][]byte, error) {
if mode != BattleMode {
return nil, nil, nil
}
if receipt == "" {
return nil, nil, fmt.Errorf("hunting: missing battle receipt")
}
fingerprint := fmt.Sprintf("%d:%d:%d:%d", pack, mode, monster, deck)
ledger, err := s.loadBattleReceipts(ctx)
if err != nil {
return nil, nil, err
}
if saved, ok := ledger[receipt]; ok {
if saved.Fingerprint != fingerprint {
return nil, nil, fmt.Errorf("hunting: battle receipt conflict")
}
return saved.Bundle, saved.Monsters, nil
}
d, g, m, err := s.validate(pack, monster, deck)
if err != nil {
return nil, nil, err
}
rewards := m.Rewards[deck]
identity := "hunting:" + receipt
currency := make([]gamedata.Reward, 0)
stack := make([]gamedata.BattleReward, 0)
for _, r := range rewards {
if r.Type == 2 || r.Type == 3 || r.Type == 4 || r.Type == 12 || r.Type == 20 {
currency = append(currency, gamedata.Reward{Type: r.Type, ID: r.ID, Count: r.Count}) //nolint:staticcheck // S1016
} else {
stack = append(stack, r)
}
}
var grantedBundle []byte
var items []assets.Item
if s.grant != nil {
rs := make([]gamedata.Reward, len(rewards))
for i, r := range rewards {
rs[i] = gamedata.Reward{Type: r.Type, ID: r.ID, Count: r.Count} //nolint:staticcheck // S1016
}
grantedBundle, err = s.grant(ctx, identity, rs)
if err != nil {
return nil, nil, err
}
} else {
if _, err := s.wallet.GrantQuestOnce(ctx, identity+":currency", currency); err != nil {
return nil, nil, err
}
items, err = s.inventory.GrantOnce(ctx, identity+":items", stack)
if err != nil {
return nil, nil, err
}
if len(items) == 0 {
items = s.inventory.GrantedItems(identity + ":items")
}
}
next := s.clone()
cost := d.NormalAP
if m.Type == 1 {
cost = d.BossAP
}
if next.Free >= cost {
next.Free -= cost
} else {
next.Bonus -= cost - next.Free
next.Free = 0
}
st := next.Packs[strconv.Itoa(pack)]
if monster == g.BossID {
if st.Highest < g.ID {
st.Highest = g.ID
}
st.Defeated = nil
} else {
st.Defeated = append(append([]uint64(nil), st.Defeated...), monster)
}
next.Packs[strconv.Itoa(pack)] = st
next.Receipts[receipt] = true
var bundle []byte
if s.grant != nil {
bundle = grantedBundle
}
for _, item := range items {
bundle = wire.AppendBytes(bundle, 1, assets.ItemWire(item))
view := wire.AppendVarint(nil, 2, item.ID)
view = wire.AppendVarint(view, 3, item.Type)
view = wire.AppendVarint(view, 4, item.Count)
bundle = wire.AppendBytes(bundle, 6, view)
}
for _, r := range currency {
if s.grant != nil {
break
}
b := wire.AppendVarint(nil, 3, r.Type)
b = wire.AppendVarint(b, 4, r.Count)
bundle = wire.AppendBytes(bundle, 1, b)
}
updates := [][]byte{monsterWire(m, false)}
if monster == g.BossID {
for _, id := range g.Monsters {
updates = append(updates, monsterWire(d.Monsters[id], true))
}
} else if len(st.Defeated) == len(g.Monsters) {
updates = append(updates, monsterWire(d.Monsters[g.BossID], true))
}
if err := s.persist(ctx, next); err != nil {
return nil, nil, err
}
latest, err := s.loadBattleReceipts(ctx)
if err != nil {
return nil, nil, err
}
maps.Copy(ledger, latest)
ledger[receipt] = battleReceipt{Fingerprint: fingerprint, Bundle: bundle, Monsters: updates}
if err := s.saveBattleReceipts(ctx, ledger); err != nil {
return nil, nil, err
}
return bundle, updates, nil
}
func packed(req []byte, n int) ([]uint64, error) {
var out []uint64
err := wire.Walk(req, func(f wire.Field) error {
if f.Number != n {
return nil
}
if f.Type == 0 {
v, _ := binary.Uvarint(f.Value)
out = append(out, v)
return nil
}
if f.Type != 2 {
return fmt.Errorf("hunting: invalid repeated integer")
}
for raw := f.Value; len(raw) > 0; {
v, n := binary.Uvarint(raw)
if n <= 0 {
return fmt.Errorf("hunting: invalid packed integer")
}
out = append(out, v)
raw = raw[n:]
}
return nil
})
return out, err
}
func (s *Service) Handle(ctx command.Context, path string, req []byte) (int, []byte, bool, error) {
commandSession := ctx.SessionID
if strings.HasPrefix(path, "/HuntDispatch") {
return s.handleDispatch(ctx, path, req, commandSession)
}
return s.handleGroundSession(ctx, path, req, commandSession)
}
func (s *Service) handleDispatch(ctx command.Context, path string, req []byte, session string) (int, []byte, bool, error) {
code := map[string]int{"/HuntDispatchInfo": 189, "/HuntDispatchStart": 190, "/HuntDispatchEnd": 191, "/HuntDispatchRewardPreview": 194, "/HuntDispatch": 0}[path]
if path != "/HuntDispatchInfo" && path != "/HuntDispatchStart" && path != "/HuntDispatchEnd" && path != "/HuntDispatchRewardPreview" && path != "/HuntDispatch" {
return 0, nil, false, nil
}
if e := s.refreshAP(ctx); e != nil {
return code, nil, true, e
}
seq, found, err := wire.Varint(req, 1)
if err != nil || !found || seq == 0 {
return code, nil, true, fmt.Errorf("hunting: missing sequence")
}
ds := dispatchState{Version: versionconfig.State(), Jobs: map[string]dispatchJob{}, Receipts: map[string]dispatchReceipt{}}
raw, err := s.storage.Load(ctx.State, "huntdispatch")
if err != nil {
return code, nil, true, err
}
if raw != nil {
if err = stateio.RequireExactJSONObject(raw, "version", "jobs", "receipts"); err != nil {
return code, nil, true, err
}
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.DisallowUnknownFields()
if err = decoder.Decode(&ds); err != nil {
return code, nil, true, err
}
if ds.Version != versionconfig.State() || ds.Jobs == nil || ds.Receipts == nil {
return code, nil, true, fmt.Errorf("hunting: invalid dispatch state")
}
}
if path == "/HuntDispatchInfo" {
var out []byte
keys := make([]string, 0, len(ds.Jobs))
for k := range ds.Jobs {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
out = wire.AppendBytes(out, 1, dispatchJobWire(ds.Jobs[k]))
}
// A client without a remembered dispatch pack needs a default. This
// server selects the smallest cleared ordinary hunting pack.
var minimum uint64
for key, state := range s.state.Packs {
if state.Highest == 0 {
continue
}
pack, err := strconv.ParseUint(key, 10, 32)
if err != nil || pack == 0 {
return code, nil, true, fmt.Errorf("hunting: invalid saved dispatch pack")
}
if minimum == 0 || pack < minimum {
minimum = pack
}
}
if minimum != 0 {
out = wire.AppendVarint(out, 2, minimum)
}
return code, out, true, nil
}
for k, j := range ds.Jobs {
if k != fmt.Sprintf("%d:%d", j.Group, j.ID) || j.Group == 0 || j.ID == 0 || j.Count == 0 || j.Count > 20 || j.Start == 0 || j.End < j.Start || len(j.Runs) != int(j.Count) || j.Free > 2147483647 || j.Bonus > 2147483647 {
return code, nil, true, fmt.Errorf("hunting: invalid saved dispatch job")
}
}
for k, r := range ds.Receipts {
if k == "" || len(r.Request) == 0 || r.Response == nil {
return code, nil, true, fmt.Errorf("hunting: invalid saved dispatch receipt")
}
}
key := session + ":" + strconv.FormatUint(seq, 10)
if r, ok := ds.Receipts[key]; ok {
if !bytes.Equal(r.Request, append([]byte(path), req...)) {
return code, nil, true, fmt.Errorf("hunting: sequence reused with different dispatch request")
}
return r.Code, r.Response, true, nil
}
group, _, e := wire.Varint(req, 2)
id, _, e2 := wire.Varint(req, 3)
if e != nil || e2 != nil || group == 0 || id == 0 {
return code, nil, true, fmt.Errorf("hunting: invalid dispatch ID")
}
jobKey := fmt.Sprintf("%d:%d", group, id)
job, exists := ds.Jobs[jobKey]
d, err := s.dispatchLoad(group, id)
if err != nil {
return code, nil, true, err
}
sky := d.TypeGroup != 0
if sky && (s.dispatchCosts == nil || s.dispatchExchange == nil || s.dispatchBonus == nil) {
return code, nil, true, fmt.Errorf("hunting: SkyWay dispatch runtime missing")
}
freeType, bonusType := uint64(21), uint64(23)
if sky && d.APType == 2 {
freeType, bonusType = 32, 33
}
var out []byte
if path == "/HuntDispatchRewardPreview" {
if !exists {
return code, nil, true, fmt.Errorf("hunting: dispatch not started")
}
played := dispatchPlayed(job, d.ClearTime)
out = wire.AppendVarint(out, 1, played)
preview := dispatchCompletedRewards(job, played)
remaining := d.AP * (job.Count - played)
bonus := min(remaining, job.Bonus)
free := remaining - bonus
if free > 0 {
preview = append(preview, gamedata.BattleReward{Type: freeType, Count: free})
}
if bonus > 0 {
preview = append(preview, gamedata.BattleReward{Type: bonusType, Count: bonus})
}
bundle := dispatchPreview(preview)
for _, r := range preview {
v := wire.AppendVarint(nil, 2, r.ID)
v = wire.AppendVarint(v, 3, r.Type)
v = wire.AppendVarint(v, 4, r.Count)
bundle = wire.AppendBytes(bundle, 1, v)
}
out = wire.AppendBytes(out, 2, bundle)
return code, out, true, nil
}
if path == "/HuntDispatchEnd" {
if !exists {
return code, nil, true, fmt.Errorf("hunting: dispatch not started")
}
played := dispatchPlayed(job, d.ClearTime)
completed := dispatchCompletedRewards(job, played)
remaining := d.AP * (job.Count - played)
refundBonus := min(remaining, job.Bonus)
refundFree := remaining - refundBonus
if !sky {
next := s.clone()
next.Free += refundFree
next.Bonus += refundBonus
if e := s.persist(ctx, next); e != nil {
return code, nil, true, e
}
}
if refundFree > 0 {
completed = append(completed, gamedata.BattleReward{Type: freeType, Count: refundFree})
}
if refundBonus > 0 {
completed = append(completed, gamedata.BattleReward{Type: bonusType, Count: refundBonus})
}
var bundle []byte
var e error
if sky {
var rewards []gamedata.Reward
for _, row := range completed {
rewards = append(rewards, gamedata.Reward(row))
}
bundle, e = s.dispatchExchange(ctx, "dispatch:"+key, nil, rewards)
} else {
bundle, e = s.dispatchGrant(ctx, "dispatch:"+key, completed)
}
if e != nil {
return code, nil, true, e
}
out = wire.AppendBytes(out, 1, bundle)
if played > 0 && s.dispatchProgress != nil {
if e = s.dispatchProgress(ctx, played); e != nil {
return code, nil, true, e
}
}
delete(ds.Jobs, jobKey)
} else {
count, _, e := wire.Varint(req, 4)
if e != nil || count == 0 || count > 20 {
return code, nil, true, fmt.Errorf("hunting: dispatch count must be 1..20")
}
if exists {
return code, nil, true, fmt.Errorf("hunting: dispatch already running")
}
for _, active := range ds.Jobs {
design, e := s.dispatchLoad(active.Group, active.ID)
if e != nil {
return code, nil, true, e
}
if design.TypeGroup == d.TypeGroup {
return code, nil, true, fmt.Errorf("hunting: dispatch category already running")
}
}
if d.TypeGroup != 0 && s.dispatchEligibility != nil {
if e = s.dispatchEligibility(ctx, d); e != nil {
return code, nil, true, e
}
} else {
if d.TypeGroup != 0 {
return code, nil, true, fmt.Errorf("hunting: SkyWay dispatch eligibility unavailable")
}
st := s.state.Packs[strconv.Itoa(int(d.Pack))]
if st.Highest < d.GroundID {
return code, nil, true, fmt.Errorf("hunting: dispatch requires cleared difficulty")
}
}
if d.AP > uint64(2147483647)/count {
return code, nil, true, fmt.Errorf("hunting: dispatch AP overflow")
}
cost := d.AP * count
var skyCosts []gamedata.Reward
if sky {
skyCosts, e = s.dispatchCosts(ctx, d, count)
if e != nil {
return code, nil, true, e
}
}
if !sky && s.state.Free < cost && s.state.Bonus < cost-s.state.Free {
return code, nil, true, fmt.Errorf("hunting: insufficient hunting AP")
}
rewards, e := d.Roll(count, func(n uint64) (uint64, error) {
v, e := rand.Int(rand.Reader, new(big.Int).SetUint64(n))
if e != nil {
return 0, e
}
return v.Uint64(), nil
})
if e != nil {
return code, nil, true, e
}
if sky {
rewards, e = s.dispatchBonus(d, rewards)
if e != nil {
return code, nil, true, e
}
}
next := s.clone()
free := min(cost, next.Free)
bonus := cost - free
if sky {
free, bonus = 0, 0
for _, c := range skyCosts {
switch c.Type {
case freeType:
free += c.Count
case bonusType:
bonus += c.Count
}
}
} else {
next.Free -= free
next.Bonus -= bonus
}
if path == "/HuntDispatchStart" {
now := uint64(time.Now().UnixMilli())
runs := make([][]gamedata.BattleReward, count)
for i := range runs {
runs[i], e = d.Roll(1, func(n uint64) (uint64, error) {
v, e := rand.Int(rand.Reader, new(big.Int).SetUint64(n))
if e != nil {
return 0, e
}
return v.Uint64(), nil
})
if e != nil {
return code, nil, true, e
}
}
if sky {
for i, run := range runs {
runs[i], e = s.dispatchBonus(d, run)
if e != nil {
return code, nil, true, e
}
}
if _, e = s.dispatchExchange(ctx, "dispatch-charge:"+key, skyCosts, nil); e != nil {
return code, nil, true, e
}
}
job = dispatchJob{Group: group, ID: id, Count: count, Start: now, End: now + d.ClearTime*count*1000, Free: free, Bonus: bonus, Rewards: rewards, Runs: runs}
ds.Jobs[jobKey] = job
out = wire.AppendBytes(out, 1, dispatchJobWire(job))
} else if sky {
var grants []gamedata.Reward
for _, row := range rewards {
grants = append(grants, gamedata.Reward(row))
}
bundle, e := s.dispatchExchange(ctx, "dispatch:"+key, skyCosts, grants)
if e != nil {
return code, nil, true, e
}
out = wire.AppendBytes(out, 1, bundle)
} else {
beforeFree, beforeBonus := s.state.Free, s.state.Bonus
bundle, e := s.dispatchGrant(ctx, "dispatch:"+key, rewards)
if e != nil {
return code, nil, true, e
}
out = wire.AppendBytes(out, 1, bundle)
if s.state.Free >= beforeFree {
next.Free += s.state.Free - beforeFree
} else {
delta := beforeFree - s.state.Free
if next.Free < delta {
return code, nil, true, fmt.Errorf("hunting: AP grant conflict")
}
next.Free -= delta
}
if s.state.Bonus >= beforeBonus {
next.Bonus += s.state.Bonus - beforeBonus
} else {
delta := beforeBonus - s.state.Bonus
if next.Bonus < delta {
return code, nil, true, fmt.Errorf("hunting: AP grant conflict")
}
next.Bonus -= delta
}
}
if !sky {
if e = s.persist(ctx, next); e != nil {
return code, nil, true, e
}
}
if path == "/HuntDispatch" && s.dispatchProgress != nil {
if e = s.dispatchProgress(ctx, count); e != nil {
return code, nil, true, e
}
}
}
ds.Receipts[key] = dispatchReceipt{append([]byte(path), req...), out, code}
b, e := json.Marshal(ds)
if e != nil {
return code, nil, true, e
}
if e = s.storage.Save(ctx.State, "huntdispatch", b); e != nil {
return code, nil, true, e
}
return code, out, true, nil
}
func dispatchJobWire(j dispatchJob) []byte {
var out []byte
for n, v := range map[int]uint64{1: j.Group, 2: j.ID, 3: j.Count, 4: j.Start, 5: j.End, 6: j.Free, 7: j.Bonus} {
out = wire.AppendVarint(out, n, v)
}
return out
}
func dispatchPreview(rs []gamedata.BattleReward) []byte {
var out []byte
for _, r := range rs {
v := wire.AppendVarint(nil, 2, r.ID)
v = wire.AppendVarint(v, 3, r.Type)
v = wire.AppendVarint(v, 4, r.Count)
out = wire.AppendBytes(out, 6, v)
}
return out
}
func (s *Service) dispatchGrant(ctx command.Context, identity string, rs []gamedata.BattleReward) ([]byte, error) {
if s.grant != nil {
var rewards []gamedata.Reward
var ap []gamedata.BattleReward
for _, r := range rs {
if r.Type == 21 || r.Type == 23 {
ap = append(ap, r)
} else {
rewards = append(rewards, gamedata.Reward{Type: r.Type, ID: r.ID, Count: r.Count}) //nolint:staticcheck // S1016
}
}
bundle, err := s.grant(ctx, identity, rewards)
if err != nil {
return nil, err
}
bundle = append(bundle, dispatchPreview(ap)...)
for _, r := range ap {
v := wire.AppendVarint(nil, 3, r.Type)
v = wire.AppendVarint(v, 4, r.Count)
bundle = wire.AppendBytes(bundle, 1, v)
}
return bundle, nil
}
var currency []gamedata.Reward
var items []gamedata.BattleReward
for _, r := range rs {
switch r.Type {
case 21, 23:
continue
case 2, 3, 4, 12, 20:
currency = append(currency, gamedata.Reward{Type: r.Type, ID: r.ID, Count: r.Count}) //nolint:staticcheck // S1016
default:
items = append(items, r)
}
}
if _, err := s.wallet.GrantQuestOnce(ctx, identity+":currency", currency); err != nil {
return nil, err
}
granted, err := s.inventory.GrantOnce(ctx, identity+":items", items)
if err != nil {
return nil, err
}
if len(granted) == 0 {
granted = s.inventory.GrantedItems(identity + ":items")
}
out := dispatchPreview(rs)
for _, r := range rs {
if r.Type == 21 || r.Type == 23 {
v := wire.AppendVarint(nil, 3, r.Type)
v = wire.AppendVarint(v, 4, r.Count)
out = wire.AppendBytes(out, 1, v)
}
}
for _, r := range currency {
v := wire.AppendVarint(nil, 2, r.ID)
v = wire.AppendVarint(v, 3, r.Type)
v = wire.AppendVarint(v, 4, r.Count)
out = wire.AppendBytes(out, 1, v)
}
for _, item := range granted {
out = wire.AppendBytes(out, 1, assets.ItemWire(item))
}
return out, nil
}
func (s *Service) APChargeInfo(ctx command.Context) ([]byte, error) {
if e := s.refreshAP(ctx); e != nil {
return nil, e
}
raw, e := s.storage.Load(ctx.State, "huntingapclock")
if e != nil || raw == nil {
return nil, e
}
var clock apClock
if e = json.Unmarshal(raw, &clock); e != nil {
return nil, e
}
b := wire.AppendVarint(nil, 1, uint64(clock.Next-86400000))
item := wire.AppendVarint(nil, 3, 21)
item = wire.AppendVarint(item, 4, s.state.Free)
b = wire.AppendBytes(b, 2, item)
return wire.AppendBytes(nil, 1, b), nil
}
func (s *Service) handleGroundBase(ctx command.Context, path string, req []byte) (int, []byte, bool, error) {
if path == "/HuntDispatch" || path == "/HuntDispatchInfo" || path == "/HuntDispatchStart" || path == "/HuntDispatchEnd" || path == "/HuntDispatchRewardPreview" {
return s.handleDispatch(ctx, path, req, ctx.SessionID)
}
if path != "/HuntingGroundInfo" && path != "/HuntingGroundInfoList" && path != "/HuntingGroundEnter" {
return 0, nil, false, nil
}
seq, found, err := wire.Varint(req, 1)
if err != nil || !found || seq == 0 {
return 0, nil, true, fmt.Errorf("hunting: missing sequence")
}
if path == "/HuntingGroundInfoList" {
ids, err := packed(req, 2)
if err != nil {
return 387, nil, true, err
}
var response []byte
seen := map[uint64]bool{}
for _, id := range ids {
if id == 0 || id > math.MaxInt32 || seen[id] {
return 387, nil, true, fmt.Errorf("hunting: invalid pack list")
}
seen[id] = true
d, err := s.load(int(id))
if err != nil {
return 387, nil, true, err
}
if len(d.Grounds) == 0 {
response = wire.AppendBytes(response, 1, wire.AppendVarint(nil, 5, id))
continue
}
info, err := s.info(int(id), d)
if err != nil {
return 387, nil, true, err
}
response = wire.AppendBytes(response, 1, info)
}
return 387, response, true, nil
}
pack, found, err := wire.Varint(req, 2)
if err != nil || !found || pack == 0 || pack > math.MaxInt32 {
return 134, nil, true, fmt.Errorf("hunting: invalid pack")
}
d, err := s.load(int(pack))
if err != nil {
return 134, nil, true, err
}
if path == "/HuntingGroundInfo" {
if s.state.Packs[strconv.Itoa(int(pack))].Current == 0 {
return 134, wire.AppendBytes(nil, 1, nil), true, nil
}
b, err := s.info(int(pack), d)
return 134, wire.AppendBytes(nil, 1, b), true, err
}
if len(d.Grounds) == 0 {
return 110, nil, true, fmt.Errorf("hunting: pack has no hunting ground")
}
current, err := s.currentPack(ctx)
if err != nil || current != int(pack) {
return 110, nil, true, fmt.Errorf("hunting: enter pack is not current")
}
id, _, err := wire.Varint(req, 3)
if err != nil {
return 110, nil, true, err
}
auto, _, err := wire.Varint(req, 4)
if err != nil || auto > 1 {
return 110, nil, true, fmt.Errorf("hunting: invalid auto flag")
}
st := s.state.Packs[strconv.Itoa(int(pack))]
g, ok := ground(d, id)
if !ok {
return 110, nil, true, fmt.Errorf("hunting: unknown ground")
}
if s.eligibility != nil {
if err := s.eligibility(ctx, int(pack), g.Difficulty); err != nil {
return 110, nil, true, err
}
} else if id != d.Grounds[0].ID {
return 110, nil, true, fmt.Errorf("hunting: main quest difficulty eligibility unavailable")
}
if st.Current != id {
st.Defeated = nil
}
st.Current, st.Auto = id, auto != 0
next := s.clone()
next.Packs[strconv.Itoa(int(pack))] = st
if err := s.persist(ctx, next); err != nil {
return 110, nil, true, err
}
var out []byte
for _, m := range monstersWithState(d, g, st.Defeated) {
out = wire.AppendBytes(out, 1, m)
}
return 110, out, true, nil
}