diff --git a/Ananta.Proxy/proxy/certs/cert.pem b/Ananta.Proxy/proxy/certs/cert.pem
new file mode 100644
index 0000000..27cd7c8
--- /dev/null
+++ b/Ananta.Proxy/proxy/certs/cert.pem
@@ -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-----
\ No newline at end of file
diff --git a/Ananta.Proxy/proxy/certs/key.pem b/Ananta.Proxy/proxy/certs/key.pem
new file mode 100644
index 0000000..934042c
--- /dev/null
+++ b/Ananta.Proxy/proxy/certs/key.pem
@@ -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-----
\ No newline at end of file
diff --git a/README.md b/README.md
index 36d252e..217f685 100644
--- a/README.md
+++ b/README.md
@@ -1,60 +1,56 @@
# 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)
+### สำหรับ Go Native Engine (แนะนำ - รันได้ทันทีโดยไม่ต้องลงรันไทม์เพิ่ม):
1. **Windows 10 / 11 (64-bit)**
-2. **.NET 8.0 SDK**: [ดาวน์โหลดที่นี่](https://dotnet.microsoft.com/download/dotnet/8.0)
-3. **Node.js (LTS v18 หรือ v20 ขึ้นไป)**: [ดาวน์โหลดที่นี่](https://nodejs.org/)
-4. **Git & Git LFS**: [ดาวน์โหลดที่นี่](https://git-lfs.com/)
-5. ตัวเกม **Ananta / Project Mugen (CBT Client build 4229938 หรือ Mobile APK 4063976)**
+2. ตัวเกม **Ananta / Project Mugen (CBT Client build 4229938 หรือ Mobile APK 4063976)**
+3. (ตัวเลือกเสริมสำหรับการพัฒนา) **Go 1.22+**: [ดาวน์โหลดที่นี่](https://go.dev/dl/)
+
+### สำหรับ C# / Node.js Engine ดั้งเดิม:
+1. **.NET 8.0 SDK** & **Node.js (v18+)**
---
## 🚀 วิธีติดตั้งและเปิดใช้งานเซิร์ฟเวอร์
-### 1. Clone Repository (จำเป็นต้องใช้ Git LFS)
-```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
+### 1. ติดตั้ง Certificate & Hosts
รันคำสั่งด้วย PowerShell (Administrator) เพื่อชี้โดเมน NetEase เข้าหาเซิร์ฟเวอร์เครื่องคุณ (`127.0.0.1`):
```powershell
powershell -ExecutionPolicy Bypass -File .\Ananta.Proxy\SETUP_PROXY_AS_ADMIN.ps1
```
-*(หากต้องการยกเลิกการตั้งค่า Hosts ในภายหลัง สามารถรัน `REMOVE_PROXY_HOSTS_AS_ADMIN.ps1`)*
-### 3. เปิดเซิร์ฟเวอร์
-ดับเบิลคลิกเปิดใช้งานผ่านไฟล์:
+### 2. เปิดเซิร์ฟเวอร์ด้วย Go Engine (รวดเร็ว, น้ำหนักเบา, รวมทุกระบบในไบนารีเดียว)
+ดับเบิลคลิกเปิดใช้งานผ่าน:
```cmd
-START.cmd
+START-GO.cmd
```
-หรือรันผ่าน PowerShell:
-```powershell
-powershell -ExecutionPolicy Bypass -File .\Run-All.ps1
+หรือรันผ่านคอมมานด์ไลน์:
+```bash
+.\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 ทันที
* **Mobile (APK):** ชี้เกตเวย์หรือรันผ่าน Wi-Fi Proxy มายัง IP เครื่องเซิร์ฟเวอร์ (พอร์ต 5801/443)
-### 5. แผงควบคุมผ่านเว็บเบราว์เซอร์
-* **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) — วาร์ปพิกัด, ปรับสภาพอากาศ/เวลา, เสกเอนทิตี, และควบคุมทราฟฟิก
+### 4. แผงควบคุมผ่านเว็บเบราว์เซอร์
+* **GM Admin Panel**: [http://127.0.0.1:17888/](http://127.0.0.1:17888/) — จัดการไอเทม, วาร์ปพิกัด, สลับตัวละคร, และระบบโลก
+* **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
Ananta-ps/
-├── Ananta.Proxy/ # Node.js HTTPS Multi-Domain Proxy & Fastpatch Generator
-│ ├── proxy/
-│ │ ├── certs/ # ใบรับรอง SSL สำหรับดักโดเมน NetEase
-│ │ ├── public/ # Static config, version JSON, serverlist, fastpatch.zip
-│ │ └── server.js # Proxy Server หลัก
-├── Ananta.SDK/ # Core Network Frame, Crypto (ChaCha8), และ UxRpc Serialization
-├── Ananta.Server/ # C# .NET 8 UX-RPC Game Server
-│ ├── Ananta.App/ # Console Entrypoint, Admin Web Server (17888) & Debug Panel (5809)
-│ ├── Ananta.Core/ # Client Data Repositories, Wire Codecs & World State
-│ ├── Ananta.Gameplay/ # ระบบ Traversal, City Traffic, Living Crowd & World Engine
-│ ├── Ananta.Handlers/ # RPC Handlers (World, Switching, Gm, Vehicles, Minigames, Police)
-│ ├── Ananta.Network/ # TCP Listener & Rpc Frame Dispatcher
-│ ├── Ananta.RpcTypes/ # DTOs และ Method Contracts ของไคลเอนต์ 4229938
-│ └── ClientData/4229938/ # ไฟล์ Database Configs ของเกม (Json, LFS)
+├── cmd/
+│ └── ananta-server/ # Go Server Entrypoint (รวม Gateway + Login + Game + Web ใน binary เดียว)
+├── internal/
+│ ├── config/ # ตัวโหลดคอนฟิก private-server.json
+│ ├── gateway/ # HTTPS/SNI Proxy (443), HTTP (80) และ Mobile Dispatch (5801)
+│ ├── logingate/ # Login & Gate TCP Server (พอร์ต 5200, 5201)
+│ ├── gameserver/ # World Entry (V7 handoff), Combat, Movement, Switching (5202)
+│ └── web/ # GM Admin Panel (17888) และ Debug WebUI (5809)
+├── pkg/
+│ ├── crypto/chacha8/ # NetEase UXChaCha8 Custom Sigma Stream Cipher
+│ └── protocol/ # Binary Serializer/Deserializer, Frame Codec, Key Exchange
+├── data/
+│ └── player_info_template.bin # เทมเพลตข้อมูลผู้เล่นและไอเทมเริ่มต้น
├── config/ # private-server.json และตารางพิกัดมินิเกม/การจราจร
-├── docs/ # เอกสารเสริม (FEATURES.md, devlog)
-├── tools/ # เครื่องมือเสริม (Car Radio, Injector, Launcher, Python)
-├── Run-All.ps1 # สคริปต์อัตโนมัติ (Fastpatch -> Build -> Launch)
-└── START.cmd # ตัวเปิดเซิร์ฟเวอร์แบบ All-in-One
+├── docs/ # FEATURES.md (ฟีเจอร์แมทริกซ์ 36 ระบบ)
+├── START-GO.cmd # รัน Go Engine ทันทีในคลิกเดียว
+└── ananta-server.exe # Single Native Binary Executable
```
---
-## ⚠️ ข้อควรระวังและการแก้ปัญหา (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)
โปรเจกต์นี้จัดทำขึ้นเพื่อการศึกษาด้าน Reverse Engineering, Network Protocol Emulation และการอนุรักษ์ซอฟต์แวร์ (Software Preservation) เท่านั้น ไม่มีส่วนเกี่ยวข้องกับ NetEase Games หรือ Naked Rain แต่อย่างใด ห้ามนำไปใช้ในเชิงพาณิชย์
diff --git a/START-GO.cmd b/START-GO.cmd
new file mode 100644
index 0000000..70dc4be
--- /dev/null
+++ b/START-GO.cmd
@@ -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
diff --git a/ananta-server.exe b/ananta-server.exe
new file mode 100644
index 0000000..94d83eb
Binary files /dev/null and b/ananta-server.exe differ
diff --git a/cmd/ananta-server/main.go b/cmd/ananta-server/main.go
new file mode 100644
index 0000000..a0bd9a3
--- /dev/null
+++ b/cmd/ananta-server/main.go
@@ -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...")
+}
diff --git a/data/player_info_template.bin b/data/player_info_template.bin
new file mode 100644
index 0000000..f689ea5
Binary files /dev/null and b/data/player_info_template.bin differ
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..993822d
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module ananta-ps
+
+go 1.27.0
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..10a13bd
--- /dev/null
+++ b/internal/config/config.go
@@ -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
+}
diff --git a/internal/gameserver/server.go b/internal/gameserver/server.go
new file mode 100644
index 0000000..85fe2d6
--- /dev/null
+++ b/internal/gameserver/server.go
@@ -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())
+}
diff --git a/internal/gameserver/server_test.go b/internal/gameserver/server_test.go
new file mode 100644
index 0000000..673d915
--- /dev/null
+++ b/internal/gameserver/server_test.go
@@ -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))
+ }
+}
diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go
new file mode 100644
index 0000000..e485875
--- /dev/null
+++ b/internal/gateway/gateway.go
@@ -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(`
+
+
MPay Login Bypass
+
+
+Authenticating Ananta...
+
+`, 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)
+}
diff --git a/internal/gateway/gateway_test.go b/internal/gateway/gateway_test.go
new file mode 100644
index 0000000..d856f88
--- /dev/null
+++ b/internal/gateway/gateway_test.go
@@ -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())
+ }
+}
diff --git a/internal/logingate/server.go b/internal/logingate/server.go
new file mode 100644
index 0000000..b634158
--- /dev/null
+++ b/internal/logingate/server.go
@@ -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
+}
diff --git a/internal/logingate/server_test.go b/internal/logingate/server_test.go
new file mode 100644
index 0000000..4a1b258
--- /dev/null
+++ b/internal/logingate/server_test.go
@@ -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)
+ }
+}
diff --git a/internal/web/admin.go b/internal/web/admin.go
new file mode 100644
index 0000000..d2a80eb
--- /dev/null
+++ b/internal/web/admin.go
@@ -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 := `
+
+
+
+
+Ananta GM Admin Dashboard
+
+
+
+
+
+
+
+
+
+
Server Status
+
+
Active Sessions
+
{{.ActiveSessions}}
+
+
+
Player UID / Account
+
{{.PlayerPID}} ({{.AccountId}})
+
+
+
Active Character Template
+
{{.InitialSpirit}}
+
+
+
+
+
+
Instant Teleportation
+
+
+
+
+
+
+
Free-Roam & Abilities
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+ 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,
+ })
+}
diff --git a/internal/web/debug.go b/internal/web/debug.go
new file mode 100644
index 0000000..1d9cb4a
--- /dev/null
+++ b/internal/web/debug.go
@@ -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 := `
+
+Ananta Debug WebUI
+
+ Ananta Private Server Debug Monitor (Port 5809)
+ Connecting to runtime...
+
+
+
+`
+ 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)
+}
diff --git a/pkg/crypto/chacha8/chacha8.go b/pkg/crypto/chacha8/chacha8.go
new file mode 100644
index 0000000..07b578b
--- /dev/null
+++ b/pkg/crypto/chacha8/chacha8.go
@@ -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)
+}
diff --git a/pkg/crypto/chacha8/chacha8_test.go b/pkg/crypto/chacha8/chacha8_test.go
new file mode 100644
index 0000000..70d9232
--- /dev/null
+++ b/pkg/crypto/chacha8/chacha8_test.go
@@ -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)
+ }
+}
diff --git a/pkg/protocol/codecs.go b/pkg/protocol/codecs.go
new file mode 100644
index 0000000..b5ab9c6
--- /dev/null
+++ b/pkg/protocol/codecs.go
@@ -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.
+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
+}
diff --git a/pkg/protocol/frame.go b/pkg/protocol/frame.go
new file mode 100644
index 0000000..bef0c56
--- /dev/null
+++ b/pkg/protocol/frame.go
@@ -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
+}
diff --git a/pkg/protocol/gameswitch.go b/pkg/protocol/gameswitch.go
new file mode 100644
index 0000000..c9ea3bf
--- /dev/null
+++ b/pkg/protocol/gameswitch.go
@@ -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()
+}
diff --git a/pkg/protocol/handshake.go b/pkg/protocol/handshake.go
new file mode 100644
index 0000000..ca1fd25
--- /dev/null
+++ b/pkg/protocol/handshake.go
@@ -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
+}
diff --git a/pkg/protocol/method_id.go b/pkg/protocol/method_id.go
new file mode 100644
index 0000000..30522ba
--- /dev/null
+++ b/pkg/protocol/method_id.go
@@ -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
+)
diff --git a/pkg/protocol/packet.go b/pkg/protocol/packet.go
new file mode 100644
index 0000000..b46b545
--- /dev/null
+++ b/pkg/protocol/packet.go
@@ -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 }
diff --git a/pkg/protocol/protocol_test.go b/pkg/protocol/protocol_test.go
new file mode 100644
index 0000000..dde5f0e
--- /dev/null
+++ b/pkg/protocol/protocol_test.go
@@ -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)
+ }
+}
diff --git a/pkg/protocol/uxreader.go b/pkg/protocol/uxreader.go
new file mode 100644
index 0000000..a7efdb4
--- /dev/null
+++ b/pkg/protocol/uxreader.go
@@ -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
+}
diff --git a/pkg/protocol/uxwriter.go b/pkg/protocol/uxwriter.go
new file mode 100644
index 0000000..4b89fea
--- /dev/null
+++ b/pkg/protocol/uxwriter.go
@@ -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()
+}