pre cw with probe and a bunch script to read game behavior
This commit is contained in:
1 parent
5120219ae2
commit
da8a32f60d
310 files changed
+219693
-872
No files matched your search
+373
-23
@@ -1,6 +1,7 @@
|
||||
package sdkserver
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -12,10 +13,18 @@ import (
|
||||
"time"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
goproto "gac-hkrpg-go/protocol/proto"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var apiMu sync.Mutex
|
||||
|
||||
var liveBattleReportState struct {
|
||||
sync.RWMutex
|
||||
report *common.BattleReport
|
||||
received time.Time
|
||||
}
|
||||
|
||||
var exRankAvatarNames = map[uint32]string{
|
||||
1204: "Jing Yuan (จิ่งหยวน)",
|
||||
1213: "Dan Heng • Imbibitor Lunae (ดันเฮง จ้าวยลจันทรา)",
|
||||
@@ -27,7 +36,7 @@ func corsWrapper(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, X-GaC-Srtool")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
@@ -37,9 +46,11 @@ func corsWrapper(next http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
|
||||
func registerAPIRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/tool-session", corsWrapper(apiToolSession))
|
||||
mux.HandleFunc("/api/status", corsWrapper(apiStatus))
|
||||
mux.HandleFunc("/api/technique", corsWrapper(apiTechnique))
|
||||
mux.HandleFunc("/api/freesr", corsWrapper(apiFreesrData))
|
||||
mux.HandleFunc("/api/inventory/materials", corsWrapper(apiInventoryMaterials))
|
||||
mux.HandleFunc("/api/freesr/avatars", corsWrapper(apiFreesrAvatars))
|
||||
mux.HandleFunc("/api/persistent", corsWrapper(apiPersistentData))
|
||||
mux.HandleFunc("/api/exrank/toggle", corsWrapper(apiExRankToggle))
|
||||
@@ -48,8 +59,11 @@ func registerAPIRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/teleport", corsWrapper(apiTeleport))
|
||||
mux.HandleFunc("/api/gachabase/fetch", corsWrapper(apiGachabaseFetch))
|
||||
mux.HandleFunc("/api/command", corsWrapper(apiCommand))
|
||||
mux.HandleFunc("/api/client/unstuck", corsWrapper(apiClientUnstuck))
|
||||
mux.HandleFunc("/api/client/unstuck/result", corsWrapper(apiClientUnstuckResult))
|
||||
mux.HandleFunc("/api/data-in-game", corsWrapper(apiDataInGame))
|
||||
mux.HandleFunc("/api/battle_report", corsWrapper(apiBattleReport))
|
||||
mux.HandleFunc("/api/battle_report/live", corsWrapper(apiBattleReportLive))
|
||||
mux.HandleFunc("/api/battle_report/reset", corsWrapper(apiBattleReportReset))
|
||||
}
|
||||
|
||||
@@ -70,16 +84,17 @@ func apiRoot(w http.ResponseWriter, r *http.Request) {
|
||||
"status": 200,
|
||||
"description": "Backend connection endpoints for freesr-data, persistent, and GaC-Srtool",
|
||||
"endpoints": map[string]string{
|
||||
"status": "GET /api/status",
|
||||
"freesr": "GET, POST /api/freesr",
|
||||
"avatars": "GET, POST /api/freesr/avatars",
|
||||
"persistent": "GET, POST /api/persistent",
|
||||
"exrank_toggle": "POST /api/exrank/toggle",
|
||||
"avatar_rank": "POST /api/avatar/rank",
|
||||
"avatar_max_all": "POST /api/avatar/max_all",
|
||||
"teleport": "POST /api/teleport",
|
||||
"command": "POST /api/command",
|
||||
"battle_report": "GET, POST /api/battle_report",
|
||||
"status": "GET /api/status",
|
||||
"freesr": "GET, POST /api/freesr",
|
||||
"avatars": "GET, POST /api/freesr/avatars",
|
||||
"persistent": "GET, POST /api/persistent",
|
||||
"exrank_toggle": "POST /api/exrank/toggle",
|
||||
"avatar_rank": "POST /api/avatar/rank",
|
||||
"avatar_max_all": "POST /api/avatar/max_all",
|
||||
"teleport": "POST /api/teleport",
|
||||
"command": "POST /api/command",
|
||||
"battle_report": "GET, POST /api/battle_report",
|
||||
"battle_report_live": "POST /api/battle_report/live",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -142,7 +157,7 @@ func apiStatus(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// apiFreesrData handles full read/write of freesr-data.json
|
||||
// apiFreesrData reads data, merges POST updates, and validates complete PUTs.
|
||||
func apiFreesrData(w http.ResponseWriter, r *http.Request) {
|
||||
apiMu.Lock()
|
||||
defer apiMu.Unlock()
|
||||
@@ -177,21 +192,19 @@ func apiFreesrData(w http.ResponseWriter, r *http.Request) {
|
||||
toWrite = wrap.Data
|
||||
}
|
||||
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal(toWrite, &parsed); err != nil {
|
||||
writeJSON(w, map[string]any{"error": "invalid json: " + err.Error(), "status": 400})
|
||||
existing, err := os.ReadFile("freesr-data.json")
|
||||
if err != nil && (r.Method != http.MethodPut || !os.IsNotExist(err)) {
|
||||
writeInventoryError(w, http.StatusInternalServerError, "failed to read player data: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
out, _ := json.MarshalIndent(parsed, "", " ")
|
||||
if err := os.WriteFile("freesr-data.json", out, 0o644); err != nil {
|
||||
writeJSON(w, map[string]any{"error": "failed to write freesr-data.json: " + err.Error(), "status": 500})
|
||||
out, err := preparePlayerDataUpdate(existing, toWrite, r.Method == http.MethodPut)
|
||||
if err != nil {
|
||||
writeInventoryError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Reload and sync persistent
|
||||
if d, err := common.LoadFreesrData(); err == nil {
|
||||
d.SavePersistent()
|
||||
if err := writePlayerDataAtomic(out); err != nil {
|
||||
writeInventoryError(w, http.StatusInternalServerError, "failed to write player data: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]any{"message": "freesr-data.json saved successfully", "status": 200})
|
||||
@@ -201,6 +214,158 @@ func apiFreesrData(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
type inventoryMaterialUpdate struct {
|
||||
ItemID uint32 `json:"item_id"`
|
||||
Count *uint32 `json:"count,omitempty"`
|
||||
Reset bool `json:"reset,omitempty"`
|
||||
}
|
||||
|
||||
func writeInventoryError(w http.ResponseWriter, status int, message string) {
|
||||
w.WriteHeader(status)
|
||||
writeJSON(w, map[string]any{"status": status, "error": message})
|
||||
}
|
||||
|
||||
// apiInventoryMaterials provides a narrow inventory editor endpoint so a UI
|
||||
// can update material counts without posting a stale full freesr-data snapshot.
|
||||
func apiInventoryMaterials(w http.ResponseWriter, r *http.Request) {
|
||||
apiMu.Lock()
|
||||
defer apiMu.Unlock()
|
||||
|
||||
resource, err := common.ReadResourceFile("resources/InventoryItemConfig.json", "InventoryItemConfig.json")
|
||||
if err != nil {
|
||||
writeInventoryError(w, http.StatusInternalServerError, "inventory catalog unavailable: "+err.Error())
|
||||
return
|
||||
}
|
||||
var catalog struct {
|
||||
ItemIDs []uint32 `json:"item_ids"`
|
||||
}
|
||||
if err := json.Unmarshal(resource, &catalog); err != nil || len(catalog.ItemIDs) == 0 {
|
||||
writeInventoryError(w, http.StatusInternalServerError, "inventory catalog is invalid")
|
||||
return
|
||||
}
|
||||
allowed := make(map[uint32]struct{}, len(catalog.ItemIDs))
|
||||
for _, id := range catalog.ItemIDs {
|
||||
allowed[id] = struct{}{}
|
||||
}
|
||||
eidolonIDs, err := common.EidolonMaterialIDs()
|
||||
if err != nil {
|
||||
writeInventoryError(w, http.StatusInternalServerError, "Eidolon catalog unavailable: "+err.Error())
|
||||
return
|
||||
}
|
||||
for _, id := range eidolonIDs {
|
||||
if _, exists := allowed[id]; !exists {
|
||||
catalog.ItemIDs = append(catalog.ItemIDs, id)
|
||||
}
|
||||
allowed[id] = struct{}{}
|
||||
}
|
||||
// These two currency IDs are emitted by the server bag handler in addition
|
||||
// to the Client item catalog.
|
||||
allowed[1] = struct{}{}
|
||||
allowed[2] = struct{}{}
|
||||
|
||||
readInventory := func() (map[string]uint32, error) {
|
||||
raw, err := os.ReadFile("freesr-data.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var data struct {
|
||||
Materials map[string]uint32 `json:"materials"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data.Materials == nil {
|
||||
data.Materials = map[string]uint32{}
|
||||
}
|
||||
return data.Materials, nil
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
materials, err := readInventory()
|
||||
if err != nil {
|
||||
writeInventoryError(w, http.StatusInternalServerError, "failed to read inventory: "+err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]any{"status": 200, "item_ids": catalog.ItemIDs, "materials": materials})
|
||||
case http.MethodPost:
|
||||
var req struct {
|
||||
Updates []inventoryMaterialUpdate `json:"updates"`
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&req); err != nil || len(req.Updates) == 0 {
|
||||
writeInventoryError(w, http.StatusBadRequest, "expected a non-empty updates list")
|
||||
return
|
||||
}
|
||||
if len(req.Updates) > 3000 {
|
||||
writeInventoryError(w, http.StatusBadRequest, "too many inventory updates")
|
||||
return
|
||||
}
|
||||
seen := make(map[uint32]struct{}, len(req.Updates))
|
||||
for _, update := range req.Updates {
|
||||
if _, ok := allowed[update.ItemID]; !ok {
|
||||
writeInventoryError(w, http.StatusBadRequest, fmt.Sprintf("item %d is not in this Client catalog", update.ItemID))
|
||||
return
|
||||
}
|
||||
if _, ok := seen[update.ItemID]; ok {
|
||||
writeInventoryError(w, http.StatusBadRequest, fmt.Sprintf("duplicate item %d", update.ItemID))
|
||||
return
|
||||
}
|
||||
if update.Reset == (update.Count != nil) {
|
||||
writeInventoryError(w, http.StatusBadRequest, fmt.Sprintf("item %d must set either count or reset", update.ItemID))
|
||||
return
|
||||
}
|
||||
seen[update.ItemID] = struct{}{}
|
||||
}
|
||||
materials, err := readInventory()
|
||||
if err != nil {
|
||||
writeInventoryError(w, http.StatusInternalServerError, "failed to read inventory: "+err.Error())
|
||||
return
|
||||
}
|
||||
for _, update := range req.Updates {
|
||||
key := strconv.FormatUint(uint64(update.ItemID), 10)
|
||||
if update.Reset {
|
||||
delete(materials, key)
|
||||
} else {
|
||||
materials[key] = *update.Count
|
||||
}
|
||||
}
|
||||
raw, err := os.ReadFile("freesr-data.json")
|
||||
if err != nil {
|
||||
writeInventoryError(w, http.StatusInternalServerError, "failed to read freesr-data.json: "+err.Error())
|
||||
return
|
||||
}
|
||||
var data map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &data); err != nil {
|
||||
writeInventoryError(w, http.StatusInternalServerError, "freesr-data.json is invalid: "+err.Error())
|
||||
return
|
||||
}
|
||||
if data == nil {
|
||||
writeInventoryError(w, http.StatusInternalServerError, "freesr-data.json must contain a JSON object")
|
||||
return
|
||||
}
|
||||
materialJSON, err := json.Marshal(materials)
|
||||
if err != nil {
|
||||
writeInventoryError(w, http.StatusInternalServerError, "failed to encode inventory: "+err.Error())
|
||||
return
|
||||
}
|
||||
data["materials"] = materialJSON
|
||||
output, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
writeInventoryError(w, http.StatusInternalServerError, "failed to encode freesr-data.json: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile("freesr-data.json", output, 0o644); err != nil {
|
||||
writeInventoryError(w, http.StatusInternalServerError, "failed to save inventory: "+err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]any{"status": 200, "updated": len(req.Updates), "materials": materials})
|
||||
default:
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// apiFreesrAvatars handles querying and modifying avatar details
|
||||
func apiFreesrAvatars(w http.ResponseWriter, r *http.Request) {
|
||||
apiMu.Lock()
|
||||
@@ -660,6 +825,19 @@ func apiCommand(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, map[string]any{"error": "empty command", "status": 400})
|
||||
return
|
||||
}
|
||||
if strings.EqualFold(cmd, "/unstuck") {
|
||||
status, err := queueClientUnstuck()
|
||||
if err != nil {
|
||||
writeJSON(w, map[string]any{"success": false, "output": "Cannot queue Client UI recovery: " + err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]any{"success": true, "state": status.State, "request_id": status.RequestID, "output": "UI recovery queued for the next game heartbeat. Saved CW progress is preserved."})
|
||||
return
|
||||
}
|
||||
if parts := strings.Fields(cmd); len(parts) > 0 && strings.EqualFold(parts[0], "/cw") {
|
||||
apiCurrencyWarsCommand(w, cmd)
|
||||
return
|
||||
}
|
||||
|
||||
apiMu.Lock()
|
||||
defer apiMu.Unlock()
|
||||
@@ -777,6 +955,16 @@ func apiCommand(w http.ResponseWriter, r *http.Request) {
|
||||
// apiBattleReport handles GET (fetch latest in-game battle) and POST (import battle report)
|
||||
func apiBattleReport(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
if report, revision, live, err := latestLiveBattleReport(); err != nil {
|
||||
writeJSON(w, map[string]any{"error": err.Error(), "status": 500})
|
||||
return
|
||||
} else if live {
|
||||
writeJSON(w, map[string]any{
|
||||
"status": 200, "has_report": true, "is_live": true,
|
||||
"revision": revision, "report": report,
|
||||
})
|
||||
return
|
||||
}
|
||||
report, err := common.LoadLatestBattleReport()
|
||||
if err != nil {
|
||||
writeJSON(w, map[string]any{
|
||||
@@ -789,6 +977,8 @@ func apiBattleReport(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, map[string]any{
|
||||
"status": 200,
|
||||
"has_report": true,
|
||||
"is_live": false,
|
||||
"revision": report.Timestamp * 1000,
|
||||
"report": report,
|
||||
})
|
||||
return
|
||||
@@ -803,6 +993,7 @@ func apiBattleReport(w http.ResponseWriter, r *http.Request) {
|
||||
if report.Timestamp == 0 {
|
||||
report.Timestamp = time.Now().Unix()
|
||||
}
|
||||
clearLiveBattleReport()
|
||||
if err := common.SaveBattleReport(&report); err != nil {
|
||||
writeJSON(w, map[string]any{"error": err.Error(), "status": 500})
|
||||
return
|
||||
@@ -818,6 +1009,164 @@ func apiBattleReport(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
type liveBattleReportRequest struct {
|
||||
Active bool `json:"active"`
|
||||
Final bool `json:"final"`
|
||||
BattleInfoBase64 string `json:"battle_info_base64"`
|
||||
StatisticsBase64 string `json:"statistics_base64"`
|
||||
AvatarStatisticsBase64 []string `json:"avatar_statistics_base64"`
|
||||
TotalBattleTurns uint32 `json:"total_battle_turns"`
|
||||
TotalAutoTurns uint32 `json:"total_auto_turns"`
|
||||
RoundCnt uint32 `json:"round_cnt"`
|
||||
CostTime float64 `json:"cost_time"`
|
||||
AvatarBreakdowns []liveAvatarBreakdown `json:"avatar_breakdowns"`
|
||||
ServantStatisticsBase64 []string `json:"servant_statistics_base64"`
|
||||
ServantBreakdowns []liveAvatarBreakdown `json:"servant_breakdowns"`
|
||||
}
|
||||
|
||||
func writeLiveBattleReportError(w http.ResponseWriter, status int, message string) {
|
||||
w.WriteHeader(status)
|
||||
writeJSON(w, map[string]any{"status": status, "error": message})
|
||||
}
|
||||
|
||||
func apiBattleReportLive(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
var request liveBattleReportRequest
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 3<<20))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
writeLiveBattleReportError(w, http.StatusBadRequest, "invalid live battle request: "+err.Error())
|
||||
return
|
||||
}
|
||||
if !request.Active && !request.Final {
|
||||
clearLiveBattleReport()
|
||||
writeJSON(w, map[string]any{"status": 200, "active": false})
|
||||
return
|
||||
}
|
||||
statistics := &goproto.BattleStatistics{}
|
||||
if request.StatisticsBase64 != "" && len(request.AvatarStatisticsBase64) > 0 {
|
||||
writeLiveBattleReportError(w, http.StatusBadRequest, "send either statistics_base64 or avatar_statistics_base64")
|
||||
return
|
||||
}
|
||||
if request.StatisticsBase64 != "" {
|
||||
wire, err := base64.StdEncoding.Strict().DecodeString(request.StatisticsBase64)
|
||||
if err != nil || len(wire) > 2<<20 {
|
||||
writeLiveBattleReportError(w, http.StatusBadRequest, "statistics_base64 is invalid or too large")
|
||||
return
|
||||
}
|
||||
if err := proto.Unmarshal(wire, statistics); err != nil {
|
||||
writeLiveBattleReportError(w, http.StatusBadRequest, "invalid BattleStatistics protobuf")
|
||||
return
|
||||
}
|
||||
} else if len(request.AvatarStatisticsBase64) > 0 {
|
||||
if len(request.AvatarStatisticsBase64) > 16 {
|
||||
writeLiveBattleReportError(w, http.StatusBadRequest, "avatar_statistics_base64 has too many entries")
|
||||
return
|
||||
}
|
||||
var totalBytes int
|
||||
for _, encoded := range request.AvatarStatisticsBase64 {
|
||||
wire, err := base64.StdEncoding.Strict().DecodeString(encoded)
|
||||
if err != nil || len(wire) == 0 || len(wire) > 2<<20-totalBytes {
|
||||
writeLiveBattleReportError(w, http.StatusBadRequest, "avatar_statistics_base64 contains invalid or oversized protobuf data")
|
||||
return
|
||||
}
|
||||
totalBytes += len(wire)
|
||||
avatar := &goproto.AvatarBattleInfo{}
|
||||
if err := proto.Unmarshal(wire, avatar); err != nil || avatar.Id == 0 {
|
||||
writeLiveBattleReportError(w, http.StatusBadRequest, "avatar_statistics_base64 contains an invalid AvatarBattleInfo protobuf")
|
||||
return
|
||||
}
|
||||
statistics.BattleAvatarList = append(statistics.BattleAvatarList, avatar)
|
||||
}
|
||||
statistics.TotalBattleTurns = request.TotalBattleTurns
|
||||
statistics.TotalAutoTurns = request.TotalAutoTurns
|
||||
statistics.RoundCnt = request.RoundCnt
|
||||
statistics.CostTime = request.CostTime
|
||||
} else {
|
||||
writeLiveBattleReportError(w, http.StatusBadRequest, "active request requires statistics_base64 or avatar_statistics_base64")
|
||||
return
|
||||
}
|
||||
if len(statistics.BattleAvatarList) == 0 {
|
||||
writeLiveBattleReportError(w, http.StatusUnprocessableEntity, "BattleStatistics has no avatar entries")
|
||||
return
|
||||
}
|
||||
if err := applyLiveAvatarBreakdowns(statistics.BattleAvatarList, request.AvatarBreakdowns); err != nil {
|
||||
writeLiveBattleReportError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if err := applyLiveServantStatistics(statistics, request.ServantStatisticsBase64, request.ServantBreakdowns); err != nil {
|
||||
writeLiveBattleReportError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
var stageID uint32
|
||||
var battleID uint32
|
||||
var battleContext *goproto.SceneBattleInfo
|
||||
if request.BattleInfoBase64 != "" {
|
||||
wire, err := base64.StdEncoding.Strict().DecodeString(request.BattleInfoBase64)
|
||||
battle := &goproto.SceneBattleInfo{}
|
||||
if err != nil || len(wire) > 2<<20 || proto.Unmarshal(wire, battle) != nil {
|
||||
writeLiveBattleReportError(w, http.StatusBadRequest, "invalid battle_info_base64")
|
||||
return
|
||||
}
|
||||
stageID, battleID = battle.StageId, battle.BattleId
|
||||
battleContext = battle
|
||||
}
|
||||
for _, avatar := range statistics.BattleAvatarList {
|
||||
if stageID == 0 && avatar != nil && avatar.StageId != 0 {
|
||||
stageID = avatar.StageId
|
||||
break
|
||||
}
|
||||
}
|
||||
report := common.BattleReportFromStatistics(statistics, battleID, stageID, "LIVE")
|
||||
common.ApplyBattleReportContext(report, battleContext)
|
||||
if report == nil {
|
||||
writeLiveBattleReportError(w, http.StatusBadRequest, "unable to build live battle report")
|
||||
return
|
||||
}
|
||||
if request.Final {
|
||||
if err := saveFinalBattleDetails(report); err != nil {
|
||||
writeLiveBattleReportError(w, http.StatusConflict, err.Error())
|
||||
return
|
||||
}
|
||||
clearLiveBattleReport()
|
||||
writeJSON(w, map[string]any{"status": 200, "active": false, "final": true})
|
||||
return
|
||||
}
|
||||
liveBattleReportState.Lock()
|
||||
liveBattleReportState.report = report
|
||||
liveBattleReportState.received = time.Now()
|
||||
liveBattleReportState.Unlock()
|
||||
writeJSON(w, map[string]any{"status": 200, "active": true})
|
||||
}
|
||||
|
||||
func latestLiveBattleReport() (*common.BattleReport, int64, bool, error) {
|
||||
config, err := common.LoadRuntimeConfig()
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
liveBattleReportState.RLock()
|
||||
defer liveBattleReportState.RUnlock()
|
||||
if liveBattleReportState.report == nil || liveBattleReportState.received.IsZero() {
|
||||
return nil, 0, false, nil
|
||||
}
|
||||
staleAfter := time.Duration(config.BattleDiagnostics.LiveReportStaleSeconds) * time.Second
|
||||
if time.Since(liveBattleReportState.received) > staleAfter {
|
||||
return nil, 0, false, nil
|
||||
}
|
||||
return liveBattleReportState.report, liveBattleReportState.received.UnixMilli(), true, nil
|
||||
}
|
||||
|
||||
func clearLiveBattleReport() {
|
||||
liveBattleReportState.Lock()
|
||||
liveBattleReportState.report = nil
|
||||
liveBattleReportState.received = time.Time{}
|
||||
liveBattleReportState.Unlock()
|
||||
}
|
||||
|
||||
// apiBattleReportReset clears the current recorded battle report
|
||||
func apiBattleReportReset(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost && r.Method != http.MethodGet {
|
||||
@@ -828,6 +1177,7 @@ func apiBattleReportReset(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, map[string]any{"error": err.Error(), "status": 500})
|
||||
return
|
||||
}
|
||||
clearLiveBattleReport()
|
||||
writeJSON(w, map[string]any{
|
||||
"status": 200,
|
||||
"message": "Battle report cleared",
|
||||
|
||||
Reference in new issue
Block a user