fix(go): repair migration wire contracts and runtime lifecycle; document incomplete port
This commit is contained in:
1 parent
e43a310617
commit
32e47014b5
35 files changed
+16760
-372
No files matched your search
@@ -1,5 +1,7 @@
|
|||||||
# Ananta (Project Mugen) CBT 4229938 Private Server
|
# Ananta (Project Mugen) CBT 4229938 Private Server
|
||||||
|
|
||||||
|
> **สถานะ Go port:** ยังไม่เทียบเท่า C# เดิม การตรวจพบฟังก์ชันที่ยังเป็น stub 2,127 ตัวใน 223 ไฟล์ และ router ที่สร้างจาก transpiler ยังไม่เชื่อมกับ executable ดูบั๊กที่แก้แล้วและงานที่ยังขาดใน [GO_MIGRATION_AUDIT.md](docs/GO_MIGRATION_AUDIT.md) ก่อนอ้างอิงรายการฟีเจอร์ด้านล่าง
|
||||||
|
|
||||||
เซิร์ฟเวอร์ส่วนตัว (Private Server) และระบบ Gateway Proxy สำหรับเกม **Ananta (Project Mugen / 代号:无限大)** ไคลเอนต์เวอร์ชัน **CBT 4229938** พัฒนาขึ้นมาด้วย **Native Go Engine (`ananta-server.exe`)** ประสิทธิภาพสูง เบา กินแรมน้อย และรันได้ในไฟล์เดียว (Single Binary) เพื่อจำลองการเชื่อมต่อและเล่นเกมในโหมด Sandbox / Free-Roam ได้อย่างสมบูรณ์แบบ รองรับทั้งการเข้าเล่นผ่าน PC และ Mobile APK
|
เซิร์ฟเวอร์ส่วนตัว (Private Server) และระบบ Gateway Proxy สำหรับเกม **Ananta (Project Mugen / 代号:无限大)** ไคลเอนต์เวอร์ชัน **CBT 4229938** พัฒนาขึ้นมาด้วย **Native Go Engine (`ananta-server.exe`)** ประสิทธิภาพสูง เบา กินแรมน้อย และรันได้ในไฟล์เดียว (Single Binary) เพื่อจำลองการเชื่อมต่อและเล่นเกมในโหมด Sandbox / Free-Roam ได้อย่างสมบูรณ์แบบ รองรับทั้งการเข้าเล่นผ่าน PC และ Mobile APK
|
||||||
|
|
||||||
> 📖 **รายการระบบและฟีเจอร์ทั้งหมดของเซิร์ฟเวอร์**: ดูรายละเอียดเชิงลึกและสถานะฟีเจอร์ทั้งหมดได้ที่ [docs/FEATURES.md](docs/FEATURES.md)
|
> 📖 **รายการระบบและฟีเจอร์ทั้งหมดของเซิร์ฟเวอร์**: ดูรายละเอียดเชิงลึกและสถานะฟีเจอร์ทั้งหมดได้ที่ [docs/FEATURES.md](docs/FEATURES.md)
|
||||||
@@ -10,7 +12,7 @@
|
|||||||
|
|
||||||
1. **Windows 10 / 11 (64-bit)**
|
1. **Windows 10 / 11 (64-bit)**
|
||||||
2. ตัวเกม **Ananta / Project Mugen (CBT Client build 4229938 หรือ Mobile APK 4063976)**
|
2. ตัวเกม **Ananta / Project Mugen (CBT Client build 4229938 หรือ Mobile APK 4063976)**
|
||||||
3. (ตัวเลือกเสริมสำหรับการพัฒนา/คอมไพล์) **Go 1.22+**: [ดาวน์โหลดที่นี่](https://go.dev/dl/)
|
3. (สำหรับการพัฒนา/คอมไพล์) **Go 1.27+** ตาม `go.mod`: [ดาวน์โหลดที่นี่](https://go.dev/dl/)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -9,11 +9,11 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"ananta-ps/internal/config"
|
"ananta-ps/internal/config"
|
||||||
"ananta-ps/pkg/protocol"
|
|
||||||
"ananta-ps/internal/gameserver"
|
"ananta-ps/internal/gameserver"
|
||||||
"ananta-ps/internal/gateway"
|
"ananta-ps/internal/gateway"
|
||||||
"ananta-ps/internal/logingate"
|
"ananta-ps/internal/logingate"
|
||||||
"ananta-ps/internal/web"
|
"ananta-ps/internal/web"
|
||||||
|
"ananta-ps/pkg/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
const banner = `
|
const banner = `
|
||||||
@@ -34,7 +34,9 @@ func main() {
|
|||||||
fmt.Print(banner)
|
fmt.Print(banner)
|
||||||
|
|
||||||
cfg, err := config.LoadConfig(*configPath)
|
cfg, err := config.LoadConfig(*configPath)
|
||||||
_ = protocol.LoadMethodCatalog("data/client_data/MethodIds.json")
|
if err := protocol.LoadMethodCatalog("data/client_data/MethodIds.json"); err != nil {
|
||||||
|
log.Printf("[PROTOCOL] Using compiled method catalog: %v", err)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("[FATAL] Could not load configuration: %v", err)
|
log.Fatalf("[FATAL] Could not load configuration: %v", err)
|
||||||
}
|
}
|
||||||
@@ -45,7 +47,7 @@ func main() {
|
|||||||
// 1. Start Gateway (Port 443, 80, 5801)
|
// 1. Start Gateway (Port 443, 80, 5801)
|
||||||
gw := gateway.New(cfg, "", "")
|
gw := gateway.New(cfg, "", "")
|
||||||
if err := gw.Start(); err != nil {
|
if err := gw.Start(); err != nil {
|
||||||
log.Printf("[GATEWAY] Error starting gateway: %v", err)
|
log.Fatalf("[GATEWAY] Failed to start gateway: %v", err)
|
||||||
}
|
}
|
||||||
defer gw.Stop()
|
defer gw.Stop()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Go migration audit — 2026-10-29
|
||||||
|
|
||||||
|
Compared Gemini's Go branch (`e43a310`) with the last pre-migration C# revision
|
||||||
|
`ca2e80f`, the shipped `MethodIds.json`, and the original wire contracts.
|
||||||
|
Compilation does not establish feature parity. This port is **incomplete**.
|
||||||
|
|
||||||
|
## Fixed in this review
|
||||||
|
|
||||||
|
- Corrected 65 active method IDs against the C# reference, including world entry,
|
||||||
|
character switching, teleport, vehicle, dialog and wardrobe methods. Removed
|
||||||
|
23 fabricated constants/routes rather than assigning unrelated retail messages
|
||||||
|
to invented handlers. `GateLogin` is an alias for the verified `Login` method.
|
||||||
|
- Restored 4,544 verified method names and 2,356 return contracts from C# metadata.
|
||||||
|
Unknown invokes now use the exact neutral payload required by their return
|
||||||
|
reader. A truly unknown invoke returns an error. Neutral replies do not implement
|
||||||
|
gameplay or persistence.
|
||||||
|
- Unknown notification names resolve to zero and are rejected before writing.
|
||||||
|
Guessed fallback IDs can no longer invoke an unrelated client handler.
|
||||||
|
- Corrected `EnterSceneInfo` loading/link type markers, integer widths and list
|
||||||
|
counts, ground-data byte fields, and nullable teleport tail fields.
|
||||||
|
- Player login data now patches account and Unicode display name as well as
|
||||||
|
aid/pid, parsing offsets and rebuilding variable-length strings. Remaining
|
||||||
|
template bytes are preserved. This is still a template, not a replacement for
|
||||||
|
the original dynamic `RuntimePayloadFactory`.
|
||||||
|
- Restored configured `SyncRoleList` behavior and the default empty-list format;
|
||||||
|
the legacy bare-pid shape is available only when explicitly configured.
|
||||||
|
- Returns are emitted only for invokes, including valid invoke ID zero.
|
||||||
|
Return/unknown packets and non-RPC frame modes are not dispatched as requests.
|
||||||
|
- Movement now handles the actual list contracts for `AskReportLogicAgentSyncData`
|
||||||
|
and `AskUnitMoveActionSimple`, validating the entire body before changing state.
|
||||||
|
- Hangup/resume gates background traffic; traffic waits for scene readiness.
|
||||||
|
Character switching sets current-spirit identity before creating the new entity.
|
||||||
|
- Heartbeats share the encrypted write lock with RPC output. Failed writes close
|
||||||
|
the stream because its cipher state has advanced. Writes have deadlines; short
|
||||||
|
writes are completed. Oversized/unterminated varints return errors.
|
||||||
|
- Login/game shutdown closes tracked sockets, including clients stalled in
|
||||||
|
handshake, and waits for connection handlers. Traffic starts only after binding
|
||||||
|
succeeds and stops with its worker joined.
|
||||||
|
- Gateway startup reports TLS/bind failures synchronously and rolls back partially
|
||||||
|
opened listeners. Admin/debug binding errors are returned to the caller. Admin
|
||||||
|
binding respects `admin.host` instead of using the public network bind host.
|
||||||
|
- Admin coordinate parsing rejects invalid/non-finite input and propagates
|
||||||
|
teleport failures. Time/weather commands use the verified client IDs and wire
|
||||||
|
fields instead of nonexistent `SyncTimeWeather`/`SyncTimeOfDay` messages.
|
||||||
|
- The item catalog reads `ItemCatalog.json` or the shipped `ConsumableConfig.json`,
|
||||||
|
projects typed entries, implements lookup, and caches by source path rather than
|
||||||
|
sharing the first directory's table with every caller.
|
||||||
|
|
||||||
|
## Remaining migration gaps
|
||||||
|
|
||||||
|
The reproducible static inventory is [GO_MIGRATION_INVENTORY.json](GO_MIGRATION_INVENTORY.json).
|
||||||
|
Run `python tools/migration/audit_go.py` to refresh it, or add `--check` to fail
|
||||||
|
while incomplete artifacts remain. Its counts detect explicit generator markers;
|
||||||
|
unmarked omissions and incorrect gameplay behavior require further review.
|
||||||
|
|
||||||
|
| Finding | Evidence / impact |
|
||||||
|
| --- | --- |
|
||||||
|
| 2,127 placeholder functions across 223 files | `Router implementation for` and `Catalog helper` bodies return nil, zero, true or false without the original logic. |
|
||||||
|
| Generated router package disconnected | `go list -deps ./cmd/ananta-server` does not contain `internal/gameserver/routers`. Its 155-file port claim does not describe the running server. |
|
||||||
|
| Catalog package disconnected | The running binary does not import `internal/catalogs`; repairing item loading alone does not restore inventory grants. |
|
||||||
|
| 74 generated catalog input references missing from shipped Configs | Includes `.json.json` paths and fabricated catalog filenames. Some require data export or joining several real config tables, not simply removing an extension. |
|
||||||
|
| 16 unresolved notification call sites in the active server | Calls remain in simplified vehicle/NPC/combat/police handlers. They now fail instead of sending guessed IDs; the missing contracts still need porting. |
|
||||||
|
| Most original configuration ignored | The active `Config` only binds a subset. Generated settings structs are not equivalent to integrating content/gameplay/UI/path settings into runtime behavior. |
|
||||||
|
| State/persistence and gameplay simplifications | Original progress, wallet, inventory, gacha, ownership, NPC behavior, police, minigames, scene transitions and many other handlers need their real logic ported and connected. A generic OK cannot restore them. |
|
||||||
|
| Fixed player template and hardcoded content | Login payloads, clothing, spawn presets, traffic lanes and several responses still contain simplified/static data. Rebuilding the original dynamic factories remains necessary. |
|
||||||
|
| Proxy parity incomplete | Existing HTTP response tests do not establish equivalence to all legacy proxy endpoints, MPay flows, patch assets or mobile clients. |
|
||||||
|
|
||||||
|
Do not connect the generated routers wholesale: many helper signatures use `any`,
|
||||||
|
drop C# `out`/`ref` behavior, or have placeholder success values. Restore one
|
||||||
|
subsystem's contracts, catalog transformations, state and handlers together, then
|
||||||
|
register it and validate it with a real client.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- `go test ./...` and `go vet ./...` pass after the fixes.
|
||||||
|
- Regression tests check independently referenced IDs and payload bytes, variable
|
||||||
|
player strings, fallback shapes, role-list behavior, invoke/notify distinction,
|
||||||
|
movement validation, socket shutdown, bind rollback, admin host and item loading.
|
||||||
|
- `go test -race ./...` could not run: this host has CGO disabled and no C compiler
|
||||||
|
available on PATH. Passing ordinary tests is not evidence of race-detector coverage.
|
||||||
|
- No real game-client or mobile playthrough was performed. The fixes are not a
|
||||||
|
claim that world entry or all advertised gameplay now works end to end.
|
||||||
|
|
||||||
|
Regenerate recovered protocol metadata with
|
||||||
|
`python tools/migration/generate_protocol.py`; its input is pinned to `ca2e80f`.
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,18 +1,14 @@
|
|||||||
package catalogs
|
package catalogs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = json.RawMessage{}
|
|
||||||
var _ = sync.Mutex{}
|
|
||||||
var _ = os.ReadFile
|
|
||||||
var _ = filepath.Join
|
|
||||||
|
|
||||||
// One grantable item row surfaced by the 道具 panel section.
|
|
||||||
type ItemCatalogEntry4229938 struct {
|
type ItemCatalogEntry4229938 struct {
|
||||||
Id uint32 `json:"id"`
|
Id uint32 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -20,62 +16,64 @@ type ItemCatalogEntry4229938 struct {
|
|||||||
SubType uint32 `json:"subType"`
|
SubType uint32 `json:"subType"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
var itemCatalogState = struct {
|
||||||
// Build-4229938 backpack item catalog, read from ItemCatalog.json in the configured client-data
|
sync.RWMutex
|
||||||
// directory.
|
byPath map[string][]ItemCatalogEntry4229938
|
||||||
// That file is a slim projection of the client's ConsumableConfig.json — the only table in the
|
current []ItemCatalogEntry4229938
|
||||||
// config dump whose rows carry the LT.ConfigGen.ICommonItemConfig interface fields, i.e.
|
}{byPath: make(map[string][]ItemCatalogEntry4229938)}
|
||||||
// the real backpack item table (Id / Name / Quality / SubType). Ids live in 36000000..36999999,
|
|
||||||
// which also contains the three wallet currencies (36920001/2/3); callers that grant "everything"
|
|
||||||
// must skip those so the wallet and the backpack do not disagree.
|
|
||||||
//
|
|
||||||
var (
|
|
||||||
ItemCatalog4229938_cacheLock sync.RWMutex
|
|
||||||
ItemCatalog4229938_cached = false
|
|
||||||
ItemCatalog4229938_entries []any
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoadItemCatalog4229938 loads catalog records from client config directory
|
// LoadItemCatalog4229938 accepts the legacy slim ItemCatalog table or the
|
||||||
|
// ConsumableConfig projection shipped in this Go repository. Cache keys include
|
||||||
|
// the source path, so loading another configuration cannot reuse the first table.
|
||||||
func LoadItemCatalog4229938(configDir string) ([]any, error) {
|
func LoadItemCatalog4229938(configDir string) ([]any, error) {
|
||||||
ItemCatalog4229938_cacheLock.RLock()
|
path, err := filepath.Abs(filepath.Join(configDir, "ItemCatalog.json"))
|
||||||
if ItemCatalog4229938_cached {
|
|
||||||
defer ItemCatalog4229938_cacheLock.RUnlock()
|
|
||||||
return ItemCatalog4229938_entries, nil
|
|
||||||
}
|
|
||||||
ItemCatalog4229938_cacheLock.RUnlock()
|
|
||||||
|
|
||||||
ItemCatalog4229938_cacheLock.Lock()
|
|
||||||
defer ItemCatalog4229938_cacheLock.Unlock()
|
|
||||||
if ItemCatalog4229938_cached { return ItemCatalog4229938_entries, nil }
|
|
||||||
|
|
||||||
filePath := filepath.Join(configDir, "ItemCatalog.json.json")
|
|
||||||
data, err := os.ReadFile(filePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
itemCatalogState.Lock()
|
||||||
var raw struct {
|
defer itemCatalogState.Unlock()
|
||||||
Records []json.RawMessage `json:"records"`
|
entries, ok := itemCatalogState.byPath[path]
|
||||||
|
if !ok {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
data, err = os.ReadFile(filepath.Join(configDir, "ConsumableConfig.json"))
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(data, &raw); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
var raw struct {
|
||||||
var result []any
|
Records []ItemCatalogEntry4229938 `json:"records"`
|
||||||
for _, item := range raw.Records {
|
}
|
||||||
var m map[string]any
|
if err = json.Unmarshal(data, &raw); err != nil {
|
||||||
if err := json.Unmarshal(item, &m); err == nil {
|
return nil, err
|
||||||
result = append(result, m)
|
}
|
||||||
|
if raw.Records == nil {
|
||||||
|
return nil, fmt.Errorf("item catalog missing records array")
|
||||||
|
}
|
||||||
|
entries = make([]ItemCatalogEntry4229938, 0, len(raw.Records))
|
||||||
|
for _, entry := range raw.Records {
|
||||||
|
if entry.Id != 0 {
|
||||||
|
entries = append(entries, entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
itemCatalogState.byPath[path] = entries
|
||||||
ItemCatalog4229938_entries = result
|
}
|
||||||
ItemCatalog4229938_cached = true
|
itemCatalogState.current = entries
|
||||||
|
result := make([]any, len(entries))
|
||||||
|
for i, entry := range entries {
|
||||||
|
result[i] = entry
|
||||||
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ItemCatalog4229938_Find(id uint32) *ItemCatalogEntry4229938 {
|
func ItemCatalog4229938_Find(id uint32) *ItemCatalogEntry4229938 {
|
||||||
// Catalog helper ItemCatalog4229938_Find
|
itemCatalogState.RLock()
|
||||||
|
defer itemCatalogState.RUnlock()
|
||||||
|
for _, entry := range itemCatalogState.current {
|
||||||
|
if entry.Id == id {
|
||||||
|
copy := entry
|
||||||
|
return ©
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package catalogs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestItemCatalogPathsAndLookup(t *testing.T) {
|
||||||
|
for _, file := range []string{"ItemCatalog.json", "ConsumableConfig.json"} {
|
||||||
|
dir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, file), []byte(`{"records":[{"Id":36000000,"Name":"Test","Quality":2,"SubType":18}]}`), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
entries, err := LoadItemCatalog4229938(dir)
|
||||||
|
if err != nil || len(entries) != 1 {
|
||||||
|
t.Fatalf("%s: %v %v", file, entries, err)
|
||||||
|
}
|
||||||
|
entry := ItemCatalog4229938_Find(36000000)
|
||||||
|
if entry == nil || entry.Name != "Test" || entry.Quality != 2 {
|
||||||
|
t.Fatalf("bad projection: %+v", entry)
|
||||||
|
}
|
||||||
|
entry.Name = "mutated"
|
||||||
|
if ItemCatalog4229938_Find(36000000).Name != "Test" {
|
||||||
|
t.Fatal("caller mutated cache")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
empty := t.TempDir()
|
||||||
|
if _, err := LoadItemCatalog4229938(empty); err == nil {
|
||||||
|
t.Fatal("reused another directory's cache")
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(empty, "ItemCatalog.json"), []byte(`{"records":[]}`), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if entries, err := LoadItemCatalog4229938(empty); err != nil || len(entries) != 0 {
|
||||||
|
t.Fatalf("retry after missing file: %v %v", entries, err)
|
||||||
|
}
|
||||||
|
if ItemCatalog4229938_Find(36000000) != nil {
|
||||||
|
t.Fatal("stale current catalog")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShippedItemCatalog(t *testing.T) {
|
||||||
|
entries, err := LoadItemCatalog4229938("../../data/client_data/Configs")
|
||||||
|
if err != nil || len(entries) == 0 {
|
||||||
|
t.Fatalf("shipped consumables not loadable: %v", err)
|
||||||
|
}
|
||||||
|
if entry := ItemCatalog4229938_Find(36000000); entry == nil || entry.SubType != 18 {
|
||||||
|
t.Fatalf("missing known item: %+v", entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
Login LoginSettings `json:"login"`
|
||||||
Client ClientConfig `json:"client"`
|
Client ClientConfig `json:"client"`
|
||||||
Network NetworkConfig `json:"network"`
|
Network NetworkConfig `json:"network"`
|
||||||
Admin AdminConfig `json:"admin"`
|
Admin AdminConfig `json:"admin"`
|
||||||
@@ -117,6 +118,9 @@ func LoadConfig(path string) (*Config, error) {
|
|||||||
return nil, fmt.Errorf("failed to parse json config from %s: %w", absPath, err)
|
return nil, fmt.Errorf("failed to parse json config from %s: %w", absPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.Login.RoleListMode == "" {
|
||||||
|
cfg.Login.RoleListMode = "list"
|
||||||
|
}
|
||||||
// Apply default values if omitted
|
// Apply default values if omitted
|
||||||
if cfg.Network.BindHost == "" {
|
if cfg.Network.BindHost == "" {
|
||||||
cfg.Network.BindHost = "0.0.0.0"
|
cfg.Network.BindHost = "0.0.0.0"
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package gameserver
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"ananta-ps/pkg/protocol"
|
"ananta-ps/pkg/protocol"
|
||||||
)
|
)
|
||||||
@@ -48,6 +50,8 @@ func (ab *AdminBridge) ExecuteCommand(cmdLine string) (string, error) {
|
|||||||
return "", fmt.Errorf("no active players in the game world")
|
return "", fmt.Errorf("no active players in the game world")
|
||||||
}
|
}
|
||||||
targetSess := sessions[0] // Apply to primary active player
|
targetSess := sessions[0] // Apply to primary active player
|
||||||
|
targetSess.stateMu.Lock()
|
||||||
|
defer targetSess.stateMu.Unlock()
|
||||||
|
|
||||||
switch cmd {
|
switch cmd {
|
||||||
case "teleport", "tp":
|
case "teleport", "tp":
|
||||||
@@ -55,15 +59,28 @@ func (ab *AdminBridge) ExecuteCommand(cmdLine string) (string, error) {
|
|||||||
return "", fmt.Errorf("usage: teleport <poi_name> OR teleport <x> <y> <z>")
|
return "", fmt.Errorf("usage: teleport <poi_name> OR teleport <x> <y> <z>")
|
||||||
}
|
}
|
||||||
if poi, ok := KnownLandmarks[strings.ToLower(parts[1])]; ok {
|
if poi, ok := KnownLandmarks[strings.ToLower(parts[1])]; ok {
|
||||||
_ = targetSess.Teleport(poi.X, poi.Y, poi.Z, poi.Facing)
|
if err := targetSess.teleport(poi.X, poi.Y, poi.Z, poi.Facing); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
return fmt.Sprintf("Teleported to %s (%.1f, %.1f, %.1f)", poi.Name, poi.X, poi.Y, poi.Z), nil
|
return fmt.Sprintf("Teleported to %s (%.1f, %.1f, %.1f)", poi.Name, poi.X, poi.Y, poi.Z), nil
|
||||||
}
|
}
|
||||||
if len(parts) >= 4 {
|
if len(parts) >= 4 {
|
||||||
var x, y, z float32
|
var x, y, z float32
|
||||||
_, _ = fmt.Sscanf(parts[1], "%f", &x)
|
if _, err := fmt.Sscanf(parts[1], "%f", &x); err != nil {
|
||||||
_, _ = fmt.Sscanf(parts[2], "%f", &y)
|
return "", fmt.Errorf("invalid x coordinate")
|
||||||
_, _ = fmt.Sscanf(parts[3], "%f", &z)
|
}
|
||||||
_ = targetSess.Teleport(x, y, z, 0)
|
if _, err := fmt.Sscanf(parts[2], "%f", &y); err != nil {
|
||||||
|
return "", fmt.Errorf("invalid y coordinate")
|
||||||
|
}
|
||||||
|
if _, err := fmt.Sscanf(parts[3], "%f", &z); err != nil {
|
||||||
|
return "", fmt.Errorf("invalid z coordinate")
|
||||||
|
}
|
||||||
|
if math.IsNaN(float64(x)) || math.IsInf(float64(x), 0) || math.IsNaN(float64(y)) || math.IsInf(float64(y), 0) || math.IsNaN(float64(z)) || math.IsInf(float64(z), 0) {
|
||||||
|
return "", fmt.Errorf("coordinates must be finite")
|
||||||
|
}
|
||||||
|
if err := targetSess.teleport(x, y, z, 0); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
return fmt.Sprintf("Teleported to coordinates (%.1f, %.1f, %.1f)", x, y, z), nil
|
return fmt.Sprintf("Teleported to coordinates (%.1f, %.1f, %.1f)", x, y, z), nil
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("unknown POI landmark '%s'", parts[1])
|
return "", fmt.Errorf("unknown POI landmark '%s'", parts[1])
|
||||||
@@ -73,8 +90,12 @@ func (ab *AdminBridge) ExecuteCommand(cmdLine string) (string, error) {
|
|||||||
return "", fmt.Errorf("usage: weather <id> (e.g. 1=Sunny, 2=Rain, 3=Fog)")
|
return "", fmt.Errorf("usage: weather <id> (e.g. 1=Sunny, 2=Rain, 3=Fog)")
|
||||||
}
|
}
|
||||||
var wId uint32
|
var wId uint32
|
||||||
_, _ = fmt.Sscanf(parts[1], "%d", &wId)
|
if _, err := fmt.Sscanf(parts[1], "%d", &wId); err != nil {
|
||||||
ab.server.SetWorldWeather(wId)
|
return "", fmt.Errorf("invalid weather id")
|
||||||
|
}
|
||||||
|
if err := ab.server.SetWorldWeather(wId); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
return fmt.Sprintf("World weather set to ID %d", wId), nil
|
return fmt.Sprintf("World weather set to ID %d", wId), nil
|
||||||
|
|
||||||
case "time":
|
case "time":
|
||||||
@@ -82,8 +103,12 @@ func (ab *AdminBridge) ExecuteCommand(cmdLine string) (string, error) {
|
|||||||
return "", fmt.Errorf("usage: time <hour> (e.g. 12.0 for noon, 0.0 for midnight)")
|
return "", fmt.Errorf("usage: time <hour> (e.g. 12.0 for noon, 0.0 for midnight)")
|
||||||
}
|
}
|
||||||
var hour float32
|
var hour float32
|
||||||
_, _ = fmt.Sscanf(parts[1], "%f", &hour)
|
if _, err := fmt.Sscanf(parts[1], "%f", &hour); err != nil {
|
||||||
ab.server.SetWorldTimeOfDay(hour)
|
return "", fmt.Errorf("invalid hour")
|
||||||
|
}
|
||||||
|
if err := ab.server.SetWorldTimeOfDay(hour); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
return fmt.Sprintf("World time set to %.1f:00", hour), nil
|
return fmt.Sprintf("World time set to %.1f:00", hour), nil
|
||||||
|
|
||||||
case "wanted":
|
case "wanted":
|
||||||
@@ -95,7 +120,9 @@ func (ab *AdminBridge) ExecuteCommand(cmdLine string) (string, error) {
|
|||||||
targetSess.wantedLevel = stars
|
targetSess.wantedLevel = stars
|
||||||
w := protocol.NewUxWriter()
|
w := protocol.NewUxWriter()
|
||||||
w.U64(targetSess.pid).U32(stars)
|
w.U64(targetSess.pid).U32(stars)
|
||||||
_ = targetSess.NotifyData(protocol.MethodGetMethodIDDefault("SyncWantedLevel", 68582860), w.Bytes())
|
if err := targetSess.NotifyData(protocol.MethodGetMethodIDDefault("SyncWantedLevel", 68582860), w.Bytes()); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
return fmt.Sprintf("Player wanted level set to %d stars", stars), nil
|
return fmt.Sprintf("Player wanted level set to %d stars", stars), nil
|
||||||
|
|
||||||
case "spawn_vehicle", "veh":
|
case "spawn_vehicle", "veh":
|
||||||
@@ -103,11 +130,13 @@ func (ab *AdminBridge) ExecuteCommand(cmdLine string) (string, error) {
|
|||||||
if len(parts) >= 2 {
|
if len(parts) >= 2 {
|
||||||
_, _ = fmt.Sscanf(parts[1], "%d", &templateId)
|
_, _ = fmt.Sscanf(parts[1], "%d", &templateId)
|
||||||
}
|
}
|
||||||
vehUnitId := uint64(0x6000000000000000) | uint64(len(ab.server.sessions)+1)
|
vehUnitId := uint64(0x6000000000000000) | uint64(time.Now().UnixNano())
|
||||||
targetSess.activeVehicleId = vehUnitId
|
targetSess.activeVehicleId = vehUnitId
|
||||||
w := protocol.NewUxWriter()
|
w := protocol.NewUxWriter()
|
||||||
w.U64(vehUnitId).U32(templateId).F32(targetSess.posX + 3.0).F32(targetSess.posY).F32(targetSess.posZ + 3.0).F32(targetSess.facing)
|
w.U64(vehUnitId).U32(templateId).F32(targetSess.posX + 3.0).F32(targetSess.posY).F32(targetSess.posZ + 3.0).F32(targetSess.facing)
|
||||||
_ = targetSess.NotifyData(protocol.MethodGetMethodIDDefault("SyncSpawnVehicle", 68582851), w.Bytes())
|
if err := targetSess.NotifyData(protocol.MethodGetMethodIDDefault("SyncSpawnVehicle", 68582851), w.Bytes()); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
return fmt.Sprintf("Spawned vehicle template %d (unit %d)", templateId, vehUnitId), nil
|
return fmt.Sprintf("Spawned vehicle template %d (unit %d)", templateId, vehUnitId), nil
|
||||||
|
|
||||||
case "spawn_enemy":
|
case "spawn_enemy":
|
||||||
@@ -119,15 +148,30 @@ func (ab *AdminBridge) ExecuteCommand(cmdLine string) (string, error) {
|
|||||||
return "", fmt.Errorf("usage: switch <spirit_template_id>")
|
return "", fmt.Errorf("usage: switch <spirit_template_id>")
|
||||||
}
|
}
|
||||||
var newId uint32
|
var newId uint32
|
||||||
_, _ = fmt.Sscanf(parts[1], "%d", &newId)
|
if _, err := fmt.Sscanf(parts[1], "%d", &newId); err != nil || newId == 0 {
|
||||||
|
return "", fmt.Errorf("invalid spirit template")
|
||||||
|
}
|
||||||
// Perform clean character swap
|
// Perform clean character swap
|
||||||
newUnitId := targetSess.unitId + 1
|
newUnitId := targetSess.unitId + 1
|
||||||
_ = targetSess.NotifyData(protocol.MethodSyncLogicAgentLeave, protocol.EncodeLogicAgentLeave(targetSess.unitId))
|
if err := targetSess.NotifyData(protocol.MethodSyncLogicAgentLeave, protocol.EncodeLogicAgentLeave(targetSess.unitId)); err != nil {
|
||||||
_ = targetSess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(newUnitId))
|
return "", err
|
||||||
_ = targetSess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(newUnitId, targetSess.pid, 0))
|
}
|
||||||
|
if err := targetSess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(targetSess.pid, newId, newUnitId, true)); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := targetSess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(newUnitId)); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := targetSess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(newUnitId, targetSess.pid, 0)); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
newProj := protocol.EncodeRaidBattleUnitSpirit(newUnitId, newId, targetSess.posX, targetSess.posY, targetSess.posZ, targetSess.facing, targetSess.pid)
|
newProj := protocol.EncodeRaidBattleUnitSpirit(newUnitId, newId, targetSess.posX, targetSess.posY, targetSess.posZ, targetSess.facing, targetSess.pid)
|
||||||
_ = targetSess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, newProj)
|
if err := targetSess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, newProj); err != nil {
|
||||||
_ = targetSess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(targetSess.pid, newId, newUnitId, true))
|
return "", err
|
||||||
|
}
|
||||||
|
if err := targetSess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(targetSess.pid, newId, newUnitId, true)); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
targetSess.templateId = newId
|
targetSess.templateId = newId
|
||||||
targetSess.unitId = newUnitId
|
targetSess.unitId = newUnitId
|
||||||
return fmt.Sprintf("Switched player character to template %d", newId), nil
|
return fmt.Sprintf("Switched player character to template %d", newId), nil
|
||||||
@@ -135,13 +179,17 @@ func (ab *AdminBridge) ExecuteCommand(cmdLine string) (string, error) {
|
|||||||
case "fog_unlock":
|
case "fog_unlock":
|
||||||
wFog := protocol.NewUxWriter()
|
wFog := protocol.NewUxWriter()
|
||||||
wFog.U32(uint32(targetSess.activeScene)).Bool(true)
|
wFog.U32(uint32(targetSess.activeScene)).Bool(true)
|
||||||
_ = targetSess.NotifyData(protocol.MethodSyncSceneFogMapAllUnlock, wFog.Bytes())
|
if err := targetSess.NotifyData(protocol.MethodSyncSceneFogMapAllUnlock, wFog.Bytes()); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
return "All map fog unlocked", nil
|
return "All map fog unlocked", nil
|
||||||
|
|
||||||
case "dive_unlock":
|
case "dive_unlock":
|
||||||
wDive := protocol.NewUxWriter()
|
wDive := protocol.NewUxWriter()
|
||||||
wDive.UxString("EnableDive").U8(0xFF).Int7(1).UxString("true")
|
wDive.UxString("EnableDive").U8(0xFF).Int7(1).UxString("true")
|
||||||
_ = targetSess.NotifyData(protocol.MethodSyncRunGmUtils, wDive.Bytes())
|
if err := targetSess.NotifyData(protocol.MethodSyncRunGmUtils, wDive.Bytes()); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
return "Diving mechanics enabled", nil
|
return "Diving mechanics enabled", nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package gameserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"ananta-ps/internal/config"
|
||||||
|
"ananta-ps/pkg/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWeatherAndTimeUseActualClientContracts(t *testing.T) {
|
||||||
|
srv := New(&config.Config{}, "")
|
||||||
|
conn := &recordingConn{}
|
||||||
|
srv.sessions["test"] = &Session{conn: conn}
|
||||||
|
if err := srv.SetWorldWeather(3); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f, err := protocol.ReadFrame(conn, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
pkt := protocol.ParseRpcPacket(f.Payload)
|
||||||
|
if pkt.MethodID != 68712141 || !bytes.Equal(pkt.Body, []byte{3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0}) {
|
||||||
|
t.Fatalf("weather contract: %+v", pkt)
|
||||||
|
}
|
||||||
|
if err := srv.SetWorldTimeOfDay(12.5); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f, err = protocol.ReadFrame(conn, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
pkt = protocol.ParseRpcPacket(f.Payload)
|
||||||
|
if pkt.MethodID != 68337673 || len(pkt.Body) != 22 || binary.LittleEndian.Uint32(pkt.Body[4:8]) != 45000 || pkt.Body[8] != 1 {
|
||||||
|
t.Fatalf("time contract: %+v", pkt)
|
||||||
|
}
|
||||||
|
for _, hour := range []float32{-1, 24, float32(math.NaN()), float32(math.Inf(1))} {
|
||||||
|
if err := srv.SetWorldTimeOfDay(hour); err == nil {
|
||||||
|
t.Fatalf("invalid hour: %v", hour)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if conn.Len() != 0 {
|
||||||
|
t.Fatal("invalid time emitted a packet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdminRejectsInvalidTeleport(t *testing.T) {
|
||||||
|
srv := New(&config.Config{}, "")
|
||||||
|
conn := &recordingConn{}
|
||||||
|
srv.sessions["test"] = &Session{conn: conn, posX: 123}
|
||||||
|
for _, cmd := range []string{"tp garbage 0 0", "tp NaN 0 0", "tp 0 +Inf 0"} {
|
||||||
|
if _, err := srv.GetAdminBridge().ExecuteCommand(cmd); err == nil {
|
||||||
|
t.Fatalf("accepted %s", cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if conn.Len() != 0 || srv.sessions["test"].posX != 123 {
|
||||||
|
t.Fatal("invalid command changed position")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type recordingConn struct{ bytes.Buffer }
|
||||||
|
|
||||||
|
func (c *recordingConn) Close() error { return nil }
|
||||||
|
func (c *recordingConn) LocalAddr() net.Addr { return &net.TCPAddr{} }
|
||||||
|
func (c *recordingConn) RemoteAddr() net.Addr { return &net.TCPAddr{} }
|
||||||
|
func (c *recordingConn) SetDeadline(time.Time) error { return nil }
|
||||||
|
func (c *recordingConn) SetReadDeadline(time.Time) error { return nil }
|
||||||
|
func (c *recordingConn) SetWriteDeadline(time.Time) error { return nil }
|
||||||
|
|
||||||
|
func TestDispatchContractFallbackAndPacketKinds(t *testing.T) {
|
||||||
|
srv := New(&config.Config{}, "")
|
||||||
|
conn := &recordingConn{}
|
||||||
|
sess := &Session{conn: conn}
|
||||||
|
// An unimplemented RPC returning bool needs one byte, not an empty OK.
|
||||||
|
if err := srv.dispatch(sess, &protocol.RpcPacket{Kind: protocol.PacketKindInvoke, MethodID: 67709054, InvokeID: 12}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f, err := protocol.ReadFrame(conn, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
pkt := protocol.ParseRpcPacket(f.Payload)
|
||||||
|
if pkt.Kind != protocol.PacketKindReturn || pkt.InvokeID != 12 || !bytes.Equal(pkt.Body, []byte{0}) {
|
||||||
|
t.Fatalf("bad bool fallback: %+v", pkt)
|
||||||
|
}
|
||||||
|
for _, kind := range []protocol.RpcPacketKind{protocol.PacketKindReturn, protocol.PacketKindUnknown} {
|
||||||
|
if err := srv.dispatch(sess, &protocol.RpcPacket{Kind: kind, MethodID: protocol.MethodAskSwitchSpirit}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if conn.Len() != 0 {
|
||||||
|
t.Fatal("dispatched a return or malformed packet as a request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := sess.ReturnEmptyOk(0, protocol.MethodLoginGame); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if conn.Len() != 0 {
|
||||||
|
t.Fatal("unsolicited return for a notification")
|
||||||
|
}
|
||||||
|
// Fabricated old AskMoveStart collided with a real client ID; it must not move state.
|
||||||
|
if err := srv.dispatch(sess, &protocol.RpcPacket{Kind: protocol.PacketKindNotify, MethodID: 68412852}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if conn.Len() != 0 {
|
||||||
|
t.Fatal("invented route still active")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHangupAndActualMovementContract(t *testing.T) {
|
||||||
|
srv := New(&config.Config{}, "")
|
||||||
|
sess := &Session{conn: &recordingConn{}, unitId: 9}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
id uint32
|
||||||
|
paused bool
|
||||||
|
}{{63842476, true}, {63910160, false}} {
|
||||||
|
if err := srv.dispatch(sess, &protocol.RpcPacket{Kind: protocol.PacketKindInvoke, MethodID: tc.id, InvokeID: 1}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if sess.hangup.Load() != tc.paused {
|
||||||
|
t.Fatal("hangup state not updated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body := protocol.NewUxWriter().U8(0xff).Int7(1).U64(9).F32(1).F32(2).F32(3).F32(0).F32(90).F32(0).Bytes()
|
||||||
|
if err := srv.dispatch(sess, &protocol.RpcPacket{Kind: protocol.PacketKindNotify, MethodID: 67543012, Body: body}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if sess.posX != 1 || sess.posY != 2 || sess.posZ != 3 || sess.facing != 90 {
|
||||||
|
t.Fatal("actual movement contract not applied")
|
||||||
|
}
|
||||||
|
if err := srv.dispatch(sess, &protocol.RpcPacket{Kind: protocol.PacketKindNotify, MethodID: 67543012, Body: body[:15]}); err == nil {
|
||||||
|
t.Fatal("truncated position accepted")
|
||||||
|
}
|
||||||
|
if sess.posX != 1 || sess.posY != 2 {
|
||||||
|
t.Fatal("partial position update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTeleportIncludesNullableTail(t *testing.T) {
|
||||||
|
conn := &recordingConn{}
|
||||||
|
sess := &Session{conn: conn, unitId: 9}
|
||||||
|
if err := sess.Teleport(1, 2, 3, 90); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f, err := protocol.ReadFrame(conn, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
pkt := protocol.ParseRpcPacket(f.Payload)
|
||||||
|
if pkt.MethodID != 68030312 || len(pkt.Body) != 29 || !bytes.Equal(pkt.Body[24:], []byte{0, 0, 6, 0, 0}) {
|
||||||
|
t.Fatalf("invalid position contract: %+v", pkt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStopClosesIdleHandshakeConnections(t *testing.T) {
|
||||||
|
srv := New(&config.Config{Network: config.NetworkConfig{BindHost: "127.0.0.1"}}, "")
|
||||||
|
if err := srv.Start(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
conn, err := net.DialTimeout("tcp", srv.ln.Addr().String(), time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
// Wait for the server to own the socket before stopping.
|
||||||
|
deadline := time.Now().Add(time.Second)
|
||||||
|
for {
|
||||||
|
srv.mu.RLock()
|
||||||
|
n := len(srv.connections)
|
||||||
|
srv.mu.RUnlock()
|
||||||
|
if n == 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
t.Fatal("connection not accepted")
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() { srv.Stop(); close(done) }()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("Stop blocked on idle handshake")
|
||||||
|
}
|
||||||
|
conn.SetReadDeadline(time.Now().Add(time.Second))
|
||||||
|
if _, err := conn.Read(make([]byte, 1)); err == nil {
|
||||||
|
t.Fatal("idle connection survived Stop")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package gameserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"ananta-ps/pkg/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Both movement RPCs carry a List7Bit of inline structs. LogicAgentSyncData
|
||||||
|
// has no MoveId; SimpleMoveActionData adds one byte after the two vectors.
|
||||||
|
func (s *Server) handleMovementReport(sess *Session, pkt *protocol.RpcPacket, simple bool) error {
|
||||||
|
r := protocol.NewUxReader(pkt.Body)
|
||||||
|
mark, err := r.U8()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if mark == 0 && r.Remaining() == 0 {
|
||||||
|
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
|
||||||
|
}
|
||||||
|
if mark != 0xff {
|
||||||
|
return fmt.Errorf("invalid movement list marker")
|
||||||
|
}
|
||||||
|
count, err := r.Int7()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
size := 32
|
||||||
|
if simple {
|
||||||
|
size++
|
||||||
|
}
|
||||||
|
if count > r.Remaining()/size || count*size != r.Remaining() {
|
||||||
|
return fmt.Errorf("invalid movement sample count")
|
||||||
|
}
|
||||||
|
x, y, z, yaw := sess.posX, sess.posY, sess.posZ, sess.facing
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
id, _ := r.U64()
|
||||||
|
var v [6]float32
|
||||||
|
for j := range v {
|
||||||
|
v[j], err = r.F32()
|
||||||
|
if err != nil || math.IsNaN(float64(v[j])) || math.IsInf(float64(v[j]), 0) {
|
||||||
|
return fmt.Errorf("invalid movement coordinate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if simple {
|
||||||
|
_, _ = r.U8()
|
||||||
|
}
|
||||||
|
if id == sess.unitId {
|
||||||
|
x, y, z, yaw = v[0], v[1], v[2], v[4]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sess.posX, sess.posY, sess.posZ, sess.facing = x, y, z, yaw
|
||||||
|
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
|
||||||
|
}
|
||||||
+163
-85
@@ -6,8 +6,11 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"ananta-ps/internal/config"
|
"ananta-ps/internal/config"
|
||||||
"ananta-ps/pkg/crypto/chacha8"
|
"ananta-ps/pkg/crypto/chacha8"
|
||||||
@@ -20,6 +23,7 @@ type Server struct {
|
|||||||
ln net.Listener
|
ln net.Listener
|
||||||
cancelFunc context.CancelFunc
|
cancelFunc context.CancelFunc
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
connections map[net.Conn]struct{}
|
||||||
sessions map[string]*Session
|
sessions map[string]*Session
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
trafficEngine *CityTrafficEngine
|
trafficEngine *CityTrafficEngine
|
||||||
@@ -28,6 +32,12 @@ type Server struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
|
replyKind protocol.RpcPacketKind
|
||||||
|
stateMu sync.Mutex
|
||||||
|
ready atomic.Bool
|
||||||
|
hangup atomic.Bool
|
||||||
|
initialStateSent bool
|
||||||
|
writeTimeout time.Duration
|
||||||
id string
|
id string
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
readCipher *chacha8.UxChaCha8
|
readCipher *chacha8.UxChaCha8
|
||||||
@@ -57,18 +67,21 @@ func New(cfg *config.Config, templatePath string) *Server {
|
|||||||
candidates := []string{
|
candidates := []string{
|
||||||
"data/player_info_template.bin",
|
"data/player_info_template.bin",
|
||||||
"../data/player_info_template.bin",
|
"../data/player_info_template.bin",
|
||||||
|
"../../data/player_info_template.bin",
|
||||||
}
|
}
|
||||||
for _, c := range candidates {
|
for _, c := range candidates {
|
||||||
abs, _ := filepath.Abs(c)
|
if _, err := os.Stat(c); err == nil {
|
||||||
templatePath = abs
|
templatePath, _ = filepath.Abs(c)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
srv := &Server{
|
srv := &Server{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
templatePath: templatePath,
|
templatePath: templatePath,
|
||||||
sessions: make(map[string]*Session),
|
sessions: make(map[string]*Session),
|
||||||
|
connections: make(map[net.Conn]struct{}),
|
||||||
}
|
}
|
||||||
srv.trafficEngine = NewCityTrafficEngine(srv)
|
srv.trafficEngine = NewCityTrafficEngine(srv)
|
||||||
srv.crowdEngine = NewUrbanCrowdEngine(srv)
|
srv.crowdEngine = NewUrbanCrowdEngine(srv)
|
||||||
@@ -84,16 +97,15 @@ func (s *Server) Start() error {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
s.cancelFunc = cancel
|
s.cancelFunc = cancel
|
||||||
|
|
||||||
// Load Road Network for city traffic simulation
|
|
||||||
_ = s.trafficEngine.LoadRoadNetwork("data/client_data/World/RoadNetwork.json")
|
|
||||||
s.trafficEngine.Start(ctx)
|
|
||||||
|
|
||||||
addr := fmt.Sprintf("%s:%d", s.cfg.Network.BindHost, s.cfg.Network.GamePort)
|
addr := fmt.Sprintf("%s:%d", s.cfg.Network.BindHost, s.cfg.Network.GamePort)
|
||||||
ln, err := net.Listen("tcp", addr)
|
ln, err := net.Listen("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
cancel()
|
||||||
return fmt.Errorf("failed to listen on game server %s: %w", addr, err)
|
return fmt.Errorf("failed to listen on game server %s: %w", addr, err)
|
||||||
}
|
}
|
||||||
s.ln = ln
|
s.ln = ln
|
||||||
|
_ = s.trafficEngine.LoadRoadNetwork("data/client_data/World/RoadNetwork.json")
|
||||||
|
s.trafficEngine.Start(ctx)
|
||||||
log.Printf("[GAME-SERVER] Listening on %s:%d", s.cfg.Network.AdvertisedHost, s.cfg.Network.GamePort)
|
log.Printf("[GAME-SERVER] Listening on %s:%d", s.cfg.Network.AdvertisedHost, s.cfg.Network.GamePort)
|
||||||
|
|
||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
@@ -103,15 +115,20 @@ func (s *Server) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Stop() error {
|
func (s *Server) Stop() error {
|
||||||
if s.trafficEngine != nil {
|
|
||||||
s.trafficEngine.Stop()
|
|
||||||
}
|
|
||||||
if s.cancelFunc != nil {
|
if s.cancelFunc != nil {
|
||||||
s.cancelFunc()
|
s.cancelFunc()
|
||||||
}
|
}
|
||||||
if s.ln != nil {
|
if s.ln != nil {
|
||||||
_ = s.ln.Close()
|
_ = s.ln.Close()
|
||||||
}
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
for conn := range s.connections {
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
if s.trafficEngine != nil {
|
||||||
|
s.trafficEngine.Stop()
|
||||||
|
}
|
||||||
s.wg.Wait()
|
s.wg.Wait()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -140,7 +157,20 @@ func (s *Server) acceptLoop(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go s.handleConnection(ctx, conn)
|
s.mu.Lock()
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
s.mu.Unlock()
|
||||||
|
conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.connections[conn] = struct{}{}
|
||||||
|
s.wg.Add(1)
|
||||||
|
s.mu.Unlock()
|
||||||
|
go func() {
|
||||||
|
defer s.wg.Done()
|
||||||
|
defer func() { s.mu.Lock(); delete(s.connections, conn); s.mu.Unlock() }()
|
||||||
|
s.handleConnection(ctx, conn)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,10 +188,29 @@ func (sess *Session) SendPacket(packet *protocol.RpcPacket) error {
|
|||||||
payload = protocol.BuildNotify(packet.MethodID, packet.Body)
|
payload = protocol.BuildNotify(packet.MethodID, packet.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
return protocol.WriteFrame(sess.conn, 0x09, payload, sess.sendCipher)
|
return sess.writeFrameLocked(0x09, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *Session) writeFrameLocked(mode byte, payload []byte) error {
|
||||||
|
timeout := sess.writeTimeout
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 15 * time.Second
|
||||||
|
}
|
||||||
|
if err := sess.conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := protocol.WriteFrame(sess.conn, mode, payload, sess.sendCipher); err != nil {
|
||||||
|
// The cipher has advanced; this stream cannot be reused after a failed write.
|
||||||
|
sess.conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sess *Session) ReturnEmptyOk(invokeID int32, methodID uint32) error {
|
func (sess *Session) ReturnEmptyOk(invokeID int32, methodID uint32) error {
|
||||||
|
if sess.replyKind != protocol.PacketKindInvoke {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return sess.SendPacket(&protocol.RpcPacket{
|
return sess.SendPacket(&protocol.RpcPacket{
|
||||||
Kind: protocol.PacketKindReturn,
|
Kind: protocol.PacketKindReturn,
|
||||||
InvokeID: invokeID,
|
InvokeID: invokeID,
|
||||||
@@ -172,6 +221,9 @@ func (sess *Session) ReturnEmptyOk(invokeID int32, methodID uint32) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sess *Session) ReturnData(invokeID int32, methodID uint32, body []byte) error {
|
func (sess *Session) ReturnData(invokeID int32, methodID uint32, body []byte) error {
|
||||||
|
if sess.replyKind != protocol.PacketKindInvoke {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return sess.SendPacket(&protocol.RpcPacket{
|
return sess.SendPacket(&protocol.RpcPacket{
|
||||||
Kind: protocol.PacketKindReturn,
|
Kind: protocol.PacketKindReturn,
|
||||||
InvokeID: invokeID,
|
InvokeID: invokeID,
|
||||||
@@ -182,6 +234,9 @@ func (sess *Session) ReturnData(invokeID int32, methodID uint32, body []byte) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sess *Session) NotifyData(methodID uint32, body []byte) error {
|
func (sess *Session) NotifyData(methodID uint32, body []byte) error {
|
||||||
|
if methodID == 0 {
|
||||||
|
return fmt.Errorf("unverified notification method")
|
||||||
|
}
|
||||||
return sess.SendPacket(&protocol.RpcPacket{
|
return sess.SendPacket(&protocol.RpcPacket{
|
||||||
Kind: protocol.PacketKindNotify,
|
Kind: protocol.PacketKindNotify,
|
||||||
MethodID: methodID,
|
MethodID: methodID,
|
||||||
@@ -202,6 +257,7 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn) {
|
|||||||
|
|
||||||
sess := &Session{
|
sess := &Session{
|
||||||
id: conn.RemoteAddr().String(),
|
id: conn.RemoteAddr().String(),
|
||||||
|
writeTimeout: time.Duration(s.cfg.Network.WriteTimeoutMs) * time.Millisecond,
|
||||||
conn: conn,
|
conn: conn,
|
||||||
readCipher: readCipher,
|
readCipher: readCipher,
|
||||||
sendCipher: writeCipher,
|
sendCipher: writeCipher,
|
||||||
@@ -243,10 +299,18 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if frame.Mode == 0x03 {
|
if frame.Mode == 0x03 {
|
||||||
_ = protocol.WriteFrame(conn, 0x03, frame.Payload, sess.sendCipher)
|
sess.sendMu.Lock()
|
||||||
|
err = sess.writeFrameLocked(0x03, frame.Payload)
|
||||||
|
sess.sendMu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if frame.Mode != 0x09 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
packet := protocol.ParseRpcPacket(frame.Payload)
|
packet := protocol.ParseRpcPacket(frame.Payload)
|
||||||
if err := s.dispatch(sess, &packet); err != nil {
|
if err := s.dispatch(sess, &packet); err != nil {
|
||||||
log.Printf("[GAME-SERVER] Dispatch error for method %d (%s): %v",
|
log.Printf("[GAME-SERVER] Dispatch error for method %d (%s): %v",
|
||||||
@@ -256,19 +320,34 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
||||||
|
if pkt.Kind != protocol.PacketKindInvoke && pkt.Kind != protocol.PacketKindNotify {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sess.stateMu.Lock()
|
||||||
|
defer sess.stateMu.Unlock()
|
||||||
|
sess.replyKind = pkt.Kind
|
||||||
|
defer func() { sess.replyKind = protocol.PacketKindUnknown }()
|
||||||
switch pkt.MethodID {
|
switch pkt.MethodID {
|
||||||
// ---------------- Login / World Entry ----------------
|
// ---------------- Login / World Entry ----------------
|
||||||
case protocol.MethodLoginGame:
|
case protocol.MethodLoginGame:
|
||||||
|
if sess.initialStateSent {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sess.initialStateSent = true
|
||||||
log.Printf("[WORLD] LoginGame received, preparing world state for player %d", sess.pid)
|
log.Printf("[WORLD] LoginGame received, preparing world state for player %d", sess.pid)
|
||||||
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
|
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = sess.NotifyData(protocol.MethodSendServerTimeGame, protocol.EncodeServerTime(0))
|
if err := sess.NotifyData(protocol.MethodSendServerTimeGame, protocol.EncodeServerTime(0)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
playerInfoBytes, err := protocol.EncodePlayerInfo(s.templatePath, s.cfg.Client.Aid, sess.pid, s.cfg.Player.AccountId, s.cfg.Player.DisplayName)
|
playerInfoBytes, err := protocol.EncodePlayerInfo(s.templatePath, s.cfg.Client.Aid, sess.pid, s.cfg.Player.AccountId, s.cfg.Player.DisplayName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
_ = sess.NotifyData(protocol.MethodSyncPlayerInfo, playerInfoBytes)
|
if err := sess.NotifyData(protocol.MethodSyncPlayerInfo, playerInfoBytes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[WORLD] Warning: player_info_template.bin load error: %v", err)
|
log.Printf("[WORLD] Warning: player_info_template.bin load error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -295,10 +374,16 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = sess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(sess.unitId))
|
if err := sess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(sess.unitId)); err != nil {
|
||||||
_ = sess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(sess.unitId, sess.pid, 0))
|
return err
|
||||||
|
}
|
||||||
|
if err := sess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(sess.unitId, sess.pid, 0)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
unitProjection := protocol.EncodeRaidBattleUnitSpirit(sess.unitId, sess.templateId, sess.posX, sess.posY, sess.posZ, sess.facing, sess.pid)
|
unitProjection := protocol.EncodeRaidBattleUnitSpirit(sess.unitId, sess.templateId, sess.posX, sess.posY, sess.posZ, sess.facing, sess.pid)
|
||||||
_ = sess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, unitProjection)
|
if err := sess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, unitProjection); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return sess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(sess.pid, sess.templateId, sess.unitId, false))
|
return sess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(sess.pid, sess.templateId, sess.unitId, false))
|
||||||
|
|
||||||
case protocol.MethodAskLoadingFinished:
|
case protocol.MethodAskLoadingFinished:
|
||||||
@@ -307,19 +392,28 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = sess.NotifyData(protocol.MethodSyncSceneLoadCompleted, protocol.EncodeSceneLoadCompleted(sess.activeScene))
|
if err := sess.NotifyData(protocol.MethodSyncSceneLoadCompleted, protocol.EncodeSceneLoadCompleted(sess.activeScene)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sess.ready.Store(true)
|
||||||
|
|
||||||
wPause := protocol.NewUxWriter()
|
wPause := protocol.NewUxWriter()
|
||||||
wPause.Bool(false)
|
wPause.Bool(false)
|
||||||
_ = sess.NotifyData(protocol.MethodSyncGamePause, wPause.Bytes())
|
if err := sess.NotifyData(protocol.MethodSyncGamePause, wPause.Bytes()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
wFog := protocol.NewUxWriter()
|
wFog := protocol.NewUxWriter()
|
||||||
wFog.U32(uint32(sess.activeScene)).Bool(true)
|
wFog.U32(uint32(sess.activeScene)).Bool(true)
|
||||||
_ = sess.NotifyData(protocol.MethodSyncSceneFogMapAllUnlock, wFog.Bytes())
|
if err := sess.NotifyData(protocol.MethodSyncSceneFogMapAllUnlock, wFog.Bytes()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
wPower := protocol.NewUxWriter()
|
wPower := protocol.NewUxWriter()
|
||||||
wPower.U8(0xFF).Int7(1).U32(sess.templateId).F32(1000.0)
|
wPower.U8(0xFF).Int7(1).U32(sess.templateId).F32(1000.0)
|
||||||
_ = sess.NotifyData(protocol.MethodSyncAllSpiritCombatPower, wPower.Bytes())
|
if err := sess.NotifyData(protocol.MethodSyncAllSpiritCombatPower, wPower.Bytes()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
wDive := protocol.NewUxWriter()
|
wDive := protocol.NewUxWriter()
|
||||||
wDive.UxString("EnableDive").U8(0xFF).Int7(1).UxString("true")
|
wDive.UxString("EnableDive").U8(0xFF).Int7(1).UxString("true")
|
||||||
@@ -330,32 +424,29 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|||||||
w.U64(sess.pid).F64(1.0)
|
w.U64(sess.pid).F64(1.0)
|
||||||
return sess.NotifyData(protocol.MethodSyncPlayerLoadRate, w.Bytes())
|
return sess.NotifyData(protocol.MethodSyncPlayerLoadRate, w.Bytes())
|
||||||
|
|
||||||
// ---------------- Movement & Position ----------------
|
case protocol.MethodRequestPlayerStartHangup:
|
||||||
case protocol.MethodAskMoveStart, protocol.MethodAskMoveStop:
|
sess.hangup.Store(true)
|
||||||
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
|
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
|
||||||
|
case protocol.MethodRequestPlayerStopHangup:
|
||||||
|
sess.hangup.Store(false)
|
||||||
|
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
|
||||||
|
case protocol.MethodAskReportLogicAgentSyncData:
|
||||||
|
return s.handleMovementReport(sess, pkt, false)
|
||||||
|
case protocol.MethodAskUnitMoveActionSimple:
|
||||||
|
return s.handleMovementReport(sess, pkt, true)
|
||||||
|
|
||||||
case protocol.MethodAskUpdatePlayerPosition:
|
// ---------------- Movement & Position ----------------
|
||||||
r := protocol.NewUxReader(pkt.Body)
|
|
||||||
if x, err := r.F32(); err == nil {
|
|
||||||
sess.posX = x
|
|
||||||
}
|
|
||||||
if y, err := r.F32(); err == nil {
|
|
||||||
sess.posY = y
|
|
||||||
}
|
|
||||||
if z, err := r.F32(); err == nil {
|
|
||||||
sess.posZ = z
|
|
||||||
}
|
|
||||||
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
|
|
||||||
|
|
||||||
// ---------------- Character Switching ----------------
|
// ---------------- Character Switching ----------------
|
||||||
case protocol.MethodAskPreSwitchSpirit:
|
|
||||||
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
|
|
||||||
|
|
||||||
case protocol.MethodAskSwitchSpirit:
|
case protocol.MethodAskSwitchSpirit:
|
||||||
r := protocol.NewUxReader(pkt.Body)
|
r := protocol.NewUxReader(pkt.Body)
|
||||||
newTemplateId := sess.templateId
|
newTemplateId, err := r.U32()
|
||||||
if tid, err := r.U32(); err == nil {
|
if err != nil {
|
||||||
newTemplateId = tid
|
return err
|
||||||
|
}
|
||||||
|
if newTemplateId == 0 {
|
||||||
|
return fmt.Errorf("spirit template must be nonzero")
|
||||||
}
|
}
|
||||||
newUnitId := sess.unitId + 1
|
newUnitId := sess.unitId + 1
|
||||||
|
|
||||||
@@ -364,17 +455,32 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = sess.NotifyData(protocol.MethodSyncLogicAgentLeave, protocol.EncodeLogicAgentLeave(sess.unitId))
|
if err := sess.NotifyData(protocol.MethodSyncLogicAgentLeave, protocol.EncodeLogicAgentLeave(sess.unitId)); err != nil {
|
||||||
_ = sess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(newUnitId))
|
return err
|
||||||
_ = sess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(newUnitId, sess.pid, 0))
|
}
|
||||||
|
if err := sess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(sess.pid, newTemplateId, newUnitId, true)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := sess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(newUnitId)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := sess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(newUnitId, sess.pid, 0)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
newProj := protocol.EncodeRaidBattleUnitSpirit(newUnitId, newTemplateId, sess.posX, sess.posY, sess.posZ, sess.facing, sess.pid)
|
newProj := protocol.EncodeRaidBattleUnitSpirit(newUnitId, newTemplateId, sess.posX, sess.posY, sess.posZ, sess.facing, sess.pid)
|
||||||
_ = sess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, newProj)
|
if err := sess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, newProj); err != nil {
|
||||||
_ = sess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(sess.pid, newTemplateId, newUnitId, true))
|
return err
|
||||||
|
}
|
||||||
|
if err := sess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(sess.pid, newTemplateId, newUnitId, true)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
wInSitu := protocol.NewUxWriter()
|
wInSitu := protocol.NewUxWriter()
|
||||||
wInSitu.U32(newTemplateId).U64(newUnitId)
|
wInSitu.U32(newTemplateId).U64(newUnitId)
|
||||||
_ = sess.NotifyData(protocol.MethodSyncSwitchSpiritInSitu, wInSitu.Bytes())
|
if err := sess.NotifyData(protocol.MethodSyncSwitchSpiritInSitu, wInSitu.Bytes()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
sess.templateId = newTemplateId
|
sess.templateId = newTemplateId
|
||||||
sess.unitId = newUnitId
|
sess.unitId = newUnitId
|
||||||
@@ -397,12 +503,6 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|||||||
case protocol.MethodAskSummonVehicle:
|
case protocol.MethodAskSummonVehicle:
|
||||||
return s.handleAskSummonVehicle(sess, pkt)
|
return s.handleAskSummonVehicle(sess, pkt)
|
||||||
|
|
||||||
case protocol.MethodAskDriveVehicle:
|
|
||||||
return s.handleAskDriveVehicle(sess, pkt)
|
|
||||||
|
|
||||||
case protocol.MethodAskExitVehicle:
|
|
||||||
return s.handleAskExitVehicle(sess, pkt)
|
|
||||||
|
|
||||||
case protocol.MethodAskVehicleMove:
|
case protocol.MethodAskVehicleMove:
|
||||||
return s.handleAskVehicleMove(sess, pkt)
|
return s.handleAskVehicleMove(sess, pkt)
|
||||||
|
|
||||||
@@ -413,20 +513,6 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|||||||
return s.handleAskVehicleSendGamePlaySignal(sess, pkt)
|
return s.handleAskVehicleSendGamePlaySignal(sess, pkt)
|
||||||
|
|
||||||
// ---------------- Police & Crime ----------------
|
// ---------------- Police & Crime ----------------
|
||||||
case protocol.MethodAskSetWantedLevel:
|
|
||||||
return s.handleAskSetWantedLevel(sess, pkt)
|
|
||||||
|
|
||||||
case protocol.MethodAskPoliceArrest:
|
|
||||||
return s.handleAskPoliceArrest(sess, pkt)
|
|
||||||
|
|
||||||
case protocol.MethodReportPoliceArrestFinish:
|
|
||||||
return s.handleReportPoliceArrestFinish(sess, pkt)
|
|
||||||
|
|
||||||
case protocol.MethodAskPayFine:
|
|
||||||
return s.handleAskPayFine(sess, pkt)
|
|
||||||
|
|
||||||
case protocol.MethodAskReleaseFromPrison:
|
|
||||||
return s.handleAskReleaseFromPrison(sess, pkt)
|
|
||||||
|
|
||||||
// ---------------- Dialog & NPC & Crowd ----------------
|
// ---------------- Dialog & NPC & Crowd ----------------
|
||||||
case protocol.MethodAskStartDialog:
|
case protocol.MethodAskStartDialog:
|
||||||
@@ -447,9 +533,6 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|||||||
case protocol.MethodAskDoNpcAction:
|
case protocol.MethodAskDoNpcAction:
|
||||||
return s.handleAskDoNpcAction(sess, pkt)
|
return s.handleAskDoNpcAction(sess, pkt)
|
||||||
|
|
||||||
case protocol.MethodAskSetCrowdDensity:
|
|
||||||
return s.handleAskSetCrowdDensity(sess, pkt)
|
|
||||||
|
|
||||||
case protocol.MethodAskPhoneAddCallRecord:
|
case protocol.MethodAskPhoneAddCallRecord:
|
||||||
return s.handleAskPhoneAddCallRecord(sess, pkt)
|
return s.handleAskPhoneAddCallRecord(sess, pkt)
|
||||||
|
|
||||||
@@ -484,21 +567,7 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|||||||
case protocol.MethodAskControlPowerHoldEnemyFallGrou:
|
case protocol.MethodAskControlPowerHoldEnemyFallGrou:
|
||||||
return s.handleAskControlPowerHoldEnemyFallGrou(sess, pkt)
|
return s.handleAskControlPowerHoldEnemyFallGrou(sess, pkt)
|
||||||
|
|
||||||
case protocol.MethodAskPlayerAttack:
|
|
||||||
return s.handleAskPlayerAttack(sess, pkt)
|
|
||||||
|
|
||||||
case protocol.MethodAskPlayerCastSkill:
|
|
||||||
return s.handleAskPlayerCastSkill(sess, pkt)
|
|
||||||
|
|
||||||
case protocol.MethodAskUnlockFightStyle:
|
|
||||||
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
|
|
||||||
|
|
||||||
// ---------------- Mini-games ----------------
|
// ---------------- Mini-games ----------------
|
||||||
case protocol.MethodAskChineseChessMove:
|
|
||||||
return s.handleAskChineseChessMove(sess, pkt)
|
|
||||||
|
|
||||||
case protocol.MethodAskMahjongAction:
|
|
||||||
return s.handleAskMahjongAction(sess, pkt)
|
|
||||||
|
|
||||||
case protocol.MethodEnterBalloonZone:
|
case protocol.MethodEnterBalloonZone:
|
||||||
return s.handleEnterBalloonZone(sess, pkt)
|
return s.handleEnterBalloonZone(sess, pkt)
|
||||||
@@ -554,7 +623,10 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|||||||
default:
|
default:
|
||||||
// Safe neutral typed fallback for unrouted RPC invokes
|
// Safe neutral typed fallback for unrouted RPC invokes
|
||||||
if pkt.Kind == protocol.PacketKindInvoke {
|
if pkt.Kind == protocol.PacketKindInvoke {
|
||||||
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
|
if body, ok := protocol.DefaultReturnBody(pkt.MethodID); ok {
|
||||||
|
return sess.ReturnData(pkt.InvokeID, pkt.MethodID, body)
|
||||||
|
}
|
||||||
|
return sess.SendPacket(&protocol.RpcPacket{Kind: protocol.PacketKindReturn, InvokeID: pkt.InvokeID, MethodID: pkt.MethodID, Error: -1})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,12 +635,18 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|||||||
|
|
||||||
// Teleport moves an active session to the specified coordinates.
|
// Teleport moves an active session to the specified coordinates.
|
||||||
func (sess *Session) Teleport(x, y, z, facing float32) error {
|
func (sess *Session) Teleport(x, y, z, facing float32) error {
|
||||||
|
sess.stateMu.Lock()
|
||||||
|
defer sess.stateMu.Unlock()
|
||||||
|
return sess.teleport(x, y, z, facing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *Session) teleport(x, y, z, facing float32) error {
|
||||||
sess.posX = x
|
sess.posX = x
|
||||||
sess.posY = y
|
sess.posY = y
|
||||||
sess.posZ = z
|
sess.posZ = z
|
||||||
sess.facing = facing
|
sess.facing = facing
|
||||||
|
|
||||||
w := protocol.NewUxWriter()
|
w := protocol.NewUxWriter()
|
||||||
w.U64(sess.unitId).F32(x).F32(y).F32(z).F32(facing).U8(0).Bool(false).U8(6) // SetPositionType.Teleport = 6
|
w.U64(sess.unitId).F32(x).F32(y).F32(z).F32(facing).U8(0).Bool(false).U8(6).U8(0).U8(0) // nullable moveGroundInfo and loadingInfo
|
||||||
return sess.NotifyData(protocol.MethodSyncUnitPositionAndFacing, w.Bytes())
|
return sess.NotifyData(protocol.MethodSyncUnitPositionAndFacing, w.Bytes())
|
||||||
}
|
}
|
||||||
@@ -133,7 +133,7 @@ func (s *Server) handleReportPreTeleportFinish(sess *Session, pkt *protocol.RpcP
|
|||||||
if opt != nil {
|
if opt != nil {
|
||||||
dest := opt.Position
|
dest := opt.Position
|
||||||
if dest.X != 0 || dest.Y != 0 || dest.Z != 0 {
|
if dest.X != 0 || dest.Y != 0 || dest.Z != 0 {
|
||||||
_ = sess.Teleport(dest.X, dest.Y, dest.Z, opt.Facing)
|
_ = sess.teleport(dest.X, dest.Y, dest.Z, opt.Facing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type RoadNetworkData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CityTrafficEngine struct {
|
type CityTrafficEngine struct {
|
||||||
|
wg sync.WaitGroup
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
vehicles map[uint64]*TrafficVehicle
|
vehicles map[uint64]*TrafficVehicle
|
||||||
lanes []RoadLane
|
lanes []RoadLane
|
||||||
@@ -97,23 +98,29 @@ func (te *CityTrafficEngine) Start(ctx context.Context) {
|
|||||||
LaneLength: lane.Length,
|
LaneLength: lane.Length,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
count := len(te.vehicles)
|
||||||
|
te.wg.Add(1)
|
||||||
te.mu.Unlock()
|
te.mu.Unlock()
|
||||||
|
|
||||||
log.Printf("[TRAFFIC] CityTrafficEngine started with %d simulated AI vehicles", len(te.vehicles))
|
log.Printf("[TRAFFIC] CityTrafficEngine started with %d simulated AI vehicles", count)
|
||||||
go te.runLoop(ctx)
|
go te.runLoop(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (te *CityTrafficEngine) Stop() {
|
func (te *CityTrafficEngine) Stop() {
|
||||||
te.mu.Lock()
|
te.mu.Lock()
|
||||||
defer te.mu.Unlock()
|
|
||||||
if te.cancel != nil {
|
if te.cancel != nil {
|
||||||
te.cancel()
|
te.cancel()
|
||||||
}
|
}
|
||||||
te.isRunning = false
|
te.isRunning = false
|
||||||
|
te.mu.Unlock()
|
||||||
|
te.wg.Wait()
|
||||||
|
te.mu.Lock()
|
||||||
te.vehicles = make(map[uint64]*TrafficVehicle)
|
te.vehicles = make(map[uint64]*TrafficVehicle)
|
||||||
|
te.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (te *CityTrafficEngine) runLoop(ctx context.Context) {
|
func (te *CityTrafficEngine) runLoop(ctx context.Context) {
|
||||||
|
defer te.wg.Done()
|
||||||
ticker := time.NewTicker(150 * time.Millisecond) // ~6.7 Hz downsampled traffic lane updates
|
ticker := time.NewTicker(150 * time.Millisecond) // ~6.7 Hz downsampled traffic lane updates
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
@@ -151,6 +158,9 @@ func (te *CityTrafficEngine) tick(dt float32) {
|
|||||||
methodID := protocol.MethodGetMethodIDDefault("SyncAetherAIVehicleLaneDatas", 68582850)
|
methodID := protocol.MethodGetMethodIDDefault("SyncAetherAIVehicleLaneDatas", 68582850)
|
||||||
sessions := te.server.GetActiveSessions()
|
sessions := te.server.GetActiveSessions()
|
||||||
for _, sess := range sessions {
|
for _, sess := range sessions {
|
||||||
|
if !sess.ready.Load() || sess.hangup.Load() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
_ = sess.NotifyData(methodID, payload)
|
_ = sess.NotifyData(methodID, payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,45 @@
|
|||||||
package gameserver
|
package gameserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
"ananta-ps/pkg/protocol"
|
"ananta-ps/pkg/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) SetWorldWeather(weatherId uint32) {
|
func (s *Server) SetWorldWeather(weatherId uint32) error {
|
||||||
log.Printf("[WEATHER] Setting world weather to %d", weatherId)
|
log.Printf("[WEATHER] Setting world weather to %d", weatherId)
|
||||||
w := protocol.NewUxWriter()
|
w := protocol.NewUxWriter()
|
||||||
w.U32(weatherId)
|
w.U32(weatherId).U32(weatherId).U32(0)
|
||||||
payload := w.Bytes()
|
payload := w.Bytes()
|
||||||
|
|
||||||
methodID := protocol.MethodGetMethodIDDefault("SyncTimeWeather", 68582910)
|
methodID := protocol.MethodGetMethodIDDefault("SyncPlayerWeather", 68712141)
|
||||||
for _, sess := range s.GetActiveSessions() {
|
for _, sess := range s.GetActiveSessions() {
|
||||||
_ = sess.NotifyData(methodID, payload)
|
if err := sess.NotifyData(methodID, payload); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) SetWorldTimeOfDay(hour float32) {
|
func (s *Server) SetWorldTimeOfDay(hour float32) error {
|
||||||
|
if math.IsNaN(float64(hour)) || math.IsInf(float64(hour), 0) || hour < 0 || hour >= 24 {
|
||||||
|
return fmt.Errorf("hour must be in [0,24)")
|
||||||
|
}
|
||||||
log.Printf("[WEATHER] Setting world time of day to %.1f:00", hour)
|
log.Printf("[WEATHER] Setting world time of day to %.1f:00", hour)
|
||||||
w := protocol.NewUxWriter()
|
w := protocol.NewUxWriter()
|
||||||
w.F32(hour)
|
w.U32(uint32(time.Now().Unix())).U32(uint32(hour * 3600)).Bool(true).Bool(false).U32(0).I32(0).I32(0)
|
||||||
payload := w.Bytes()
|
payload := w.Bytes()
|
||||||
|
|
||||||
methodID := protocol.MethodGetMethodIDDefault("SyncTimeOfDay", 68582911)
|
methodID := protocol.MethodGetMethodIDDefault("SyncPlayerCurrentTime", 68337673)
|
||||||
for _, sess := range s.GetActiveSessions() {
|
for _, sess := range s.GetActiveSessions() {
|
||||||
_ = sess.NotifyData(methodID, payload)
|
if err := sess.NotifyData(methodID, payload); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleGmUnlockAllFogMap(sess *Session, pkt *protocol.RpcPacket) error {
|
func (s *Server) handleGmUnlockAllFogMap(sess *Session, pkt *protocol.RpcPacket) error {
|
||||||
|
|||||||
+28
-67
@@ -60,80 +60,41 @@ func New(cfg *config.Config, publicDir, certDir string) *Gateway {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Gateway) Start() error {
|
func (g *Gateway) Start() error {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
cert, err := tls.LoadX509KeyPair(filepath.Join(g.certDir, "cert.pem"), filepath.Join(g.certDir, "key.pem"))
|
||||||
g.cancelFunc = cancel
|
if err != nil {
|
||||||
|
return fmt.Errorf("load gateway TLS certificate: %w", err)
|
||||||
|
}
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", g.handleDispatch)
|
mux.HandleFunc("/", g.handleDispatch)
|
||||||
|
addr := func(port int) string { return net.JoinHostPort(g.cfg.Network.BindHost, fmt.Sprint(port)) }
|
||||||
// 1. Port 5801: LoginList & Mobile Dispatch Server
|
g.listSrv = &http.Server{Addr: addr(g.cfg.Network.Proxy.LoginListPort), Handler: mux}
|
||||||
listAddr := fmt.Sprintf("%s:%d", g.cfg.Network.BindHost, g.cfg.Network.Proxy.LoginListPort)
|
g.httpSrv = &http.Server{Addr: addr(g.cfg.Network.Proxy.HttpPort), Handler: mux}
|
||||||
g.listSrv = &http.Server{
|
g.httpsSrv = &http.Server{Addr: addr(g.cfg.Network.Proxy.HttpsPort), Handler: mux,
|
||||||
Addr: listAddr,
|
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12}}
|
||||||
Handler: mux,
|
servers := []*http.Server{g.listSrv, g.httpSrv, g.httpsSrv}
|
||||||
}
|
listeners := make([]net.Listener, 0, len(servers))
|
||||||
|
for _, srv := range servers {
|
||||||
go func() {
|
ln, err := net.Listen("tcp", srv.Addr)
|
||||||
log.Printf("[GATEWAY] LoginList & Mobile Dispatch listening on http://%s (Port %d)", g.cfg.Network.AdvertisedHost, g.cfg.Network.Proxy.LoginListPort)
|
|
||||||
if err := g.listSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
log.Printf("[GATEWAY] LoginList server error: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 2. Port 80: HTTP Gateway
|
|
||||||
httpAddr := fmt.Sprintf("%s:%d", g.cfg.Network.BindHost, g.cfg.Network.Proxy.HttpPort)
|
|
||||||
g.httpSrv = &http.Server{
|
|
||||||
Addr: httpAddr,
|
|
||||||
Handler: mux,
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
log.Printf("[GATEWAY] HTTP Proxy listening on port %d", g.cfg.Network.Proxy.HttpPort)
|
|
||||||
if err := g.httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
log.Printf("[GATEWAY] HTTP Proxy error (might need Admin privileges for port 80): %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 3. Port 443: HTTPS Proxy with TLS SNI
|
|
||||||
certFile := filepath.Join(g.certDir, "cert.pem")
|
|
||||||
keyFile := filepath.Join(g.certDir, "key.pem")
|
|
||||||
if _, err := os.Stat(certFile); err == nil {
|
|
||||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
|
||||||
if err == nil {
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{cert},
|
|
||||||
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
||||||
return &cert, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
httpsAddr := fmt.Sprintf("%s:%d", g.cfg.Network.BindHost, g.cfg.Network.Proxy.HttpsPort)
|
|
||||||
g.httpsSrv = &http.Server{
|
|
||||||
Addr: httpsAddr,
|
|
||||||
Handler: mux,
|
|
||||||
TLSConfig: tlsConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
log.Printf("[GATEWAY] HTTPS SNI Proxy listening on port %d", g.cfg.Network.Proxy.HttpsPort)
|
|
||||||
ln, err := net.Listen("tcp", httpsAddr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[GATEWAY] HTTPS Proxy listen error (port 443): %v", err)
|
for _, opened := range listeners {
|
||||||
return
|
opened.Close()
|
||||||
}
|
}
|
||||||
tlsLn := tls.NewListener(ln, tlsConfig)
|
return fmt.Errorf("listen gateway %s: %w", srv.Addr, err)
|
||||||
if err := g.httpsSrv.Serve(tlsLn); err != nil && err != http.ErrServerClosed {
|
}
|
||||||
log.Printf("[GATEWAY] HTTPS Proxy error: %v", err)
|
listeners = append(listeners, ln)
|
||||||
|
}
|
||||||
|
for i, srv := range servers {
|
||||||
|
ln := listeners[i]
|
||||||
|
if srv == g.httpsSrv {
|
||||||
|
ln = tls.NewListener(ln, srv.TLSConfig)
|
||||||
|
}
|
||||||
|
log.Printf("[GATEWAY] Listening on %s", ln.Addr())
|
||||||
|
go func() {
|
||||||
|
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Printf("[GATEWAY] Serve error: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
} else {
|
|
||||||
log.Printf("[GATEWAY] Warning: failed to load TLS certificate %s: %v", certFile, err)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
log.Printf("[GATEWAY] Warning: cert.pem not found in %s; HTTPS port 443 disabled", g.certDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = ctx
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package gateway
|
package gateway
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -9,6 +11,41 @@ import (
|
|||||||
"ananta-ps/internal/config"
|
"ananta-ps/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestGatewayReportsBindFailureAndRollsBack(t *testing.T) {
|
||||||
|
blocker, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer blocker.Close()
|
||||||
|
probe, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
listPort := probe.Addr().(*net.TCPAddr).Port
|
||||||
|
probe.Close()
|
||||||
|
cfg := &config.Config{Network: config.NetworkConfig{BindHost: "127.0.0.1", Proxy: config.ProxyConfig{
|
||||||
|
LoginListPort: listPort, HttpPort: blocker.Addr().(*net.TCPAddr).Port,
|
||||||
|
}}}
|
||||||
|
gw := New(cfg, "../../public", "../../certs")
|
||||||
|
if err := gw.Start(); err == nil {
|
||||||
|
gw.Stop()
|
||||||
|
t.Fatal("occupied HTTP port reported as ready")
|
||||||
|
}
|
||||||
|
ln, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", fmt.Sprint(listPort)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("partial startup leaked login-list listener: %v", err)
|
||||||
|
}
|
||||||
|
ln.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGatewayRequiresUsableTLSCertificate(t *testing.T) {
|
||||||
|
gw := New(&config.Config{}, t.TempDir(), t.TempDir())
|
||||||
|
if err := gw.Start(); err == nil {
|
||||||
|
gw.Stop()
|
||||||
|
t.Fatal("missing certificate silently disabled HTTPS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGatewayDispatch(t *testing.T) {
|
func TestGatewayDispatch(t *testing.T) {
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Client: config.ClientConfig{
|
Client: config.ClientConfig{
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package logingate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"ananta-ps/internal/config"
|
||||||
|
"ananta-ps/pkg/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
type recordingConn struct{ bytes.Buffer }
|
||||||
|
|
||||||
|
func (c *recordingConn) Close() error { return nil }
|
||||||
|
func (c *recordingConn) LocalAddr() net.Addr { return &net.TCPAddr{} }
|
||||||
|
func (c *recordingConn) RemoteAddr() net.Addr { return &net.TCPAddr{} }
|
||||||
|
func (c *recordingConn) SetDeadline(time.Time) error { return nil }
|
||||||
|
func (c *recordingConn) SetReadDeadline(time.Time) error { return nil }
|
||||||
|
func (c *recordingConn) SetWriteDeadline(time.Time) error { return nil }
|
||||||
|
|
||||||
|
func TestColdLoginRoleList(t *testing.T) {
|
||||||
|
cfg := &config.Config{Login: config.LoginSettings{RoleListMode: "list"}, Player: config.PlayerConfig{Pid: 100}}
|
||||||
|
srv := New(cfg)
|
||||||
|
conn := &recordingConn{}
|
||||||
|
sess := &Session{conn: conn}
|
||||||
|
// Invoke ID zero is valid: packet kind, rather than ID, controls replies.
|
||||||
|
if err := srv.dispatch(sess, &protocol.RpcPacket{Kind: protocol.PacketKindInvoke, MethodID: 34529582}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f, err := protocol.ReadFrame(conn, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if pkt := protocol.ParseRpcPacket(f.Payload); pkt.Kind != protocol.PacketKindReturn || pkt.InvokeID != 0 {
|
||||||
|
t.Fatalf("missing invoke response: %+v", pkt)
|
||||||
|
}
|
||||||
|
f, err = protocol.ReadFrame(conn, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
pkt := protocol.ParseRpcPacket(f.Payload)
|
||||||
|
if pkt.MethodID != 35167428 || !bytes.Equal(pkt.Body, []byte{0xff, 1}) {
|
||||||
|
t.Fatalf("malformed role list: %+v", pkt)
|
||||||
|
}
|
||||||
|
if err := srv.dispatch(sess, &protocol.RpcPacket{Kind: protocol.PacketKindNotify, MethodID: 34700853}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if conn.Len() != 0 {
|
||||||
|
t.Fatal("reply emitted for notify")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStopClosesIdleHandshake(t *testing.T) {
|
||||||
|
srv := New(&config.Config{Network: config.NetworkConfig{BindHost: "127.0.0.1", LoginPorts: []int{0}}})
|
||||||
|
if err := srv.Start(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
conn, err := net.DialTimeout("tcp", srv.listeners[0].Addr().String(), time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
deadline := time.Now().Add(time.Second)
|
||||||
|
for {
|
||||||
|
srv.mu.Lock()
|
||||||
|
n := len(srv.connections)
|
||||||
|
srv.mu.Unlock()
|
||||||
|
if n == 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
t.Fatal("connection not accepted")
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() { srv.Stop(); close(done) }()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("Stop blocked")
|
||||||
|
}
|
||||||
|
conn.SetReadDeadline(time.Now().Add(time.Second))
|
||||||
|
if _, err := conn.Read(make([]byte, 1)); err == nil {
|
||||||
|
t.Fatal("idle socket survived Stop")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"ananta-ps/internal/config"
|
"ananta-ps/internal/config"
|
||||||
"ananta-ps/pkg/crypto/chacha8"
|
"ananta-ps/pkg/crypto/chacha8"
|
||||||
@@ -18,11 +19,14 @@ type Server struct {
|
|||||||
listeners []net.Listener
|
listeners []net.Listener
|
||||||
cancelFunc context.CancelFunc
|
cancelFunc context.CancelFunc
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
mu sync.Mutex
|
||||||
|
connections map[net.Conn]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *config.Config) *Server {
|
func New(cfg *config.Config) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
connections: make(map[net.Conn]struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +58,11 @@ func (s *Server) Stop() error {
|
|||||||
for _, ln := range s.listeners {
|
for _, ln := range s.listeners {
|
||||||
_ = ln.Close()
|
_ = ln.Close()
|
||||||
}
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
for conn := range s.connections {
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
s.wg.Wait()
|
s.wg.Wait()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -72,11 +81,26 @@ func (s *Server) acceptLoop(ctx context.Context, ln net.Listener, port int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go s.handleConnection(ctx, conn, port)
|
s.mu.Lock()
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
s.mu.Unlock()
|
||||||
|
conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.connections[conn] = struct{}{}
|
||||||
|
s.wg.Add(1)
|
||||||
|
s.mu.Unlock()
|
||||||
|
go func() {
|
||||||
|
defer s.wg.Done()
|
||||||
|
defer func() { s.mu.Lock(); delete(s.connections, conn); s.mu.Unlock() }()
|
||||||
|
s.handleConnection(ctx, conn, port)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
|
replyKind protocol.RpcPacketKind
|
||||||
|
writeTimeout time.Duration
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
readCipher *chacha8.UxChaCha8
|
readCipher *chacha8.UxChaCha8
|
||||||
sendCipher *chacha8.UxChaCha8
|
sendCipher *chacha8.UxChaCha8
|
||||||
@@ -97,10 +121,28 @@ func (sess *Session) SendPacket(packet *protocol.RpcPacket) error {
|
|||||||
payload = protocol.BuildNotify(packet.MethodID, packet.Body)
|
payload = protocol.BuildNotify(packet.MethodID, packet.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
return protocol.WriteFrame(sess.conn, 0x09, payload, sess.sendCipher)
|
return sess.writeFrameLocked(0x09, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *Session) writeFrameLocked(mode byte, payload []byte) error {
|
||||||
|
timeout := sess.writeTimeout
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 15 * time.Second
|
||||||
|
}
|
||||||
|
if err := sess.conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := protocol.WriteFrame(sess.conn, mode, payload, sess.sendCipher); err != nil {
|
||||||
|
sess.conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sess *Session) ReturnEmptyOk(invokeID int32, methodID uint32) error {
|
func (sess *Session) ReturnEmptyOk(invokeID int32, methodID uint32) error {
|
||||||
|
if sess.replyKind != protocol.PacketKindInvoke {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return sess.SendPacket(&protocol.RpcPacket{
|
return sess.SendPacket(&protocol.RpcPacket{
|
||||||
Kind: protocol.PacketKindReturn,
|
Kind: protocol.PacketKindReturn,
|
||||||
InvokeID: invokeID,
|
InvokeID: invokeID,
|
||||||
@@ -111,6 +153,9 @@ func (sess *Session) ReturnEmptyOk(invokeID int32, methodID uint32) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sess *Session) ReturnEmpty(invokeID int32, methodID uint32, errCode int32) error {
|
func (sess *Session) ReturnEmpty(invokeID int32, methodID uint32, errCode int32) error {
|
||||||
|
if sess.replyKind != protocol.PacketKindInvoke {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return sess.SendPacket(&protocol.RpcPacket{
|
return sess.SendPacket(&protocol.RpcPacket{
|
||||||
Kind: protocol.PacketKindReturn,
|
Kind: protocol.PacketKindReturn,
|
||||||
InvokeID: invokeID,
|
InvokeID: invokeID,
|
||||||
@@ -121,6 +166,9 @@ func (sess *Session) ReturnEmpty(invokeID int32, methodID uint32, errCode int32)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sess *Session) ReturnData(invokeID int32, methodID uint32, body []byte) error {
|
func (sess *Session) ReturnData(invokeID int32, methodID uint32, body []byte) error {
|
||||||
|
if sess.replyKind != protocol.PacketKindInvoke {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return sess.SendPacket(&protocol.RpcPacket{
|
return sess.SendPacket(&protocol.RpcPacket{
|
||||||
Kind: protocol.PacketKindReturn,
|
Kind: protocol.PacketKindReturn,
|
||||||
InvokeID: invokeID,
|
InvokeID: invokeID,
|
||||||
@@ -131,6 +179,9 @@ func (sess *Session) ReturnData(invokeID int32, methodID uint32, body []byte) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sess *Session) NotifyData(methodID uint32, body []byte) error {
|
func (sess *Session) NotifyData(methodID uint32, body []byte) error {
|
||||||
|
if methodID == 0 {
|
||||||
|
return fmt.Errorf("unverified notification method")
|
||||||
|
}
|
||||||
return sess.SendPacket(&protocol.RpcPacket{
|
return sess.SendPacket(&protocol.RpcPacket{
|
||||||
Kind: protocol.PacketKindNotify,
|
Kind: protocol.PacketKindNotify,
|
||||||
MethodID: methodID,
|
MethodID: methodID,
|
||||||
@@ -151,6 +202,7 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn, port int)
|
|||||||
|
|
||||||
sess := &Session{
|
sess := &Session{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
|
writeTimeout: time.Duration(s.cfg.Network.WriteTimeoutMs) * time.Millisecond,
|
||||||
readCipher: readCipher,
|
readCipher: readCipher,
|
||||||
sendCipher: writeCipher,
|
sendCipher: writeCipher,
|
||||||
}
|
}
|
||||||
@@ -173,10 +225,18 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn, port int)
|
|||||||
// Frame mode 0x03 is heartbeat
|
// Frame mode 0x03 is heartbeat
|
||||||
if frame.Mode == 0x03 {
|
if frame.Mode == 0x03 {
|
||||||
// Echo heartbeat back
|
// Echo heartbeat back
|
||||||
_ = protocol.WriteFrame(conn, 0x03, frame.Payload, sess.sendCipher)
|
sess.mu.Lock()
|
||||||
|
err = sess.writeFrameLocked(0x03, frame.Payload)
|
||||||
|
sess.mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if frame.Mode != 0x09 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
packet := protocol.ParseRpcPacket(frame.Payload)
|
packet := protocol.ParseRpcPacket(frame.Payload)
|
||||||
if err := s.dispatch(sess, &packet); err != nil {
|
if err := s.dispatch(sess, &packet); err != nil {
|
||||||
log.Printf("[LOGIN-GATE] Dispatch error for method %d: %v", packet.MethodID, err)
|
log.Printf("[LOGIN-GATE] Dispatch error for method %d: %v", packet.MethodID, err)
|
||||||
@@ -185,6 +245,11 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn, port int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
||||||
|
if pkt.Kind != protocol.PacketKindInvoke && pkt.Kind != protocol.PacketKindNotify {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sess.replyKind = pkt.Kind
|
||||||
|
defer func() { sess.replyKind = protocol.PacketKindUnknown }()
|
||||||
switch pkt.MethodID {
|
switch pkt.MethodID {
|
||||||
case protocol.MethodCheckVersion, protocol.MethodHasOnlinePlayer:
|
case protocol.MethodCheckVersion, protocol.MethodHasOnlinePlayer:
|
||||||
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
|
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
|
||||||
@@ -213,6 +278,7 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|||||||
s.cfg.Player.AccountId,
|
s.cfg.Player.AccountId,
|
||||||
s.cfg.Player.UserName,
|
s.cfg.Player.UserName,
|
||||||
s.cfg.Player.LoginToken,
|
s.cfg.Player.LoginToken,
|
||||||
|
s.cfg.Player.DisplayName,
|
||||||
s.cfg.Client.Aid,
|
s.cfg.Client.Aid,
|
||||||
s.cfg.Client.ServerID,
|
s.cfg.Client.ServerID,
|
||||||
s.cfg.Player.Pid,
|
s.cfg.Player.Pid,
|
||||||
@@ -223,10 +289,7 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|||||||
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
|
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Send SyncRoleList (35167428)
|
return s.syncRoleList(sess)
|
||||||
w := protocol.NewUxWriter()
|
|
||||||
w.U64(s.cfg.Player.Pid)
|
|
||||||
return sess.NotifyData(protocol.MethodSyncRoleList, w.Bytes())
|
|
||||||
|
|
||||||
case protocol.MethodRequestCreateRoleEx:
|
case protocol.MethodRequestCreateRoleEx:
|
||||||
w := protocol.NewUxWriter()
|
w := protocol.NewUxWriter()
|
||||||
@@ -234,7 +297,7 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|||||||
if err := sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes()); err != nil {
|
if err := sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return sess.NotifyData(protocol.MethodSyncRoleList, w.Bytes())
|
return s.syncRoleList(sess)
|
||||||
|
|
||||||
case protocol.MethodRequestEnterGame, protocol.MethodDebugRequestEnterGame:
|
case protocol.MethodRequestEnterGame, protocol.MethodDebugRequestEnterGame:
|
||||||
body := protocol.EncodeEnterGame(
|
body := protocol.EncodeEnterGame(
|
||||||
@@ -294,10 +357,26 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
if pkt.Kind == protocol.PacketKindInvoke {
|
if pkt.Kind == protocol.PacketKindInvoke {
|
||||||
log.Printf("[LOGIN-GATE] Unhandled Invoke MethodID %d, returning empty OK", pkt.MethodID)
|
log.Printf("[LOGIN-GATE] Unhandled Invoke MethodID %d, using contract fallback", pkt.MethodID)
|
||||||
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
|
if body, ok := protocol.DefaultReturnBody(pkt.MethodID); ok {
|
||||||
|
return sess.ReturnData(pkt.InvokeID, pkt.MethodID, body)
|
||||||
|
}
|
||||||
|
return sess.SendPacket(&protocol.RpcPacket{Kind: protocol.PacketKindReturn, InvokeID: pkt.InvokeID, MethodID: pkt.MethodID, Error: -1})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Match the original server: unsolicited role lists are opt-in because the legacy pid
|
||||||
|
// payload is not a list and can stall a cold login.
|
||||||
|
func (s *Server) syncRoleList(sess *Session) error {
|
||||||
|
switch s.cfg.Login.RoleListMode {
|
||||||
|
case "pid":
|
||||||
|
return sess.NotifyData(protocol.MethodSyncRoleList, protocol.NewUxWriter().U64(s.cfg.Player.Pid).Bytes())
|
||||||
|
case "list":
|
||||||
|
return sess.NotifyData(protocol.MethodSyncRoleList, protocol.EncodeEmptyList())
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-2
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -43,15 +44,24 @@ func (a *AdminServer) Start() error {
|
|||||||
mux.HandleFunc("/api/command", a.handleCommand)
|
mux.HandleFunc("/api/command", a.handleCommand)
|
||||||
mux.HandleFunc("/api/sessions", a.handleSessions)
|
mux.HandleFunc("/api/sessions", a.handleSessions)
|
||||||
|
|
||||||
addr := fmt.Sprintf("%s:%d", a.cfg.Network.BindHost, a.cfg.Admin.Port)
|
host := a.cfg.Admin.Host
|
||||||
|
if host == "" {
|
||||||
|
host = "127.0.0.1"
|
||||||
|
}
|
||||||
|
addr := net.JoinHostPort(host, fmt.Sprint(a.cfg.Admin.Port))
|
||||||
a.httpSrv = &http.Server{
|
a.httpSrv = &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
go func() {
|
go func() {
|
||||||
log.Printf("[ADMIN-GM] Admin Web Panel running at http://%s:%d/", a.cfg.Network.AdvertisedHost, a.cfg.Admin.Port)
|
log.Printf("[ADMIN-GM] Admin Web Panel running at http://%s:%d/", a.cfg.Network.AdvertisedHost, a.cfg.Admin.Port)
|
||||||
if err := a.httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := a.httpSrv.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||||
log.Printf("[ADMIN-GM] Web server error: %v", err)
|
log.Printf("[ADMIN-GM] Web server error: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -40,9 +41,14 @@ func (d *DebugServer) Start() error {
|
|||||||
Handler: mux,
|
Handler: mux,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
go func() {
|
go func() {
|
||||||
log.Printf("[DEBUG-API] Debug WebUI panel at http://%s:5809/", d.cfg.Network.AdvertisedHost)
|
log.Printf("[DEBUG-API] Debug WebUI panel at http://%s:5809/", d.cfg.Network.AdvertisedHost)
|
||||||
if err := d.httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := d.httpSrv.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||||
log.Printf("[DEBUG-API] Debug server error: %v", err)
|
log.Printf("[DEBUG-API] Debug server error: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ananta-ps/internal/config"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAdminHonorsConfiguredHost(t *testing.T) {
|
||||||
|
cfg := &config.Config{Network: config.NetworkConfig{BindHost: "0.0.0.0"}, Admin: config.AdminConfig{Enabled: true, Host: "127.0.0.1"}}
|
||||||
|
a := NewAdmin(cfg, nil)
|
||||||
|
if err := a.Start(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer a.Stop()
|
||||||
|
if a.httpSrv.Addr != "127.0.0.1:0" {
|
||||||
|
t.Fatalf("admin bind ignored: %s", a.httpSrv.Addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
-19
@@ -1,7 +1,6 @@
|
|||||||
package protocol
|
package protocol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -40,7 +39,7 @@ func EncodeEmptyList() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EncodeCheckAccount encodes CheckAccountResult.
|
// EncodeCheckAccount encodes CheckAccountResult.
|
||||||
func EncodeCheckAccount(accountId, userName, token string, aid, serverId int, pid uint64) []byte {
|
func EncodeCheckAccount(accountId, userName, token, displayName string, aid, serverId int, pid uint64) []byte {
|
||||||
loginData := map[string]interface{}{
|
loginData := map[string]interface{}{
|
||||||
"code": 200,
|
"code": 200,
|
||||||
"subcode": 0,
|
"subcode": 0,
|
||||||
@@ -52,7 +51,7 @@ func EncodeCheckAccount(accountId, userName, token string, aid, serverId int, pi
|
|||||||
"pid": pid,
|
"pid": pid,
|
||||||
"player_id": fmt.Sprintf("%d", pid),
|
"player_id": fmt.Sprintf("%d", pid),
|
||||||
"role_id": fmt.Sprintf("%d", pid),
|
"role_id": fmt.Sprintf("%d", pid),
|
||||||
"role_name": "辰宿",
|
"role_name": displayName,
|
||||||
"server_id": fmt.Sprintf("%d", serverId),
|
"server_id": fmt.Sprintf("%d", serverId),
|
||||||
"host_id": serverId,
|
"host_id": serverId,
|
||||||
"username": userName,
|
"username": userName,
|
||||||
@@ -144,10 +143,10 @@ func EncodeEnterScene(pid uint64, raidId uint32, instanceId uint64, universeId u
|
|||||||
// SectorControlIds (empty list)
|
// SectorControlIds (empty list)
|
||||||
w.U8(0xFF).Int7(0)
|
w.U8(0xFF).Int7(0)
|
||||||
w.U32(0) // DynamicLayerId
|
w.U32(0) // DynamicLayerId
|
||||||
// LoadingType (Complex 0xFF: Type, Members list, Ripple)
|
// LoadingType (TypeMark 1: byte Type, Int32 Members list, Ripple)
|
||||||
w.U8(0xFF).U32(0).U8(0xFF).Int7(0).Bool(false)
|
w.U8(1).U8(0).U8(0xFF).I32(0).Bool(false)
|
||||||
// LinkSimpleInfo (Complex 0xFF: Id, Mode, DeviceLevel, Tag, PublicEventId, LastPublicEventTime, HostPid)
|
// LinkSimpleInfo (TypeMark 1: U64 Id, three bytes, U32 event/time, U64 host)
|
||||||
w.U8(0xFF).U32(0).U32(0).U32(1).U32(0).U32(0).U64(0).U64(0)
|
w.U8(1).U64(0).U8(0).U8(1).U8(0).U32(0).U32(0).U64(0)
|
||||||
// DataLayerIndices (empty list)
|
// DataLayerIndices (empty list)
|
||||||
w.U8(0xFF).Int7(0)
|
w.U8(0xFF).Int7(0)
|
||||||
w.Bool(false) // Seamless
|
w.Bool(false) // Seamless
|
||||||
@@ -230,7 +229,7 @@ func EncodeRaidBattleUnitSpirit(unitId uint64, templateId uint32, posX, posY, po
|
|||||||
w.U8(0).U32(0)
|
w.U8(0).U32(0)
|
||||||
|
|
||||||
// GroundData (MoveActionGroundData Complex 0xFF)
|
// GroundData (MoveActionGroundData Complex 0xFF)
|
||||||
w.U8(0xFF).U32(0).U64(0).U32(0)
|
w.U8(0xFF).U8(0).U64(0).U8(0)
|
||||||
// LocalPos (UXVector3)
|
// LocalPos (UXVector3)
|
||||||
w.F32(0).F32(0).F32(0)
|
w.F32(0).F32(0).F32(0)
|
||||||
|
|
||||||
@@ -244,17 +243,43 @@ func EncodePlayerInfo(templatePath string, aid int, pid uint64, accountId, name
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res := make([]byte, len(data))
|
// PlayerClientInfo: complex marker, Config buffer (Int7), InfoLogin marker,
|
||||||
copy(res, data)
|
// Aid, Pid, AccountId, Name. String lengths may change; preserve the rest verbatim.
|
||||||
|
r := NewUxReader(data)
|
||||||
// Patch Aid at offset 4 (4 bytes)
|
mark, err := r.U8()
|
||||||
if len(res) > 8 {
|
if err != nil || mark != 0xff {
|
||||||
binary.LittleEndian.PutUint32(res[4:8], uint32(aid))
|
return nil, fmt.Errorf("invalid PlayerClientInfo marker")
|
||||||
}
|
}
|
||||||
// Patch Pid at offset 8 (8 bytes)
|
mark, err = r.U8()
|
||||||
if len(res) > 16 {
|
if err != nil || mark != 0xff {
|
||||||
binary.LittleEndian.PutUint64(res[8:16], pid)
|
return nil, fmt.Errorf("invalid Config buffer marker")
|
||||||
}
|
}
|
||||||
|
n, err := r.Int7()
|
||||||
return res, nil
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err = r.Bytes(n); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mark, err = r.U8()
|
||||||
|
if err != nil || mark != 0xff {
|
||||||
|
return nil, fmt.Errorf("invalid InfoLogin marker")
|
||||||
|
}
|
||||||
|
prefixEnd := r.Position()
|
||||||
|
if _, err = r.I32(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err = r.U64(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err = r.UxString(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err = r.UxString(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w := NewUxWriter()
|
||||||
|
w.Raw(data[:prefixEnd]).I32(int32(aid)).U64(pid).UxString(accountId).UxString(name)
|
||||||
|
w.Raw(data[r.Position():])
|
||||||
|
return w.Bytes(), nil
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
+11
-2
@@ -24,7 +24,7 @@ func ReadFrame(r io.Reader, cryptor *chacha8.UxChaCha8) (*Frame, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
payloadLength := int(binary.LittleEndian.Uint32(head[0:4]))
|
payloadLength := int(binary.LittleEndian.Uint32(head[0:4]))
|
||||||
if payloadLength < 0 || payloadLength > 16*1024*1024 {
|
if payloadLength < 0 || payloadLength > 8*1024*1024 {
|
||||||
return nil, errors.New("invalid frame payload length after decrypt")
|
return nil, errors.New("invalid frame payload length after decrypt")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,8 +54,17 @@ func WriteFrame(w io.Writer, mode byte, payload []byte, cryptor *chacha8.UxChaCh
|
|||||||
cryptor.XorInPlace(buf)
|
cryptor.XorInPlace(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := w.Write(buf)
|
for len(buf) > 0 {
|
||||||
|
n, err := w.Write(buf)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
if n <= 0 || n > len(buf) {
|
||||||
|
return io.ErrShortWrite
|
||||||
|
}
|
||||||
|
buf = buf[n:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseRpcPacket(payload []byte) RpcPacket {
|
func ParseRpcPacket(payload []byte) RpcPacket {
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ func HandleHandshake(conn net.Conn) (*chacha8.UxChaCha8, *chacha8.UxChaCha8, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
if frame.Mode != 0x01 {
|
||||||
|
return nil, nil, errors.New("invalid handshake frame mode")
|
||||||
|
}
|
||||||
if len(frame.Payload) < 20 {
|
if len(frame.Payload) < 20 {
|
||||||
return nil, nil, errors.New("client handshake too short")
|
return nil, nil, errors.New("client handshake too short")
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-89
@@ -21,111 +21,88 @@ const (
|
|||||||
MethodDebugRequestEnterGame uint32 = 34478273
|
MethodDebugRequestEnterGame uint32 = 34478273
|
||||||
MethodAskCloseConnectionToGate uint32 = 52140059
|
MethodAskCloseConnectionToGate uint32 = 52140059
|
||||||
MethodGetServerTime uint32 = 52951195
|
MethodGetServerTime uint32 = 52951195
|
||||||
MethodSendServerTime uint32 = 52296181
|
MethodSendServerTime uint32 = 53477227
|
||||||
MethodGateLogin uint32 = 52023760
|
MethodGateLogin uint32 = 52023760 // Legacy name: Login
|
||||||
MethodSyncPlayerGameServerInfo uint32 = 52737039
|
MethodSyncPlayerGameServerInfo uint32 = 154693736
|
||||||
MethodSyncGameSwitchToClient_3 uint32 = 53940944
|
MethodSyncGameSwitchToClient_3 uint32 = 53940944
|
||||||
MethodQuerySkey uint32 = 52367757
|
MethodQuerySkey uint32 = 153090217
|
||||||
MethodReportNetworkQuality uint32 = 52623348
|
MethodReportNetworkQuality uint32 = 52443839
|
||||||
MethodDavinciCode uint32 = 52458997
|
MethodDavinciCode uint32 = 153804824
|
||||||
MethodGetFriendApplicationListToMe uint32 = 52123985
|
MethodGetFriendApplicationListToMe uint32 = 153718831
|
||||||
MethodGetAllChatGroupLatestMessage uint32 = 52345719
|
MethodGetAllChatGroupLatestMessage uint32 = 153934570
|
||||||
MethodGetSimplePlayerInfoByPidList uint32 = 52781934
|
MethodGetSimplePlayerInfoByPidList uint32 = 153582555
|
||||||
|
|
||||||
// Game / World
|
// Game / World
|
||||||
MethodLoginGame uint32 = 63142082
|
MethodLoginGame uint32 = 63142082
|
||||||
MethodAskEnterWorld uint32 = 63142082
|
MethodAskEnterWorld uint32 = 63142082
|
||||||
MethodSendServerTimeGame uint32 = 63853503
|
MethodSendServerTimeGame uint32 = 64114176
|
||||||
MethodSyncPlayerInfo uint32 = 63200762
|
MethodSyncPlayerInfo uint32 = 64682159
|
||||||
MethodSyncEnterScene uint32 = 67711467
|
MethodSyncEnterScene uint32 = 64406364
|
||||||
MethodAskLoadSceneCompleted uint32 = 66879899
|
MethodAskLoadSceneCompleted uint32 = 67933251
|
||||||
MethodAskLoadingFinished uint32 = 67017252
|
MethodAskLoadingFinished uint32 = 67722670
|
||||||
MethodSyncLogicAgentEnter uint32 = 68057283
|
MethodSyncLogicAgentEnter uint32 = 68719890
|
||||||
MethodSyncLogicAgentLeave uint32 = 68057315
|
MethodSyncLogicAgentLeave uint32 = 68281244
|
||||||
MethodSyncManagedLogicAgent uint32 = 68611146
|
MethodSyncManagedLogicAgent uint32 = 68771737
|
||||||
MethodSyncRaidBattleUnitSpirit uint32 = 68953151
|
MethodSyncRaidBattleUnitSpirit uint32 = 68984102
|
||||||
MethodSyncPlayerCurrentSpirit uint32 = 68582738
|
MethodSyncPlayerCurrentSpirit uint32 = 68830687
|
||||||
MethodSyncSceneLoadCompleted uint32 = 67610360
|
MethodSyncSceneLoadCompleted uint32 = 68772583
|
||||||
MethodSyncGamePause uint32 = 67039327
|
MethodSyncGamePause uint32 = 68031250
|
||||||
MethodSyncPlayerLoadRate uint32 = 68383503
|
MethodSyncPlayerLoadRate uint32 = 68547190
|
||||||
MethodAskLoadedInSameScene uint32 = 68838634
|
MethodAskLoadedInSameScene uint32 = 67741470
|
||||||
MethodSyncUnitPositionAndFacing uint32 = 68664188
|
MethodSyncUnitPositionAndFacing uint32 = 68030312
|
||||||
MethodSyncPlayerPositionAndFacing uint32 = 68702330
|
MethodSyncSceneFogMapAllUnlock uint32 = 64915741
|
||||||
MethodSyncSceneFogMapAllUnlock uint32 = 68677150
|
|
||||||
MethodSyncRunGmUtils uint32 = 66042045
|
MethodSyncRunGmUtils uint32 = 66042045
|
||||||
MethodSyncAllSpiritCombatPower uint32 = 68798150
|
MethodSyncAllSpiritCombatPower uint32 = 64826316
|
||||||
MethodSyncSwitchSpiritInSitu uint32 = 68582770
|
MethodSyncSwitchSpiritInSitu uint32 = 68412586
|
||||||
|
|
||||||
// Movement
|
// Movement
|
||||||
MethodAskMoveStart uint32 = 68412852
|
|
||||||
MethodAskMoveStop uint32 = 68412853
|
|
||||||
MethodAskUpdatePlayerPosition uint32 = 68058291
|
|
||||||
|
|
||||||
// Character Switching
|
// Character Switching
|
||||||
MethodAskPreSwitchSpirit uint32 = 68415783
|
MethodAskSwitchSpirit uint32 = 67482531
|
||||||
MethodAskSwitchSpirit uint32 = 68415784
|
|
||||||
|
|
||||||
// Teleportation & Scene
|
// Teleportation & Scene
|
||||||
MethodAskTeleport uint32 = 67891230
|
MethodAskTeleport uint32 = 63175104
|
||||||
MethodReportPreTeleportFinish uint32 = 67891231
|
MethodReportPreTeleportFinish uint32 = 63032762
|
||||||
MethodReportPostTeleportFinish uint32 = 67891232
|
MethodReportPostTeleportFinish uint32 = 63682240
|
||||||
MethodAskEnterRaidByMapEntrance uint32 = 67891233
|
MethodAskEnterRaidByMapEntrance uint32 = 63510026
|
||||||
MethodAskTeleportToScene uint32 = 67891234
|
|
||||||
MethodSwitchSceneDirect uint32 = 67891235
|
|
||||||
|
|
||||||
// Vehicles
|
// Vehicles
|
||||||
MethodAskSummonVehicle uint32 = 68221895
|
MethodAskSummonVehicle uint32 = 67012447
|
||||||
MethodAskDriveVehicle uint32 = 68221893
|
MethodAskVehicleMove uint32 = 67743393
|
||||||
MethodAskExitVehicle uint32 = 68221894
|
MethodChangeVehicleDoorState uint32 = 67018217
|
||||||
MethodAskVehicleMove uint32 = 68221896
|
MethodAskVehicleSendGamePlaySignal uint32 = 63816400
|
||||||
MethodChangeVehicleDoorState uint32 = 68221897
|
|
||||||
MethodAskVehicleSendGamePlaySignal uint32 = 68221898
|
|
||||||
MethodSyncPlayerVehicleInfo uint32 = 68221926
|
|
||||||
MethodSyncAetherVehicleInit uint32 = 68221958
|
|
||||||
|
|
||||||
// Police & Wanted
|
// Police & Wanted
|
||||||
MethodAskSetWantedLevel uint32 = 67600001
|
|
||||||
MethodAskPoliceArrest uint32 = 67600002
|
|
||||||
MethodReportPoliceArrestFinish uint32 = 67600003
|
|
||||||
MethodAskPayFine uint32 = 67600004
|
|
||||||
MethodAskReleaseFromPrison uint32 = 67600005
|
|
||||||
|
|
||||||
// Dialog & NPC & Crowd
|
// Dialog & NPC & Crowd
|
||||||
MethodAskStartDialog uint32 = 67400001
|
MethodAskStartDialog uint32 = 63754798
|
||||||
MethodAskDoDialogAction uint32 = 67400002
|
MethodAskDoDialogAction uint32 = 63723634
|
||||||
MethodAskForceFinishDialog uint32 = 67400003
|
MethodAskForceFinishDialog uint32 = 63010951
|
||||||
MethodGetCharacterRandomDialog uint32 = 67400004
|
MethodGetCharacterRandomDialog uint32 = 63175540
|
||||||
MethodAskAgentStartNpcSound uint32 = 67400005
|
MethodAskAgentStartNpcSound uint32 = 67003057
|
||||||
MethodAskDoNpcAction uint32 = 67400006
|
MethodAskDoNpcAction uint32 = 67257034
|
||||||
MethodAskSetCrowdDensity uint32 = 67400007
|
MethodAskPhoneAddCallRecord uint32 = 63487875
|
||||||
MethodAskPhoneAddCallRecord uint32 = 67400010
|
MethodAskPhoneContactOptionAction uint32 = 63918677
|
||||||
MethodAskPhoneContactOptionAction uint32 = 67400011
|
MethodAskSummonGangMember uint32 = 63745420
|
||||||
MethodAskSummonGangMember uint32 = 67400020
|
MethodAskDestroyAllGangMember uint32 = 63243016
|
||||||
MethodAskDestroyAllGangMember uint32 = 67400021
|
|
||||||
|
|
||||||
// Combat & Skills
|
// Combat & Skills
|
||||||
MethodAskPlayerAttack uint32 = 68412884
|
MethodGmWorldRefreshEnemyGroup uint32 = 69676618
|
||||||
MethodAskPlayerCastSkill uint32 = 68412916
|
MethodReportUnitHitFly uint32 = 67430580
|
||||||
MethodAskUnlockFightStyle uint32 = 68412948
|
MethodReportUnitHitFlyEnd uint32 = 67015647
|
||||||
MethodSyncChangeSpirit uint32 = 68582802
|
MethodReportUnitFallGround uint32 = 67719880
|
||||||
MethodGmWorldRefreshEnemyGroup uint32 = 67500001
|
MethodReportUnitFallGroundEnd uint32 = 67516089
|
||||||
MethodReportUnitHitFly uint32 = 67500002
|
MethodAskControlPowerHoldEnemyUp uint32 = 67802701
|
||||||
MethodReportUnitHitFlyEnd uint32 = 67500003
|
MethodAskControlPowerHoldEnemyFallGrou uint32 = 67043294
|
||||||
MethodReportUnitFallGround uint32 = 67500004
|
|
||||||
MethodReportUnitFallGroundEnd uint32 = 67500005
|
|
||||||
MethodAskControlPowerHoldEnemyUp uint32 = 67500006
|
|
||||||
MethodAskControlPowerHoldEnemyFallGrou uint32 = 67500007
|
|
||||||
|
|
||||||
// Mini-games
|
// Mini-games
|
||||||
MethodAskChineseChessMove uint32 = 67700001
|
MethodEnterBalloonZone uint32 = 67417935
|
||||||
MethodAskMahjongAction uint32 = 67700002
|
MethodLeaveBalloon uint32 = 67075062
|
||||||
MethodEnterBalloonZone uint32 = 67700003
|
MethodAskBreakDestructibleObject uint32 = 67463592
|
||||||
MethodLeaveBalloon uint32 = 67700004
|
MethodEnterBowlingZone uint32 = 67832701
|
||||||
MethodAskBreakDestructibleObject uint32 = 67700005
|
MethodAskClawBuyTicket uint32 = 63690710
|
||||||
MethodEnterBowlingZone uint32 = 67700006
|
MethodAskClawSettlement uint32 = 63451209
|
||||||
MethodAskClawBuyTicket uint32 = 67700007
|
MethodAskBuyMiniGameTicket uint32 = 63606259
|
||||||
MethodAskClawSettlement uint32 = 67700008
|
|
||||||
MethodAskBuyMiniGameTicket uint32 = 67700009
|
|
||||||
|
|
||||||
// Racing
|
// Racing
|
||||||
MethodAskAcceptAndSetCurrentTask uint32 = 63961538
|
MethodAskAcceptAndSetCurrentTask uint32 = 63961538
|
||||||
@@ -133,12 +110,20 @@ const (
|
|||||||
MethodAskSetTaskCounterValue uint32 = 63753212
|
MethodAskSetTaskCounterValue uint32 = 63753212
|
||||||
|
|
||||||
// Fashion & Wardrobe
|
// Fashion & Wardrobe
|
||||||
MethodAskSetSpiritFashionsWithSource uint32 = 67800001
|
MethodAskSetSpiritFashionsWithSource uint32 = 63167148
|
||||||
MethodAskApplyFashionColoringSchemeInfos uint32 = 67800002
|
MethodAskApplyFashionColoringSchemeInfos uint32 = 63045354
|
||||||
MethodAskSetWeaponSkins uint32 = 67800003
|
MethodAskSetWeaponSkins uint32 = 63900850
|
||||||
MethodAskSyncWeaponSkinToSpirits uint32 = 67800004
|
MethodAskSyncWeaponSkinToSpirits uint32 = 63623831
|
||||||
|
|
||||||
// Weather & Fog
|
// Weather & Fog
|
||||||
MethodGmUnlockAllFogMap uint32 = 67900001
|
MethodGmUnlockAllFogMap uint32 = 65914419
|
||||||
MethodGmSetAmbianceEnabled uint32 = 67900002
|
MethodGmSetAmbianceEnabled uint32 = 69166506
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MethodRequestPlayerStartHangup uint32 = 63842476
|
||||||
|
MethodRequestPlayerStopHangup uint32 = 63910160
|
||||||
|
MethodAskReportLogicAgentSyncData uint32 = 67543012
|
||||||
|
)
|
||||||
|
|
||||||
|
const MethodAskUnitMoveActionSimple uint32 = 67158757
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -27,6 +27,13 @@ var (
|
|||||||
isRegistryLoaded bool
|
isRegistryLoaded bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for name, id := range canonicalMethodIDs {
|
||||||
|
nameToIdMap[name] = id
|
||||||
|
idToNameMap[id] = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LoadMethodCatalog loads the method catalog from a JSON file (e.g. data/client_data/MethodIds.json).
|
// LoadMethodCatalog loads the method catalog from a JSON file (e.g. data/client_data/MethodIds.json).
|
||||||
func LoadMethodCatalog(path string) error {
|
func LoadMethodCatalog(path string) error {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
@@ -72,10 +79,11 @@ func GetMethodID(name string) (uint32, bool) {
|
|||||||
return id, ok
|
return id, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// MethodGetMethodIDDefault returns the numeric ID for a method name or a fallback.
|
// MethodGetMethodIDDefault resolves a verified name. Legacy callers still pass
|
||||||
|
// a fallback, but guessed IDs must never be sent to an unrelated client handler.
|
||||||
func MethodGetMethodIDDefault(name string, fallback uint32) uint32 {
|
func MethodGetMethodIDDefault(name string, fallback uint32) uint32 {
|
||||||
if id, ok := GetMethodID(name); ok {
|
if id, ok := GetMethodID(name); ok {
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
return fallback
|
return 0
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"go/ast"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestActiveMethodIDsMatchLegacyContracts(t *testing.T) {
|
||||||
|
data, err := os.ReadFile("testdata/legacy_method_ids.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var expected map[string]uint32
|
||||||
|
if err := json.Unmarshal(data, &expected); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
aliases := map[string]string{"GateLogin": "Login", "AskEnterWorld": "LoginGame", "SyncSwitchSpiritInSitu": "SyncPlayerSwitchSpiritInSitu"}
|
||||||
|
f, err := parser.ParseFile(token.NewFileSet(), "method_id.go", nil, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ast.Inspect(f, func(n ast.Node) bool {
|
||||||
|
v, ok := n.(*ast.ValueSpec)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for i, id := range v.Names {
|
||||||
|
name := strings.TrimPrefix(id.Name, "Method")
|
||||||
|
if alias, ok := aliases[name]; ok {
|
||||||
|
name = alias
|
||||||
|
}
|
||||||
|
lit, ok := v.Values[i].(*ast.BasicLit)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("nonliteral method %s", name)
|
||||||
|
}
|
||||||
|
value, err := strconv.ParseUint(lit.Value, 10, 32)
|
||||||
|
want, exists := expected[name]
|
||||||
|
if err != nil || !exists || uint32(value) != want {
|
||||||
|
t.Errorf("%s = %d, legacy = %d (exists=%v)", name, value, want, exists)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultReturnContracts(t *testing.T) {
|
||||||
|
if len(defaultReturnBodies) != 2356 {
|
||||||
|
t.Fatalf("missing return contracts: %d", len(defaultReturnBodies))
|
||||||
|
}
|
||||||
|
// Independent examples from the original Lua-reader catalog, not round trips.
|
||||||
|
for _, tc := range []struct {
|
||||||
|
id uint32
|
||||||
|
hex string
|
||||||
|
}{
|
||||||
|
{34700853, ""}, {34022227, "ff01"}, {13224129, "00000000"},
|
||||||
|
{12279476, "01"}, {34383517, "0000000000000000"}, {34601301, "00000000ff01"},
|
||||||
|
} {
|
||||||
|
body, ok := DefaultReturnBody(tc.id)
|
||||||
|
if !ok || hex.EncodeToString(body) != tc.hex {
|
||||||
|
t.Errorf("%d: %x, exists=%v", tc.id, body, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := DefaultReturnBody(0xffffffff); ok {
|
||||||
|
t.Fatal("unknown contract accepted")
|
||||||
|
}
|
||||||
|
a, _ := DefaultReturnBody(34022227)
|
||||||
|
a[0] = 0
|
||||||
|
b, _ := DefaultReturnBody(34022227)
|
||||||
|
if b[0] != 0xff {
|
||||||
|
t.Fatal("shared body mutated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnknownMethodCannotUseGuessedID(t *testing.T) {
|
||||||
|
if id := MethodGetMethodIDDefault("InventedMethod", 68030312); id != 0 {
|
||||||
|
t.Fatalf("guessed id accepted: %d", id)
|
||||||
|
}
|
||||||
|
if id := MethodGetMethodIDDefault("SyncEnterScene", 1); id != 64406364 {
|
||||||
|
t.Fatalf("wrong canonical id: %d", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnterSceneWireLayout(t *testing.T) {
|
||||||
|
b := EncodeEnterScene(100, 23300888, 20001222, 76000888, 9, 15020967, 1, 2, 3, 4)
|
||||||
|
// Offsets derived from C# EnterSceneInfo, SpiritInitData and ServerSimpleGridInfo.
|
||||||
|
// LoadingTypeInfo uses TypeMark=1, byte Type and an Int32 member count.
|
||||||
|
if len(b) != 146 {
|
||||||
|
t.Fatalf("EnterSceneInfo size = %d, want 146", len(b))
|
||||||
|
}
|
||||||
|
if got := hex.EncodeToString(b[103:111]); got != "0100ff0000000000" {
|
||||||
|
t.Fatalf("LoadingTypeInfo: %s", got)
|
||||||
|
}
|
||||||
|
// LinkSimpleInfo: mark, ulong id, three bytes, uint event, uint time, ulong host.
|
||||||
|
if got := hex.EncodeToString(b[111:139]); got != "01000000000000000000010000000000000000000000000000000000" {
|
||||||
|
t.Fatalf("LinkSimpleInfo: %s", got)
|
||||||
|
}
|
||||||
|
if got := hex.EncodeToString(b[139:]); got != "ff01000000ff01" {
|
||||||
|
t.Fatalf("scene tail: %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlayerInfoPatchesVariableLengthLogin(t *testing.T) {
|
||||||
|
tail := []byte{10, 0, 0, 0, 1, 0xff, 0, 0, 0, 0, 0xff}
|
||||||
|
// Nonempty config buffer exercises dynamic offsets rather than fixed offsets 4/8.
|
||||||
|
original := NewUxWriter().U8(0xff).U8(0xff).Int7(3).Raw([]byte{7, 8, 9}).U8(0xff).
|
||||||
|
I32(1).U64(2).UxString("old").UxString("old name").Raw(tail).Bytes()
|
||||||
|
path := filepath.Join(t.TempDir(), "player.bin")
|
||||||
|
if err := os.WriteFile(path, original, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
b, err := EncodePlayerInfo(path, 5944, 100, "longer-account", "辰宿")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r := NewUxReader(b)
|
||||||
|
_, _ = r.Bytes(7)
|
||||||
|
aid, _ := r.I32()
|
||||||
|
pid, _ := r.U64()
|
||||||
|
account, _ := r.UxString()
|
||||||
|
name, _ := r.UxString()
|
||||||
|
if aid != 5944 || pid != 100 || account != "longer-account" || name != "辰宿" {
|
||||||
|
t.Fatalf("login fields: %d %d %s %s", aid, pid, account, name)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(b[r.Position():], tail) {
|
||||||
|
t.Fatal("unrelated player data changed")
|
||||||
|
}
|
||||||
|
for _, bad := range [][]byte{nil, {0}, {0xff, 0xff, 1, 0xff, 0}} {
|
||||||
|
if err := os.WriteFile(path, bad, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := EncodePlayerInfo(path, 1, 2, "", ""); err == nil {
|
||||||
|
t.Fatalf("accepted malformed template: %x", bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type shortWriter struct {
|
||||||
|
bytes.Buffer
|
||||||
|
limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *shortWriter) Write(b []byte) (int, error) {
|
||||||
|
if len(b) > w.limit {
|
||||||
|
b = b[:w.limit]
|
||||||
|
}
|
||||||
|
return w.Buffer.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
type zeroWriter struct{}
|
||||||
|
|
||||||
|
func (zeroWriter) Write([]byte) (int, error) { return 0, nil }
|
||||||
|
|
||||||
|
func TestWriteFrameHandlesShortWrites(t *testing.T) {
|
||||||
|
w := &shortWriter{limit: 2}
|
||||||
|
if err := WriteFrame(w, 9, []byte{1, 2, 3}, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if hex.EncodeToString(w.Bytes()) != "0300000009010203" {
|
||||||
|
t.Fatalf("truncated frame: %x", w.Bytes())
|
||||||
|
}
|
||||||
|
if err := WriteFrame(zeroWriter{}, 9, nil, nil); err != io.ErrShortWrite {
|
||||||
|
t.Fatalf("zero writer: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVarIntRejectsOverflow(t *testing.T) {
|
||||||
|
for _, b := range [][]byte{{0x80}, {0xff, 0xff, 0xff, 0xff, 0x08}, {0x80, 0x80, 0x80, 0x80, 0x80, 0}} {
|
||||||
|
if _, err := NewUxReader(b).UxString(); err == nil {
|
||||||
|
t.Fatalf("overflow accepted: %x", b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if n, err := NewUxReader([]byte{0xff, 0xff, 0xff, 0xff, 0x07}).Raw7(); err != nil || n != 2147483647 {
|
||||||
|
t.Fatalf("max Int32: %d, %v", n, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroundDataUsesByteFields(t *testing.T) {
|
||||||
|
b := EncodeRaidBattleUnitSpirit(9, 15020967, 1, 2, 3, 4, 100)
|
||||||
|
// GroundData is the last field: marker + byte + ulong + byte + Vector3.
|
||||||
|
if len(b) < 23 || b[len(b)-23] != 0xff {
|
||||||
|
t.Fatalf("invalid GroundData tail: %x", b)
|
||||||
|
}
|
||||||
|
if binary.LittleEndian.Uint64(b[len(b)-21:]) != 0 {
|
||||||
|
t.Fatal("nonzero ground id")
|
||||||
|
}
|
||||||
|
}
|
||||||
+4546
File diff suppressed because it is too large.
Load diff
+14
-10
@@ -103,18 +103,22 @@ func (r *UxReader) F64() (float64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *UxReader) ReadVarInt() (int, error) {
|
func (r *UxReader) ReadVarInt() (int, error) {
|
||||||
shift := 0
|
// UX lengths/counts are nonnegative Int32 values, at most five bytes.
|
||||||
result := 0
|
var result uint32
|
||||||
for r.pos < len(r.data) {
|
for shift := uint(0); shift < 35; shift += 7 {
|
||||||
b := r.data[r.pos]
|
b, err := r.U8()
|
||||||
r.pos++
|
if err != nil {
|
||||||
result |= int(b&0x7F) << shift
|
return 0, errors.New("unterminated varint")
|
||||||
if (b & 0x80) == 0 {
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
shift += 7
|
if shift == 28 && b > 7 {
|
||||||
|
return 0, errors.New("varint exceeds Int32")
|
||||||
}
|
}
|
||||||
return result, errors.New("unterminated varint")
|
result |= uint32(b&0x7f) << shift
|
||||||
|
if b&0x80 == 0 {
|
||||||
|
return int(result), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, errors.New("varint exceeds Int32")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UxReader) Int7() (int, error) {
|
func (r *UxReader) Int7() (int, error) {
|
||||||
|
|||||||
Vendored
+52
@@ -0,0 +1,52 @@
|
|||||||
|
"""List known incomplete migration artifacts; --check fails until they are ported.
|
||||||
|
|
||||||
|
This is a conservative static inventory, not a claim of gameplay compatibility.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--check", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
root = Path(__file__).resolve().parents[2]
|
||||||
|
ids = json.loads((root / "pkg/protocol/testdata/legacy_method_ids.json").read_text(encoding="utf-8"))
|
||||||
|
deps = set(subprocess.check_output(
|
||||||
|
["go", "list", "-deps", "./cmd/ananta-server"], cwd=root).decode().splitlines())
|
||||||
|
stubs, missing_tables, unknown_calls = [], [], []
|
||||||
|
for base in ("internal", "pkg"):
|
||||||
|
for path in sorted((root / base).rglob("*.go")):
|
||||||
|
if path.name.endswith("_test.go"):
|
||||||
|
continue
|
||||||
|
text = path.read_text(encoding="utf-8-sig")
|
||||||
|
rel = path.relative_to(root).as_posix()
|
||||||
|
names = re.findall(r"// (?:Router implementation for|Catalog helper) (\w+)", text)
|
||||||
|
if names:
|
||||||
|
stubs.append({"file": rel, "count": len(names), "functions": names})
|
||||||
|
for filename in re.findall(r'filepath.Join\(configDir, "([^"]+)"\)', text):
|
||||||
|
if not (root / "data/client_data/Configs" / filename).exists():
|
||||||
|
missing_tables.append({"file": rel, "table": filename})
|
||||||
|
# Only inspect the active server, not the unused generated skeletons.
|
||||||
|
if rel.startswith("internal/gameserver/") and "/routers/" not in rel:
|
||||||
|
for name in re.findall(r'MethodGetMethodIDDefault\("([^"]+)"', text):
|
||||||
|
if name not in ids:
|
||||||
|
unknown_calls.append({"file": rel, "method": name})
|
||||||
|
report = {
|
||||||
|
"baseline": "ca2e80f",
|
||||||
|
"stub_functions": sum(row["count"] for row in stubs),
|
||||||
|
"stub_files": len(stubs),
|
||||||
|
"generated_router_connected_to_main": "ananta-ps/internal/gameserver/routers" in deps,
|
||||||
|
"catalogs_connected_to_main": "ananta-ps/internal/catalogs" in deps,
|
||||||
|
"stubs": stubs,
|
||||||
|
"missing_catalog_inputs": missing_tables,
|
||||||
|
"unverified_notification_names": unknown_calls,
|
||||||
|
}
|
||||||
|
out = root / "docs/GO_MIGRATION_INVENTORY.json"
|
||||||
|
out.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||||
|
print(f"{report['stub_functions']} stub functions in {len(stubs)} files; "
|
||||||
|
f"{len(missing_tables)} missing catalog inputs; {len(unknown_calls)} unverified notifications")
|
||||||
|
print(f"Inventory: {out.relative_to(root)}")
|
||||||
|
if args.check and (stubs or missing_tables or unknown_calls):
|
||||||
|
raise SystemExit(1)
|
||||||
Vendored
+44
@@ -0,0 +1,44 @@
|
|||||||
|
"""Recover wire metadata from the last C# revision, without transpiling method bodies.
|
||||||
|
|
||||||
|
Run from the repository root: python tools/migration/generate_protocol.py
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
BASE = "ca2e80f"
|
||||||
|
PREFIX = "Ananta.Server/Ananta.Core/Protocol/Client4229938/"
|
||||||
|
|
||||||
|
|
||||||
|
def source(name):
|
||||||
|
return subprocess.check_output(["git", "show", f"{BASE}:{PREFIX}{name}"]).decode("utf-8-sig")
|
||||||
|
|
||||||
|
|
||||||
|
ids = {name: int(value) for name, value in re.findall(
|
||||||
|
r"const uint (\w+)\s*=\s*(\d+)", source("MethodId.cs"))}
|
||||||
|
Path("pkg/protocol/testdata/legacy_method_ids.json").write_text(
|
||||||
|
json.dumps(ids, indent=2) + "\n", encoding="utf-8")
|
||||||
|
header = f"// Code generated from {BASE} C# protocol metadata; DO NOT EDIT.\npackage protocol\n\n"
|
||||||
|
names = header + "var canonicalMethodIDs = map[string]uint32{\n"
|
||||||
|
names += "".join(f'{json.dumps(name)}: {value},\n' for name, value in ids.items())
|
||||||
|
names += "}\n"
|
||||||
|
Path("pkg/protocol/method_names_generated.go").write_text(names, encoding="utf-8")
|
||||||
|
|
||||||
|
rows = re.findall(
|
||||||
|
r'case (\d+)u: body = (Array.Empty<byte>\(\)|\[[^\]]*\]); shape = "([^"]*)"; return true;',
|
||||||
|
source("DefaultReturnCatalog4229938.cs"))
|
||||||
|
assert len(rows) == 2356, "C# return catalog changed; review before generating"
|
||||||
|
out = [header, "var defaultReturnBodies = map[uint32]string{"]
|
||||||
|
for mid, body, shape in rows:
|
||||||
|
data = b"" if body.startswith("Array") else bytes(
|
||||||
|
int(value.strip(), 16) for value in body[1:-1].split(",") if value.strip())
|
||||||
|
encoded = "".join(f"\\x{value:02x}" for value in data)
|
||||||
|
out.append(f'{mid}: "{encoded}", // {shape}')
|
||||||
|
out += ["}", "", "// DefaultReturnBody returns an independent payload for the client return reader.",
|
||||||
|
"func DefaultReturnBody(methodID uint32) ([]byte, bool) {",
|
||||||
|
"body, ok := defaultReturnBodies[methodID]", "return []byte(body), ok", "}"]
|
||||||
|
Path("pkg/protocol/default_returns_generated.go").write_text("\n".join(out) + "\n", encoding="utf-8")
|
||||||
|
subprocess.run(["gofmt", "-w", "pkg/protocol/default_returns_generated.go",
|
||||||
|
"pkg/protocol/method_names_generated.go"], check=True)
|
||||||
|
print(f"Recovered {len(ids)} names and {len(rows)} return contracts from {BASE}")
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
[
|
||||||
|
"SyncPlayerPositionAndFacing",
|
||||||
|
"AskMoveStart",
|
||||||
|
"AskMoveStop",
|
||||||
|
"AskUpdatePlayerPosition",
|
||||||
|
"AskPreSwitchSpirit",
|
||||||
|
"AskTeleportToScene",
|
||||||
|
"SwitchSceneDirect",
|
||||||
|
"AskDriveVehicle",
|
||||||
|
"AskExitVehicle",
|
||||||
|
"SyncPlayerVehicleInfo",
|
||||||
|
"SyncAetherVehicleInit",
|
||||||
|
"AskSetWantedLevel",
|
||||||
|
"AskPoliceArrest",
|
||||||
|
"ReportPoliceArrestFinish",
|
||||||
|
"AskPayFine",
|
||||||
|
"AskReleaseFromPrison",
|
||||||
|
"AskSetCrowdDensity",
|
||||||
|
"AskPlayerAttack",
|
||||||
|
"AskPlayerCastSkill",
|
||||||
|
"AskUnlockFightStyle",
|
||||||
|
"SyncChangeSpirit",
|
||||||
|
"AskChineseChessMove",
|
||||||
|
"AskMahjongAction"
|
||||||
|
]
|
||||||
Reference in new issue
Block a user