141 lines
4.4 KiB
Go
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
|
|
}
|