416 lines
12 KiB
Go
416 lines
12 KiB
Go
package world
|
|
|
|
import (
|
|
"bd2server/internal/server/gamedata"
|
|
"bd2server/internal/server/stateio"
|
|
"bd2server/internal/server/wire"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
type fieldMonsterState struct {
|
|
Generation uint64
|
|
Start, Respawn, LifeEnd int64
|
|
Defeated bool
|
|
Period string
|
|
}
|
|
type fieldMonsterSnapshot struct {
|
|
Monsters map[string]fieldMonsterState
|
|
Claims map[string]bool
|
|
Requests map[string]fieldMonsterReply
|
|
}
|
|
type fieldMonsterReply struct{ Request, Response []byte }
|
|
|
|
func (s *Service) BeginSession(id string) { s.monsterSession = id }
|
|
func (s *Service) AttachFieldMonsterDamage(f func(int, uint64, string) ([][]byte, error)) {
|
|
s.monsterDamage = f
|
|
}
|
|
|
|
func (s *Service) AttachFieldMonsterState(store stateio.Store) error {
|
|
if store == nil {
|
|
return fmt.Errorf("world: missing monster state store")
|
|
}
|
|
s.monsterStore = store
|
|
return nil
|
|
}
|
|
func (s *Service) monsterTime() time.Time {
|
|
if s.monsterNow != nil {
|
|
return s.monsterNow()
|
|
}
|
|
return time.Now()
|
|
}
|
|
func (s *Service) loadMonsterState() (fieldMonsterSnapshot, error) {
|
|
v := fieldMonsterSnapshot{Monsters: map[string]fieldMonsterState{}, Claims: map[string]bool{}, Requests: map[string]fieldMonsterReply{}}
|
|
if s.monsterStore == nil {
|
|
return v, nil
|
|
}
|
|
b, e := s.monsterStore.Load("field_monster_runtime")
|
|
if e != nil || b == nil {
|
|
return v, e
|
|
}
|
|
if e = stateio.RequireExactJSONObject(b, "Monsters", "Claims", "Requests"); e != nil {
|
|
return v, e
|
|
}
|
|
if e = json.Unmarshal(b, &v); e != nil || v.Monsters == nil || v.Claims == nil || v.Requests == nil {
|
|
return v, fmt.Errorf("world: invalid field monster state")
|
|
}
|
|
for _, m := range v.Monsters {
|
|
if m.Generation == 0 || m.Start <= 0 || m.Respawn < 0 || m.LifeEnd < 0 {
|
|
return v, fmt.Errorf("world: invalid monster timestamps")
|
|
}
|
|
}
|
|
return v, nil
|
|
}
|
|
func (s *Service) saveMonsterState(v fieldMonsterSnapshot) error {
|
|
if s.monsterStore == nil {
|
|
return fmt.Errorf("world: monster persistence unavailable")
|
|
}
|
|
b, e := json.Marshal(v)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
return s.monsterStore.Save("field_monster_runtime", b)
|
|
}
|
|
func monsterKey(pack, difficulty, id int) string {
|
|
return fmt.Sprintf("%d/%d/%d", pack, difficulty, id)
|
|
}
|
|
func (s *Service) monsterPeriod(m gamedata.FieldMonsterDesign) (string, error) {
|
|
if m.ResetType == 0 {
|
|
return "own", nil
|
|
}
|
|
reset := 0
|
|
if m.ResetType == 2 {
|
|
reset = 3
|
|
}
|
|
return s.fieldReset.Period(reset, s.monsterTime())
|
|
}
|
|
func (s *Service) monsterState(v *fieldMonsterSnapshot, pack int, m gamedata.FieldMonsterDesign) (fieldMonsterState, error) {
|
|
key := monsterKey(pack, s.questDifficulty(pack), m.ID)
|
|
old, exists := v.Monsters[key]
|
|
period, e := s.monsterPeriod(m)
|
|
if e != nil {
|
|
return old, e
|
|
}
|
|
now := s.monsterTime().UnixMilli()
|
|
fresh := func(generation uint64, start int64) fieldMonsterState {
|
|
r := fieldMonsterState{Generation: generation, Start: start, Period: period}
|
|
if m.LifeSeconds > 0 {
|
|
r.LifeEnd = start + int64(m.LifeSeconds)*1000
|
|
}
|
|
return r
|
|
}
|
|
if !exists {
|
|
old = fresh(1, now)
|
|
} else if old.Period != period {
|
|
old = fresh(old.Generation+1, now)
|
|
} else if old.Respawn > 0 && now >= old.Respawn {
|
|
old = fresh(old.Generation+1, old.Respawn)
|
|
} else if old.LifeEnd > 0 && now >= old.LifeEnd && !old.Defeated {
|
|
old.Defeated = true
|
|
old.Respawn = old.LifeEnd + int64(m.RegenSeconds)*1000
|
|
if old.Respawn <= now {
|
|
old = fresh(old.Generation+1, now)
|
|
}
|
|
}
|
|
v.Monsters[key] = old
|
|
return old, nil
|
|
}
|
|
func (s *Service) monsterEligible(pack int, m gamedata.FieldMonsterDesign) bool {
|
|
return m.QuestID == 0 || s.state.QuestCleared(m.QuestID, pack, s.questDifficulty(pack))
|
|
}
|
|
func monsterWire(m gamedata.FieldMonsterDesign, state fieldMonsterState, eligible bool) []byte {
|
|
b := wire.AppendVarint(nil, 1, uint64(m.ID))
|
|
if m.BattleDeck != 0 {
|
|
b = wire.AppendVarint(b, 2, m.BattleDeck)
|
|
}
|
|
if state.Respawn > 0 {
|
|
b = wire.AppendVarint(b, 3, uint64(state.Respawn))
|
|
}
|
|
if state.LifeEnd > 0 {
|
|
b = wire.AppendVarint(b, 4, uint64(state.LifeEnd))
|
|
}
|
|
b = wire.AppendVarint(b, 5, uint64(m.GroupID))
|
|
if eligible {
|
|
b = wire.AppendVarint(b, 6, 1)
|
|
}
|
|
return b
|
|
}
|
|
func (s *Service) monsterRows(pack int, filter map[int]bool) ([][]byte, error) {
|
|
if s.monsterLoader == nil {
|
|
return nil, fmt.Errorf("%w: missing field monster design", ErrInvalidRequest)
|
|
}
|
|
design, e := s.monsterLoader(pack)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
v, e := s.loadMonsterState()
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
var rows [][]byte
|
|
changed := false
|
|
for _, m := range design {
|
|
if m.GroupID == 0 {
|
|
continue
|
|
}
|
|
if filter != nil && !filter[m.ID] {
|
|
continue
|
|
}
|
|
eligible := s.monsterEligible(pack, m)
|
|
if eligible {
|
|
available, err := s.rewardMonsterAvailable(pack, m.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
eligible = available
|
|
}
|
|
state := fieldMonsterState{}
|
|
if eligible {
|
|
state, e = s.monsterState(&v, pack, m)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
changed = true
|
|
}
|
|
rows = append(rows, monsterWire(m, state, eligible))
|
|
}
|
|
if changed && s.monsterStore != nil {
|
|
if e = s.saveMonsterState(v); e != nil {
|
|
return nil, e
|
|
}
|
|
}
|
|
return rows, nil
|
|
}
|
|
func (s *Service) handleMonsterInfo(request []byte) (int, []byte, bool, error) {
|
|
seq, present, e := wire.Varint(request, 1)
|
|
if e != nil || !present || seq == 0 || seq > 0x7fffffff {
|
|
return 0, nil, true, ErrInvalidRequest
|
|
}
|
|
groups, e := intsRequest(request, 2)
|
|
if e != nil {
|
|
return 0, nil, true, e
|
|
}
|
|
wanted := map[int]bool{}
|
|
for _, g := range groups {
|
|
if g == 0 || g > 0x7fffffff {
|
|
return 0, nil, true, ErrInvalidRequest
|
|
}
|
|
wanted[int(g)] = true
|
|
}
|
|
pack, e := s.CurrentPackID()
|
|
if e != nil || !s.packUnlocked(pack) || s.monsterLoader == nil {
|
|
return 0, nil, true, ErrInvalidRequest
|
|
}
|
|
design, e := s.monsterLoader(pack)
|
|
if e != nil {
|
|
return 0, nil, true, e
|
|
}
|
|
filter := map[int]bool{}
|
|
for _, m := range design {
|
|
if wanted[m.GroupID] {
|
|
filter[m.ID] = true
|
|
}
|
|
}
|
|
rows, e := s.monsterRows(pack, filter)
|
|
if e != nil {
|
|
return 0, nil, true, e
|
|
}
|
|
var b []byte
|
|
for _, r := range rows {
|
|
b = wire.AppendBytes(b, 1, r)
|
|
}
|
|
return 51, b, true, nil
|
|
}
|
|
func (s *Service) attachFieldMonsterDesign(root, version string) {
|
|
s.monsterLoader = func(pack int) ([]gamedata.FieldMonsterDesign, error) {
|
|
return gamedata.LoadFieldMonsters(root, version, pack)
|
|
}
|
|
s.monsterRewards = func(pack int, deck uint64) ([]gamedata.BattleReward, error) {
|
|
return gamedata.BattleDeckRewards(root, version, pack, deck)
|
|
}
|
|
s.monsterMaps = func(pack int) (map[int][]int, error) { return gamedata.LoadFieldMonsterMaps(root, version, pack) }
|
|
}
|
|
func (s *Service) authorizeMonsterMap(pack, id int) error {
|
|
available, err := s.rewardMonsterAvailable(pack, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !available {
|
|
return fmt.Errorf("world: reward monster not summoned")
|
|
}
|
|
if s.monsterMaps == nil {
|
|
return nil
|
|
}
|
|
maps, e := s.monsterMaps(pack)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
current, e := s.currentFieldMap(pack)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
for _, mapID := range maps[id] {
|
|
if current == mapID {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("world: monster outside current map")
|
|
}
|
|
func (s *Service) findFieldMonster(pack, id int) (gamedata.FieldMonsterDesign, bool, error) {
|
|
if s.monsterLoader == nil {
|
|
return gamedata.FieldMonsterDesign{}, false, nil
|
|
}
|
|
d, e := s.monsterLoader(pack)
|
|
if e != nil {
|
|
return gamedata.FieldMonsterDesign{}, false, e
|
|
}
|
|
for _, m := range d {
|
|
if m.ID == id {
|
|
return m, true, nil
|
|
}
|
|
}
|
|
return gamedata.FieldMonsterDesign{}, false, nil
|
|
}
|
|
|
|
// BeginFieldMonsterBattle locks the regenerated instance for retry-safe rewards.
|
|
// Scripted monsters without regeneration return handled=false.
|
|
func (s *Service) BeginFieldMonsterBattle(pack int, id, deck uint64) (string, bool, error) {
|
|
m, found, e := s.findFieldMonster(pack, int(id))
|
|
if e == nil && found {
|
|
available, err := s.rewardMonsterAvailable(pack, int(id))
|
|
if err != nil {
|
|
return "", true, err
|
|
}
|
|
if !available {
|
|
return "", true, fmt.Errorf("world: reward monster not summoned")
|
|
}
|
|
}
|
|
if found && m.GroupID == 0 {
|
|
return "", false, nil
|
|
}
|
|
if e != nil || !found {
|
|
return "", found, e
|
|
}
|
|
if !s.packUnlocked(pack) || !s.monsterEligible(pack, m) {
|
|
return "", true, ErrInvalidRequest
|
|
}
|
|
deckOK := deck == m.BattleDeck
|
|
for _, allowed := range m.BattleDecks {
|
|
if deck == allowed {
|
|
deckOK = true
|
|
}
|
|
}
|
|
if m.BattleDeck != 0 && !deckOK {
|
|
return "", true, fmt.Errorf("world: field monster deck mismatch")
|
|
}
|
|
if e = s.authorizeMonsterMap(pack, int(id)); e != nil {
|
|
return "", true, e
|
|
}
|
|
v, e := s.loadMonsterState()
|
|
if e != nil {
|
|
return "", true, e
|
|
}
|
|
state, e := s.monsterState(&v, pack, m)
|
|
if e != nil {
|
|
return "", true, e
|
|
}
|
|
if state.Defeated || state.Respawn > s.monsterTime().UnixMilli() {
|
|
return "", true, fmt.Errorf("world: monster is not spawned")
|
|
}
|
|
if e = s.saveMonsterState(v); e != nil {
|
|
return "", true, e
|
|
}
|
|
return fmt.Sprintf("fieldmonster:%s:%d", monsterKey(pack, s.questDifficulty(pack), m.ID), state.Generation), true, nil
|
|
}
|
|
func (s *Service) CompleteFieldMonsterBattle(pack int, id uint64, instance string) ([]byte, error) {
|
|
m, found, e := s.findFieldMonster(pack, int(id))
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if !found {
|
|
return nil, ErrInvalidRequest
|
|
}
|
|
v, e := s.loadMonsterState()
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
key := monsterKey(pack, s.questDifficulty(pack), m.ID)
|
|
state, exists := v.Monsters[key]
|
|
if !exists {
|
|
return nil, ErrInvalidRequest
|
|
}
|
|
if !v.Claims[instance] {
|
|
if instance != fmt.Sprintf("fieldmonster:%s:%d", key, state.Generation) {
|
|
return nil, fmt.Errorf("world: stale monster battle")
|
|
}
|
|
state.Defeated = true
|
|
state.Respawn = s.nextMonsterSpawn(m)
|
|
v.Monsters[key] = state
|
|
v.Claims[instance] = true
|
|
if e = s.saveMonsterState(v); e != nil {
|
|
return nil, e
|
|
}
|
|
}
|
|
return monsterWire(m, state, s.monsterEligible(pack, m)), nil
|
|
}
|
|
func (s *Service) nextMonsterSpawn(m gamedata.FieldMonsterDesign) int64 {
|
|
now := s.monsterTime()
|
|
if m.ResetType == 0 {
|
|
return now.UnixMilli() + int64(m.RegenSeconds)*1000
|
|
}
|
|
shift := 9*time.Hour - s.fieldReset.DailyReset
|
|
t := now.UTC().Add(shift)
|
|
day := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, 1)
|
|
if m.ResetType == 2 {
|
|
delta := (int(s.fieldReset.WeeklyDay) - int(day.Weekday()) + 7) % 7
|
|
day = day.AddDate(0, 0, delta)
|
|
}
|
|
return day.Add(-shift).UnixMilli()
|
|
}
|
|
func (s *Service) handleFieldMonsterRegen(request []byte) (int, []byte, bool, error) {
|
|
id, e := requestPack(request)
|
|
if e != nil {
|
|
return 0, nil, true, e
|
|
}
|
|
pack, e := s.CurrentPackID()
|
|
if e != nil || !s.packUnlocked(pack) {
|
|
return 0, nil, true, ErrInvalidRequest
|
|
}
|
|
m, found, e := s.findFieldMonster(pack, id)
|
|
if e != nil || !found {
|
|
return 0, nil, true, ErrInvalidRequest
|
|
}
|
|
v, e := s.loadMonsterState()
|
|
if e != nil {
|
|
return 0, nil, true, e
|
|
}
|
|
seq, _, _ := wire.Varint(request, 1)
|
|
identity := fmt.Sprintf("regen:%s:%d", s.monsterSession, seq)
|
|
if s.monsterSession == "" {
|
|
return 0, nil, true, fmt.Errorf("world: missing monster session")
|
|
}
|
|
if reply, ok := v.Requests[identity]; ok {
|
|
if !bytes.Equal(request, reply.Request) {
|
|
return 0, nil, true, ErrInvalidRequest
|
|
}
|
|
return 139, reply.Response, true, nil
|
|
}
|
|
state, e := s.monsterState(&v, pack, m)
|
|
if e != nil {
|
|
return 0, nil, true, e
|
|
}
|
|
if !state.Defeated {
|
|
state.Defeated = true
|
|
state.Respawn = s.nextMonsterSpawn(m)
|
|
v.Monsters[monsterKey(pack, s.questDifficulty(pack), m.ID)] = state
|
|
}
|
|
response := wire.AppendBytes(nil, 1, monsterWire(m, state, s.monsterEligible(pack, m)))
|
|
v.Requests[identity] = fieldMonsterReply{Request: append([]byte(nil), request...), Response: response}
|
|
if e = s.saveMonsterState(v); e != nil {
|
|
return 0, nil, true, e
|
|
}
|
|
return 139, response, true, nil
|
|
}
|