Files
GaCy-GO/internal/sdkserver/api_client_unstuck.go
T

188 lines
6.7 KiB
Go
Raw Normal View History

package sdkserver
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
const clientUnstuckRequestFile = "scripts/client_unstuck_request.lua"
type clientUnstuckStatus struct {
RequestID string `json:"request_id"`
State string `json:"state"`
Error string `json:"error,omitempty"`
QueuedAt int64 `json:"queued_at"`
CompletedAt int64 `json:"completed_at,omitempty"`
}
var clientUnstuckState struct {
sync.Mutex
status clientUnstuckStatus
}
func queueClientUnstuck() (clientUnstuckStatus, error) {
clientUnstuckState.Lock()
defer clientUnstuckState.Unlock()
var nonce [16]byte
if _, err := rand.Read(nonce[:]); err != nil {
return clientUnstuckStatus{}, err
}
id := hex.EncodeToString(nonce[:])
// Only a generated hexadecimal nonce is interpolated. No user-supplied
// command, path, endpoint or Lua source can enter this heartbeat script.
script := fmt.Sprintf(clientUnstuckLua, id)
if err := os.MkdirAll(filepath.Dir(clientUnstuckRequestFile), 0700); err != nil {
return clientUnstuckStatus{}, err
}
file, err := os.CreateTemp(filepath.Dir(clientUnstuckRequestFile), ".client-unstuck-*.tmp")
if err != nil {
return clientUnstuckStatus{}, err
}
defer os.Remove(file.Name())
if _, err = file.WriteString(script); err != nil {
file.Close()
return clientUnstuckStatus{}, err
}
if err = file.Close(); err != nil {
return clientUnstuckStatus{}, err
}
if err = os.Rename(file.Name(), clientUnstuckRequestFile); err != nil {
return clientUnstuckStatus{}, err
}
status := clientUnstuckStatus{RequestID: id, State: "queued", QueuedAt: time.Now().UnixMilli()}
clientUnstuckState.status = status
Log("[UNSTUCK] UI recovery queued request=" + id)
return status, nil
}
func apiClientUnstuck(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
clientUnstuckState.Lock()
status := clientUnstuckState.status
clientUnstuckState.Unlock()
writeJSON(w, status)
case http.MethodPost:
status, err := queueClientUnstuck()
if err != nil {
http.Error(w, "cannot queue UI recovery", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
writeJSON(w, status)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func apiClientUnstuckResult(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var result struct {
RequestID string `json:"request_id"`
Success bool `json:"success"`
Error string `json:"error"`
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8192))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&result); err != nil {
http.Error(w, "invalid recovery result", http.StatusBadRequest)
return
}
clientUnstuckState.Lock()
defer clientUnstuckState.Unlock()
if result.RequestID == "" || result.RequestID != clientUnstuckState.status.RequestID {
http.Error(w, "recovery request is no longer current", http.StatusConflict)
return
}
clientUnstuckState.status.State = "failed"
if result.Success {
clientUnstuckState.status.State = "completed"
}
clientUnstuckState.status.Error = result.Error
clientUnstuckState.status.CompletedAt = time.Now().UnixMilli()
2026-10-28 18:54:43 +07:00
// Queueing and acknowledgement share the lock, so removing this owned
// request cannot delete a newer recovery. A leftover script still checks
// the live nonce before touching UI, including after an SDK restart.
if err := os.Remove(clientUnstuckRequestFile); err != nil && !os.IsNotExist(err) {
Log("[UNSTUCK] cannot remove completed request: " + err.Error())
}
Log("[UNSTUCK] UI recovery " + clientUnstuckState.status.State + " request=" + result.RequestID)
writeJSON(w, clientUnstuckState.status)
}
// Named UIManager API observed on the current Client. Clear only network UI
// waits: never close a Battle, change a scene, fabricate a successful gameplay
// response, erase a run, or reset the Client's loading/phase flags.
const clientUnstuckLua = `do
local id = %q
2026-10-28 18:54:43 +07:00
local function tick()
local url=_G.__GAC_SRTOOL_LIVE_URL
if type(url)~='string' or url=='' then return end
local s=_G.__GAC_UNSTUCK_WORK
if not s or s.id~=id then
if s and s.request then pcall(function()s.request:Dispose()end) end
s={id=id,phase='check',next=0};_G.__GAC_UNSTUCK_WORK=s
end
if s.phase=='done' or s.phase=='stale' then return end
local now=CS.UnityEngine.Time.unscaledTime
if s.request then
if not s.request.isDone then return end
local request=s.request;s.request=nil
local ok,err=pcall(function()
local code=request.responseCode
if code<200 or code>=300 then error('recovery HTTP '..tostring(code)) end
if s.phase=='check' then
local status=CS.Newtonsoft.Json.Linq.JObject.Parse(request.downloadHandler.text)
local request_id=status:get_Item('request_id')
local phase=status:get_Item('state')
if not request_id or tostring(request_id)~=id or not phase or tostring(phase)~='queued' then
s.phase='stale';return
end
-- Only the current queued SDK request authorizes a UI recovery. Old
-- generated files are inert after reconnect or SDK process restart.
local success,message=pcall(function()
local ui=CS.RPG.Client.GlobalVars.s_UIManager
if not ui then error('Client UIManager is unavailable') end
ui:ClearWaitNetPacketFullScreenBlock()
end)
s.body='{"request_id":"'..id..'","success":'..tostring(success)..',"error":'..CS.Newtonsoft.Json.JsonConvert.SerializeObject(tostring(message or ''))..'}'
s.phase='ack'
CS.UnityEngine.Debug.Log('[GAC_UNSTUCK] request='..id..' success='..tostring(success)..' '..tostring(message or ''))
else
s.phase='done';_G.__GAC_UNSTUCK_LAST=id
end
end)
pcall(function()request:Dispose()end)
if not ok then
s.next=now+5
if s.error~=tostring(err) then CS.UnityEngine.Debug.Log('[GAC_UNSTUCK] '..tostring(err));s.error=tostring(err) end
return
end
end
if s.phase=='done' or s.phase=='stale' or now<s.next then return end
local req
if s.phase=='check' then
req=CS.UnityEngine.Networking.UnityWebRequest.Get(url..'/api/client/unstuck')
else
req=CS.UnityEngine.Networking.UnityWebRequest(url..'/api/client/unstuck/result','POST')
req.uploadHandler=CS.UnityEngine.Networking.UploadHandlerRaw(CS.System.Text.Encoding.UTF8:GetBytes(s.body))
req.downloadHandler=CS.UnityEngine.Networking.DownloadHandlerBuffer()
2026-10-28 18:54:43 +07:00
req:SetRequestHeader('Content-Type','application/json')
end
req.timeout=10;s.request=req;req:SendWebRequest()
end
2026-10-28 18:54:43 +07:00
local ok,err=pcall(tick)
if not ok then CS.UnityEngine.Debug.Log('[GAC_UNSTUCK] '..tostring(err)) end
end
`