Files
bd2/go/internal/server/eventactions/battle.go
T
Flechazo d8771caef2 feat(all): add hunting and events, commerce and rewards, binary calendars, GameData caching and login diagnostics
Implement hunting AP and progress, event tasks, exchanges, games, stories and actions; connect battle and reward handling.

Add CashShop plugin and installation, server purchases and entitlements, recharge rules and plugin version metadata.

Distribute binary calendars independently; cache GameData in memory and fix single-connection query deadlocks.

Improve automatic login and recovery diagnostics; document configuration and release flows.
2026-10-05 11:50:31 +08:00

165 lines
4.5 KiB
Go

package eventactions
import (
"bd2server/internal/server/events"
"bd2server/internal/server/wire"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
)
// Tactics uses the client's custom battle stage fields 8/9 and has fixed
// tutorial decks. Character health and ordinary pack rewards do not apply.
func (s *Service) HandlesBattle(mode uint64) bool { return mode == 29 }
func (s *Service) EnterBattle(req []byte, receipt string) ([]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
digest := battleDigest(req)
if r, ok := s.state.Receipts[s.session+":enter:"+receipt]; ok {
if r.Digest != digest {
return nil, errors.New("eventactions: changed battle enter replay")
}
return r.Reply, nil
}
before, _ := json.Marshal(s.state)
if val(req, 5) != 29 {
return nil, errors.New("eventactions: wrong tactics battle mode")
}
group, stage, deck := val(req, 8), val(req, 9), val(req, 4)
row, ok := s.row("TacticsBingoTable", 4, 5, group, stage)
if !ok || row.V(2) != deck {
return nil, errors.New("eventactions: invalid tactics stage/deck")
}
var uid uint64
for _, v := range s.registry.List() {
if v.Type != 20 || !s.active(v) {
continue
}
g, ok := s.design.Row("TacticsBingoGroupTable", 3, v.ID)
if ok && g.V(1) == group {
uid = v.UID
break
}
}
if uid == 0 {
return nil, errors.New("eventactions: tactics event inactive")
}
s.state.BattleUID, s.state.BattleStage, s.state.BattleDeck = uid, stage, deck
s.state.Receipts[s.session+":enter:"+receipt] = receiptRecord(nil, digest)
if e := s.save(); e != nil {
_ = json.Unmarshal(before, &s.state)
return nil, e
}
return nil, nil
}
func (s *Service) CompleteBattle(req []byte, receipt string) ([]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
if r, ok := s.state.Receipts[s.session+":battle:"+receipt]; ok {
if r.Digest != battleDigest(req) {
return nil, errors.New("eventactions: changed battle completion replay")
}
return r.Reply, nil
}
before, _ := json.Marshal(s.state)
uid, stage := s.state.BattleUID, s.state.BattleStage
if uid == 0 || stage == 0 {
return nil, errors.New("eventactions: tactics battle not entered")
}
if _, e := s.resolve(uid, 20); e != nil {
return nil, e
}
if val(req, 2) == 1 && !contains(s.state.Tactics[uid], stage) {
priorLines := s.tacticsLines(uid)
s.state.Tactics[uid] = append(s.state.Tactics[uid], stage)
if s.progress != nil {
if e := s.progress(346, stage, 1); e != nil {
_ = json.Unmarshal(before, &s.state)
return nil, e
}
lines := s.tacticsLines(uid)
if lines > priorLines {
if e := s.progress(347, 0, lines-priorLines); e != nil {
_ = json.Unmarshal(before, &s.state)
return nil, e
}
}
v, _ := s.registry.Resolve(uid)
g, _ := s.design.Row("TacticsBingoGroupTable", 3, v.ID)
n := uint64(5) - g.V(4)
if uint64(len(s.state.Tactics[uid])) == n*n {
if e := s.progress(348, 0, 1); e != nil {
_ = json.Unmarshal(before, &s.state)
return nil, e
}
}
}
}
var out []byte
for _, id := range s.state.Tactics[uid] {
out = wire.AppendVarint(out, 29, id)
}
s.state.BattleUID, s.state.BattleStage, s.state.BattleDeck = 0, 0, 0
s.state.Receipts[s.session+":battle:"+receipt] = receiptRecord(out, battleDigest(req))
if e := s.save(); e != nil {
_ = json.Unmarshal(before, &s.state)
return nil, e
}
return out, nil
}
func (s *Service) tacticsLines(uid uint64) uint64 {
v, _ := s.registry.Resolve(uid)
g, ok := s.design.Row("TacticsBingoGroupTable", 3, v.ID)
if !ok || g.V(4) > 2 {
return 0
}
n := uint64(5) - g.V(4)
clear := s.state.Tactics[uid]
var lines uint64
for i := uint64(0); i < n; i++ {
row, col := true, true
for j := uint64(0); j < n; j++ {
row = row && contains(clear, i*n+j+1)
col = col && contains(clear, j*n+i+1)
}
if row {
lines++
}
if col {
lines++
}
}
a, b := true, true
for i := uint64(0); i < n; i++ {
a = a && contains(clear, i*n+i+1)
b = b && contains(clear, i*n+(n-i))
}
if a {
lines++
}
if b {
lines++
}
return lines
}
func (s *Service) AssociatedMissionGroup(v events.Schedule) uint64 {
if v.Type == 20 {
r, ok := s.design.Row("TacticsBingoGroupTable", 3, v.ID)
if ok {
return r.V(2)
}
}
if v.Type == 23 {
r, ok := s.design.Row("VotingEventTable", 6, v.ID)
if ok {
return r.V(12)
}
}
return 0
}
func receiptRecord(b []byte, digest string) receipt { return receipt{Digest: digest, Reply: b} }
func battleDigest(b []byte) string { sum := sha256.Sum256(b); return hex.EncodeToString(sum[:]) }