Files
GaCy-GO/internal/gameserver/battle_capture.go
T

141 lines
4.4 KiB
Go

package gameserver
import (
"crypto/sha256"
_ "embed"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"gac-hkrpg-go/internal/common"
goproto "gac-hkrpg-go/protocol/proto"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/proto"
)
//go:embed battle_wire_probe.lua
var battleWireProbe string
type battleCapture struct {
ResponseName string `json:"response_name"`
CmdID uint16 `json:"cmd_id"`
ResponseSHA256 string `json:"response_sha256"`
BattleSHA256 string `json:"battle_sha256"`
CapturedUTC time.Time `json:"captured_utc"`
StageID uint32 `json:"stage_id"`
}
func saveBattleCapture(msg proto.Message, cmdID uint16, response []byte) ([]byte, *battleCapture, error) {
carrier, ok := msg.(interface {
GetBattleInfo() *goproto.SceneBattleInfo
})
if !ok || carrier.GetBattleInfo() == nil {
return nil, nil, nil
}
battle := carrier.GetBattleInfo()
wire, err := extractBattleWire(msg, response)
if err != nil {
return nil, nil, err
}
pretty, err := (protojson.MarshalOptions{Indent: " "}).Marshal(battle)
if err != nil {
return nil, nil, err
}
meta := &battleCapture{
ResponseName: string(msg.ProtoReflect().Descriptor().Name()), CmdID: cmdID,
ResponseSHA256: fmt.Sprintf("%x", sha256.Sum256(response)),
BattleSHA256: fmt.Sprintf("%x", sha256.Sum256(wire)),
CapturedUTC: time.Now().UTC(), StageID: battle.StageId,
}
info, err := json.MarshalIndent(meta, "", " ")
if err != nil {
return nil, nil, err
}
files := map[string][]byte{"battle_last_response.bin": response, "battle_last.bin": wire, "battle_last.json": pretty, "battle_last_capture.json": info}
// Keep earlier failures intact so consecutive attempts can be compared.
archive := filepath.Join("battle_diagnostics", meta.CapturedUTC.Format("20060102T150405.000000000Z")+"_"+meta.ResponseSHA256[:12])
if err := os.MkdirAll(archive, 0700); err != nil {
return nil, nil, err
}
for name, data := range files {
if err := os.WriteFile(filepath.Join(archive, name), data, 0600); err != nil {
return nil, nil, err
}
if err := os.WriteFile(name, data, 0600); err != nil {
return nil, nil, err
}
}
return wire, meta, nil
}
func extractBattleWire(msg proto.Message, response []byte) ([]byte, error) {
field := msg.ProtoReflect().Descriptor().Fields().ByName("battle_info")
if field == nil {
return nil, fmt.Errorf("response has no battle_info field")
}
for len(response) > 0 {
number, kind, n := protowire.ConsumeTag(response)
if n < 0 {
return nil, protowire.ParseError(n)
}
response = response[n:]
if number == protowire.Number(field.Number()) && kind == protowire.BytesType {
wire, n := protowire.ConsumeBytes(response)
if n < 0 {
return nil, protowire.ParseError(n)
}
return append([]byte(nil), wire...), nil
}
n = protowire.ConsumeFieldValue(number, kind, response)
if n < 0 {
return nil, protowire.ParseError(n)
}
response = response[n:]
}
return nil, fmt.Errorf("battle_info missing from serialized response")
}
func makeBattleProbe(wire []byte, typeName, hash string) []byte {
// Values are quoted as Lua strings; base64 contains no executable syntax.
replacer := strings.NewReplacer("__TYPE_NAME__", strconv.Quote(typeName), "__PAYLOAD__", strconv.Quote(base64.StdEncoding.EncodeToString(wire)), "__CAPTURE_HASH__", strconv.Quote(hash))
return []byte(replacer.Replace(battleWireProbe))
}
func (s *Session) captureBattleResponse(msg proto.Message, cmdID uint16, body []byte) {
wire, meta, err := saveBattleCapture(msg, cmdID, body)
if err != nil {
Log(fmt.Sprintf("[BATTLE CAPTURE] %v", err))
return
}
if meta == nil {
return
}
Log(fmt.Sprintf("[BATTLE CAPTURE] cmd=%d stage=%d response_sha256=%s battle_sha256=%s", cmdID, meta.StageID, meta.ResponseSHA256, meta.BattleSHA256))
cfg, err := common.LoadRuntimeConfig()
if err != nil {
Log(fmt.Sprintf("[BATTLE PROBE] config error: %v", err))
return
}
if cfg.BattleDiagnostics.ClientBattleType == "" {
return
}
s.battleProbeMu.Lock()
s.battleProbe = makeBattleProbe(wire, cfg.BattleDiagnostics.ClientBattleType, meta.BattleSHA256)
s.battleProbeMu.Unlock()
Log("[BATTLE PROBE] queued read-only client parser check for next heartbeat")
}
func (s *Session) takeBattleProbe() []byte {
s.battleProbeMu.Lock()
defer s.battleProbeMu.Unlock()
probe := s.battleProbe
s.battleProbe = nil
return probe
}