fix: improve endgame clock and gameplay stability
This commit is contained in:
1 parent
ce6aabfc55
commit
658298ee86
46 files changed
+1915
-99
No files matched your search
+32
-31
@@ -835,35 +835,42 @@ func apiCommand(w http.ResponseWriter, r *http.Request) {
|
||||
success = false
|
||||
|
||||
case "/max":
|
||||
for _, av := range data.Avatars {
|
||||
av.Level = 80
|
||||
av.Promotion = 6
|
||||
av.Data.Rank = 6
|
||||
count, err := common.MaxAvatarProgress()
|
||||
if err != nil {
|
||||
output, success = "Cannot update avatars: "+err.Error(), false
|
||||
} else {
|
||||
output = fmt.Sprintf("All %d avatars saved at Level 80, Rank 6. Reconnect the game to load the change.", count)
|
||||
}
|
||||
output = fmt.Sprintf("[OK] All %d avatars set to Level 80, Rank 6.", len(data.Avatars))
|
||||
|
||||
case "/teleport", "/scene":
|
||||
if len(parts) > 1 {
|
||||
planeID, _ := strconv.ParseUint(parts[1], 10, 32)
|
||||
if planeID != 0 {
|
||||
data.Scene.PlaneID = uint32(planeID)
|
||||
output = fmt.Sprintf("[OK] Teleported to PlaneID %d.", planeID)
|
||||
}
|
||||
if len(parts) > 2 {
|
||||
floorID, _ := strconv.ParseUint(parts[2], 10, 32)
|
||||
if floorID != 0 {
|
||||
data.Scene.FloorID = uint32(floorID)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(parts) < 2 || len(parts) > 3 {
|
||||
output = "Usage: /teleport <plane_id> [floor_id]"
|
||||
success = false
|
||||
break
|
||||
}
|
||||
planeID, parseErr := strconv.ParseUint(parts[1], 10, 32)
|
||||
if parseErr != nil || planeID == 0 {
|
||||
output, success = "Invalid plane ID.", false
|
||||
break
|
||||
}
|
||||
var floorID uint64
|
||||
if len(parts) == 3 {
|
||||
floorID, parseErr = strconv.ParseUint(parts[2], 10, 32)
|
||||
if parseErr != nil || floorID == 0 {
|
||||
output, success = "Invalid floor ID.", false
|
||||
break
|
||||
}
|
||||
}
|
||||
data.Scene.PlaneID = uint32(planeID)
|
||||
if floorID != 0 {
|
||||
data.Scene.FloorID = uint32(floorID)
|
||||
}
|
||||
output = fmt.Sprintf("Scene saved for plane %d. Reconnect the game to load the change.", planeID)
|
||||
|
||||
case "/clear":
|
||||
if len(parts) > 1 && parts[1] == "lineup" {
|
||||
data.Lineups = make(map[uint32]uint32)
|
||||
output = "[OK] Active lineup cleared."
|
||||
if len(parts) == 2 && strings.EqualFold(parts[1], "lineup") {
|
||||
output = "The active lineup cannot be empty. Change your team in the game."
|
||||
success = false
|
||||
} else {
|
||||
output = "Usage: /clear lineup"
|
||||
success = false
|
||||
@@ -989,8 +996,8 @@ func apiCommand(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
case "/help":
|
||||
output = "/faketime [status|off|Unix seconds|RFC3339 with timezone] - Set game calendar time\n"
|
||||
output += "Available GM Commands:\n /max - Set all avatars to Max Lv.80 E6\n /teleport <plane_id> [floor_id] - Set scene\n /avatar <id> <level> [rank] - Modify avatar\n /clear lineup - Clear active lineup slots\n /unlockall warp [all|avatar|lightcone|off] - Toggle unlocked gacha banners\n /reload banner - Reload banner configurations"
|
||||
output = "/faketime [status|off|window <begin Unix seconds> <end Unix seconds>|Unix seconds|RFC3339 with timezone] - Set game calendar time\n"
|
||||
output += "Available GM Commands:\n /max - Set all avatars to Max Lv.80 E6\n /teleport <plane_id> [floor_id] - Set scene\n /avatar <id> <level> [rank] - Modify avatar\n /unlockall warp [all|avatar|lightcone|off] - Toggle unlocked gacha banners\n /reload banner - Reload banner configurations"
|
||||
|
||||
default:
|
||||
output = fmt.Sprintf("Unknown command: %s", cmd)
|
||||
@@ -1007,14 +1014,8 @@ func apiCommand(w http.ResponseWriter, r *http.Request) {
|
||||
output = err.Error()
|
||||
}
|
||||
}
|
||||
} else if success && rootCmd != "/help" && rootCmd != "/avatar" {
|
||||
raw, err := json.MarshalIndent(data, "", " ")
|
||||
if err == nil {
|
||||
err = os.WriteFile("freesr-data.json", raw, 0o644)
|
||||
}
|
||||
if err == nil {
|
||||
err = data.SavePersistentChecked()
|
||||
}
|
||||
} else if success && rootCmd != "/help" && rootCmd != "/avatar" && rootCmd != "/max" {
|
||||
err := data.SavePersistentChecked()
|
||||
if err != nil {
|
||||
success = false
|
||||
output = err.Error()
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
func apiCurrencyWarsCommand(w http.ResponseWriter, command string) {
|
||||
fail := func(err error) { writeJSON(w, map[string]any{"success": false, "output": err.Error()}) }
|
||||
parts := strings.Fields(command)
|
||||
usage := "Usage: /cw status | /cw list role|equip|orb|consumable|forge | /cw gold <total> | /cw hp <total> | /cw exp <gain> | /cw tbpower <total> [costed] | /cw equip|orb|consumable|forge <itemID> [count] (for orb: <itemID> or 'random') | /cw role <roleID> <star> [count]. Changes apply in CW Preparation."
|
||||
usage := "Usage: /cw status | /cw end | /cw list role|equip|orb|consumable|forge | /cw gold <total> | /cw hp <total> | /cw exp <gain> | /cw tbpower <total> [costed] | /cw equip|orb|consumable|forge <itemID> [count] (for orb: <itemID> or 'random') | /cw role <roleID> <star> [count]. Changes apply in CW Preparation."
|
||||
if len(parts) < 2 {
|
||||
fail(fmt.Errorf("%s", usage))
|
||||
return
|
||||
@@ -132,6 +132,19 @@ func apiCurrencyWarsCommand(w http.ResponseWriter, command string) {
|
||||
return
|
||||
}
|
||||
field := strings.ToLower(parts[1])
|
||||
if field == common.CurrencyWarsCommandEnd {
|
||||
if len(parts) != 2 {
|
||||
fail(fmt.Errorf("Usage: /cw end"))
|
||||
return
|
||||
}
|
||||
id, err := common.QueueCurrencyWarsCommand(cfg.PlayerUID, run.UniqueId, common.CurrencyWarsCommandEnd, 0)
|
||||
if err != nil {
|
||||
fail(err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]any{"success": true, "state": "queued", "request_id": id, "output": fmt.Sprintf("Queued CW end for run %d. The game server will settle it as a quit on heartbeat after any active Battle; this is not a victory. Keep the game connected and check /cw status or CW_GM logs.", run.UniqueId)})
|
||||
return
|
||||
}
|
||||
if field == "tb" || field == "trailblaze" {
|
||||
field = "tbpower"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package sdkserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
pb "gac-hkrpg-go/protocol/proto"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestCurrencyWarsEndCommandQueuesOnlyActiveRun(t *testing.T) {
|
||||
chdirRepoRootSDK(t)
|
||||
t.Setenv("GAC_CONFIG", "server.json")
|
||||
if err := os.WriteFile("persistent", []byte(`{}`), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := common.LoadCurrencyWarsCatalog()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := common.LoadRuntimeConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data := &pb.GridFightGetDataScRsp{DHFOFLFCLAH: &pb.MPIMAEMCEFH{UniqueId: 5, DivisionId: c.CapturedStages[0].ID, LJNGPCKPJLF: c.ActiveSeasonID, NAGDJGMMCBE: []*pb.GridFightGameInfo{{LEOIAOFBBFA: &pb.GridFightGameInfo_FAOCFBOFCJP{FAOCFBOFCJP: &pb.NIHNGPKHEPC{}}}}}}
|
||||
wire, err := proto.Marshal(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
snapshot := common.NewCurrencyWarsSnapshot(cfg.PlayerUID, c.ClientVersion, c.Provenance.GameAssemblySHA256, wire)
|
||||
if _, err := common.CommitCurrencyWarsSnapshot(0, snapshot); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
invoke := func(command string) map[string]any {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
apiCurrencyWarsCommand(w, command)
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
if result := invoke("/cw end now"); result["success"] != false {
|
||||
t.Fatal("end accepted extra arguments")
|
||||
}
|
||||
queued, err := common.ReadCurrencyWarsCommands()
|
||||
if err != nil || len(queued) != 0 {
|
||||
t.Fatal("invalid request queued a command")
|
||||
}
|
||||
result := invoke("/cw end")
|
||||
if result["success"] != true || result["state"] != "queued" || !strings.Contains(result["output"].(string), "quit") {
|
||||
t.Fatalf("unexpected response: %v", result)
|
||||
}
|
||||
queued, err = common.ReadCurrencyWarsCommands()
|
||||
if err != nil || len(queued) != 1 || queued[0].UID != cfg.PlayerUID || queued[0].RunID != data.DHFOFLFCLAH.UniqueId || queued[0].Field != common.CurrencyWarsCommandEnd {
|
||||
t.Fatalf("unexpected queue: %v %v", queued, err)
|
||||
}
|
||||
current, err := common.ReadCurrencyWarsSnapshot(cfg.PlayerUID, c.ClientVersion, c.Provenance.GameAssemblySHA256)
|
||||
if err != nil || current.Revision != 1 || string(current.WireData) != string(wire) {
|
||||
t.Fatal("API changed active run instead of queuing")
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func apiFakeTimeCommand(w http.ResponseWriter, cmd string) {
|
||||
parts := strings.Fields(cmd)
|
||||
usage := "Usage: /faketime [status | off | <Unix seconds> | <RFC3339 date with timezone>]. Time keeps advancing. Reopen the guide after the next heartbeat; relogin if it caches schedules."
|
||||
usage := "Usage: /faketime [status | off | window <begin Unix seconds> <end Unix seconds> | <Unix seconds> | <RFC3339 date with timezone>]. Window mode wraps the game clock at the end; off restores real time. Reopen the guide after the next heartbeat; relogin if it caches schedules."
|
||||
var now time.Time
|
||||
var err error
|
||||
arg := strings.TrimSpace(strings.TrimPrefix(cmd, parts[0]))
|
||||
@@ -21,6 +21,20 @@ func apiFakeTimeCommand(w http.ResponseWriter, cmd string) {
|
||||
case "off", "reset", "real":
|
||||
now, err = common.SetGameTime(nil)
|
||||
default:
|
||||
if len(parts) > 1 && strings.EqualFold(parts[1], "window") {
|
||||
if len(parts) != 4 {
|
||||
err = fmt.Errorf("window requires begin and end Unix seconds")
|
||||
} else {
|
||||
begin, e1 := strconv.ParseInt(parts[2], 10, 64)
|
||||
end, e2 := strconv.ParseInt(parts[3], 10, 64)
|
||||
if e1 != nil || e2 != nil || begin < 0 || end > 253402300799 || end <= begin {
|
||||
err = fmt.Errorf("invalid window Unix seconds")
|
||||
} else {
|
||||
now, err = common.SetGameTimeWindow(time.Unix(begin, 0), time.Unix(end, 0))
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
var target time.Time
|
||||
if seconds, e := strconv.ParseInt(arg, 10, 64); e == nil {
|
||||
if seconds < 0 || seconds > 253402300799 {
|
||||
@@ -44,5 +58,5 @@ func apiFakeTimeCommand(w http.ResponseWriter, cmd string) {
|
||||
writeJSON(w, map[string]any{"success": false, "output": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]any{"success": true, "command": cmd, "game_time": now.UTC().Format(time.RFC3339), "unix_seconds": now.Unix(), "offset_ms": state.OffsetMS, "output": fmt.Sprintf("Game time: %s (offset %d ms). %s", now.UTC().Format(time.RFC3339), state.OffsetMS, usage)})
|
||||
writeJSON(w, map[string]any{"success": true, "command": cmd, "game_time": now.UTC().Format(time.RFC3339), "unix_seconds": now.Unix(), "offset_ms": state.OffsetMS, "window_begin_ms": state.WindowBeginMS, "window_end_ms": state.WindowEndMS, "output": fmt.Sprintf("Game time: %s (offset %d ms, window %d..%d ms). %s", now.UTC().Format(time.RFC3339), state.OffsetMS, state.WindowBeginMS, state.WindowEndMS, usage)})
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package sdkserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
)
|
||||
|
||||
func TestFakeTimeWindowCommand(t *testing.T) {
|
||||
chdirRepoRootSDK(t)
|
||||
t.Setenv("GAC_CONFIG", "server.json")
|
||||
t.Setenv("GAC_GAME_CLOCK_STATE_FILE", filepath.Join(t.TempDir(), "clock.json"))
|
||||
call := func(command string) map[string]any {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
apiFakeTimeCommand(w, command)
|
||||
var response map[string]any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return response
|
||||
}
|
||||
if result := call("/faketime window 1000 2000"); result["success"] != true || result["unix_seconds"] != float64(1500) {
|
||||
t.Fatalf("window command: %v", result)
|
||||
}
|
||||
before, err := common.ReadGameClock()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result := call("/faketime window 2000 1000"); result["success"] != false {
|
||||
t.Fatalf("invalid window accepted: %v", result)
|
||||
}
|
||||
after, err := common.ReadGameClock()
|
||||
if err != nil || before != after {
|
||||
t.Fatalf("invalid window changed clock: %+v %v", after, err)
|
||||
}
|
||||
if result := call("/faketime off"); result["success"] != true {
|
||||
t.Fatalf("reset: %v", result)
|
||||
}
|
||||
if state, err := common.ReadGameClock(); err != nil || state.WindowEndMS != 0 {
|
||||
t.Fatalf("reset did not remove window: %+v %v", state, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package sdkserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMaxCommandPreservesUnknownData(t *testing.T) {
|
||||
chdirRepoRootSDK(t)
|
||||
raw, err := os.ReadFile("freesr-data.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var root map[string]json.RawMessage
|
||||
if err = json.Unmarshal(raw, &root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root["future_root"] = json.RawMessage(`{"keep":true}`)
|
||||
var avatars map[string]json.RawMessage
|
||||
if err = json.Unmarshal(root["avatars"], &avatars); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var avatar map[string]json.RawMessage
|
||||
if err = json.Unmarshal(avatars["1204"], &avatar); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
avatar["future_avatar"] = json.RawMessage(`"preserved"`)
|
||||
avatars["1204"], _ = json.Marshal(avatar)
|
||||
root["avatars"], _ = json.Marshal(avatars)
|
||||
raw, _ = json.Marshal(root)
|
||||
if err = os.WriteFile("freesr-data.json", raw, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
beforePersistent, err := os.ReadFile("persistent")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
apiCommand(w, httptest.NewRequest(http.MethodPost, "/api/command", strings.NewReader(`{"command":"/max"}`)))
|
||||
var result struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
if err = json.Unmarshal(w.Body.Bytes(), &result); err != nil || !result.Success {
|
||||
t.Fatalf("command failed: %s (%v)", w.Body.String(), err)
|
||||
}
|
||||
raw, _ = os.ReadFile("freesr-data.json")
|
||||
if err = json.Unmarshal(raw, &root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var futureRoot struct {
|
||||
Keep bool `json:"keep"`
|
||||
}
|
||||
if err = json.Unmarshal(root["future_root"], &futureRoot); err != nil || !futureRoot.Keep {
|
||||
t.Fatal("root data lost")
|
||||
}
|
||||
if err = json.Unmarshal(root["avatars"], &avatars); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = json.Unmarshal(avatars["1204"], &avatar); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var data struct {
|
||||
Rank int `json:"rank"`
|
||||
}
|
||||
if err = json.Unmarshal(avatar["data"], &data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(avatar["future_avatar"]) != `"preserved"` || string(avatar["level"]) != "80" || string(avatar["promotion"]) != "6" || data.Rank != 6 {
|
||||
t.Fatal("avatar fields lost or not updated")
|
||||
}
|
||||
afterPersistent, err := os.ReadFile("persistent")
|
||||
if err != nil || string(beforePersistent) != string(afterPersistent) {
|
||||
t.Fatal("persistent data was rewritten")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package sdkserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSceneCommandsPreserveImportedSave(t *testing.T) {
|
||||
chdirRepoRootSDK(t)
|
||||
before, err := os.ReadFile("freesr-data.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
run := func(command string) bool {
|
||||
body, _ := json.Marshal(map[string]string{"command": command})
|
||||
w := httptest.NewRecorder()
|
||||
apiCommand(w, httptest.NewRequest(http.MethodPost, "/api/command", strings.NewReader(string(body))))
|
||||
var result struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return result.Success
|
||||
}
|
||||
persistentBefore, err := os.ReadFile("persistent")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, invalid := range []string{"/teleport invalid", "/scene 0", "/teleport 123 bad", "/teleport 123 456 extra", "/clear lineup"} {
|
||||
if run(invalid) {
|
||||
t.Fatalf("accepted %s", invalid)
|
||||
}
|
||||
after, _ := os.ReadFile("persistent")
|
||||
if string(after) != string(persistentBefore) {
|
||||
t.Fatalf("invalid command %s wrote persistent", invalid)
|
||||
}
|
||||
}
|
||||
for _, command := range []string{"/teleport 123 456"} {
|
||||
if !run(command) {
|
||||
t.Fatalf("command failed: %s", command)
|
||||
}
|
||||
after, err := os.ReadFile("freesr-data.json")
|
||||
if err != nil || string(after) != string(before) {
|
||||
t.Fatalf("%s changed imported save", command)
|
||||
}
|
||||
}
|
||||
raw, err := os.ReadFile("persistent")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var saved struct {
|
||||
Scene struct {
|
||||
PlaneID uint32 `json:"plane_id"`
|
||||
FloorID uint32 `json:"floor_id"`
|
||||
} `json:"scene"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &saved); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.Scene.PlaneID != 123 || saved.Scene.FloorID != 456 {
|
||||
t.Fatalf("persistent changes missing: %+v", saved)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package sdkserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBeforeVerifyReturnsDataObject(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
beforeVerifyReply(w)
|
||||
var body struct {
|
||||
Retcode int `json:"retcode"`
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.Code != 200 || body.Retcode != 0 || body.Data == nil {
|
||||
t.Fatalf("beforeVerify response cannot initialize SDK data: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package sdkserver
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadHotfixIgnoresEmptyCacheWhenBundledEntryExists(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
if err := os.WriteFile("hotfix.json", []byte(`{"bundled":{"asset_bundle_url":"bundle"},"override":{"asset_bundle_url":"old"}}`), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile("versions.json", []byte(`{"bundled":{},"override":{"asset_bundle_url":"user"},"cached":{"asset_bundle_url":"cached"}}`), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := loadHotfix()
|
||||
if got["bundled"].AssetBundleURL != "bundle" || got["override"].AssetBundleURL != "user" || got["cached"].AssetBundleURL != "cached" {
|
||||
t.Fatalf("incorrect hotfix cache merge: %+v", got)
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,21 @@ func loadHotfix() map[string]VersionConfig {
|
||||
if raw, err := os.ReadFile("hotfix.json"); err == nil {
|
||||
_ = json.Unmarshal(raw, &out)
|
||||
}
|
||||
if out == nil {
|
||||
out = map[string]VersionConfig{}
|
||||
}
|
||||
if raw, err := os.ReadFile("versions.json"); err == nil {
|
||||
_ = json.Unmarshal(raw, &out)
|
||||
var cached map[string]VersionConfig
|
||||
if json.Unmarshal(raw, &cached) == nil {
|
||||
for version, cfg := range cached {
|
||||
// A failed fetch previously persisted an empty cache entry. It
|
||||
// must not hide usable bundled metadata for the same version.
|
||||
if cfg == (VersionConfig{}) && out[version] != (VersionConfig{}) {
|
||||
continue
|
||||
}
|
||||
out[version] = cfg
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -201,6 +214,7 @@ func queryGateway(state *appState) http.HandlerFunc {
|
||||
_ = params{}
|
||||
|
||||
cfg := state.getOrInsertHotfix(version, seed)
|
||||
Log(fmt.Sprintf("[gateway] version=%q game=%s:%d asset=%t ex_resource=%t lua=%t", version, state.runtime.PublicGameHost, state.runtime.GamePort, cfg.AssetBundleURL != "", cfg.ExResourceURL != "", cfg.LuaURL != ""))
|
||||
if state.runtime.LuaOverlay.Enabled {
|
||||
cfg.LuaURL = state.runtime.PublicSDKURL + luaOverlayRoute
|
||||
}
|
||||
@@ -305,6 +319,12 @@ func (a *appState) granterLoginReply(w http.ResponseWriter) {
|
||||
})
|
||||
}
|
||||
|
||||
// The Android SDK decodes data as BeforeVerifyEntity before continuing
|
||||
// into the game. An empty object supplies its default false flags.
|
||||
func beforeVerifyReply(w http.ResponseWriter) {
|
||||
writeJSON(w, map[string]any{"data": map[string]any{}, "message": "OK", "retcode": 0})
|
||||
}
|
||||
|
||||
func riskyCheckReply(w http.ResponseWriter) {
|
||||
writeJSON(w, map[string]any{
|
||||
"data": map[string]any{
|
||||
@@ -628,6 +648,7 @@ func Listen() error {
|
||||
mux.HandleFunc("/{product_name}/combo/granter/api/getConfig", func(w http.ResponseWriter, _ *http.Request) { comboGetConfigReply(w) })
|
||||
mux.HandleFunc("/{product_name}/combo/granter/api/compareProtocolVersion", func(w http.ResponseWriter, _ *http.Request) { compareProtocolVersionReply(w) })
|
||||
mux.HandleFunc("/{product_name}/combo/granter/login/v2/login", func(w http.ResponseWriter, _ *http.Request) { state.granterLoginReply(w) })
|
||||
mux.HandleFunc("/{product_name}/combo/granter/login/beforeVerify", func(w http.ResponseWriter, _ *http.Request) { beforeVerifyReply(w) })
|
||||
mux.HandleFunc("/{product_name}/mdk/agreement/api/getAgreementInfos", func(w http.ResponseWriter, _ *http.Request) { agreementInfosReply(w) })
|
||||
|
||||
mux.HandleFunc("/combo/box/api/config/sdk/combo", func(w http.ResponseWriter, _ *http.Request) { comboConfigReply(w) })
|
||||
@@ -664,6 +685,8 @@ func Listen() error {
|
||||
state.loginReply(w, r)
|
||||
case strings.Contains(path, "combo/granter/login/v2/login"):
|
||||
state.granterLoginReply(w)
|
||||
case strings.Contains(path, "combo/granter/login/beforeVerify"):
|
||||
beforeVerifyReply(w)
|
||||
case strings.Contains(path, "mdk/shield/api/loadConfig"):
|
||||
shieldLoadConfigReply(w)
|
||||
case strings.Contains(path, "combo/granter/api/getConfig"):
|
||||
|
||||
Reference in new issue
Block a user