feat(go): Port full Ananta private server to Go architecture

This commit is contained in:
CAtlas0784 committed 2026-10-29 03:12:00 +07:00
1 parent ca2e80f9b7
commit c73d744649
28 files changed
+3585 -57

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
-----BEGIN CERTIFICATE-----
MIIH0zCCBjugAwIBAgIQbavnz5kXLZxKOHZTzB3p4DANBgkqhkiG9w0BAQsFADAi
MSAwHgYDVQQDDBdBbmFudGEgTG9jYWwgUHJveHkgUm9vdDAeFw0yNjA5MTUxNzIz
NTNaFw0yNzAxMTMxNzMzNTNaMCExHzAdBgNVBAMMFmw1MC51cGRhdGUubmV0ZWFz
ZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9LRaa1AQCmz0x
7KnxcNzi0VMULg2EOc330viwSPoDMNj9/t3E9xfpU5KSipy5qKf2BMix76E6Mtlg
l4QVuvrGiFetR4P1UQPxsFrExt9gzZKmbJLwWO5mW5GIiQ76ufR2VEkfHKqqLbNO
/plq297NdoVTHZ7VwljYUb619/S4xMJMOy/0CoWBiyCIUOZ/XjRSsesnDQrhAG3+
ktR/dImCtrT3T1vtBD9eZ+dT5+C0QBm1rUgZW6oHk+UNe4hPIrO3ZqG6pXgjC0Zn
ZNnSRYfZJ8f85tTFDaQi3VQSku/GZgb4RUTBOV7QCIIHhlWEvEkaL6p8ke0xARDD
9+2v9rZlAgMBAAGjggSEMIIEgDAOBgNVHQ8BAf8EBAMCBaAwggQJBgNVHREEggQA
MIID/IIWbDUwLnVwZGF0ZS5uZXRlYXNlLmNvbYImc2VydmVybGlzdC10ZXN0Lmw1
MC5sZWlodW8ubmV0ZWFzZS5jb22CE2w1MC5nZGwubmV0ZWFzZS5jb22CE2w1MC5n
cGgubmV0ZWFzZS5jb22CFWw1MC5nc2dwaC5uZXRlYXNlLmNvbYIUc2VydmljZS5t
a2V5LjE2My5jb22CFXFhdGVzdC5nLm1rZXkuMTYzLmNvbYIXcWF0ZXN0LTEuZy5t
a2V5LjE2My5jb22CF3FhdGVzdC0yLmcubWtleS4xNjMuY29tghdxYXRlc3QtMy5n
Lm1rZXkuMTYzLmNvbYIXcWF0ZXN0LTQuZy5ta2V5LjE2My5jb22CF3FhdGVzdC01
LmcubWtleS4xNjMuY29tghdxYXRlc3QtNi5nLm1rZXkuMTYzLmNvbYIXcWF0ZXN0
LTcuZy5ta2V5LjE2My5jb22CF3FhdGVzdC04LmcubWtleS4xNjMuY29tghpiaW5k
LW1vYmlsZS5nLm1rZXkuMTYzLmNvbYIfYmluZC1tb2JpbGUtdGVzdC5nLm1rZXku
MTYzLmNvbYIhbXBheS1jb21tb24tc2VydmVyLmcubWtleS4xNjMuY29tgiZtcGF5
LWNvbW1vbi1zZXJ2ZXItdGVzdC5nLm1rZXkuMTYzLmNvbYIvbXBheS1wZXJzb25h
bC1wcml2YWN5LXByb3RlY3Rpb24uZy5ta2V5LjE2My5jb22CM21wYXktcGVyc29u
YWwtcHJpdmFjeS1wcm90ZWN0aW9uLWRldi5nLm1rZXkuMTYzLmNvbYIWd2hvYW1p
Lm5pZS5uZXRlYXNlLmNvbYIWd2hvYW1pLm5pZS5lYXNlYmFyLmNvbYIbcHJvdG9j
b2wudW5pc2RrLm5ldGVhc2UuY29tghR0cHNsLm5pZS5uZXRlYXNlLmNvbYISZzAu
Z3NmLm5ldGVhc2UuY29tghJnMC5nc2YuZWFzZWJhci5jb22CEm1jb3VudC5lYXNl
YmFyLmNvbYIaYW5hbHl0aWNzLm1wYXkubmV0ZWFzZS5jb22CGWFwcGxvZy5tYXRy
aXgubmV0ZWFzZS5jb22CGWFwcGxvZy5tYXRyaXguZWFzZWJhci5jb22CHW1nYnNk
a3Rlc3QubWF0cml4Lm5ldGVhc2UuY29tghltZ2JzZGsubWF0cml4Lm5ldGVhc2Uu
Y29tghltZ2JzZGsubWF0cml4LmVhc2ViYXIuY29tghZkbnMudXBkYXRlLm5ldGVh
c2UuY29tghZkbnMudXBkYXRlLmVhc2ViYXIuY29tghVvcGVuYXBpLm11c2ljLjE2
My5jb22CCWxvY2FsaG9zdDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUF
BwMBMB8GA1UdIwQYMBaAFOGuN6GZzpCkTJ6JBp8KIBzaPf+wMB0GA1UdDgQWBBS6
oSuUo2A9bUQT5HEU6Y8pKUAkYDANBgkqhkiG9w0BAQsFAAOCAYEAvYA2k1y4NdwZ
beJCZYy08AvipyFIWvFrS686tjp/RJuroTH7/rpIucQEhXW3I4CFhvFjEnImlZ3Y
eHLoVtJUyDmVc4Oe0USZN2g3qjD0EJ7l6EUvPIdAGK2TssWpK1R671oI/eOiacrl
lSjp9VFfjG6XLQg2CRndlSW9SIyHTneVyhxNdficxzDdUUlwzCWSa5cVSh70mq83
nJs6T5XlxQ9nBnj3nG/bvam3aL2d/PZCF9k4jMNevkmcynV1UvrWTf5levMARqnJ
+00erje6M8U21TQFtt3OOE5oSiFaM/Dyb2VkRADzCqzJSUpCt3ksGh8xbhG0pnPj
6VFqMBfjATfBZSt/raUGfmRSZ/RnsaswN/emj4zhBgGiC2BINw70lb4prACBIq6/
6luX/Br3/MChoMW2GPj2loY0Iv2Gxyfn1frU4d89K0joMrpLi9sXrF6NL1bxsKEy
7cIam3G2wYhpGQsKTDVBWIr/4/wTqz3U26/R48eBlTyKFebgcbHj
-----END CERTIFICATE-----
+28
View File
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC9LRaa1AQCmz0x
7KnxcNzi0VMULg2EOc330viwSPoDMNj9/t3E9xfpU5KSipy5qKf2BMix76E6Mtlg
l4QVuvrGiFetR4P1UQPxsFrExt9gzZKmbJLwWO5mW5GIiQ76ufR2VEkfHKqqLbNO
/plq297NdoVTHZ7VwljYUb619/S4xMJMOy/0CoWBiyCIUOZ/XjRSsesnDQrhAG3+
ktR/dImCtrT3T1vtBD9eZ+dT5+C0QBm1rUgZW6oHk+UNe4hPIrO3ZqG6pXgjC0Zn
ZNnSRYfZJ8f85tTFDaQi3VQSku/GZgb4RUTBOV7QCIIHhlWEvEkaL6p8ke0xARDD
9+2v9rZlAgMBAAECggEAL4KCWdIDS9BG9yHTsFJC9yfQ8JF8TYfi5WlzkXpSZ7i9
1lRKx+uycqPNfMYZBDuKKywaZy2iVSZuF2cWAgsabf/R3S/HwTVegzViJmc3tG0M
doVwBbKf9M7sZtjdDTTkY09ZqdmeNu0RSjaAuyiK9Rjd9PjMaizL+U/WUIb8S+aX
xv45P7z3rKB7wUhh1+gLg12gNu5kDlKFK5CD26obtApfGL9BsiE8a6Y8KkBdgXcw
3yrmoESrUFPa35rOJHwdpqlE1v4BUEuqlHchVaW2MzVi9u986DtXzGp/LGQokWTY
Vecuy9qYtkUJHEHJYKcxYIH9Yyc9+VEno86TzVxQhQKBgQDaO87otyH3D7Mo+hxb
eHOHSX6x5JFKSm8+tqtPyjQJxHiJ98jeBwI6QdJq2BsX0CuD+QwPjNwDqTccvxQe
QteuyLKFyi0Jkw/1Wjsz8j1OyBY/M7k2eZkE6OfG1BrKomw2TEk47ol+j5GvlsxB
8LKAlLBdfx+KbnKuq2D8IbbbzwKBgQDd6flU+PdPgTlP8406mAUUamjANhvVC72R
TLMoYLut64jK/40McWrr+n7PzDhrue8wNiuzGDa9e0o1RN/d511DAtjS6QHMXNtP
DviwDP9YFQqITHX2JZ/0UzLLHmhjxGAxxzIVsvd6FLsU7aQaydR24l0BmTlRIPs2
u48YURMTiwKBgHhv2ViE+LGocG+E2HGCn/9hiBIU2jntqkcp+JEoPl/FleBOZwRh
SdFs0fs1GH6j2+ARaZ6LVkCO+mNa8Hdi3DURfbRoR0Kpbn0xNssMPNllgyGOIgD2
SoRzJLaOB9scmCu/HgnVWvwFr5j/1+nsNBbR+CMhLP7wxUmwOu3qjUfxAoGAa4/7
UJqUlS7Xrs8oh1wkKi8hsvkHKI7WRdVdiZbJoZVm6q5PmdZnsxXp3aLpPh4Tt9Sp
/dcgwVs/Lu7Ba/e3FsIMgr56JqpjisCA/iRuqC0au9ID41l9XJnvJtEIXENVNQuO
VUI3/i6KN6ywPGPT3gP+fMXeKFrKKU9kHq89hd0CgYA+vpfM2Pq9zhEIxmyR8cSF
P6MSTcY5ttJjBL3hBYI4RTIEU4I6QmL/Ln3NgK7JN8TM/22mGa4ceJVnPGxSQ/AA
qtIENQX9wPcnvY1sQW4nLOoMQvp1r3bYLW4EuGi6K1L3VTX6+XgtjmAW6dTMbOeE
ge9J8YA0vz+pAP/iloTchQ==
-----END PRIVATE KEY-----
+43 -57
View File
@@ -1,60 +1,56 @@
# Ananta (Project Mugen) CBT 4229938 Private Server & Proxy Engine # Ananta (Project Mugen) CBT 4229938 Private Server & Proxy Engine
เซิร์ฟเวอร์ส่วนตัว (Private Server) และระบบ HTTPS/SNI Proxy สำหรับเกม **Ananta (Project Mugen / 代号:无限大)** ไคลเอนต์เวอร์ชัน **CBT 4229938** พัฒนาด้วยสถาปัตยกรรม Clean Architecture C# .NET 8 (UX-RPC Binary Protocol) ร่วมกับ Node.js Proxy เพื่อจำลองการเชื่อมต่อและเล่นเกมในโหมด Sandbox / Free-Roam ได้อย่างสมบูรณ์ รองรับทั้งการเข้าเล่นผ่าน PC และ Mobile APK เซิร์ฟเวอร์ส่วนตัว (Private Server) และระบบ HTTPS/SNI Proxy สำหรับเกม **Ananta (Project Mugen / 代号:无限大)** ไคลเอนต์เวอร์ชัน **CBT 4229938** พัฒนาขึ้นมาทั้งในรูปแบบ **High-Performance Native Go Engine (`ananta-server.exe`)** และ Clean Architecture C# .NET 8 ร่วมกับ Node.js Proxy เพื่อจำลองการเชื่อมต่อและเล่นเกมในโหมด Sandbox / Free-Roam ได้อย่างสมบูรณ์แบบ รองรับทั้งการเข้าเล่นผ่าน PC และ Mobile APK
> 📖 **รายการระบบและฟีเจอร์ทั้งหมดของเซิร์ฟเวอร์**: ดูรายละเอียดฟีเจอร์เต็มได้ที่ [docs/FEATURES.md](docs/FEATURES.md) > 📖 **รายการระบบและฟีเจอร์ทั้งหมดของเซิร์ฟเวอร์ (36+ ระบบ)**: ดูรายละเอียดเชิงลึกและสถานะฟีเจอร์ทั้งหมดได้ที่ [docs/FEATURES.md](docs/FEATURES.md)
--- ---
## 🛠️ ความต้องการของระบบ (Prerequisites) ## 🛠️ ความต้องการของระบบ (Prerequisites)
### สำหรับ Go Native Engine (แนะนำ - รันได้ทันทีโดยไม่ต้องลงรันไทม์เพิ่ม):
1. **Windows 10 / 11 (64-bit)** 1. **Windows 10 / 11 (64-bit)**
2. **.NET 8.0 SDK**: [ดาวน์โหลดที่นี่](https://dotnet.microsoft.com/download/dotnet/8.0) 2. ตัวเกม **Ananta / Project Mugen (CBT Client build 4229938 หรือ Mobile APK 4063976)**
3. **Node.js (LTS v18 หรือ v20 ขึ้นไป)**: [ดาวน์โหลดที่นี่](https://nodejs.org/) 3. (ตัวเลือกเสริมสำหรับการพัฒนา) **Go 1.22+**: [ดาวน์โหลดที่นี่](https://go.dev/dl/)
4. **Git & Git LFS**: [ดาวน์โหลดที่นี่](https://git-lfs.com/)
5. ตัวเกม **Ananta / Project Mugen (CBT Client build 4229938 หรือ Mobile APK 4063976)** ### สำหรับ C# / Node.js Engine ดั้งเดิม:
1. **.NET 8.0 SDK** & **Node.js (v18+)**
--- ---
## 🚀 วิธีติดตั้งและเปิดใช้งานเซิร์ฟเวอร์ ## 🚀 วิธีติดตั้งและเปิดใช้งานเซิร์ฟเวอร์
### 1. Clone Repository (จำเป็นต้องใช้ Git LFS) ### 1. ติดตั้ง Certificate & Hosts
```bash
git clone https://gaytea.cod-algieba.ts.net/GaCy/Ananta-ps.git
cd Ananta-ps
git checkout dev
git lfs pull
```
### 2. ติดตั้ง Certificate & Hosts
รันคำสั่งด้วย PowerShell (Administrator) เพื่อชี้โดเมน NetEase เข้าหาเซิร์ฟเวอร์เครื่องคุณ (`127.0.0.1`): รันคำสั่งด้วย PowerShell (Administrator) เพื่อชี้โดเมน NetEase เข้าหาเซิร์ฟเวอร์เครื่องคุณ (`127.0.0.1`):
```powershell ```powershell
powershell -ExecutionPolicy Bypass -File .\Ananta.Proxy\SETUP_PROXY_AS_ADMIN.ps1 powershell -ExecutionPolicy Bypass -File .\Ananta.Proxy\SETUP_PROXY_AS_ADMIN.ps1
``` ```
*(หากต้องการยกเลิกการตั้งค่า Hosts ในภายหลัง สามารถรัน `REMOVE_PROXY_HOSTS_AS_ADMIN.ps1`)*
### 3. เปิดเซิร์ฟเวอร์ ### 2. เปิดเซิร์ฟเวอร์ด้วย Go Engine (รวดเร็ว, น้ำหนักเบา, รวมทุกระบบในไบนารีเดียว)
ดับเบิลคลิกเปิดใช้งานผ่านไฟล์: ดับเบิลคลิกเปิดใช้งานผ่าน:
```cmd ```cmd
START.cmd START-GO.cmd
``` ```
หรือรันผ่าน PowerShell: หรือรันผ่านคอมมานด์ไลน์:
```powershell ```bash
powershell -ExecutionPolicy Bypass -File .\Run-All.ps1 .\ananta-server.exe
``` ```
เมื่อขึ้นข้อความ `[READY]` แสดงว่าระบบพร้อมทำงาน:
* **HTTPS Gateway Proxy** (พอร์ต 443/80 และ Mobile Dispatch พอร์ต 5801)
* **Game Server** (พอร์ต 5200-5202)
* **GM Admin Web Panel** (พอร์ต 17888)
* **Debug WebUI Panel** (พอร์ต 5809)
### 4. เข้าเล่นเกม *(หรือหากต้องการใช้งาน C# Engine ดั้งเดิม สามารถดับเบิลคลิก `START.cmd` ได้เช่นกัน)*
เมื่อขึ้นข้อความ `[READY]` แสดงว่าระบบพร้อมทำงาน:
* **HTTPS Gateway Proxy**: พอร์ต 443 (TLS SNI) / 80 (HTTP) และ Mobile Dispatch พอร์ต 5801
* **Game Server**: Login Ports 5200, 5201 และ World Port 5202
* **GM Admin Web Panel**: พอร์ต 17888
* **Debug WebUI Panel**: พอร์ต 5809
### 3. เข้าเล่นเกม
* **PC:** เปิดไฟล์ `Ananta.exe` แล้วกดเริ่มเกม ตัวเกมจะทำการบายพาสล็อกอินและนำตัวละครเข้าสู่โลก Sandbox ทันที * **PC:** เปิดไฟล์ `Ananta.exe` แล้วกดเริ่มเกม ตัวเกมจะทำการบายพาสล็อกอินและนำตัวละครเข้าสู่โลก Sandbox ทันที
* **Mobile (APK):** ชี้เกตเวย์หรือรันผ่าน Wi-Fi Proxy มายัง IP เครื่องเซิร์ฟเวอร์ (พอร์ต 5801/443) * **Mobile (APK):** ชี้เกตเวย์หรือรันผ่าน Wi-Fi Proxy มายัง IP เครื่องเซิร์ฟเวอร์ (พอร์ต 5801/443)
### 5. แผงควบคุมผ่านเว็บเบราว์เซอร์ ### 4. แผงควบคุมผ่านเว็บเบราว์เซอร์
* **GM Admin Panel**: [http://127.0.0.1:17888/](http://127.0.0.1:17888/) — จัดการไอเทม, กระเป๋า, ผู้เล่น, และอีโคโนมี * **GM Admin Panel**: [http://127.0.0.1:17888/](http://127.0.0.1:17888/) — จัดการไอเทม, วาร์ปพิกัด, สลับตัวละคร, และระบบโลก
* **Debug WebUI Panel**: [http://127.0.0.1:5809/debug](http://127.0.0.1:5809/debug) — วาร์ปพิกัด, ปรับสภาพอากาศ/เวลา, เสกเอนทิตี, และควบคุมทราฟฟิก * **Debug WebUI Panel**: [http://127.0.0.1:5809/](http://127.0.0.1:5809/) — มอนิเตอร์การเชื่อมต่อ, เซสชันแบบ Real-Time, และ RPC Inspector
--- ---
@@ -62,36 +58,26 @@ powershell -ExecutionPolicy Bypass -File .\Run-All.ps1
```text ```text
Ananta-ps/ Ananta-ps/
├── Ananta.Proxy/ # Node.js HTTPS Multi-Domain Proxy & Fastpatch Generator ├── cmd/
│ ├── proxy/ │ └── ananta-server/ # Go Server Entrypoint (รวม Gateway + Login + Game + Web ใน binary เดียว)
│ │ ├── certs/ # ใบรับรอง SSL สำหรับดักโดเมน NetEase ├── internal/
│ │ ├── public/ # Static config, version JSON, serverlist, fastpatch.zip │ ├── config/ # ตัวโหลดคอนฟิก private-server.json
│ │ └── server.js # Proxy Server หลัก │ ├── gateway/ # HTTPS/SNI Proxy (443), HTTP (80) และ Mobile Dispatch (5801)
├── Ananta.SDK/ # Core Network Frame, Crypto (ChaCha8), และ UxRpc Serialization │ ├── logingate/ # Login & Gate TCP Server (พอร์ต 5200, 5201)
├── Ananta.Server/ # C# .NET 8 UX-RPC Game Server │ ├── gameserver/ # World Entry (V7 handoff), Combat, Movement, Switching (5202)
│ ├── Ananta.App/ # Console Entrypoint, Admin Web Server (17888) & Debug Panel (5809) │ └── web/ # GM Admin Panel (17888) และ Debug WebUI (5809)
│ ├── Ananta.Core/ # Client Data Repositories, Wire Codecs & World State ├── pkg/
│ ├── Ananta.Gameplay/ # ระบบ Traversal, City Traffic, Living Crowd & World Engine │ ├── crypto/chacha8/ # NetEase UXChaCha8 Custom Sigma Stream Cipher
│ ├── Ananta.Handlers/ # RPC Handlers (World, Switching, Gm, Vehicles, Minigames, Police) │ └── protocol/ # Binary Serializer/Deserializer, Frame Codec, Key Exchange
│ ├── Ananta.Network/ # TCP Listener & Rpc Frame Dispatcher ├── data/
│ ├── Ananta.RpcTypes/ # DTOs และ Method Contracts ของไคลเอนต์ 4229938 │ └── player_info_template.bin # เทมเพลตข้อมูลผู้เล่นและไอเทมเริ่มต้น
│ └── ClientData/4229938/ # ไฟล์ Database Configs ของเกม (Json, LFS)
├── config/ # private-server.json และตารางพิกัดมินิเกม/การจราจร ├── config/ # private-server.json และตารางพิกัดมินิเกม/การจราจร
├── docs/ # เอกสารเสริม (FEATURES.md, devlog) ├── docs/ # FEATURES.md (ฟีเจอร์แมทริกซ์ 36 ระบบ)
├── tools/ # เครื่องมือเสริม (Car Radio, Injector, Launcher, Python) ├── START-GO.cmd # รัน Go Engine ทันทีในคลิกเดียว
├── Run-All.ps1 # สคริปต์อัตโนมัติ (Fastpatch -> Build -> Launch) └── ananta-server.exe # Single Native Binary Executable
└── START.cmd # ตัวเปิดเซิร์ฟเวอร์แบบ All-in-One
``` ```
--- ---
## ⚠️ ข้อควรระวังและการแก้ปัญหา (Troubleshooting)
- **เกมค้างหน้า "Initializing Game" หรือ "Initialize Client"**: ตรวจสอบว่า Fastpatch ถูกสร้างและวางไว้ในโฟลเดอร์รันไทม์เรียบร้อยแล้ว
- **พอร์ตชน (Ports already in use)**: หากมี Process ค้างอยู่ สามารถปิดผ่าน PowerShell: `Stop-Process -Name node, Ananta.App -Force`
- **ดาวน์โหลดไม่ผ่าน / ติด LFS Bandwidth**: ตรวจสอบว่า `git lfs pull` ดึงไฟล์โมเดลและ Database ขนาดใหญ่มาครบถ้วน
---
## ⚖️ ข้อกำหนดและเงื่อนไข (Disclaimer) ## ⚖️ ข้อกำหนดและเงื่อนไข (Disclaimer)
โปรเจกต์นี้จัดทำขึ้นเพื่อการศึกษาด้าน Reverse Engineering, Network Protocol Emulation และการอนุรักษ์ซอฟต์แวร์ (Software Preservation) เท่านั้น ไม่มีส่วนเกี่ยวข้องกับ NetEase Games หรือ Naked Rain แต่อย่างใด ห้ามนำไปใช้ในเชิงพาณิชย์ โปรเจกต์นี้จัดทำขึ้นเพื่อการศึกษาด้าน Reverse Engineering, Network Protocol Emulation และการอนุรักษ์ซอฟต์แวร์ (Software Preservation) เท่านั้น ไม่มีส่วนเกี่ยวข้องกับ NetEase Games หรือ Naked Rain แต่อย่างใด ห้ามนำไปใช้ในเชิงพาณิชย์
+9
View File
@@ -0,0 +1,9 @@
@echo off
title Ananta Private Server (Native Go Engine)
cd /d "%~dp0"
if not exist ananta-server.exe (
echo [BUILD] Building ananta-server.exe...
go build -o ananta-server.exe ./cmd/ananta-server
)
ananta-server.exe
pause
BIN
View File
Binary file not shown.
+91
View File
@@ -0,0 +1,91 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"ananta-ps/internal/config"
"ananta-ps/internal/gameserver"
"ananta-ps/internal/gateway"
"ananta-ps/internal/logingate"
"ananta-ps/internal/web"
)
const banner = `
=====================================================================
_ _ ____ ____
/ \ _ __ __ _ _ __ _ _ _| |_ __ _ | _ \ / ___|
/ _ \ | '_ \ / _` + "`" + ` | '_ \| |_| | _/ _` + "`" + ` | | |_) |\___ \
/ ___ \| | | | (_| | | | | _ | || (_| | | __/ ___) |
/_/ \_\_| |_|\__,_|_| |_|_| |_|\__\__,_| |_| |____/
(Native Go Engine)
=====================================================================
`
func main() {
configPath := flag.String("config", "config/private-server.json", "Path to private-server.json")
flag.Parse()
fmt.Print(banner)
cfg, err := config.LoadConfig(*configPath)
if err != nil {
log.Fatalf("[FATAL] Could not load configuration: %v", err)
}
log.Printf("[INIT] Client Version: %d | Server ID: %d | Aid: %d", cfg.Client.Version, cfg.Client.ServerID, cfg.Client.Aid)
log.Printf("[INIT] Player Profile: PID=%d | Account=%s | InitialUnit=%d", cfg.Player.Pid, cfg.Player.AccountId, cfg.Player.InitialUnitId)
// 1. Start Gateway (Port 443, 80, 5801)
gw := gateway.New(cfg, "", "")
if err := gw.Start(); err != nil {
log.Printf("[GATEWAY] Error starting gateway: %v", err)
}
defer gw.Stop()
// 2. Start Login Gate Server (Port 5200, 5201)
loginSrv := logingate.New(cfg)
if err := loginSrv.Start(); err != nil {
log.Fatalf("[LOGIN-GATE] Failed to start login gate: %v", err)
}
defer loginSrv.Stop()
// 3. Start Game Server (Port 5202)
gameSrv := gameserver.New(cfg, "")
if err := gameSrv.Start(); err != nil {
log.Fatalf("[GAME-SERVER] Failed to start game server: %v", err)
}
defer gameSrv.Stop()
// 4. Start Admin GM Web Panel (Port 17888)
adminSrv := web.NewAdmin(cfg, gameSrv)
if err := adminSrv.Start(); err != nil {
log.Printf("[ADMIN-GM] Warning: admin server error: %v", err)
}
defer adminSrv.Stop()
// 5. Start Debug WebUI (Port 5809)
debugSrv := web.NewDebug(cfg, gameSrv)
if err := debugSrv.Start(); err != nil {
log.Printf("[DEBUG-API] Warning: debug server error: %v", err)
}
defer debugSrv.Stop()
log.Println("[READY] Ananta Private Server is fully operational!")
log.Printf("[READY] Gateway: HTTPS=443, HTTP=80, Mobile/LoginList=%d", cfg.Network.Proxy.LoginListPort)
log.Printf("[READY] Game Ports: Login=%v, Game=%d", cfg.Network.LoginPorts, cfg.Network.GamePort)
log.Printf("[READY] GM Admin Panel: http://%s:%d/", cfg.Network.AdvertisedHost, cfg.Admin.Port)
log.Printf("[READY] Debug WebUI: http://%s:5809/", cfg.Network.AdvertisedHost)
log.Println("[INFO] Press Ctrl+C to stop the server.")
// Wait for termination signal
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
log.Println("[SHUTDOWN] Stopping all Ananta Private Server services...")
}
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
module ananta-ps
go 1.27.0
+156
View File
@@ -0,0 +1,156 @@
package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
type Config struct {
Client ClientConfig `json:"client"`
Network NetworkConfig `json:"network"`
Admin AdminConfig `json:"admin"`
Player PlayerConfig `json:"player"`
World WorldConfig `json:"world"`
}
type ClientConfig struct {
Version int `json:"version"`
ArtifactVersion string `json:"artifactVersion"`
ServerID int `json:"serverId"`
Aid int `json:"aid"`
RpcMd5 string `json:"rpcMd5"`
NoMoreHotfixPatchError uint32 `json:"noMoreHotfixPatchError"`
}
type NetworkConfig struct {
BindHost string `json:"bindHost"`
AdvertisedHost string `json:"advertisedHost"`
LoginPorts []int `json:"loginPorts"`
GamePort int `json:"gamePort"`
SocketHeartbeatMs int `json:"socketHeartbeatMs"`
WriteTimeoutMs int `json:"writeTimeoutMs"`
Proxy ProxyConfig `json:"proxy"`
}
type ProxyConfig struct {
HttpsPort int `json:"httpsPort"`
HttpPort int `json:"httpPort"`
LoginListPort int `json:"loginListPort"`
LoginTcpPort int `json:"loginTcpPort"`
GameTcpPort int `json:"gameTcpPort"`
SceneSubPort int `json:"sceneSubPort"`
FastPatchPort int `json:"fastPatchPort"`
UpdateHost string `json:"updateHost"`
CertificatePassphrase string `json:"certificatePassphrase"`
}
type AdminConfig struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port int `json:"port"`
}
type PlayerConfig struct {
Pid uint64 `json:"pid"`
InitialUnitId uint64 `json:"initialUnitId"`
InitialSpiritTemplateId uint32 `json:"initialSpiritTemplateId"`
AccountId string `json:"accountId"`
UserName string `json:"userName"`
DisplayName string `json:"displayName"`
LoginToken string `json:"loginToken"`
GameToken string `json:"gameToken"`
LoginUniverseId uint32 `json:"loginUniverseId"`
ShareToken string `json:"shareToken"`
FpPassToken string `json:"fpPassToken"`
Skey string `json:"skey"`
}
type Vec3 struct {
X float64 `json:"x"`
Y float64 `json:"y"`
Z float64 `json:"z"`
}
type WorldConfig struct {
RaidId uint32 `json:"raidId"`
SceneInstanceId uint64 `json:"sceneInstanceId"`
UniverseId uint32 `json:"universeId"`
Spawn Vec3 `json:"spawn"`
Facing float32 `json:"facing"`
EntryOpeningEnabled bool `json:"entryOpeningEnabled"`
}
// LoadConfig reads and parses private-server.json from the specified path or standard locations.
func LoadConfig(path string) (*Config, error) {
if path == "" {
candidates := []string{
"config/private-server.json",
"../config/private-server.json",
"../../config/private-server.json",
}
for _, c := range candidates {
if _, err := os.Stat(c); err == nil {
path = c
break
}
}
}
if path == "" {
return nil, fmt.Errorf("config/private-server.json not found")
}
absPath, err := filepath.Abs(path)
if err != nil {
absPath = path
}
data, err := os.ReadFile(absPath)
if err != nil {
return nil, fmt.Errorf("failed to read config from %s: %w", absPath, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse json config from %s: %w", absPath, err)
}
// Apply default values if omitted
if cfg.Network.BindHost == "" {
cfg.Network.BindHost = "0.0.0.0"
}
if cfg.Network.AdvertisedHost == "" {
cfg.Network.AdvertisedHost = "127.0.0.1"
}
if len(cfg.Network.LoginPorts) == 0 {
cfg.Network.LoginPorts = []int{5200, 5201}
}
if cfg.Network.GamePort == 0 {
cfg.Network.GamePort = 5202
}
if cfg.Network.Proxy.HttpsPort == 0 {
cfg.Network.Proxy.HttpsPort = 443
}
if cfg.Network.Proxy.HttpPort == 0 {
cfg.Network.Proxy.HttpPort = 80
}
if cfg.Network.Proxy.LoginListPort == 0 {
cfg.Network.Proxy.LoginListPort = 5801
}
if cfg.Admin.Port == 0 {
cfg.Admin.Port = 17888
}
if cfg.Player.Pid == 0 {
cfg.Player.Pid = 100
}
if cfg.Player.InitialUnitId == 0 {
cfg.Player.InitialUnitId = 100000000000
}
if cfg.Player.InitialSpiritTemplateId == 0 {
cfg.Player.InitialSpiritTemplateId = 15020967 // Default character
}
return &cfg, nil
}
+397
View File
@@ -0,0 +1,397 @@
package gameserver
import (
"context"
"fmt"
"io"
"log"
"net"
"path/filepath"
"sync"
"ananta-ps/internal/config"
"ananta-ps/pkg/crypto/chacha8"
"ananta-ps/pkg/protocol"
)
type Server struct {
cfg *config.Config
templatePath string
ln net.Listener
cancelFunc context.CancelFunc
wg sync.WaitGroup
sessions map[string]*Session
mu sync.RWMutex
}
type Session struct {
id string
conn net.Conn
readCipher *chacha8.UxChaCha8
sendCipher *chacha8.UxChaCha8
sendMu sync.Mutex
pid uint64
unitId uint64
templateId uint32
posX float32
posY float32
posZ float32
facing float32
activeScene uint64
activeRaid uint32
universeId uint32
}
func New(cfg *config.Config, templatePath string) *Server {
if templatePath == "" {
candidates := []string{
"data/player_info_template.bin",
"../data/player_info_template.bin",
}
for _, c := range candidates {
abs, _ := filepath.Abs(c)
templatePath = abs
break
}
}
return &Server{
cfg: cfg,
templatePath: templatePath,
sessions: make(map[string]*Session),
}
}
func (s *Server) Start() error {
ctx, cancel := context.WithCancel(context.Background())
s.cancelFunc = cancel
addr := fmt.Sprintf("%s:%d", s.cfg.Network.BindHost, s.cfg.Network.GamePort)
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("failed to listen on game server %s: %w", addr, err)
}
s.ln = ln
log.Printf("[GAME-SERVER] Listening on %s:%d", s.cfg.Network.AdvertisedHost, s.cfg.Network.GamePort)
s.wg.Add(1)
go s.acceptLoop(ctx)
return nil
}
func (s *Server) Stop() error {
if s.cancelFunc != nil {
s.cancelFunc()
}
if s.ln != nil {
_ = s.ln.Close()
}
s.wg.Wait()
return nil
}
func (s *Server) GetActiveSessions() []*Session {
s.mu.RLock()
defer s.mu.RUnlock()
list := make([]*Session, 0, len(s.sessions))
for _, sess := range s.sessions {
list = append(list, sess)
}
return list
}
func (s *Server) acceptLoop(ctx context.Context) {
defer s.wg.Done()
for {
conn, err := s.ln.Accept()
if err != nil {
select {
case <-ctx.Done():
return
default:
log.Printf("[GAME-SERVER] Accept error: %v", err)
return
}
}
go s.handleConnection(ctx, conn)
}
}
func (sess *Session) SendPacket(packet *protocol.RpcPacket) error {
sess.sendMu.Lock()
defer sess.sendMu.Unlock()
var payload []byte
switch packet.Kind {
case protocol.PacketKindInvoke:
payload = protocol.BuildInvoke(packet.MethodID, packet.InvokeID, packet.Body)
case protocol.PacketKindReturn:
payload = protocol.BuildReturn(packet.MethodID, packet.InvokeID, packet.Error, packet.Body)
case protocol.PacketKindNotify:
payload = protocol.BuildNotify(packet.MethodID, packet.Body)
}
return protocol.WriteFrame(sess.conn, 0x09, payload, sess.sendCipher)
}
func (sess *Session) ReturnEmptyOk(invokeID int32, methodID uint32) error {
return sess.SendPacket(&protocol.RpcPacket{
Kind: protocol.PacketKindReturn,
InvokeID: invokeID,
MethodID: methodID,
Error: 0,
Body: nil,
})
}
func (sess *Session) ReturnData(invokeID int32, methodID uint32, body []byte) error {
return sess.SendPacket(&protocol.RpcPacket{
Kind: protocol.PacketKindReturn,
InvokeID: invokeID,
MethodID: methodID,
Error: 0,
Body: body,
})
}
func (sess *Session) NotifyData(methodID uint32, body []byte) error {
return sess.SendPacket(&protocol.RpcPacket{
Kind: protocol.PacketKindNotify,
MethodID: methodID,
Body: body,
})
}
func (s *Server) handleConnection(ctx context.Context, conn net.Conn) {
defer conn.Close()
log.Printf("[GAME-SERVER] Client connected from %s", conn.RemoteAddr())
readCipher, writeCipher, err := protocol.HandleHandshake(conn)
if err != nil {
log.Printf("[GAME-SERVER] Handshake error from %s: %v", conn.RemoteAddr(), err)
return
}
sess := &Session{
id: conn.RemoteAddr().String(),
conn: conn,
readCipher: readCipher,
sendCipher: writeCipher,
pid: s.cfg.Player.Pid,
unitId: s.cfg.Player.InitialUnitId,
templateId: s.cfg.Player.InitialSpiritTemplateId,
posX: float32(s.cfg.World.Spawn.X),
posY: float32(s.cfg.World.Spawn.Y),
posZ: float32(s.cfg.World.Spawn.Z),
facing: s.cfg.World.Facing,
activeRaid: s.cfg.World.RaidId,
activeScene: s.cfg.World.SceneInstanceId,
universeId: s.cfg.World.UniverseId,
}
s.mu.Lock()
s.sessions[sess.id] = sess
s.mu.Unlock()
defer func() {
s.mu.Lock()
delete(s.sessions, sess.id)
s.mu.Unlock()
}()
for {
select {
case <-ctx.Done():
return
default:
}
frame, err := protocol.ReadFrame(conn, sess.readCipher)
if err != nil {
if err != io.EOF {
log.Printf("[GAME-SERVER] Connection %s read error: %v", conn.RemoteAddr(), err)
}
return
}
// Frame mode 0x03 is heartbeat
if frame.Mode == 0x03 {
_ = protocol.WriteFrame(conn, 0x03, frame.Payload, sess.sendCipher)
continue
}
packet := protocol.ParseRpcPacket(frame.Payload)
if err := s.dispatch(sess, &packet); err != nil {
log.Printf("[GAME-SERVER] Dispatch error for method %d: %v", packet.MethodID, err)
}
}
}
func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
switch pkt.MethodID {
case protocol.MethodLoginGame:
log.Printf("[WORLD] LoginGame received, preparing world state for player %d", sess.pid)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
// 1. SendServerTimeGame
_ = sess.NotifyData(protocol.MethodSendServerTimeGame, protocol.EncodeServerTime(0))
// 2. SyncPlayerInfo (from template binary)
playerInfoBytes, err := protocol.EncodePlayerInfo(s.templatePath, s.cfg.Client.Aid, sess.pid, s.cfg.Player.AccountId, s.cfg.Player.DisplayName)
if err == nil {
_ = sess.NotifyData(protocol.MethodSyncPlayerInfo, playerInfoBytes)
} else {
log.Printf("[WORLD] Warning: player_info_template.bin load error: %v", err)
}
// 3. SyncEnterScene
enterScenePayload := protocol.EncodeEnterScene(
sess.pid,
sess.activeRaid,
sess.activeScene,
sess.universeId,
sess.unitId,
sess.templateId,
sess.posX,
sess.posY,
sess.posZ,
sess.facing,
)
log.Printf("[WORLD-V7] Sending SyncEnterScene raid=%d instance=%d universe=%d unit=%d template=%d pos=(%.2f,%.2f,%.2f)",
sess.activeRaid, sess.activeScene, sess.universeId, sess.unitId, sess.templateId, sess.posX, sess.posY, sess.posZ)
return sess.NotifyData(protocol.MethodSyncEnterScene, enterScenePayload)
case protocol.MethodAskLoadSceneCompleted:
log.Printf("[WORLD-V7] AskLoadSceneCompleted ack received, committing V7 4-step handoff")
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
// V7 4-step Handoff:
// Step 1: SyncLogicAgentEnter
_ = sess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(sess.unitId))
// Step 2: SyncManagedLogicAgent
_ = sess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(sess.unitId, sess.pid, 0))
// Step 3: SyncRaidBattleUnitSpirit
unitProjection := protocol.EncodeRaidBattleUnitSpirit(sess.unitId, sess.templateId, sess.posX, sess.posY, sess.posZ, sess.facing, sess.pid)
_ = sess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, unitProjection)
// Step 4: SyncPlayerCurrentSpirit
return sess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(sess.pid, sess.templateId, sess.unitId, false))
case protocol.MethodAskLoadingFinished:
log.Printf("[WORLD-V7] AskLoadingFinished received, scene ready")
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
// 1. SyncSceneLoadCompleted
_ = sess.NotifyData(protocol.MethodSyncSceneLoadCompleted, protocol.EncodeSceneLoadCompleted(sess.activeScene))
// 2. SyncGamePause (false)
wPause := protocol.NewUxWriter()
wPause.Bool(false)
_ = sess.NotifyData(protocol.MethodSyncGamePause, wPause.Bytes())
// 3. Unlock Map Fog
wFog := protocol.NewUxWriter()
wFog.U32(uint32(sess.activeScene)).Bool(true)
_ = sess.NotifyData(protocol.MethodSyncSceneFogMapAllUnlock, wFog.Bytes())
// 4. Combat power
wPower := protocol.NewUxWriter()
wPower.U8(0xFF).Int7(1).U32(sess.templateId).F32(1000.0)
_ = sess.NotifyData(protocol.MethodSyncAllSpiritCombatPower, wPower.Bytes())
// 5. Unlock Diving
wDive := protocol.NewUxWriter()
wDive.UxString("EnableDive").U8(0xFF).Int7(1).UxString("true")
return sess.NotifyData(protocol.MethodSyncRunGmUtils, wDive.Bytes())
case protocol.MethodAskLoadedInSameScene:
w := protocol.NewUxWriter()
w.U64(sess.pid).F64(1.0)
return sess.NotifyData(protocol.MethodSyncPlayerLoadRate, w.Bytes())
case protocol.MethodAskMoveStart, protocol.MethodAskMoveStop:
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
case protocol.MethodAskUpdatePlayerPosition:
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)
case protocol.MethodAskPreSwitchSpirit:
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
case protocol.MethodAskSwitchSpirit:
// Clean DRMK character swap
r := protocol.NewUxReader(pkt.Body)
newTemplateId := sess.templateId
if tid, err := r.U32(); err == nil {
newTemplateId = tid
}
newUnitId := sess.unitId + 1
log.Printf("[SWITCH] Switching character %d -> %d", sess.templateId, newTemplateId)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
_ = sess.NotifyData(protocol.MethodSyncLogicAgentLeave, protocol.EncodeLogicAgentLeave(sess.unitId))
_ = sess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(newUnitId))
_ = sess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(newUnitId, sess.pid, 0))
newProj := protocol.EncodeRaidBattleUnitSpirit(newUnitId, newTemplateId, sess.posX, sess.posY, sess.posZ, sess.facing, sess.pid)
_ = sess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, newProj)
_ = sess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(sess.pid, newTemplateId, newUnitId, true))
wInSitu := protocol.NewUxWriter()
wInSitu.U32(newTemplateId).U64(newUnitId)
_ = sess.NotifyData(protocol.MethodSyncSwitchSpiritInSitu, wInSitu.Bytes())
sess.templateId = newTemplateId
sess.unitId = newUnitId
return nil
case protocol.MethodAskSummonVehicle, protocol.MethodAskDriveVehicle, protocol.MethodAskExitVehicle:
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
case protocol.MethodAskPlayerAttack, protocol.MethodAskPlayerCastSkill, protocol.MethodAskUnlockFightStyle:
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
default:
if pkt.Kind == protocol.PacketKindInvoke {
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
}
return nil
}
// Teleport moves an active session to the specified coordinates.
func (sess *Session) Teleport(x, y, z, facing float32) error {
sess.posX = x
sess.posY = y
sess.posZ = z
sess.facing = facing
w := protocol.NewUxWriter()
w.U64(sess.unitId).F32(x).F32(y).F32(z).F32(facing).U8(0).Bool(false).U8(6) // SetPositionType.Teleport = 6
return sess.NotifyData(protocol.MethodSyncUnitPositionAndFacing, w.Bytes())
}
+129
View File
@@ -0,0 +1,129 @@
package gameserver
import (
"crypto/aes"
"crypto/cipher"
"net"
"testing"
"time"
"ananta-ps/internal/config"
"ananta-ps/pkg/crypto/chacha8"
"ananta-ps/pkg/protocol"
)
func TestGameServerWorldEntry(t *testing.T) {
cfg := &config.Config{
Client: config.ClientConfig{
ServerID: 801,
Aid: 5944,
},
Network: config.NetworkConfig{
BindHost: "127.0.0.1",
AdvertisedHost: "127.0.0.1",
GamePort: 15203,
},
Player: config.PlayerConfig{
Pid: 100,
InitialUnitId: 100000000000,
InitialSpiritTemplateId: 15020967,
AccountId: "test_acc",
DisplayName: "test_player",
},
World: config.WorldConfig{
RaidId: 23300888,
SceneInstanceId: 20001222,
UniverseId: 76000888,
Spawn: config.Vec3{X: 100, Y: 0, Z: 200},
Facing: 0,
},
}
srv := New(cfg, "../../data/player_info_template.bin")
if err := srv.Start(); err != nil {
t.Fatalf("failed to start game server: %v", err)
}
defer srv.Stop()
// Connect client
conn, err := net.DialTimeout("tcp", "127.0.0.1:15203", 2*time.Second)
if err != nil {
t.Fatalf("failed to connect: %v", err)
}
defer conn.Close()
// 1. Client handshake
clientHandshake := make([]byte, 260)
aesKey := []byte("0123456789abcdef")
copy(clientHandshake[4:20], aesKey)
if err := protocol.WriteFrame(conn, 0x01, clientHandshake, nil); err != nil {
t.Fatalf("failed to write handshake: %v", err)
}
// 2. Server reply
replyFrame, err := protocol.ReadFrame(conn, nil)
if err != nil {
t.Fatalf("failed to read reply: %v", err)
}
aesNonce := replyFrame.Payload[8:20]
encryptedHead := replyFrame.Payload[20:96]
tag := replyFrame.Payload[96:112]
cipherBlock, err := aes.NewCipher(aesKey)
if err != nil {
t.Fatalf("failed to create aes cipher: %v", err)
}
aesGcm, err := cipher.NewGCM(cipherBlock)
if err != nil {
t.Fatalf("failed to create aes gcm: %v", err)
}
fullCipher := append(encryptedHead, tag...)
chaChaHead, err := aesGcm.Open(nil, aesNonce, fullCipher, nil)
if err != nil {
t.Fatalf("failed to decrypt chacha head: %v", err)
}
clientSendCipher, _ := chacha8.New(chaChaHead[0:32], chaChaHead[64:76])
clientReadCipher, _ := chacha8.New(chaChaHead[32:64], chaChaHead[64:76])
// 3. Send LoginGame Invoke
invokePayload := protocol.BuildInvoke(protocol.MethodLoginGame, 1, nil)
if err := protocol.WriteFrame(conn, 0x09, invokePayload, clientSendCipher); err != nil {
t.Fatalf("failed to write LoginGame: %v", err)
}
// 4. Read Return frame
retFrame, err := protocol.ReadFrame(conn, clientReadCipher)
if err != nil {
t.Fatalf("failed to read return: %v", err)
}
pkt := protocol.ParseRpcPacket(retFrame.Payload)
if pkt.Kind != protocol.PacketKindReturn || pkt.InvokeID != 1 {
t.Fatalf("expected return for invoke 1, got %v", pkt)
}
// 5. Read S2C notifications (ServerTime, SyncPlayerInfo, SyncEnterScene)
foundEnterScene := false
for i := 0; i < 3; i++ {
notifyFrame, err := protocol.ReadFrame(conn, clientReadCipher)
if err != nil {
t.Fatalf("failed to read notify frame: %v", err)
}
nPkt := protocol.ParseRpcPacket(notifyFrame.Payload)
if nPkt.MethodID == protocol.MethodSyncEnterScene {
foundEnterScene = true
}
}
if !foundEnterScene {
t.Fatalf("expected SyncEnterScene in notifications")
}
// Verify session tracking
sessions := srv.GetActiveSessions()
if len(sessions) != 1 {
t.Fatalf("expected 1 active session, got %d", len(sessions))
}
}
+433
View File
@@ -0,0 +1,433 @@
package gateway
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"ananta-ps/internal/config"
)
type Gateway struct {
cfg *config.Config
publicDir string
certDir string
httpSrv *http.Server
httpsSrv *http.Server
listSrv *http.Server
cancelFunc context.CancelFunc
}
func New(cfg *config.Config, publicDir, certDir string) *Gateway {
if publicDir == "" {
candidates := []string{
"Ananta.Proxy/proxy/public",
"proxy/public",
"../Ananta.Proxy/proxy/public",
}
for _, c := range candidates {
if _, err := os.Stat(c); err == nil {
publicDir = c
break
}
}
}
if certDir == "" {
candidates := []string{
"Ananta.Proxy/proxy/certs",
"proxy/certs",
"../Ananta.Proxy/proxy/certs",
}
for _, c := range candidates {
if _, err := os.Stat(c); err == nil {
certDir = c
break
}
}
}
return &Gateway{
cfg: cfg,
publicDir: publicDir,
certDir: certDir,
}
}
func (g *Gateway) Start() error {
ctx, cancel := context.WithCancel(context.Background())
g.cancelFunc = cancel
mux := http.NewServeMux()
mux.HandleFunc("/", g.handleDispatch)
// 1. Port 5801: LoginList & Mobile Dispatch Server
listAddr := fmt.Sprintf("%s:%d", g.cfg.Network.BindHost, g.cfg.Network.Proxy.LoginListPort)
g.listSrv = &http.Server{
Addr: listAddr,
Handler: mux,
}
go func() {
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 {
log.Printf("[GATEWAY] HTTPS Proxy listen error (port 443): %v", err)
return
}
tlsLn := tls.NewListener(ln, tlsConfig)
if err := g.httpsSrv.Serve(tlsLn); err != nil && err != http.ErrServerClosed {
log.Printf("[GATEWAY] HTTPS Proxy 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
}
func (g *Gateway) Stop() error {
if g.cancelFunc != nil {
g.cancelFunc()
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if g.listSrv != nil {
_ = g.listSrv.Shutdown(ctx)
}
if g.httpSrv != nil {
_ = g.httpSrv.Shutdown(ctx)
}
if g.httpsSrv != nil {
_ = g.httpsSrv.Shutdown(ctx)
}
return nil
}
func (g *Gateway) handleDispatch(w http.ResponseWriter, r *http.Request) {
urlPath := strings.ToLower(r.URL.Path)
host := strings.ToLower(r.Host)
// Clean up double slashes
for strings.Contains(urlPath, "//") {
urlPath = strings.ReplaceAll(urlPath, "//", "/")
}
// 1. LoginList (Port 5801 or via Host /LoginList)
if strings.HasSuffix(urlPath, "/loginlist") || urlPath == "/loginlist" {
g.serveLoginList(w, r)
return
}
// 2. ServerList (Contract MD5 7-column bypass)
if strings.HasSuffix(urlPath, "/serverlist.txt") || strings.HasSuffix(urlPath, "/serverlist_review.txt") {
g.serveServerList(w)
return
}
// 3. MPay / UniSDK WebView Auth Bypass
if urlPath == "/local-mpay-login" {
g.serveMpayLogin(w)
return
}
if urlPath == "/local-empty" {
g.serveJSON(w, map[string]interface{}{
"code": 0, "ret": 0, "result": 0, "errno": 0, "msg": "ok",
})
return
}
// 4. UniSDK Check Enter & Gray Release
if strings.Contains(urlPath, "/sdk/check_enter") {
g.serveJSON(w, map[string]interface{}{
"code": 200, "status": 0, "success": true, "message": "ok", "result": 0, "ret": 0, "subcode": 0,
})
return
}
if strings.Contains(urlPath, "/sdk/get_gray_release_info") {
g.serveJSON(w, map[string]interface{}{
"code": 200, "status": 0, "success": true, "message": "ok", "result": 0, "ret": 0, "subcode": 0,
})
return
}
if strings.Contains(urlPath, "/sdk/uni_sauth") {
g.serveJSON(w, map[string]interface{}{
"code": 200, "status": 0, "success": true, "message": "ok", "result": 0, "ret": 0, "subcode": 0,
"data": map[string]interface{}{
"uid": g.cfg.Player.AccountId,
"token": g.cfg.Player.LoginToken,
},
})
return
}
// 5. ConstConfig.cfg
if strings.HasSuffix(urlPath, "/constconfig.cfg") {
g.serveFile(w, filepath.Join(g.publicDir, "OuterTest1_ConstConfig.cfg"), "text/plain")
return
}
// 6. pc_boot.config (Disable shader warmup)
if strings.HasSuffix(urlPath, "/pc_boot.config") {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte("enable-cs-warmup=0\nenable-ecs-shader-warmup=0\n"))
return
}
// 7. pc_netease_version.json (Disable triggerWarmupBelowVersion)
if strings.HasSuffix(urlPath, "/pc_netease_version.json") {
g.serveFile(w, filepath.Join(g.publicDir, "pc_netease_version.json"), "application/json")
return
}
// 8. device_check_pc.json
if strings.HasSuffix(urlPath, "/device_check_pc.json") {
g.serveJSON(w, map[string]interface{}{})
return
}
// 9. Startup Patch Info
if strings.HasSuffix(urlPath, "/trunk-client_startup_patch_info.txt") {
g.serveFile(w, filepath.Join(g.publicDir, "trunk-client_startup_patch_info.txt"), "text/plain")
return
}
// 10. FastPatch ZIP
if strings.HasPrefix(urlPath, "/fastpatch/") && strings.HasSuffix(urlPath, "/fastpatch.zip") {
g.serveFile(w, filepath.Join(g.publicDir, "fastpatch_4229938.zip"), "application/zip")
return
}
// 11. Resource Patch Info
if strings.HasSuffix(urlPath, "/patch.info") {
// Prefer latest Patch.info
patchFile := filepath.Join(g.publicDir, "Patch_20260805061659654p0.info")
if _, err := os.Stat(patchFile); err != nil {
patchFile = filepath.Join(g.publicDir, "Patch_20260707064825384p0.info")
}
g.serveFile(w, patchFile, "application/octet-stream")
return
}
// 12. vfs_res_version
if strings.Contains(urlPath, "vfs_res_version") {
g.serveFile(w, filepath.Join(g.publicDir, "vfs_res_version"), "application/octet-stream")
return
}
// 13. download_header.ehd
if strings.Contains(urlPath, "download_header.ehd") {
g.serveFile(w, filepath.Join(g.publicDir, "Blocks", "download_header.ehd"), "application/octet-stream")
return
}
// 14. stage0 patch
if strings.HasSuffix(urlPath, ".stage0.patch.bytes.standalone") {
g.serveFile(w, filepath.Join(g.publicDir, "stage0_patch.bytes.standalone"), "application/octet-stream")
return
}
// 15. Audit & Login.bin
if urlPath == "/audit.txt" {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte("8000000\nlocal private server compatibility stub\nok\n\n"))
return
}
if urlPath == "/game_notice/login.bin" {
loginBin := filepath.Join(g.publicDir, "Login.bin")
if _, err := os.Stat(loginBin); err == nil {
g.serveFile(w, loginBin, "application/octet-stream")
return
}
w.WriteHeader(http.StatusOK)
return
}
// Generic handler for all NetEase matrix / uniSDK subdomains
if strings.Contains(host, "netease.com") || strings.Contains(host, "163.com") {
g.serveJSON(w, map[string]interface{}{
"code": 200,
"subcode": 0,
"ret": 0,
"result": 0,
"errno": 0,
"status": 0,
"success": true,
"msg": "ok",
"message": "ok",
})
return
}
// Default fallback
w.WriteHeader(http.StatusOK)
w.Write([]byte("Ananta Private Server Gateway Running"))
}
func (g *Gateway) serveLoginList(w http.ResponseWriter, r *http.Request) {
login0 := g.cfg.Network.LoginPorts[0]
login1 := login0
if len(g.cfg.Network.LoginPorts) > 1 {
login1 = g.cfg.Network.LoginPorts[1]
}
host := g.cfg.Network.AdvertisedHost
body := fmt.Sprintf("%s:%d\n%s:%d\n", host, login0, host, login1)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
}
func (g *Gateway) serveServerList(w http.ResponseWriter) {
loginListUrl := fmt.Sprintf("http://%s:%d/LoginList", g.cfg.Network.AdvertisedHost, g.cfg.Network.Proxy.LoginListPort)
// Exactly 7 basic columns:
// *,801,QA101,Outer,http://127.0.0.1:5801/LoginList,1,14
body := fmt.Sprintf("*,%d,QA101,Outer,%s,1,14\n", g.cfg.Client.ServerID, loginListUrl)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
}
func (g *Gateway) serveMpayLogin(w http.ResponseWriter) {
html := fmt.Sprintf(`<!doctype html>
<html>
<head><meta charset="utf-8"><title>MPay Login Bypass</title></head>
<body>
<script>
const user = {
account: "%s",
user_id: "%s",
uid: "%s",
aid: "%d",
token: "%s",
access_token: "%s",
login_ticket: "%s",
device_id: "ananta-pc-client",
deviceid: "ananta-pc-client",
udid: "ananta-pc-client",
server_id: "%d",
code: 200,
subcode: 0,
msg: "ok"
};
const payload = {
methodId: "ngwebview_notify_native",
reqData: {
methodId: "onUserLogin",
device_id: user.device_id,
deviceid: user.deviceid,
udid: user.udid,
user: user
}
};
function sendLogin() {
try {
if (window.NeteaseMpayJSBridge && window.NeteaseMpayJSBridge.Common && window.NeteaseMpayJSBridge.Common.onUserLogin) {
window.NeteaseMpayJSBridge.Common.onUserLogin(user);
}
} catch (e) {}
try {
if (window.UniSDKJSBridge && window.UniSDKJSBridge.postMsgToNative) {
window.UniSDKJSBridge.postMsgToNative(payload);
}
} catch (e) {}
}
window.addEventListener("NeteaseMpayJSBridgeReady", sendLogin);
setTimeout(sendLogin, 100);
setTimeout(sendLogin, 300);
setTimeout(sendLogin, 1000);
</script>
<h3>Authenticating Ananta...</h3>
</body>
</html>`, g.cfg.Player.AccountId, g.cfg.Player.AccountId, g.cfg.Player.AccountId, g.cfg.Client.Aid, g.cfg.Player.LoginToken, g.cfg.Player.LoginToken, g.cfg.Player.LoginToken, g.cfg.Client.ServerID)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(html))
}
func (g *Gateway) serveJSON(w http.ResponseWriter, v interface{}) {
data, _ := json.Marshal(v)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(data)
}
func (g *Gateway) serveFile(w http.ResponseWriter, filePath, contentType string) {
data, err := os.ReadFile(filePath)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
if contentType != "" {
w.Header().Set("Content-Type", contentType)
}
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
w.WriteHeader(http.StatusOK)
w.Write(data)
}
+51
View File
@@ -0,0 +1,51 @@
package gateway
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"ananta-ps/internal/config"
)
func TestGatewayDispatch(t *testing.T) {
cfg := &config.Config{
Client: config.ClientConfig{
ServerID: 801,
},
Network: config.NetworkConfig{
AdvertisedHost: "127.0.0.1",
LoginPorts: []int{5200, 5201},
Proxy: config.ProxyConfig{
LoginListPort: 5801,
},
},
}
gw := New(cfg, "../../Ananta.Proxy/proxy/public", "../../Ananta.Proxy/proxy/certs")
// Test LoginList
req := httptest.NewRequest("GET", "/LoginList", nil)
w := httptest.NewRecorder()
gw.handleDispatch(w, req)
resp := w.Result()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
body := w.Body.String()
if !strings.Contains(body, "127.0.0.1:5200") {
t.Fatalf("expected 127.0.0.1:5200 in LoginList, got %s", body)
}
// Test ServerList
req = httptest.NewRequest("GET", "/OuterTest1/serverlist.txt", nil)
w = httptest.NewRecorder()
gw.handleDispatch(w, req)
if w.Result().StatusCode != http.StatusOK {
t.Fatalf("expected 200 for serverlist.txt, got %d", w.Result().StatusCode)
}
if !strings.Contains(w.Body.String(), "QA101,Outer,http://127.0.0.1:5801/LoginList,1,14") {
t.Fatalf("unexpected serverlist body: %s", w.Body.String())
}
}
+303
View File
@@ -0,0 +1,303 @@
package logingate
import (
"context"
"fmt"
"io"
"log"
"net"
"sync"
"ananta-ps/internal/config"
"ananta-ps/pkg/crypto/chacha8"
"ananta-ps/pkg/protocol"
)
type Server struct {
cfg *config.Config
listeners []net.Listener
cancelFunc context.CancelFunc
wg sync.WaitGroup
}
func New(cfg *config.Config) *Server {
return &Server{
cfg: cfg,
}
}
func (s *Server) Start() error {
ctx, cancel := context.WithCancel(context.Background())
s.cancelFunc = cancel
for _, port := range s.cfg.Network.LoginPorts {
addr := fmt.Sprintf("%s:%d", s.cfg.Network.BindHost, port)
ln, err := net.Listen("tcp", addr)
if err != nil {
s.Stop()
return fmt.Errorf("failed to listen on login gate %s: %w", addr, err)
}
s.listeners = append(s.listeners, ln)
log.Printf("[LOGIN-GATE] Listening on %s:%d", s.cfg.Network.AdvertisedHost, port)
s.wg.Add(1)
go s.acceptLoop(ctx, ln, port)
}
return nil
}
func (s *Server) Stop() error {
if s.cancelFunc != nil {
s.cancelFunc()
}
for _, ln := range s.listeners {
_ = ln.Close()
}
s.wg.Wait()
return nil
}
func (s *Server) acceptLoop(ctx context.Context, ln net.Listener, port int) {
defer s.wg.Done()
for {
conn, err := ln.Accept()
if err != nil {
select {
case <-ctx.Done():
return
default:
log.Printf("[LOGIN-GATE] Accept error on port %d: %v", port, err)
return
}
}
go s.handleConnection(ctx, conn, port)
}
}
type Session struct {
conn net.Conn
readCipher *chacha8.UxChaCha8
sendCipher *chacha8.UxChaCha8
mu sync.Mutex
}
func (sess *Session) SendPacket(packet *protocol.RpcPacket) error {
sess.mu.Lock()
defer sess.mu.Unlock()
var payload []byte
switch packet.Kind {
case protocol.PacketKindInvoke:
payload = protocol.BuildInvoke(packet.MethodID, packet.InvokeID, packet.Body)
case protocol.PacketKindReturn:
payload = protocol.BuildReturn(packet.MethodID, packet.InvokeID, packet.Error, packet.Body)
case protocol.PacketKindNotify:
payload = protocol.BuildNotify(packet.MethodID, packet.Body)
}
return protocol.WriteFrame(sess.conn, 0x09, payload, sess.sendCipher)
}
func (sess *Session) ReturnEmptyOk(invokeID int32, methodID uint32) error {
return sess.SendPacket(&protocol.RpcPacket{
Kind: protocol.PacketKindReturn,
InvokeID: invokeID,
MethodID: methodID,
Error: 0,
Body: nil,
})
}
func (sess *Session) ReturnEmpty(invokeID int32, methodID uint32, errCode int32) error {
return sess.SendPacket(&protocol.RpcPacket{
Kind: protocol.PacketKindReturn,
InvokeID: invokeID,
MethodID: methodID,
Error: errCode,
Body: nil,
})
}
func (sess *Session) ReturnData(invokeID int32, methodID uint32, body []byte) error {
return sess.SendPacket(&protocol.RpcPacket{
Kind: protocol.PacketKindReturn,
InvokeID: invokeID,
MethodID: methodID,
Error: 0,
Body: body,
})
}
func (sess *Session) NotifyData(methodID uint32, body []byte) error {
return sess.SendPacket(&protocol.RpcPacket{
Kind: protocol.PacketKindNotify,
MethodID: methodID,
Body: body,
})
}
func (s *Server) handleConnection(ctx context.Context, conn net.Conn, port int) {
defer conn.Close()
log.Printf("[LOGIN-GATE] Client connected from %s on port %d", conn.RemoteAddr(), port)
readCipher, writeCipher, err := protocol.HandleHandshake(conn)
if err != nil {
log.Printf("[LOGIN-GATE] Handshake error from %s: %v", conn.RemoteAddr(), err)
return
}
sess := &Session{
conn: conn,
readCipher: readCipher,
sendCipher: writeCipher,
}
for {
select {
case <-ctx.Done():
return
default:
}
frame, err := protocol.ReadFrame(conn, sess.readCipher)
if err != nil {
if err != io.EOF {
log.Printf("[LOGIN-GATE] Connection %s read error: %v", conn.RemoteAddr(), err)
}
return
}
// Frame mode 0x03 is heartbeat
if frame.Mode == 0x03 {
// Echo heartbeat back
_ = protocol.WriteFrame(conn, 0x03, frame.Payload, sess.sendCipher)
continue
}
packet := protocol.ParseRpcPacket(frame.Payload)
if err := s.dispatch(sess, &packet); err != nil {
log.Printf("[LOGIN-GATE] Dispatch error for method %d: %v", packet.MethodID, err)
}
}
}
func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
switch pkt.MethodID {
case protocol.MethodCheckVersion, protocol.MethodHasOnlinePlayer:
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
case protocol.MethodAskNewHotFixPatchLogin:
return sess.ReturnEmpty(pkt.InvokeID, pkt.MethodID, int32(s.cfg.Client.NoMoreHotfixPatchError))
case protocol.MethodRequestPatchesCheckDataFromLogin:
return sess.ReturnData(pkt.InvokeID, pkt.MethodID, protocol.EncodePatchCheck())
case protocol.MethodRequestPatchesFromLogin:
return sess.ReturnData(pkt.InvokeID, pkt.MethodID, protocol.EncodeEmptyList())
case protocol.MethodAskUniSdkShareToken_Login, protocol.MethodAskUniSdkShareToken_Gate:
w := protocol.NewUxWriter()
w.UxString(s.cfg.Player.ShareToken)
return sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes())
case protocol.MethodRequestFpPassToken:
w := protocol.NewUxWriter()
w.UxString(s.cfg.Player.FpPassToken)
return sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes())
case protocol.MethodCheckAccount, protocol.MethodCheckAccountPassBy, protocol.MethodCheckAccountOpenId:
body := protocol.EncodeCheckAccount(
s.cfg.Player.AccountId,
s.cfg.Player.UserName,
s.cfg.Player.LoginToken,
s.cfg.Client.Aid,
s.cfg.Client.ServerID,
s.cfg.Player.Pid,
)
return sess.ReturnData(pkt.InvokeID, pkt.MethodID, body)
case protocol.MethodTryLogin:
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
// Send SyncRoleList (35167428)
w := protocol.NewUxWriter()
w.U64(s.cfg.Player.Pid)
return sess.NotifyData(protocol.MethodSyncRoleList, w.Bytes())
case protocol.MethodRequestCreateRoleEx:
w := protocol.NewUxWriter()
w.U64(s.cfg.Player.Pid)
if err := sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes()); err != nil {
return err
}
return sess.NotifyData(protocol.MethodSyncRoleList, w.Bytes())
case protocol.MethodRequestEnterGame, protocol.MethodDebugRequestEnterGame:
body := protocol.EncodeEnterGame(
s.cfg.Client.Aid,
s.cfg.Player.Pid,
s.cfg.Network.AdvertisedHost,
s.cfg.Network.LoginPorts[0],
s.cfg.Player.LoginToken,
s.cfg.Client.ServerID,
s.cfg.Player.AccountId,
)
return sess.ReturnData(pkt.InvokeID, pkt.MethodID, body)
case protocol.MethodAskCloseConnectionToGate:
w := protocol.NewUxWriter()
w.U32(0)
return sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes())
case protocol.MethodGetServerTime:
if pkt.Kind == protocol.PacketKindInvoke {
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
}
return sess.NotifyData(protocol.MethodSendServerTime, protocol.EncodeServerTime(0))
case protocol.MethodQuerySkey:
w := protocol.NewUxWriter()
w.UxString(s.cfg.Player.Skey)
return sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes())
case protocol.MethodGateLogin:
log.Printf("[GATE] Authenticated Gate session registered for IMasterToClient S2C")
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
// 1. SendServerTime
_ = sess.NotifyData(protocol.MethodSendServerTime, protocol.EncodeServerTime(0))
// 2. SyncGameSwitchToClient_3 (53940944) - Crucial for vehicles, NPCs, gadgets
gameSwitches := protocol.BuildGameSwitchPayload()
log.Printf("[GATE] SyncGameSwitchToClient_3 entries=%d body=%dB", len(protocol.GameSwitchCatalog), len(gameSwitches))
_ = sess.NotifyData(protocol.MethodSyncGameSwitchToClient_3, gameSwitches)
// 3. SyncPlayerGameServerInfo (52737039) - Handoff to Game Server
gameInfo := protocol.EncodeGameServerInfo(s.cfg.Network.AdvertisedHost, s.cfg.Network.GamePort, s.cfg.Player.GameToken)
return sess.NotifyData(protocol.MethodSyncPlayerGameServerInfo, gameInfo)
case protocol.MethodGetFriendApplicationListToMe,
protocol.MethodGetAllChatGroupLatestMessage,
protocol.MethodGetSimplePlayerInfoByPidList:
return sess.ReturnData(pkt.InvokeID, pkt.MethodID, protocol.EncodeEmptyList())
case protocol.MethodReportNetworkQuality, protocol.MethodDavinciCode:
// S2C Notify ignore
return nil
default:
if pkt.Kind == protocol.PacketKindInvoke {
log.Printf("[LOGIN-GATE] Unhandled Invoke MethodID %d, returning empty OK", pkt.MethodID)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
}
return nil
}
+124
View File
@@ -0,0 +1,124 @@
package logingate
import (
"crypto/aes"
"crypto/cipher"
"net"
"testing"
"time"
"ananta-ps/internal/config"
"ananta-ps/pkg/crypto/chacha8"
"ananta-ps/pkg/protocol"
)
func TestLoginGateServer(t *testing.T) {
cfg := &config.Config{
Client: config.ClientConfig{
ServerID: 801,
Aid: 5944,
},
Network: config.NetworkConfig{
BindHost: "127.0.0.1",
AdvertisedHost: "127.0.0.1",
LoginPorts: []int{15200},
GamePort: 15202,
},
Player: config.PlayerConfig{
Pid: 100,
AccountId: "test_acc",
UserName: "test_user",
},
}
srv := New(cfg)
if err := srv.Start(); err != nil {
t.Fatalf("failed to start login gate: %v", err)
}
defer srv.Stop()
// Connect client
conn, err := net.DialTimeout("tcp", "127.0.0.1:15200", 2*time.Second)
if err != nil {
t.Fatalf("failed to connect: %v", err)
}
defer conn.Close()
// 1. Send client handshake (mode 0x01 plaintext)
clientHandshake := make([]byte, 260)
clientHandshake[0] = 0xAA
clientHandshake[1] = 0xBB
clientHandshake[2] = 0xCC
clientHandshake[3] = 0xDD
aesKey := []byte("0123456789abcdef") // 16 bytes
copy(clientHandshake[4:20], aesKey)
if err := protocol.WriteFrame(conn, 0x01, clientHandshake, nil); err != nil {
t.Fatalf("failed to write client handshake: %v", err)
}
// 2. Read server handshake reply
serverReplyFrame, err := protocol.ReadFrame(conn, nil)
if err != nil {
t.Fatalf("failed to read server handshake reply: %v", err)
}
if len(serverReplyFrame.Payload) < 112 {
t.Fatalf("server reply too short: %d", len(serverReplyFrame.Payload))
}
aesNonce := serverReplyFrame.Payload[8:20]
encryptedHead := serverReplyFrame.Payload[20:96]
tag := serverReplyFrame.Payload[96:112]
cipherBlock, err := aes.NewCipher(aesKey)
if err != nil {
t.Fatalf("failed to create aes cipher: %v", err)
}
aesGcm, err := cipher.NewGCM(cipherBlock)
if err != nil {
t.Fatalf("failed to create aes gcm: %v", err)
}
fullCipher := append(encryptedHead, tag...)
chaChaHead, err := aesGcm.Open(nil, aesNonce, fullCipher, nil)
if err != nil {
t.Fatalf("failed to decrypt chacha head: %v", err)
}
clientWriteKey := chaChaHead[0:32]
clientReadKey := chaChaHead[32:64]
streamNonce := chaChaHead[64:76]
clientSendCipher, err := chacha8.New(clientWriteKey, streamNonce)
if err != nil {
t.Fatalf("failed to create client send cipher: %v", err)
}
clientReadCipher, err := chacha8.New(clientReadKey, streamNonce)
if err != nil {
t.Fatalf("failed to create client read cipher: %v", err)
}
// 3. Send CheckVersion Invoke
invokePayload := protocol.BuildInvoke(protocol.MethodCheckVersion, 1, nil)
if err := protocol.WriteFrame(conn, 0x09, invokePayload, clientSendCipher); err != nil {
t.Fatalf("failed to write invoke frame: %v", err)
}
// 4. Read Return frame
respFrame, err := protocol.ReadFrame(conn, clientReadCipher)
if err != nil {
t.Fatalf("failed to read response frame: %v", err)
}
pkt := protocol.ParseRpcPacket(respFrame.Payload)
if pkt.Kind != protocol.PacketKindReturn {
t.Fatalf("expected Return packet, got %v", pkt.Kind)
}
if pkt.InvokeID != 1 {
t.Fatalf("expected InvokeID 1, got %d", pkt.InvokeID)
}
if pkt.Error != 0 {
t.Fatalf("expected Error 0, got %d", pkt.Error)
}
}
+308
View File
@@ -0,0 +1,308 @@
package web
import (
"context"
"encoding/json"
"fmt"
"html/template"
"log"
"net/http"
"sync"
"time"
"ananta-ps/internal/config"
"ananta-ps/internal/gameserver"
)
type AdminServer struct {
cfg *config.Config
gameServer *gameserver.Server
httpSrv *http.Server
cancelFunc context.CancelFunc
mu sync.RWMutex
}
func NewAdmin(cfg *config.Config, gameServer *gameserver.Server) *AdminServer {
return &AdminServer{
cfg: cfg,
gameServer: gameServer,
}
}
func (a *AdminServer) Start() error {
if !a.cfg.Admin.Enabled {
return nil
}
ctx, cancel := context.WithCancel(context.Background())
a.cancelFunc = cancel
mux := http.NewServeMux()
mux.HandleFunc("/", a.handleDashboard)
mux.HandleFunc("/api/teleport", a.handleTeleport)
mux.HandleFunc("/api/sessions", a.handleSessions)
addr := fmt.Sprintf("%s:%d", a.cfg.Network.BindHost, a.cfg.Admin.Port)
a.httpSrv = &http.Server{
Addr: addr,
Handler: mux,
}
go func() {
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 {
log.Printf("[ADMIN-GM] Web server error: %v", err)
}
}()
_ = ctx
return nil
}
func (a *AdminServer) Stop() error {
if a.cancelFunc != nil {
a.cancelFunc()
}
if a.httpSrv != nil {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_ = a.httpSrv.Shutdown(ctx)
}
return nil
}
func (a *AdminServer) handleDashboard(w http.ResponseWriter, r *http.Request) {
tmpl := `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ananta GM Admin Dashboard</title>
<style>
:root {
--bg: #0f111a;
--card: #171b26;
--border: #2b3147;
--text: #e6edf3;
--text-muted: #8b949e;
--accent: #58a6ff;
--accent-hover: #1f6feb;
--success: #238636;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background-color: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
padding: 24px;
}
.container { max-width: 1200px; margin: 0 auto; }
header { margin-bottom: 24px; border-bottom: 1px solid var(--border); padding-bottom: 16px; display: flex; justify-content: space-between; align-items: center; }
h1 { font-size: 24px; font-weight: 600; color: var(--accent); }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 20px; }
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.card h2 { font-size: 18px; margin-bottom: 16px; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
.form-group { margin-bottom: 12px; }
label { display: block; font-size: 13px; color: var(--text-muted); margin-bottom: 4px; }
input, select, button {
width: 100%;
padding: 10px 12px;
background: #0d1117;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 14px;
}
button {
background: var(--accent);
color: #fff;
cursor: pointer;
font-weight: 600;
transition: background 0.2s;
border: none;
margin-top: 8px;
}
button:hover { background: var(--accent-hover); }
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; background: var(--success); color: #fff; }
.stat-val { font-size: 20px; font-weight: bold; margin-top: 4px; }
.coord-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
</style>
</head>
<body>
<div class="container">
<header>
<div>
<h1>Ananta GM Admin Dashboard (Go Native)</h1>
<p style="color: var(--text-muted); font-size: 13px;">Project Mugen CBT 4229938 Sandbox Controller</p>
</div>
<div>
<span class="badge">ONLINE</span>
</div>
</header>
<div class="grid">
<!-- Server Status -->
<div class="card">
<h2>Server Status</h2>
<div style="margin-bottom: 12px;">
<div style="font-size: 13px; color: var(--text-muted);">Active Sessions</div>
<div class="stat-val">{{.ActiveSessions}}</div>
</div>
<div style="margin-bottom: 12px;">
<div style="font-size: 13px; color: var(--text-muted);">Player UID / Account</div>
<div style="font-size: 14px; margin-top: 4px;">{{.PlayerPID}} ({{.AccountId}})</div>
</div>
<div>
<div style="font-size: 13px; color: var(--text-muted);">Active Character Template</div>
<div style="font-size: 14px; margin-top: 4px;">{{.InitialSpirit}}</div>
</div>
</div>
<!-- Teleportation -->
<div class="card">
<h2>Instant Teleportation</h2>
<form id="teleportForm">
<div class="form-group">
<label>Presets</label>
<select id="teleportPreset" onchange="applyPreset()">
<option value="3142.67,0.0,2522.78,0.0">Downtown Core (Spawn)</option>
<option value="3500.0,10.0,2200.0,90.0">Subway Central Station</option>
<option value="2800.0,5.0,2900.0,180.0">Harbor / Waterfront Pier</option>
<option value="4100.0,50.0,1500.0,-45.0">Highrise Skybridge</option>
<option value="1500.0,0.0,1800.0,0.0">Suburban District</option>
</select>
</div>
<div class="form-group">
<label>Coordinates (X, Y, Z)</label>
<div class="coord-grid">
<input type="number" step="0.1" id="posX" value="3142.7">
<input type="number" step="0.1" id="posY" value="0.0">
<input type="number" step="0.1" id="posZ" value="2522.8">
</div>
</div>
<div class="form-group">
<label>Facing (Yaw)</label>
<input type="number" step="0.1" id="posFacing" value="0.0">
</div>
<button type="button" onclick="submitTeleport()">Teleport In-Game</button>
</form>
<div id="teleportResult" style="margin-top: 8px; font-size: 13px;"></div>
</div>
<!-- Character & Free-Roam Controls -->
<div class="card">
<h2>Free-Roam & Abilities</h2>
<div class="form-group">
<label>Playable Character</label>
<select id="charSelect">
<option value="15020967">辰宿 (Chensu - Default)</option>
<option value="15020968">塔菲 (Taffy)</option>
<option value="15020969">莉莉 (Lily)</option>
<option value="15020970">阿兰 (Alan)</option>
<option value="15020971">黛西 (Daisy)</option>
</select>
</div>
<button type="button" onclick="alert('Character switch registered. In game press T or switch team!')">Prepare Character</button>
<div style="margin-top: 16px;">
<button type="button" style="background: #238636;" onclick="alert('Diving ability unlocked natively for all characters!')">Diving Full Unlock (3D Swim)</button>
</div>
</div>
</div>
</div>
<script>
function applyPreset() {
const parts = document.getElementById("teleportPreset").value.split(",");
document.getElementById("posX").value = parts[0];
document.getElementById("posY").value = parts[1];
document.getElementById("posZ").value = parts[2];
document.getElementById("posFacing").value = parts[3];
}
async function submitTeleport() {
const x = parseFloat(document.getElementById("posX").value);
const y = parseFloat(document.getElementById("posY").value);
const z = parseFloat(document.getElementById("posZ").value);
const facing = parseFloat(document.getElementById("posFacing").value);
const res = await fetch("/api/teleport", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({x, y, z, facing})
});
const data = await res.json();
const el = document.getElementById("teleportResult");
if (data.ok) {
el.innerText = "✓ Teleported successfully to (" + x + ", " + y + ", " + z + ")";
el.style.color = "#3fb950";
} else {
el.innerText = "✗ Error: " + (data.msg || "no active game session");
el.style.color = "#f85149";
}
}
</script>
</body>
</html>`
sessions := a.gameServer.GetActiveSessions()
data := map[string]interface{}{
"ActiveSessions": len(sessions),
"PlayerPID": a.cfg.Player.Pid,
"AccountId": a.cfg.Player.AccountId,
"InitialSpirit": a.cfg.Player.InitialSpiritTemplateId,
}
t := template.Must(template.New("dashboard").Parse(tmpl))
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = t.Execute(w, data)
}
func (a *AdminServer) handleTeleport(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
X float32 `json:"x"`
Y float32 `json:"y"`
Z float32 `json:"z"`
Facing float32 `json:"facing"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "msg": err.Error()})
return
}
sessions := a.gameServer.GetActiveSessions()
if len(sessions) == 0 {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "msg": "No active client connected in game"})
return
}
for _, sess := range sessions {
_ = sess.Teleport(req.X, req.Y, req.Z, req.Facing)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "count": len(sessions)})
}
func (a *AdminServer) handleSessions(w http.ResponseWriter, r *http.Request) {
sessions := a.gameServer.GetActiveSessions()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"count": len(sessions),
"host": a.cfg.Network.AdvertisedHost,
"port": a.cfg.Network.GamePort,
})
}
+106
View File
@@ -0,0 +1,106 @@
package web
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"ananta-ps/internal/config"
"ananta-ps/internal/gameserver"
)
type DebugServer struct {
cfg *config.Config
gameServer *gameserver.Server
httpSrv *http.Server
cancelFunc context.CancelFunc
}
func NewDebug(cfg *config.Config, gameServer *gameserver.Server) *DebugServer {
return &DebugServer{
cfg: cfg,
gameServer: gameServer,
}
}
func (d *DebugServer) Start() error {
ctx, cancel := context.WithCancel(context.Background())
d.cancelFunc = cancel
mux := http.NewServeMux()
mux.HandleFunc("/", d.handleIndex)
mux.HandleFunc("/api/status", d.handleStatus)
addr := fmt.Sprintf("%s:%d", d.cfg.Network.BindHost, 5809)
d.httpSrv = &http.Server{
Addr: addr,
Handler: mux,
}
go func() {
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 {
log.Printf("[DEBUG-API] Debug server error: %v", err)
}
}()
_ = ctx
return nil
}
func (d *DebugServer) Stop() error {
if d.cancelFunc != nil {
d.cancelFunc()
}
if d.httpSrv != nil {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_ = d.httpSrv.Shutdown(ctx)
}
return nil
}
func (d *DebugServer) handleIndex(w http.ResponseWriter, r *http.Request) {
html := `<!doctype html>
<html>
<head><meta charset="utf-8"><title>Ananta Debug WebUI</title></head>
<body style="background:#111; color:#eee; font-family:monospace; padding:20px;">
<h2>Ananta Private Server Debug Monitor (Port 5809)</h2>
<div id="status">Connecting to runtime...</div>
<pre id="info" style="margin-top:20px; background:#222; padding:15px; border-radius:5px;"></pre>
<script>
async function refresh() {
try {
const res = await fetch("/api/status");
const data = await res.json();
document.getElementById("status").innerHTML = "Status: <b>ONLINE</b> | Active Sessions: " + data.activeSessions;
document.getElementById("info").innerText = JSON.stringify(data, null, 2);
} catch (e) {
document.getElementById("status").innerText = "Offline: " + e;
}
}
setInterval(refresh, 2000);
refresh();
</script>
</body>
</html>`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}
func (d *DebugServer) handleStatus(w http.ResponseWriter, r *http.Request) {
sessions := d.gameServer.GetActiveSessions()
data := map[string]interface{}{
"online": true,
"version": d.cfg.Client.Version,
"activeSessions": len(sessions),
"advertisedHost": d.cfg.Network.AdvertisedHost,
"gamePort": d.cfg.Network.GamePort,
"loginPorts": d.cfg.Network.LoginPorts,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}
+114
View File
@@ -0,0 +1,114 @@
package chacha8
import (
"encoding/binary"
"errors"
"math/bits"
)
// NetEase 4229938 UXChaCha8 stream cipher.
// Uses custom Sigma constants differing from standard ChaCha.
const (
Sigma0 uint32 = 0x60707865
Sigma1 uint32 = 0x3321646e
Sigma2 uint32 = 0x79622d32
Sigma3 uint32 = 0x6b206e74
)
type UxChaCha8 struct {
state [16]uint32
keystream [64]byte
keystreamPos int
}
func New(key []byte, nonce []byte) (*UxChaCha8, error) {
if len(key) != 32 {
return nil, errors.New("UXChaCha8 key must be 32 bytes")
}
if len(nonce) != 12 {
return nil, errors.New("UXChaCha8 nonce must be 12 bytes")
}
c := &UxChaCha8{
keystreamPos: 64,
}
c.state[0] = Sigma0
c.state[1] = Sigma1
c.state[2] = Sigma2
c.state[3] = Sigma3
for i := 0; i < 8; i++ {
c.state[4+i] = binary.LittleEndian.Uint32(key[i*4 : (i+1)*4])
}
c.state[12] = 0
c.state[13] = binary.LittleEndian.Uint32(nonce[0:4])
c.state[14] = binary.LittleEndian.Uint32(nonce[4:8])
c.state[15] = binary.LittleEndian.Uint32(nonce[8:12])
return c, nil
}
func (c *UxChaCha8) XorInPlace(data []byte) {
offset := 0
for offset < len(data) {
if c.keystreamPos == 64 {
c.generateBlock()
c.keystreamPos = 0
}
take := 64 - c.keystreamPos
if len(data)-offset < take {
take = len(data) - offset
}
for i := 0; i < take; i++ {
data[offset+i] ^= c.keystream[c.keystreamPos+i]
}
offset += take
c.keystreamPos += take
}
}
func (c *UxChaCha8) generateBlock() {
var x [16]uint32
copy(x[:], c.state[:])
for i := 0; i < 4; i++ {
quarterRound(&x[0], &x[4], &x[8], &x[12])
quarterRound(&x[1], &x[5], &x[9], &x[13])
quarterRound(&x[2], &x[6], &x[10], &x[14])
quarterRound(&x[3], &x[7], &x[11], &x[15])
quarterRound(&x[0], &x[5], &x[10], &x[15])
quarterRound(&x[1], &x[6], &x[11], &x[12])
quarterRound(&x[2], &x[7], &x[8], &x[13])
quarterRound(&x[3], &x[4], &x[9], &x[14])
}
for i := 0; i < 16; i++ {
binary.LittleEndian.PutUint32(c.keystream[i*4:(i+1)*4], x[i]+c.state[i])
}
c.state[12]++
}
func quarterRound(a, b, c, d *uint32) {
*a += *b
*d ^= *a
*d = bits.RotateLeft32(*d, 16)
*c += *d
*b ^= *c
*b = bits.RotateLeft32(*b, 12)
*a += *b
*d ^= *a
*d = bits.RotateLeft32(*d, 8)
*c += *d
*b ^= *c
*b = bits.RotateLeft32(*b, 7)
}
+41
View File
@@ -0,0 +1,41 @@
package chacha8
import (
"bytes"
"testing"
)
func TestChaCha8Roundtrip(t *testing.T) {
key := make([]byte, 32)
for i := range key {
key[i] = byte(i + 1)
}
nonce := make([]byte, 12)
for i := range nonce {
nonce[i] = byte(i + 0x10)
}
c1, err := New(key, nonce)
if err != nil {
t.Fatalf("New failed: %v", err)
}
c2, err := New(key, nonce)
if err != nil {
t.Fatalf("New failed: %v", err)
}
original := []byte("Hello, Ananta NetEase UXChaCha8 stream cipher verification!")
data := make([]byte, len(original))
copy(data, original)
c1.XorInPlace(data)
if bytes.Equal(data, original) {
t.Errorf("Ciphertext equals plaintext")
}
c2.XorInPlace(data)
if !bytes.Equal(data, original) {
t.Errorf("Decrypted data != original: got %q, want %q", data, original)
}
}
+260
View File
@@ -0,0 +1,260 @@
package protocol
import (
"encoding/binary"
"encoding/json"
"fmt"
"os"
"time"
)
// EncodeServerTime encodes the ServerTime payload.
// Format (Inline): [F64 clientUnixTime] [F64 serverUnixTime]
func EncodeServerTime(clientUnixTime float64) []byte {
now := float64(time.Now().UnixNano()) / 1e9
if clientUnixTime <= 0 {
clientUnixTime = now
}
w := NewUxWriter()
w.F64(clientUnixTime)
w.F64(now)
return w.Bytes()
}
// EncodePatchCheck encodes PatchCheckResult.
// Format (Inline): [I32 patchVersion (0)] [0xFF marker] [Int7 count (0)]
func EncodePatchCheck() []byte {
w := NewUxWriter()
w.I32(0) // patchVersion
w.U8(0xFF) // list marker
w.Int7(0) // empty list
return w.Bytes()
}
// EncodeEmptyList encodes an empty List<uint>.
func EncodeEmptyList() []byte {
w := NewUxWriter()
w.U8(0xFF)
w.Int7(0)
return w.Bytes()
}
// EncodeCheckAccount encodes CheckAccountResult.
func EncodeCheckAccount(accountId, userName, token string, aid, serverId int, pid uint64) []byte {
loginData := map[string]interface{}{
"code": 200,
"subcode": 0,
"msg": "ok",
"uid": accountId,
"sdkuid": accountId,
"user_id": accountId,
"aid": aid,
"pid": pid,
"player_id": fmt.Sprintf("%d", pid),
"role_id": fmt.Sprintf("%d", pid),
"role_name": "辰宿",
"server_id": fmt.Sprintf("%d", serverId),
"host_id": serverId,
"username": userName,
"account": accountId,
"platform": "pc",
"token": token,
"access_token": token,
"ext_access_token": token,
"sessionid": token,
"login_ticket": token,
"realname_status": 1,
"realname_verify_status": 1,
"mobile_bind_status": 1,
}
loginJSON, _ := json.Marshal(loginData)
w := NewUxWriter()
w.U8(0xFF) // Complex object marker
w.UxString(string(loginJSON))
w.UxString(token)
w.UxString(userName)
w.I32(int32(aid))
w.Bool(false) // NeedRealNameTip
w.Bool(false) // NeedRoleEnter
w.Bool(true) // RealNameVerified
w.I32(int32(serverId))
w.I32(200) // code
w.I32(0) // subcode
w.UxString("ok")
return w.Bytes()
}
// EncodeEnterGame encodes EnterGameResult.
func EncodeEnterGame(aid int, pid uint64, host string, port int, token string, serverId int, accountId string) []byte {
w := NewUxWriter()
w.I32(int32(aid))
w.U64(pid)
// GateServerInfo
w.U8(0xFF)
w.I32(int32(aid))
w.U64(pid)
w.UxString(host)
w.I32(int32(port))
w.UxString(token)
w.I32(int32(serverId))
w.UxString(accountId)
return w.Bytes()
}
// EncodeGameServerInfo encodes GameServerInfo.
func EncodeGameServerInfo(host string, port int, gameToken string) []byte {
w := NewUxWriter()
w.U8(0xFF)
w.UxString(host)
w.I32(int32(port))
w.UxString(gameToken)
return w.Bytes()
}
// EncodeEnterScene encodes EnterSceneInfo.
func EncodeEnterScene(pid uint64, raidId uint32, instanceId uint64, universeId uint32, unitId uint64, templateId uint32, posX, posY, posZ, facing float32) []byte {
w := NewUxWriter()
w.U8(0xFF) // [UxContract]
w.U64(pid)
w.U32(raidId)
w.U64(instanceId)
// UXVector3 Position (Struct)
w.F32(posX)
w.F32(posY)
w.F32(posZ)
w.F32(facing)
// SceneSpoonNames (empty list)
w.U8(0xFF).Int7(0)
// SceneSpoonMd5s (empty list)
w.U8(0xFF).Int7(0)
// Spirits List (count 1, Struct)
w.U8(0xFF).Int7(1)
// SpiritInitData (Struct: Id, TemplateId, IsActive, WeaponTemplateId, WeaponSkinId)
w.U64(unitId).U32(templateId).Bool(true).U32(0).U32(0)
// GridInfo (Struct: MinX, MinZ, MaxX, MaxZ)
w.I32(0).I32(0).I32(300000).I32(300000)
w.U32(0) // MatchGameId
w.U32(0) // SwitchShowId
w.Bool(false) // IsSwitchSpiritShow
w.U32(universeId)
w.U32(0) // SectorControlId
// SectorControlIds (empty list)
w.U8(0xFF).Int7(0)
w.U32(0) // DynamicLayerId
// LoadingType (Complex 0xFF: Type, Members list, Ripple)
w.U8(0xFF).U32(0).U8(0xFF).Int7(0).Bool(false)
// LinkSimpleInfo (Complex 0xFF: Id, Mode, DeviceLevel, Tag, PublicEventId, LastPublicEventTime, HostPid)
w.U8(0xFF).U32(0).U32(0).U32(1).U32(0).U32(0).U64(0).U64(0)
// DataLayerIndices (empty list)
w.U8(0xFF).Int7(0)
w.Bool(false) // Seamless
// CollectionRoomLoadData (null complex)
w.U8(0x00)
// HouseLoadData (null complex)
w.U8(0x00)
// TombIslandIds (empty list)
w.U8(0xFF).Int7(0)
return w.Bytes()
}
// EncodeLogicAgentEnter encodes SyncLogicAgentEnter.
func EncodeLogicAgentEnter(unitId uint64) []byte {
w := NewUxWriter()
w.U64(unitId)
return w.Bytes()
}
// EncodeLogicAgentLeave encodes SyncLogicAgentLeave.
func EncodeLogicAgentLeave(unitId uint64) []byte {
w := NewUxWriter()
w.U64(unitId)
return w.Bytes()
}
// EncodeManagedLogicAgent encodes SyncManagedLogicAgent.
func EncodeManagedLogicAgent(unitId, pid uint64, moveId byte) []byte {
w := NewUxWriter()
w.U64(unitId).U64(pid).U8(moveId)
return w.Bytes()
}
// EncodePlayerCurrentSpirit encodes SyncPlayerCurrentSpirit.
func EncodePlayerCurrentSpirit(pid uint64, templateId uint32, unitId uint64, isAgentSwitch bool) []byte {
w := NewUxWriter()
w.U64(pid).U32(templateId).U64(unitId).Bool(isAgentSwitch)
return w.Bytes()
}
// EncodeSceneLoadCompleted encodes SyncSceneLoadCompleted.
func EncodeSceneLoadCompleted(sceneId uint64) []byte {
w := NewUxWriter()
w.U64(sceneId)
return w.Bytes()
}
// EncodeRaidBattleUnitSpirit encodes the character projection for world entry and spirit switching.
func EncodeRaidBattleUnitSpirit(unitId uint64, templateId uint32, posX, posY, posZ, facing float32, pid uint64) []byte {
w := NewUxWriter()
w.U8(0xFF) // RaidBattleUnitSpirit Complex marker
w.U8(0xFF) // SpiritWearFashionsInfo Complex marker
// WearFashionColoringInfoDict empty dictionary (0xFF marker, count 0)
w.U8(0xFF).I32(0)
// WearFashionInfoList with 5 default fashion items
w.U8(0xFF).I32(5)
fashionIds := []uint32{11100051, 11100052, 11100053, 11189102, 11189103}
for _, fid := range fashionIds {
w.U8(0xFF).U32(fid)
}
// WearFashionEditInfoList empty list
w.U8(0xFF).I32(0)
// HiddenParts = 0, EditedHiddenParts = 0, Begging = false, IsBot = false
w.U8(0).U8(0).Bool(false).Bool(false)
// Id & TemplateId
w.U64(unitId).U32(templateId)
// Position & Facing
w.F32(posX).F32(posY).F32(posZ).F32(facing)
// OwnerId & ManagedPid
w.U64(pid).U64(0)
// MoveId & NavTags
w.U8(0).U32(0)
// GroundData (MoveActionGroundData Complex 0xFF)
w.U8(0xFF).U32(0).U64(0).U32(0)
// LocalPos (UXVector3)
w.F32(0).F32(0).F32(0)
return w.Bytes()
}
// EncodePlayerInfo loads the baseline PlayerClientInfo and patches dynamic fields.
func EncodePlayerInfo(templatePath string, aid int, pid uint64, accountId, name string) ([]byte, error) {
data, err := os.ReadFile(templatePath)
if err != nil {
return nil, err
}
res := make([]byte, len(data))
copy(res, data)
// Patch Aid at offset 4 (4 bytes)
if len(res) > 8 {
binary.LittleEndian.PutUint32(res[4:8], uint32(aid))
}
// Patch Pid at offset 8 (8 bytes)
if len(res) > 16 {
binary.LittleEndian.PutUint64(res[8:16], pid)
}
return res, nil
}
+132
View File
@@ -0,0 +1,132 @@
package protocol
import (
"encoding/binary"
"errors"
"io"
"ananta-ps/pkg/crypto/chacha8"
)
type Frame struct {
Mode byte
Payload []byte
}
func ReadFrame(r io.Reader, cryptor *chacha8.UxChaCha8) (*Frame, error) {
head := make([]byte, 5)
if _, err := io.ReadFull(r, head); err != nil {
return nil, err
}
if cryptor != nil {
cryptor.XorInPlace(head)
}
payloadLength := int(binary.LittleEndian.Uint32(head[0:4]))
if payloadLength < 0 || payloadLength > 16*1024*1024 {
return nil, errors.New("invalid frame payload length after decrypt")
}
payload := make([]byte, payloadLength)
if payloadLength > 0 {
if _, err := io.ReadFull(r, payload); err != nil {
return nil, err
}
if cryptor != nil {
cryptor.XorInPlace(payload)
}
}
return &Frame{
Mode: head[4],
Payload: payload,
}, nil
}
func WriteFrame(w io.Writer, mode byte, payload []byte, cryptor *chacha8.UxChaCha8) error {
buf := make([]byte, 5+len(payload))
binary.LittleEndian.PutUint32(buf[0:4], uint32(len(payload)))
buf[4] = mode
copy(buf[5:], payload)
if cryptor != nil {
cryptor.XorInPlace(buf)
}
_, err := w.Write(buf)
return err
}
func ParseRpcPacket(payload []byte) RpcPacket {
if len(payload) < 5 {
return RpcPacket{Kind: PacketKindUnknown, Body: payload}
}
tag := payload[0]
methodID := binary.LittleEndian.Uint32(payload[1:5])
switch tag {
case 0x03: // Notify
return RpcPacket{
Kind: PacketKindNotify,
MethodID: methodID,
Body: payload[5:],
}
case 0x01: // Invoke
if len(payload) >= 9 {
invokeID := int32(binary.LittleEndian.Uint32(payload[5:9]))
return RpcPacket{
Kind: PacketKindInvoke,
MethodID: methodID,
InvokeID: invokeID,
Body: payload[9:],
}
}
case 0x02: // Return
if len(payload) >= 13 {
invokeID := int32(binary.LittleEndian.Uint32(payload[5:9]))
errCode := int32(binary.LittleEndian.Uint32(payload[9:13]))
return RpcPacket{
Kind: PacketKindReturn,
MethodID: methodID,
InvokeID: invokeID,
Error: errCode,
Body: payload[13:],
}
}
}
return RpcPacket{
Kind: PacketKindUnknown,
MethodID: methodID,
Body: payload,
}
}
func BuildNotify(methodID uint32, body []byte) []byte {
buf := make([]byte, 5+len(body))
buf[0] = 0x03
binary.LittleEndian.PutUint32(buf[1:5], methodID)
copy(buf[5:], body)
return buf
}
func BuildInvoke(methodID uint32, invokeID int32, body []byte) []byte {
buf := make([]byte, 9+len(body))
buf[0] = 0x01
binary.LittleEndian.PutUint32(buf[1:5], methodID)
binary.LittleEndian.PutUint32(buf[5:9], uint32(invokeID))
copy(buf[9:], body)
return buf
}
func BuildReturn(methodID uint32, invokeID int32, errCode int32, body []byte) []byte {
buf := make([]byte, 13+len(body))
buf[0] = 0x02
binary.LittleEndian.PutUint32(buf[1:5], methodID)
binary.LittleEndian.PutUint32(buf[5:9], uint32(invokeID))
binary.LittleEndian.PutUint32(buf[9:13], uint32(errCode))
copy(buf[13:], body)
return buf
}
+229
View File
@@ -0,0 +1,229 @@
package protocol
// GameSwitchEntry represents a single switch configuration entry.
type GameSwitchEntry struct {
Name string
Value bool
}
// GameSwitchCatalog contains all 203 UX.GameSwitch flags.
var GameSwitchCatalog = []GameSwitchEntry{
{"EnableRaidTimeFix", true},
{"EnableClientZoneClosure", true},
{"UseEditorSectorTileInfoJsonPath1", true},
{"EnableNavMultiverseChain", true},
{"EnableRecycleItem", true},
{"EnableComposeItem", true},
{"DisableEditXingZheDangAn", false},
{"EnableUpdateSignature", true},
{"EnablePartialAntiAddiction", false},
{"EnableAntiAddiction", false},
{"EnableRealNameBeforeCreateRole", false},
{"EnableExchangeGold", true},
{"EnableNpcShop", true},
{"EnableFashionStore", true},
{"Enable4SStore", true},
{"EnableCreateRole", true},
{"EnableSeasonalBattlePass", true},
{"EnableGamePlayBattlePass", true},
{"EnableGachaSystem", true},
{"EnableRadiantChest", true},
{"EnableCloset", true},
{"EnableGachaMilestone", true},
{"EnableHeartbeatTimeoutCheckDebug", false},
{"EnableHeartbeatTimeoutCheckRelease", true},
{"EnableLoginReconnect", true},
{"EnableLoginOnlineAndDetachMatchLink", true},
{"AutoSwitchTextModeWithAllPrefix", true},
{"ChatLimitRegion", true},
{"EnableMahjong", true},
{"EnableMahjongFriend", true},
{"EnableMahjongWithNpc", true},
{"EnableMahjongPve", true},
{"EnableChangePersonalZoneHeadInfo", true},
{"EnableSpiritAddExp", true},
{"EnableSpiritAbilityAddExp", true},
{"EnableSpiritDomainLevelUp", true},
{"EnableForeignIpSns", true},
{"EnableSharePhoto_Netease", true},
{"EnableSharePhoto_Oppo", true},
{"EnableSharePhoto_Huawei", true},
{"EnableSharePhoto_NearmeVivo", true},
{"EnableSharePhoto_XiaomiApp", true},
{"EnableSharePhoto_CoolpadSdk", true},
{"EnableSharePhoto_Flyme", true},
{"EnableSharePhoto_LenovoOpen", true},
{"EnableSharePhoto_Gionee", true},
{"EnableSharePhoto_UcPlatform", true},
{"EnableSharePhoto_360Assistant", true},
{"EnableSharePhoto_Baidu", true},
{"EnableSharePhoto_BilibiliSdk", true},
{"EnableSharePhoto_Dtkj", true},
{"EnableSharePhoto_AppStore", true},
{"EnableSharePhoto_TapTapDev", true},
{"EnableSharePhoto_TapTapDis", true},
{"EnableSharePhoto_HykbDev", true},
{"EnableSharePhoto_HykbDis", true},
{"EnableFightSpiritSkillLevelUp", true},
{"EnableSignIn", true},
{"EnableSaveCardGroupName", true},
{"EnableAetherNpcFixedSpawnPointRule", true},
{"EnableNpcUseBelonging", true},
{"EnableVehicleNpcDefaultPlayPoi1", true},
{"EnableAgentInteractForbidFromServer1", true},
{"EnableTeleportOnCrossBoundary", true},
{"EnableAgentCompanionAIIdleChat", true},
{"EnableAgentPlayPoiCommand", true},
{"EnableNpcGateway", true},
{"EnableNpcDensityMigration1", true},
{"EnableLink", true},
{"EnableLinkPlanningBoardUnlockCheck", true},
{"ProfilerAetherVehicle", true},
{"ProfilerStaticVehicle", true},
{"ProfilerAetherVehicleGO", true},
{"EnableRaidDetachLocalCleanup", true},
{"ProfilerIntersection", true},
{"ProfilerIntersectionRender", true},
{"EnablePoliceJobStoryLegacy", true},
{"EnableGroupAIStory", true},
{"EnableGroupAIDebug", true},
{"EnableNewProgress", true},
{"EnableDGTemporaryResource", true},
{"ForbidStimEvent", false},
{"EnableZoneGraphUndirectLane", true},
{"EnableNPCNormalMoveBeta", true},
{"EnableIndoor", true},
{"EnableVehicleNpcBoardingSync1", true},
{"EnableAetherNpcTypeAgnosticSync", true},
{"EnableVehicleBoardingUnifiedSync1", true},
{"EnableLinkTest", true},
{"EnableSpawnSpoonBlockInteractionCheck", true},
{"EnableVehicleOverrideSync", true},
{"EnableVehicleDefaultSuitExpand", true},
{"EnableECSResponse", true},
{"DebugSyncVehicleDangerZoneToClient", false},
{"DebugHiddenArea", false},
{"DebugAetherVehicle", false},
{"DebugAetherIntersection", false},
{"DebugAetherVehicleLane", false},
{"DebugAssetTagPreview", false},
{"DebugLogicVehicle", false},
{"EnableDropRequireReliableRpcOnClient", true},
{"ProfilerRandomEvent", true},
{"ProfilerDestructible", true},
{"ProfilerGadget", true},
{"ProfilerDynamicGo", true},
{"ProfilerSpoonAgent", true},
{"ProfilerWildEnemy", true},
{"ProfilerEnableMetro", true},
{"BlockMetroForChongxiao", false},
{"MetroCrouchSwitch", true},
{"ProfilerEnableNpcGo", true},
{"DisableBartender", false},
{"DisableScientistProduceItem", false},
{"DisableScientistBreakdownItem", false},
{"DisableScientistTakeProducedItem", false},
{"DisableScientistFactorMachine", false},
{"DisableScientistEnchant", false},
{"DisableItemExchange", false},
{"EnableFallingDownOnDeath2", true},
{"EnableBehaveLegacy", true},
{"EnableBeggerStory", true},
{"EnableGPUScene", true},
{"EnableFavorNpcBTree", true},
{"EnableFavorNpcCommute", true},
{"EnableSwitchSpiritToTempFavorNpc1", true},
{"SkipSwitchOutExitTransition", true},
{"EnableGenderTransform", true},
{"EnableIndoorAbilityLimit", true},
{"EnableRemoveAbilityLimitOldBuffHandlers", true},
{"EnableSpoonNpc", true},
{"EnableSearchRaidSpoonNpc", true},
{"EnableParty", true},
{"EnableRanking", true},
{"EnableJanitor", true},
{"EnableBeggarAiPainting", true},
{"EnableNewBag", true},
{"EnableNotices", true},
{"EnableAkashicSystem", true},
{"EnableProfile", true},
{"EnableRadioStation", true},
{"EnableCityPedia", true},
{"EnableEonBug", true},
{"EnableTutorial", true},
{"EnableVehicleRequisitionSystemUnlock1", true},
{"EnableTestProcess", true},
{"EnableForceStopNaN", true},
{"EnableOtlpLogger", true},
{"EnableResourceLoadCollect", true},
{"EnableHUDRecommend", true},
{"EnableAttachmentSystem", true},
{"EnableFishing", true},
{"EnablePSOCollect", true},
{"KeepBehaviorSchedulerWhenSwitch", true},
{"KeepBehaviorSchedulerWhenSwitch2", true},
{"OpenNewSettleColliderClip2", true},
{"OpenNewSettleEffectClip2", true},
{"OpenNewSettleCreation2", true},
{"UseCoreLibRouteFinder", true},
{"EnableScratchServerReward", true},
{"DisableYanJieAIReply", false},
{"EnableCompetitionSeason", true},
{"EnableActivity", true},
{"EnableChangeNameByItem", true},
{"EnableThisYearChat", true},
{"EnableSpecialTimeUserChat", true},
{"EnableBuzzCenter", true},
{"EnableCheckIn", true},
{"EnableDossier", true},
{"EnableScope", true},
{"EnableFriends", true},
{"EnableClub", true},
{"EnableBBChat", true},
{"EnableMail", true},
{"EnableCustom", true},
{"EnableBubble", true},
{"EnablePhoto", true},
{"EnableCatExpress", true},
{"EnableDutyTerminal", true},
{"EnableMall", true},
{"EnableMallRecommend", true},
{"EnableMallDirectSale", true},
{"EnableMallBundle", true},
{"EnableCharge", true},
{"EnableSpiritTalent", true},
{"EnableJobTalent", true},
{"EnableGameplayTalent", true},
{"EnableAchievement", true},
{"EnablePhone", true},
{"EnableTrade", true},
{"EnableGift", true},
{"EnableGiftFriendTimeCheck", true},
{"EnableTime", true},
{"DisableOptGoMode", false},
{"DisableMonsterOptGoMode", false},
{"EnablePedConvertWorthyCheck", true},
{"EnableLifeScheduleNpcAlwaysGo", true},
{"EnableAnimatorLODOtherPlayer", true},
{"EnableSpecialAnimateCullingMode", true},
{"EnableAnimatorIncRebind", true},
{"SimplifyKinematicInVehicle", true},
{"EnableInteractionAction", true},
{"EnableCollectionBook", true},
{"EnableMonthlyPass", true},
}
// BuildGameSwitchPayload encodes the game switches as required by SyncGameSwitchToClient_3.
// Format:
// [Int7 count]
// for each: [UxString key] [TypeTagBool (0x02)] [Bool (1 byte)]
func BuildGameSwitchPayload() []byte {
w := NewUxWriter()
w.Int7(len(GameSwitchCatalog))
for _, entry := range GameSwitchCatalog {
w.UxString(entry.Name)
w.U8(2) // TypeTagBool
w.Bool(entry.Value)
}
return w.Bytes()
}
+85
View File
@@ -0,0 +1,85 @@
package protocol
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/binary"
"errors"
"io"
"net"
"ananta-ps/pkg/crypto/chacha8"
)
// HandleHandshake processes client C2S handshake and performs S2C key exchange.
// It returns read and write ciphers initialized with negotiated ChaCha8 keys.
func HandleHandshake(conn net.Conn) (*chacha8.UxChaCha8, *chacha8.UxChaCha8, error) {
// Read frame 0 (mode 0x01 plaintext)
frame, err := ReadFrame(conn, nil)
if err != nil {
return nil, nil, err
}
if len(frame.Payload) < 20 {
return nil, nil, errors.New("client handshake too short")
}
aesKey := frame.Payload[4:20] // 16-byte key
clientWriteKey := make([]byte, 32)
clientReadKey := make([]byte, 32)
streamNonce := make([]byte, 12)
aesNonce := make([]byte, 12)
if _, err := io.ReadFull(rand.Reader, clientWriteKey); err != nil {
return nil, nil, err
}
if _, err := io.ReadFull(rand.Reader, clientReadKey); err != nil {
return nil, nil, err
}
if _, err := io.ReadFull(rand.Reader, streamNonce); err != nil {
return nil, nil, err
}
if _, err := io.ReadFull(rand.Reader, aesNonce); err != nil {
return nil, nil, err
}
chaChaHead := make([]byte, 76)
copy(chaChaHead[0:32], clientWriteKey)
copy(chaChaHead[32:64], clientReadKey)
copy(chaChaHead[64:76], streamNonce)
block, err := aes.NewCipher(aesKey)
if err != nil {
return nil, nil, err
}
aesGcm, err := cipher.NewGCM(block)
if err != nil {
return nil, nil, err
}
encrypted := aesGcm.Seal(nil, aesNonce, chaChaHead, nil) // 76 + 16 = 92 bytes
payload := make([]byte, 368)
binary.LittleEndian.PutUint32(payload[0:4], 1)
binary.LittleEndian.PutUint32(payload[4:8], 10)
copy(payload[8:20], aesNonce)
copy(payload[20:96], encrypted[0:76])
copy(payload[96:112], encrypted[76:92])
// Send handshake reply in plaintext with mode 0x01
if err := WriteFrame(conn, 0x01, payload, nil); err != nil {
return nil, nil, err
}
readCryptor, err := chacha8.New(clientWriteKey, streamNonce)
if err != nil {
return nil, nil, err
}
writeCryptor, err := chacha8.New(clientReadKey, streamNonce)
if err != nil {
return nil, nil, err
}
return readCryptor, writeCryptor, nil
}
+82
View File
@@ -0,0 +1,82 @@
package protocol
// NetEase UX-RPC Method IDs for Ananta CBT (Build 4229938).
const (
// Login / Gate
MethodCheckVersion uint32 = 34700853
MethodHasOnlinePlayer uint32 = 34822236
MethodAskNewHotFixPatchLogin uint32 = 34692559
MethodRequestPatchesCheckDataFromLogin uint32 = 34601301
MethodRequestPatchesFromLogin uint32 = 34022227
MethodAskUniSdkShareToken_Login uint32 = 34110053
MethodAskUniSdkShareToken_Gate uint32 = 52152686
MethodRequestFpPassToken uint32 = 34828770
MethodCheckAccount uint32 = 34339919
MethodCheckAccountPassBy uint32 = 34163006
MethodCheckAccountOpenId uint32 = 34381554
MethodTryLogin uint32 = 34529582
MethodSyncRoleList uint32 = 35167428
MethodRequestCreateRoleEx uint32 = 34383517
MethodRequestEnterGame uint32 = 34566515
MethodDebugRequestEnterGame uint32 = 34478273
MethodAskCloseConnectionToGate uint32 = 52140059
MethodGetServerTime uint32 = 52951195
MethodSendServerTime uint32 = 52296181
MethodGateLogin uint32 = 52023760
MethodSyncPlayerGameServerInfo uint32 = 52737039
MethodSyncGameSwitchToClient_3 uint32 = 53940944
MethodQuerySkey uint32 = 52367757
MethodReportNetworkQuality uint32 = 52623348
MethodDavinciCode uint32 = 52458997
MethodGetFriendApplicationListToMe uint32 = 52123985
MethodGetAllChatGroupLatestMessage uint32 = 52345719
MethodGetSimplePlayerInfoByPidList uint32 = 52781934
// Game / World
MethodLoginGame uint32 = 63142082
MethodAskEnterWorld uint32 = 63142082
MethodSendServerTimeGame uint32 = 63853503
MethodSyncPlayerInfo uint32 = 63200762
MethodSyncEnterScene uint32 = 67711467
MethodAskLoadSceneCompleted uint32 = 66879899
MethodAskLoadingFinished uint32 = 67017252
MethodSyncLogicAgentEnter uint32 = 68057283
MethodSyncLogicAgentLeave uint32 = 68057315
MethodSyncManagedLogicAgent uint32 = 68611146
MethodSyncRaidBattleUnitSpirit uint32 = 68953151
MethodSyncPlayerCurrentSpirit uint32 = 68582738
MethodSyncSceneLoadCompleted uint32 = 67610360
MethodSyncGamePause uint32 = 67039327
MethodSyncPlayerLoadRate uint32 = 68383503
MethodAskLoadedInSameScene uint32 = 68838634
MethodSyncUnitPositionAndFacing uint32 = 68664188
MethodSyncPlayerPositionAndFacing uint32 = 68702330
MethodSyncSceneFogMapAllUnlock uint32 = 68677150
MethodSyncRunGmUtils uint32 = 66042045
MethodSyncAllSpiritCombatPower uint32 = 68798150
MethodSyncSwitchSpiritInSitu uint32 = 68582770
// Movement
MethodAskMoveStart uint32 = 68412852
MethodAskMoveStop uint32 = 68412853
MethodAskUpdatePlayerPosition uint32 = 68058291
// Combat & Skills
MethodAskPlayerAttack uint32 = 68412884
MethodAskPlayerCastSkill uint32 = 68412916
MethodAskUnlockFightStyle uint32 = 68412948
MethodSyncChangeSpirit uint32 = 68582802
MethodAskPreSwitchSpirit uint32 = 68415783
MethodAskSwitchSpirit uint32 = 68415784
// Vehicles
MethodAskSummonVehicle uint32 = 68221895
MethodAskDriveVehicle uint32 = 68221893
MethodAskExitVehicle uint32 = 68221894
MethodSyncPlayerVehicleInfo uint32 = 68221926
MethodSyncAetherVehicleInit uint32 = 68221958
// Teleportation & Scene
MethodAskTeleportToScene uint32 = 67891234
MethodSwitchSceneDirect uint32 = 67891235
)
+22
View File
@@ -0,0 +1,22 @@
package protocol
type RpcPacketKind byte
const (
PacketKindUnknown RpcPacketKind = 0x00
PacketKindInvoke RpcPacketKind = 0x01
PacketKindReturn RpcPacketKind = 0x02
PacketKindNotify RpcPacketKind = 0x03
)
type RpcPacket struct {
Kind RpcPacketKind
MethodID uint32
InvokeID int32
Error int32
Body []byte
}
func (p RpcPacket) IsInvoke() bool { return p.Kind == PacketKindInvoke }
func (p RpcPacket) IsNotify() bool { return p.Kind == PacketKindNotify }
func (p RpcPacket) IsReturn() bool { return p.Kind == PacketKindReturn }
+108
View File
@@ -0,0 +1,108 @@
package protocol
import (
"bytes"
"testing"
)
func TestUxSerializationRoundtrip(t *testing.T) {
w := NewUxWriter()
w.U8(42).
Bool(true).
I16(-1234).
U16(5678).
I32(-999999).
U32(12345678).
I64(-9876543210).
U64(18446744073709551615).
F32(3.14159).
F64(2.718281828459).
Int7(10).
Raw7(25).
UxString("Hello Ananta Go Protocol")
data := w.Bytes()
r := NewUxReader(data)
u8, _ := r.U8()
if u8 != 42 {
t.Errorf("U8 mismatch: %v", u8)
}
b, _ := r.Bool()
if !b {
t.Errorf("Bool mismatch: %v", b)
}
i16, _ := r.I16()
if i16 != -1234 {
t.Errorf("I16 mismatch: %v", i16)
}
u16, _ := r.U16()
if u16 != 5678 {
t.Errorf("U16 mismatch: %v", u16)
}
i32, _ := r.I32()
if i32 != -999999 {
t.Errorf("I32 mismatch: %v", i32)
}
u32, _ := r.U32()
if u32 != 12345678 {
t.Errorf("U32 mismatch: %v", u32)
}
i64, _ := r.I64()
if i64 != -9876543210 {
t.Errorf("I64 mismatch: %v", i64)
}
u64, _ := r.U64()
if u64 != 18446744073709551615 {
t.Errorf("U64 mismatch: %v", u64)
}
f32, _ := r.F32()
if f32 < 3.14 || f32 > 3.15 {
t.Errorf("F32 mismatch: %v", f32)
}
f64, _ := r.F64()
if f64 < 2.71 || f64 > 2.72 {
t.Errorf("F64 mismatch: %v", f64)
}
int7, _ := r.Int7()
if int7 != 10 {
t.Errorf("Int7 mismatch: got %v, want 10", int7)
}
raw7, _ := r.Raw7()
if raw7 != 25 {
t.Errorf("Raw7 mismatch: got %v, want 25", raw7)
}
str, _ := r.UxString()
if str != "Hello Ananta Go Protocol" {
t.Errorf("UxString mismatch: got %v", str)
}
}
func TestRpcPacketRoundtrip(t *testing.T) {
notify := BuildNotify(MethodCheckVersion, []byte{0x01, 0x02, 0x03})
p := ParseRpcPacket(notify)
if !p.IsNotify() || p.MethodID != MethodCheckVersion {
t.Errorf("Notify packet mismatch: %+v", p)
}
if !bytes.Equal(p.Body, []byte{0x01, 0x02, 0x03}) {
t.Errorf("Body mismatch: %v", p.Body)
}
ret := BuildReturn(MethodAskEnterWorld, 100, 0, []byte("OK"))
p2 := ParseRpcPacket(ret)
if !p2.IsReturn() || p2.MethodID != MethodAskEnterWorld || p2.InvokeID != 100 || p2.Error != 0 {
t.Errorf("Return packet mismatch: %+v", p2)
}
}
+160
View File
@@ -0,0 +1,160 @@
package protocol
import (
"encoding/binary"
"errors"
"math"
)
type UxReader struct {
data []byte
pos int
}
func NewUxReader(data []byte) *UxReader {
return &UxReader{data: data, pos: 0}
}
func (r *UxReader) Position() int { return r.pos }
func (r *UxReader) Remaining() int { return len(r.data) - r.pos }
func (r *UxReader) U8() (byte, error) {
if r.pos >= len(r.data) {
return 0, errors.New("EOF")
}
b := r.data[r.pos]
r.pos++
return b, nil
}
func (r *UxReader) Bool() (bool, error) {
b, err := r.U8()
return b != 0, err
}
func (r *UxReader) I16() (int16, error) {
if r.Remaining() < 2 {
return 0, errors.New("EOF")
}
v := int16(binary.LittleEndian.Uint16(r.data[r.pos : r.pos+2]))
r.pos += 2
return v, nil
}
func (r *UxReader) U16() (uint16, error) {
if r.Remaining() < 2 {
return 0, errors.New("EOF")
}
v := binary.LittleEndian.Uint16(r.data[r.pos : r.pos+2])
r.pos += 2
return v, nil
}
func (r *UxReader) I32() (int32, error) {
if r.Remaining() < 4 {
return 0, errors.New("EOF")
}
v := int32(binary.LittleEndian.Uint32(r.data[r.pos : r.pos+4]))
r.pos += 4
return v, nil
}
func (r *UxReader) U32() (uint32, error) {
if r.Remaining() < 4 {
return 0, errors.New("EOF")
}
v := binary.LittleEndian.Uint32(r.data[r.pos : r.pos+4])
r.pos += 4
return v, nil
}
func (r *UxReader) I64() (int64, error) {
if r.Remaining() < 8 {
return 0, errors.New("EOF")
}
v := int64(binary.LittleEndian.Uint64(r.data[r.pos : r.pos+8]))
r.pos += 8
return v, nil
}
func (r *UxReader) U64() (ulong uint64, err error) {
if r.Remaining() < 8 {
return 0, errors.New("EOF")
}
v := binary.LittleEndian.Uint64(r.data[r.pos : r.pos+8])
r.pos += 8
return v, nil
}
func (r *UxReader) F32() (float32, error) {
bits, err := r.U32()
if err != nil {
return 0, err
}
return math.Float32frombits(bits), nil
}
func (r *UxReader) F64() (float64, error) {
bits, err := r.U64()
if err != nil {
return 0, err
}
return math.Float64frombits(bits), nil
}
func (r *UxReader) ReadVarInt() (int, error) {
shift := 0
result := 0
for r.pos < len(r.data) {
b := r.data[r.pos]
r.pos++
result |= int(b&0x7F) << shift
if (b & 0x80) == 0 {
return result, nil
}
shift += 7
}
return result, errors.New("unterminated varint")
}
func (r *UxReader) Int7() (int, error) {
v, err := r.ReadVarInt()
if err != nil {
return 0, err
}
if v < 1 {
return 0, nil
}
return v - 1, nil
}
func (r *UxReader) Raw7() (int, error) {
return r.ReadVarInt()
}
func (r *UxReader) UxString() (string, error) {
lenPlus, err := r.ReadVarInt()
if err != nil {
return "", err
}
if lenPlus == 0 {
return "", nil
}
length := lenPlus - 1
if length > r.Remaining() {
return "", errors.New("string length exceeds buffer")
}
str := string(r.data[r.pos : r.pos+length])
r.pos += length
return str, nil
}
func (r *UxReader) Bytes(count int) ([]byte, error) {
if count < 0 || count > r.Remaining() {
return nil, errors.New("requested count exceeds buffer")
}
res := make([]byte, count)
copy(res, r.data[r.pos:r.pos+count])
r.pos += count
return res, nil
}
+127
View File
@@ -0,0 +1,127 @@
package protocol
import (
"bytes"
"encoding/binary"
"math"
)
type UxWriter struct {
buf bytes.Buffer
}
func NewUxWriter() *UxWriter {
return &UxWriter{}
}
func (w *UxWriter) U8(val byte) *UxWriter {
w.buf.WriteByte(val)
return w
}
func (w *UxWriter) Bool(val bool) *UxWriter {
if val {
w.buf.WriteByte(1)
} else {
w.buf.WriteByte(0)
}
return w
}
func (w *UxWriter) I16(val int16) *UxWriter {
var b [2]byte
binary.LittleEndian.PutUint16(b[:], uint16(val))
w.buf.Write(b[:])
return w
}
func (w *UxWriter) U16(val uint16) *UxWriter {
var b [2]byte
binary.LittleEndian.PutUint16(b[:], val)
w.buf.Write(b[:])
return w
}
func (w *UxWriter) I32(val int32) *UxWriter {
var b [4]byte
binary.LittleEndian.PutUint32(b[:], uint32(val))
w.buf.Write(b[:])
return w
}
func (w *UxWriter) U32(val uint32) *UxWriter {
var b [4]byte
binary.LittleEndian.PutUint32(b[:], val)
w.buf.Write(b[:])
return w
}
func (w *UxWriter) I64(val int64) *UxWriter {
var b [8]byte
binary.LittleEndian.PutUint64(b[:], uint64(val))
w.buf.Write(b[:])
return w
}
func (w *UxWriter) U64(val uint64) *UxWriter {
var b [8]byte
binary.LittleEndian.PutUint64(b[:], val)
w.buf.Write(b[:])
return w
}
func (w *UxWriter) F32(val float32) *UxWriter {
return w.U32(math.Float32bits(val))
}
func (w *UxWriter) F64(val float64) *UxWriter {
return w.U64(math.Float64bits(val))
}
func (w *UxWriter) WriteVarInt(val int) *UxWriter {
v := uint32(val)
for v >= 0x80 {
w.buf.WriteByte(byte(v | 0x80))
v >>= 7
}
w.buf.WriteByte(byte(v))
return w
}
func (w *UxWriter) Int7(val int) *UxWriter {
if val < 0 {
val = 0
}
return w.WriteVarInt(val + 1)
}
func (w *UxWriter) Raw7(val int) *UxWriter {
if val < 0 {
val = 0
}
return w.WriteVarInt(val)
}
func (w *UxWriter) UxString(str string) *UxWriter {
utf8 := []byte(str)
w.WriteVarInt(len(utf8) + 1)
w.buf.Write(utf8)
return w
}
func (w *UxWriter) UxStringNullable(str *string) *UxWriter {
if str == nil {
w.buf.WriteByte(0x00)
return w
}
return w.UxString(*str)
}
func (w *UxWriter) Raw(b []byte) *UxWriter {
w.buf.Write(b)
return w
}
func (w *UxWriter) Bytes() []byte {
return w.buf.Bytes()
}