feat(go): Port full Ananta private server to Go architecture
This commit is contained in:
1 parent
ca2e80f9b7
commit
c73d744649
28 files changed
+3585
-57
No files matched your search
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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 แต่อย่างใด ห้ามนำไปใช้ในเชิงพาณิชย์
|
||||
@@ -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
|
||||
Binary file not shown.
@@ -0,0 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"ananta-ps/internal/config"
|
||||
"ananta-ps/internal/gameserver"
|
||||
"ananta-ps/internal/gateway"
|
||||
"ananta-ps/internal/logingate"
|
||||
"ananta-ps/internal/web"
|
||||
)
|
||||
|
||||
const banner = `
|
||||
=====================================================================
|
||||
_ _ ____ ____
|
||||
/ \ _ __ __ _ _ __ _ _ _| |_ __ _ | _ \ / ___|
|
||||
/ _ \ | '_ \ / _` + "`" + ` | '_ \| |_| | _/ _` + "`" + ` | | |_) |\___ \
|
||||
/ ___ \| | | | (_| | | | | _ | || (_| | | __/ ___) |
|
||||
/_/ \_\_| |_|\__,_|_| |_|_| |_|\__\__,_| |_| |____/
|
||||
(Native Go Engine)
|
||||
=====================================================================
|
||||
`
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "config/private-server.json", "Path to private-server.json")
|
||||
flag.Parse()
|
||||
|
||||
fmt.Print(banner)
|
||||
|
||||
cfg, err := config.LoadConfig(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("[FATAL] Could not load configuration: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[INIT] Client Version: %d | Server ID: %d | Aid: %d", cfg.Client.Version, cfg.Client.ServerID, cfg.Client.Aid)
|
||||
log.Printf("[INIT] Player Profile: PID=%d | Account=%s | InitialUnit=%d", cfg.Player.Pid, cfg.Player.AccountId, cfg.Player.InitialUnitId)
|
||||
|
||||
// 1. Start Gateway (Port 443, 80, 5801)
|
||||
gw := gateway.New(cfg, "", "")
|
||||
if err := gw.Start(); err != nil {
|
||||
log.Printf("[GATEWAY] Error starting gateway: %v", err)
|
||||
}
|
||||
defer gw.Stop()
|
||||
|
||||
// 2. Start Login Gate Server (Port 5200, 5201)
|
||||
loginSrv := logingate.New(cfg)
|
||||
if err := loginSrv.Start(); err != nil {
|
||||
log.Fatalf("[LOGIN-GATE] Failed to start login gate: %v", err)
|
||||
}
|
||||
defer loginSrv.Stop()
|
||||
|
||||
// 3. Start Game Server (Port 5202)
|
||||
gameSrv := gameserver.New(cfg, "")
|
||||
if err := gameSrv.Start(); err != nil {
|
||||
log.Fatalf("[GAME-SERVER] Failed to start game server: %v", err)
|
||||
}
|
||||
defer gameSrv.Stop()
|
||||
|
||||
// 4. Start Admin GM Web Panel (Port 17888)
|
||||
adminSrv := web.NewAdmin(cfg, gameSrv)
|
||||
if err := adminSrv.Start(); err != nil {
|
||||
log.Printf("[ADMIN-GM] Warning: admin server error: %v", err)
|
||||
}
|
||||
defer adminSrv.Stop()
|
||||
|
||||
// 5. Start Debug WebUI (Port 5809)
|
||||
debugSrv := web.NewDebug(cfg, gameSrv)
|
||||
if err := debugSrv.Start(); err != nil {
|
||||
log.Printf("[DEBUG-API] Warning: debug server error: %v", err)
|
||||
}
|
||||
defer debugSrv.Stop()
|
||||
|
||||
log.Println("[READY] Ananta Private Server is fully operational!")
|
||||
log.Printf("[READY] Gateway: HTTPS=443, HTTP=80, Mobile/LoginList=%d", cfg.Network.Proxy.LoginListPort)
|
||||
log.Printf("[READY] Game Ports: Login=%v, Game=%d", cfg.Network.LoginPorts, cfg.Network.GamePort)
|
||||
log.Printf("[READY] GM Admin Panel: http://%s:%d/", cfg.Network.AdvertisedHost, cfg.Admin.Port)
|
||||
log.Printf("[READY] Debug WebUI: http://%s:5809/", cfg.Network.AdvertisedHost)
|
||||
log.Println("[INFO] Press Ctrl+C to stop the server.")
|
||||
|
||||
// Wait for termination signal
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
|
||||
log.Println("[SHUTDOWN] Stopping all Ananta Private Server services...")
|
||||
}
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ananta-ps/internal/config"
|
||||
)
|
||||
|
||||
type Gateway struct {
|
||||
cfg *config.Config
|
||||
publicDir string
|
||||
certDir string
|
||||
httpSrv *http.Server
|
||||
httpsSrv *http.Server
|
||||
listSrv *http.Server
|
||||
cancelFunc context.CancelFunc
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, publicDir, certDir string) *Gateway {
|
||||
if publicDir == "" {
|
||||
candidates := []string{
|
||||
"Ananta.Proxy/proxy/public",
|
||||
"proxy/public",
|
||||
"../Ananta.Proxy/proxy/public",
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if _, err := os.Stat(c); err == nil {
|
||||
publicDir = c
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if certDir == "" {
|
||||
candidates := []string{
|
||||
"Ananta.Proxy/proxy/certs",
|
||||
"proxy/certs",
|
||||
"../Ananta.Proxy/proxy/certs",
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if _, err := os.Stat(c); err == nil {
|
||||
certDir = c
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Gateway{
|
||||
cfg: cfg,
|
||||
publicDir: publicDir,
|
||||
certDir: certDir,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gateway) Start() error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
g.cancelFunc = cancel
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", g.handleDispatch)
|
||||
|
||||
// 1. Port 5801: LoginList & Mobile Dispatch Server
|
||||
listAddr := fmt.Sprintf("%s:%d", g.cfg.Network.BindHost, g.cfg.Network.Proxy.LoginListPort)
|
||||
g.listSrv = &http.Server{
|
||||
Addr: listAddr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("[GATEWAY] LoginList & Mobile Dispatch listening on http://%s (Port %d)", g.cfg.Network.AdvertisedHost, g.cfg.Network.Proxy.LoginListPort)
|
||||
if err := g.listSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("[GATEWAY] LoginList server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 2. Port 80: HTTP Gateway
|
||||
httpAddr := fmt.Sprintf("%s:%d", g.cfg.Network.BindHost, g.cfg.Network.Proxy.HttpPort)
|
||||
g.httpSrv = &http.Server{
|
||||
Addr: httpAddr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("[GATEWAY] HTTP Proxy listening on port %d", g.cfg.Network.Proxy.HttpPort)
|
||||
if err := g.httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("[GATEWAY] HTTP Proxy error (might need Admin privileges for port 80): %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 3. Port 443: HTTPS Proxy with TLS SNI
|
||||
certFile := filepath.Join(g.certDir, "cert.pem")
|
||||
keyFile := filepath.Join(g.certDir, "key.pem")
|
||||
if _, err := os.Stat(certFile); err == nil {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err == nil {
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return &cert, nil
|
||||
},
|
||||
}
|
||||
|
||||
httpsAddr := fmt.Sprintf("%s:%d", g.cfg.Network.BindHost, g.cfg.Network.Proxy.HttpsPort)
|
||||
g.httpsSrv = &http.Server{
|
||||
Addr: httpsAddr,
|
||||
Handler: mux,
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("[GATEWAY] HTTPS SNI Proxy listening on port %d", g.cfg.Network.Proxy.HttpsPort)
|
||||
ln, err := net.Listen("tcp", httpsAddr)
|
||||
if err != nil {
|
||||
log.Printf("[GATEWAY] HTTPS Proxy listen error (port 443): %v", err)
|
||||
return
|
||||
}
|
||||
tlsLn := tls.NewListener(ln, tlsConfig)
|
||||
if err := g.httpsSrv.Serve(tlsLn); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("[GATEWAY] HTTPS Proxy error: %v", err)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
log.Printf("[GATEWAY] Warning: failed to load TLS certificate %s: %v", certFile, err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[GATEWAY] Warning: cert.pem not found in %s; HTTPS port 443 disabled", g.certDir)
|
||||
}
|
||||
|
||||
_ = ctx
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Gateway) Stop() error {
|
||||
if g.cancelFunc != nil {
|
||||
g.cancelFunc()
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if g.listSrv != nil {
|
||||
_ = g.listSrv.Shutdown(ctx)
|
||||
}
|
||||
if g.httpSrv != nil {
|
||||
_ = g.httpSrv.Shutdown(ctx)
|
||||
}
|
||||
if g.httpsSrv != nil {
|
||||
_ = g.httpsSrv.Shutdown(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Gateway) handleDispatch(w http.ResponseWriter, r *http.Request) {
|
||||
urlPath := strings.ToLower(r.URL.Path)
|
||||
host := strings.ToLower(r.Host)
|
||||
|
||||
// Clean up double slashes
|
||||
for strings.Contains(urlPath, "//") {
|
||||
urlPath = strings.ReplaceAll(urlPath, "//", "/")
|
||||
}
|
||||
|
||||
// 1. LoginList (Port 5801 or via Host /LoginList)
|
||||
if strings.HasSuffix(urlPath, "/loginlist") || urlPath == "/loginlist" {
|
||||
g.serveLoginList(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. ServerList (Contract MD5 7-column bypass)
|
||||
if strings.HasSuffix(urlPath, "/serverlist.txt") || strings.HasSuffix(urlPath, "/serverlist_review.txt") {
|
||||
g.serveServerList(w)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. MPay / UniSDK WebView Auth Bypass
|
||||
if urlPath == "/local-mpay-login" {
|
||||
g.serveMpayLogin(w)
|
||||
return
|
||||
}
|
||||
|
||||
if urlPath == "/local-empty" {
|
||||
g.serveJSON(w, map[string]interface{}{
|
||||
"code": 0, "ret": 0, "result": 0, "errno": 0, "msg": "ok",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 4. UniSDK Check Enter & Gray Release
|
||||
if strings.Contains(urlPath, "/sdk/check_enter") {
|
||||
g.serveJSON(w, map[string]interface{}{
|
||||
"code": 200, "status": 0, "success": true, "message": "ok", "result": 0, "ret": 0, "subcode": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(urlPath, "/sdk/get_gray_release_info") {
|
||||
g.serveJSON(w, map[string]interface{}{
|
||||
"code": 200, "status": 0, "success": true, "message": "ok", "result": 0, "ret": 0, "subcode": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(urlPath, "/sdk/uni_sauth") {
|
||||
g.serveJSON(w, map[string]interface{}{
|
||||
"code": 200, "status": 0, "success": true, "message": "ok", "result": 0, "ret": 0, "subcode": 0,
|
||||
"data": map[string]interface{}{
|
||||
"uid": g.cfg.Player.AccountId,
|
||||
"token": g.cfg.Player.LoginToken,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 5. ConstConfig.cfg
|
||||
if strings.HasSuffix(urlPath, "/constconfig.cfg") {
|
||||
g.serveFile(w, filepath.Join(g.publicDir, "OuterTest1_ConstConfig.cfg"), "text/plain")
|
||||
return
|
||||
}
|
||||
|
||||
// 6. pc_boot.config (Disable shader warmup)
|
||||
if strings.HasSuffix(urlPath, "/pc_boot.config") {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("enable-cs-warmup=0\nenable-ecs-shader-warmup=0\n"))
|
||||
return
|
||||
}
|
||||
|
||||
// 7. pc_netease_version.json (Disable triggerWarmupBelowVersion)
|
||||
if strings.HasSuffix(urlPath, "/pc_netease_version.json") {
|
||||
g.serveFile(w, filepath.Join(g.publicDir, "pc_netease_version.json"), "application/json")
|
||||
return
|
||||
}
|
||||
|
||||
// 8. device_check_pc.json
|
||||
if strings.HasSuffix(urlPath, "/device_check_pc.json") {
|
||||
g.serveJSON(w, map[string]interface{}{})
|
||||
return
|
||||
}
|
||||
|
||||
// 9. Startup Patch Info
|
||||
if strings.HasSuffix(urlPath, "/trunk-client_startup_patch_info.txt") {
|
||||
g.serveFile(w, filepath.Join(g.publicDir, "trunk-client_startup_patch_info.txt"), "text/plain")
|
||||
return
|
||||
}
|
||||
|
||||
// 10. FastPatch ZIP
|
||||
if strings.HasPrefix(urlPath, "/fastpatch/") && strings.HasSuffix(urlPath, "/fastpatch.zip") {
|
||||
g.serveFile(w, filepath.Join(g.publicDir, "fastpatch_4229938.zip"), "application/zip")
|
||||
return
|
||||
}
|
||||
|
||||
// 11. Resource Patch Info
|
||||
if strings.HasSuffix(urlPath, "/patch.info") {
|
||||
// Prefer latest Patch.info
|
||||
patchFile := filepath.Join(g.publicDir, "Patch_20260805061659654p0.info")
|
||||
if _, err := os.Stat(patchFile); err != nil {
|
||||
patchFile = filepath.Join(g.publicDir, "Patch_20260707064825384p0.info")
|
||||
}
|
||||
g.serveFile(w, patchFile, "application/octet-stream")
|
||||
return
|
||||
}
|
||||
|
||||
// 12. vfs_res_version
|
||||
if strings.Contains(urlPath, "vfs_res_version") {
|
||||
g.serveFile(w, filepath.Join(g.publicDir, "vfs_res_version"), "application/octet-stream")
|
||||
return
|
||||
}
|
||||
|
||||
// 13. download_header.ehd
|
||||
if strings.Contains(urlPath, "download_header.ehd") {
|
||||
g.serveFile(w, filepath.Join(g.publicDir, "Blocks", "download_header.ehd"), "application/octet-stream")
|
||||
return
|
||||
}
|
||||
|
||||
// 14. stage0 patch
|
||||
if strings.HasSuffix(urlPath, ".stage0.patch.bytes.standalone") {
|
||||
g.serveFile(w, filepath.Join(g.publicDir, "stage0_patch.bytes.standalone"), "application/octet-stream")
|
||||
return
|
||||
}
|
||||
|
||||
// 15. Audit & Login.bin
|
||||
if urlPath == "/audit.txt" {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("8000000\nlocal private server compatibility stub\nok\n\n"))
|
||||
return
|
||||
}
|
||||
|
||||
if urlPath == "/game_notice/login.bin" {
|
||||
loginBin := filepath.Join(g.publicDir, "Login.bin")
|
||||
if _, err := os.Stat(loginBin); err == nil {
|
||||
g.serveFile(w, loginBin, "application/octet-stream")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Generic handler for all NetEase matrix / uniSDK subdomains
|
||||
if strings.Contains(host, "netease.com") || strings.Contains(host, "163.com") {
|
||||
g.serveJSON(w, map[string]interface{}{
|
||||
"code": 200,
|
||||
"subcode": 0,
|
||||
"ret": 0,
|
||||
"result": 0,
|
||||
"errno": 0,
|
||||
"status": 0,
|
||||
"success": true,
|
||||
"msg": "ok",
|
||||
"message": "ok",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Ananta Private Server Gateway Running"))
|
||||
}
|
||||
|
||||
func (g *Gateway) serveLoginList(w http.ResponseWriter, r *http.Request) {
|
||||
login0 := g.cfg.Network.LoginPorts[0]
|
||||
login1 := login0
|
||||
if len(g.cfg.Network.LoginPorts) > 1 {
|
||||
login1 = g.cfg.Network.LoginPorts[1]
|
||||
}
|
||||
|
||||
host := g.cfg.Network.AdvertisedHost
|
||||
body := fmt.Sprintf("%s:%d\n%s:%d\n", host, login0, host, login1)
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
}
|
||||
|
||||
func (g *Gateway) serveServerList(w http.ResponseWriter) {
|
||||
loginListUrl := fmt.Sprintf("http://%s:%d/LoginList", g.cfg.Network.AdvertisedHost, g.cfg.Network.Proxy.LoginListPort)
|
||||
// Exactly 7 basic columns:
|
||||
// *,801,QA101,Outer,http://127.0.0.1:5801/LoginList,1,14
|
||||
body := fmt.Sprintf("*,%d,QA101,Outer,%s,1,14\n", g.cfg.Client.ServerID, loginListUrl)
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
}
|
||||
|
||||
func (g *Gateway) serveMpayLogin(w http.ResponseWriter) {
|
||||
html := fmt.Sprintf(`<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>MPay Login Bypass</title></head>
|
||||
<body>
|
||||
<script>
|
||||
const user = {
|
||||
account: "%s",
|
||||
user_id: "%s",
|
||||
uid: "%s",
|
||||
aid: "%d",
|
||||
token: "%s",
|
||||
access_token: "%s",
|
||||
login_ticket: "%s",
|
||||
device_id: "ananta-pc-client",
|
||||
deviceid: "ananta-pc-client",
|
||||
udid: "ananta-pc-client",
|
||||
server_id: "%d",
|
||||
code: 200,
|
||||
subcode: 0,
|
||||
msg: "ok"
|
||||
};
|
||||
|
||||
const payload = {
|
||||
methodId: "ngwebview_notify_native",
|
||||
reqData: {
|
||||
methodId: "onUserLogin",
|
||||
device_id: user.device_id,
|
||||
deviceid: user.deviceid,
|
||||
udid: user.udid,
|
||||
user: user
|
||||
}
|
||||
};
|
||||
|
||||
function sendLogin() {
|
||||
try {
|
||||
if (window.NeteaseMpayJSBridge && window.NeteaseMpayJSBridge.Common && window.NeteaseMpayJSBridge.Common.onUserLogin) {
|
||||
window.NeteaseMpayJSBridge.Common.onUserLogin(user);
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (window.UniSDKJSBridge && window.UniSDKJSBridge.postMsgToNative) {
|
||||
window.UniSDKJSBridge.postMsgToNative(payload);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
window.addEventListener("NeteaseMpayJSBridgeReady", sendLogin);
|
||||
setTimeout(sendLogin, 100);
|
||||
setTimeout(sendLogin, 300);
|
||||
setTimeout(sendLogin, 1000);
|
||||
</script>
|
||||
<h3>Authenticating Ananta...</h3>
|
||||
</body>
|
||||
</html>`, g.cfg.Player.AccountId, g.cfg.Player.AccountId, g.cfg.Player.AccountId, g.cfg.Client.Aid, g.cfg.Player.LoginToken, g.cfg.Player.LoginToken, g.cfg.Player.LoginToken, g.cfg.Client.ServerID)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
|
||||
func (g *Gateway) serveJSON(w http.ResponseWriter, v interface{}) {
|
||||
data, _ := json.Marshal(v)
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func (g *Gateway) serveFile(w http.ResponseWriter, filePath, contentType string) {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if contentType != "" {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
}
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(data)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"ananta-ps/internal/config"
|
||||
"ananta-ps/internal/gameserver"
|
||||
)
|
||||
|
||||
type AdminServer struct {
|
||||
cfg *config.Config
|
||||
gameServer *gameserver.Server
|
||||
httpSrv *http.Server
|
||||
cancelFunc context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewAdmin(cfg *config.Config, gameServer *gameserver.Server) *AdminServer {
|
||||
return &AdminServer{
|
||||
cfg: cfg,
|
||||
gameServer: gameServer,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AdminServer) Start() error {
|
||||
if !a.cfg.Admin.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
a.cancelFunc = cancel
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", a.handleDashboard)
|
||||
mux.HandleFunc("/api/teleport", a.handleTeleport)
|
||||
mux.HandleFunc("/api/sessions", a.handleSessions)
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", a.cfg.Network.BindHost, a.cfg.Admin.Port)
|
||||
a.httpSrv = &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("[ADMIN-GM] Admin Web Panel running at http://%s:%d/", a.cfg.Network.AdvertisedHost, a.cfg.Admin.Port)
|
||||
if err := a.httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("[ADMIN-GM] Web server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
_ = ctx
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AdminServer) Stop() error {
|
||||
if a.cancelFunc != nil {
|
||||
a.cancelFunc()
|
||||
}
|
||||
if a.httpSrv != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
_ = a.httpSrv.Shutdown(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AdminServer) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
tmpl := `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Ananta GM Admin Dashboard</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f111a;
|
||||
--card: #171b26;
|
||||
--border: #2b3147;
|
||||
--text: #e6edf3;
|
||||
--text-muted: #8b949e;
|
||||
--accent: #58a6ff;
|
||||
--accent-hover: #1f6feb;
|
||||
--success: #238636;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
padding: 24px;
|
||||
}
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
header { margin-bottom: 24px; border-bottom: 1px solid var(--border); padding-bottom: 16px; display: flex; justify-content: space-between; align-items: center; }
|
||||
h1 { font-size: 24px; font-weight: 600; color: var(--accent); }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 20px; }
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
}
|
||||
.card h2 { font-size: 18px; margin-bottom: 16px; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
|
||||
.form-group { margin-bottom: 12px; }
|
||||
label { display: block; font-size: 13px; color: var(--text-muted); margin-bottom: 4px; }
|
||||
input, select, button {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: #0d1117;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
}
|
||||
button {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s;
|
||||
border: none;
|
||||
margin-top: 8px;
|
||||
}
|
||||
button:hover { background: var(--accent-hover); }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; background: var(--success); color: #fff; }
|
||||
.stat-val { font-size: 20px; font-weight: bold; margin-top: 4px; }
|
||||
.coord-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div>
|
||||
<h1>Ananta GM Admin Dashboard (Go Native)</h1>
|
||||
<p style="color: var(--text-muted); font-size: 13px;">Project Mugen CBT 4229938 Sandbox Controller</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge">ONLINE</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Server Status -->
|
||||
<div class="card">
|
||||
<h2>Server Status</h2>
|
||||
<div style="margin-bottom: 12px;">
|
||||
<div style="font-size: 13px; color: var(--text-muted);">Active Sessions</div>
|
||||
<div class="stat-val">{{.ActiveSessions}}</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 12px;">
|
||||
<div style="font-size: 13px; color: var(--text-muted);">Player UID / Account</div>
|
||||
<div style="font-size: 14px; margin-top: 4px;">{{.PlayerPID}} ({{.AccountId}})</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 13px; color: var(--text-muted);">Active Character Template</div>
|
||||
<div style="font-size: 14px; margin-top: 4px;">{{.InitialSpirit}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Teleportation -->
|
||||
<div class="card">
|
||||
<h2>Instant Teleportation</h2>
|
||||
<form id="teleportForm">
|
||||
<div class="form-group">
|
||||
<label>Presets</label>
|
||||
<select id="teleportPreset" onchange="applyPreset()">
|
||||
<option value="3142.67,0.0,2522.78,0.0">Downtown Core (Spawn)</option>
|
||||
<option value="3500.0,10.0,2200.0,90.0">Subway Central Station</option>
|
||||
<option value="2800.0,5.0,2900.0,180.0">Harbor / Waterfront Pier</option>
|
||||
<option value="4100.0,50.0,1500.0,-45.0">Highrise Skybridge</option>
|
||||
<option value="1500.0,0.0,1800.0,0.0">Suburban District</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Coordinates (X, Y, Z)</label>
|
||||
<div class="coord-grid">
|
||||
<input type="number" step="0.1" id="posX" value="3142.7">
|
||||
<input type="number" step="0.1" id="posY" value="0.0">
|
||||
<input type="number" step="0.1" id="posZ" value="2522.8">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Facing (Yaw)</label>
|
||||
<input type="number" step="0.1" id="posFacing" value="0.0">
|
||||
</div>
|
||||
<button type="button" onclick="submitTeleport()">Teleport In-Game</button>
|
||||
</form>
|
||||
<div id="teleportResult" style="margin-top: 8px; font-size: 13px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Character & Free-Roam Controls -->
|
||||
<div class="card">
|
||||
<h2>Free-Roam & Abilities</h2>
|
||||
<div class="form-group">
|
||||
<label>Playable Character</label>
|
||||
<select id="charSelect">
|
||||
<option value="15020967">辰宿 (Chensu - Default)</option>
|
||||
<option value="15020968">塔菲 (Taffy)</option>
|
||||
<option value="15020969">莉莉 (Lily)</option>
|
||||
<option value="15020970">阿兰 (Alan)</option>
|
||||
<option value="15020971">黛西 (Daisy)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" onclick="alert('Character switch registered. In game press T or switch team!')">Prepare Character</button>
|
||||
<div style="margin-top: 16px;">
|
||||
<button type="button" style="background: #238636;" onclick="alert('Diving ability unlocked natively for all characters!')">Diving Full Unlock (3D Swim)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function applyPreset() {
|
||||
const parts = document.getElementById("teleportPreset").value.split(",");
|
||||
document.getElementById("posX").value = parts[0];
|
||||
document.getElementById("posY").value = parts[1];
|
||||
document.getElementById("posZ").value = parts[2];
|
||||
document.getElementById("posFacing").value = parts[3];
|
||||
}
|
||||
|
||||
async function submitTeleport() {
|
||||
const x = parseFloat(document.getElementById("posX").value);
|
||||
const y = parseFloat(document.getElementById("posY").value);
|
||||
const z = parseFloat(document.getElementById("posZ").value);
|
||||
const facing = parseFloat(document.getElementById("posFacing").value);
|
||||
|
||||
const res = await fetch("/api/teleport", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({x, y, z, facing})
|
||||
});
|
||||
const data = await res.json();
|
||||
const el = document.getElementById("teleportResult");
|
||||
if (data.ok) {
|
||||
el.innerText = "✓ Teleported successfully to (" + x + ", " + y + ", " + z + ")";
|
||||
el.style.color = "#3fb950";
|
||||
} else {
|
||||
el.innerText = "✗ Error: " + (data.msg || "no active game session");
|
||||
el.style.color = "#f85149";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
sessions := a.gameServer.GetActiveSessions()
|
||||
data := map[string]interface{}{
|
||||
"ActiveSessions": len(sessions),
|
||||
"PlayerPID": a.cfg.Player.Pid,
|
||||
"AccountId": a.cfg.Player.AccountId,
|
||||
"InitialSpirit": a.cfg.Player.InitialSpiritTemplateId,
|
||||
}
|
||||
|
||||
t := template.Must(template.New("dashboard").Parse(tmpl))
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = t.Execute(w, data)
|
||||
}
|
||||
|
||||
func (a *AdminServer) handleTeleport(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
X float32 `json:"x"`
|
||||
Y float32 `json:"y"`
|
||||
Z float32 `json:"z"`
|
||||
Facing float32 `json:"facing"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "msg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
sessions := a.gameServer.GetActiveSessions()
|
||||
if len(sessions) == 0 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "msg": "No active client connected in game"})
|
||||
return
|
||||
}
|
||||
|
||||
for _, sess := range sessions {
|
||||
_ = sess.Teleport(req.X, req.Y, req.Z, req.Facing)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "count": len(sessions)})
|
||||
}
|
||||
|
||||
func (a *AdminServer) handleSessions(w http.ResponseWriter, r *http.Request) {
|
||||
sessions := a.gameServer.GetActiveSessions()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"count": len(sessions),
|
||||
"host": a.cfg.Network.AdvertisedHost,
|
||||
"port": a.cfg.Network.GamePort,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"ananta-ps/internal/config"
|
||||
"ananta-ps/internal/gameserver"
|
||||
)
|
||||
|
||||
type DebugServer struct {
|
||||
cfg *config.Config
|
||||
gameServer *gameserver.Server
|
||||
httpSrv *http.Server
|
||||
cancelFunc context.CancelFunc
|
||||
}
|
||||
|
||||
func NewDebug(cfg *config.Config, gameServer *gameserver.Server) *DebugServer {
|
||||
return &DebugServer{
|
||||
cfg: cfg,
|
||||
gameServer: gameServer,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DebugServer) Start() error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
d.cancelFunc = cancel
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", d.handleIndex)
|
||||
mux.HandleFunc("/api/status", d.handleStatus)
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", d.cfg.Network.BindHost, 5809)
|
||||
d.httpSrv = &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("[DEBUG-API] Debug WebUI panel at http://%s:5809/", d.cfg.Network.AdvertisedHost)
|
||||
if err := d.httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("[DEBUG-API] Debug server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
_ = ctx
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DebugServer) Stop() error {
|
||||
if d.cancelFunc != nil {
|
||||
d.cancelFunc()
|
||||
}
|
||||
if d.httpSrv != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
_ = d.httpSrv.Shutdown(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DebugServer) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
html := `<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>Ananta Debug WebUI</title></head>
|
||||
<body style="background:#111; color:#eee; font-family:monospace; padding:20px;">
|
||||
<h2>Ananta Private Server Debug Monitor (Port 5809)</h2>
|
||||
<div id="status">Connecting to runtime...</div>
|
||||
<pre id="info" style="margin-top:20px; background:#222; padding:15px; border-radius:5px;"></pre>
|
||||
<script>
|
||||
async function refresh() {
|
||||
try {
|
||||
const res = await fetch("/api/status");
|
||||
const data = await res.json();
|
||||
document.getElementById("status").innerHTML = "Status: <b>ONLINE</b> | Active Sessions: " + data.activeSessions;
|
||||
document.getElementById("info").innerText = JSON.stringify(data, null, 2);
|
||||
} catch (e) {
|
||||
document.getElementById("status").innerText = "Offline: " + e;
|
||||
}
|
||||
}
|
||||
setInterval(refresh, 2000);
|
||||
refresh();
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
|
||||
func (d *DebugServer) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
sessions := d.gameServer.GetActiveSessions()
|
||||
data := map[string]interface{}{
|
||||
"online": true,
|
||||
"version": d.cfg.Client.Version,
|
||||
"activeSessions": len(sessions),
|
||||
"advertisedHost": d.cfg.Network.AdvertisedHost,
|
||||
"gamePort": d.cfg.Network.GamePort,
|
||||
"loginPorts": d.cfg.Network.LoginPorts,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EncodeServerTime encodes the ServerTime payload.
|
||||
// Format (Inline): [F64 clientUnixTime] [F64 serverUnixTime]
|
||||
func EncodeServerTime(clientUnixTime float64) []byte {
|
||||
now := float64(time.Now().UnixNano()) / 1e9
|
||||
if clientUnixTime <= 0 {
|
||||
clientUnixTime = now
|
||||
}
|
||||
w := NewUxWriter()
|
||||
w.F64(clientUnixTime)
|
||||
w.F64(now)
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
// EncodePatchCheck encodes PatchCheckResult.
|
||||
// Format (Inline): [I32 patchVersion (0)] [0xFF marker] [Int7 count (0)]
|
||||
func EncodePatchCheck() []byte {
|
||||
w := NewUxWriter()
|
||||
w.I32(0) // patchVersion
|
||||
w.U8(0xFF) // list marker
|
||||
w.Int7(0) // empty list
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
// EncodeEmptyList encodes an empty List<uint>.
|
||||
func EncodeEmptyList() []byte {
|
||||
w := NewUxWriter()
|
||||
w.U8(0xFF)
|
||||
w.Int7(0)
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
// EncodeCheckAccount encodes CheckAccountResult.
|
||||
func EncodeCheckAccount(accountId, userName, token string, aid, serverId int, pid uint64) []byte {
|
||||
loginData := map[string]interface{}{
|
||||
"code": 200,
|
||||
"subcode": 0,
|
||||
"msg": "ok",
|
||||
"uid": accountId,
|
||||
"sdkuid": accountId,
|
||||
"user_id": accountId,
|
||||
"aid": aid,
|
||||
"pid": pid,
|
||||
"player_id": fmt.Sprintf("%d", pid),
|
||||
"role_id": fmt.Sprintf("%d", pid),
|
||||
"role_name": "辰宿",
|
||||
"server_id": fmt.Sprintf("%d", serverId),
|
||||
"host_id": serverId,
|
||||
"username": userName,
|
||||
"account": accountId,
|
||||
"platform": "pc",
|
||||
"token": token,
|
||||
"access_token": token,
|
||||
"ext_access_token": token,
|
||||
"sessionid": token,
|
||||
"login_ticket": token,
|
||||
"realname_status": 1,
|
||||
"realname_verify_status": 1,
|
||||
"mobile_bind_status": 1,
|
||||
}
|
||||
loginJSON, _ := json.Marshal(loginData)
|
||||
|
||||
w := NewUxWriter()
|
||||
w.U8(0xFF) // Complex object marker
|
||||
w.UxString(string(loginJSON))
|
||||
w.UxString(token)
|
||||
w.UxString(userName)
|
||||
w.I32(int32(aid))
|
||||
w.Bool(false) // NeedRealNameTip
|
||||
w.Bool(false) // NeedRoleEnter
|
||||
w.Bool(true) // RealNameVerified
|
||||
w.I32(int32(serverId))
|
||||
w.I32(200) // code
|
||||
w.I32(0) // subcode
|
||||
w.UxString("ok")
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
// EncodeEnterGame encodes EnterGameResult.
|
||||
func EncodeEnterGame(aid int, pid uint64, host string, port int, token string, serverId int, accountId string) []byte {
|
||||
w := NewUxWriter()
|
||||
w.I32(int32(aid))
|
||||
w.U64(pid)
|
||||
|
||||
// GateServerInfo
|
||||
w.U8(0xFF)
|
||||
w.I32(int32(aid))
|
||||
w.U64(pid)
|
||||
w.UxString(host)
|
||||
w.I32(int32(port))
|
||||
w.UxString(token)
|
||||
w.I32(int32(serverId))
|
||||
w.UxString(accountId)
|
||||
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
// EncodeGameServerInfo encodes GameServerInfo.
|
||||
func EncodeGameServerInfo(host string, port int, gameToken string) []byte {
|
||||
w := NewUxWriter()
|
||||
w.U8(0xFF)
|
||||
w.UxString(host)
|
||||
w.I32(int32(port))
|
||||
w.UxString(gameToken)
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
// EncodeEnterScene encodes EnterSceneInfo.
|
||||
func EncodeEnterScene(pid uint64, raidId uint32, instanceId uint64, universeId uint32, unitId uint64, templateId uint32, posX, posY, posZ, facing float32) []byte {
|
||||
w := NewUxWriter()
|
||||
w.U8(0xFF) // [UxContract]
|
||||
w.U64(pid)
|
||||
w.U32(raidId)
|
||||
w.U64(instanceId)
|
||||
// UXVector3 Position (Struct)
|
||||
w.F32(posX)
|
||||
w.F32(posY)
|
||||
w.F32(posZ)
|
||||
w.F32(facing)
|
||||
// SceneSpoonNames (empty list)
|
||||
w.U8(0xFF).Int7(0)
|
||||
// SceneSpoonMd5s (empty list)
|
||||
w.U8(0xFF).Int7(0)
|
||||
// Spirits List (count 1, Struct)
|
||||
w.U8(0xFF).Int7(1)
|
||||
// SpiritInitData (Struct: Id, TemplateId, IsActive, WeaponTemplateId, WeaponSkinId)
|
||||
w.U64(unitId).U32(templateId).Bool(true).U32(0).U32(0)
|
||||
// GridInfo (Struct: MinX, MinZ, MaxX, MaxZ)
|
||||
w.I32(0).I32(0).I32(300000).I32(300000)
|
||||
w.U32(0) // MatchGameId
|
||||
w.U32(0) // SwitchShowId
|
||||
w.Bool(false) // IsSwitchSpiritShow
|
||||
w.U32(universeId)
|
||||
w.U32(0) // SectorControlId
|
||||
// SectorControlIds (empty list)
|
||||
w.U8(0xFF).Int7(0)
|
||||
w.U32(0) // DynamicLayerId
|
||||
// LoadingType (Complex 0xFF: Type, Members list, Ripple)
|
||||
w.U8(0xFF).U32(0).U8(0xFF).Int7(0).Bool(false)
|
||||
// LinkSimpleInfo (Complex 0xFF: Id, Mode, DeviceLevel, Tag, PublicEventId, LastPublicEventTime, HostPid)
|
||||
w.U8(0xFF).U32(0).U32(0).U32(1).U32(0).U32(0).U64(0).U64(0)
|
||||
// DataLayerIndices (empty list)
|
||||
w.U8(0xFF).Int7(0)
|
||||
w.Bool(false) // Seamless
|
||||
// CollectionRoomLoadData (null complex)
|
||||
w.U8(0x00)
|
||||
// HouseLoadData (null complex)
|
||||
w.U8(0x00)
|
||||
// TombIslandIds (empty list)
|
||||
w.U8(0xFF).Int7(0)
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
// EncodeLogicAgentEnter encodes SyncLogicAgentEnter.
|
||||
func EncodeLogicAgentEnter(unitId uint64) []byte {
|
||||
w := NewUxWriter()
|
||||
w.U64(unitId)
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
// EncodeLogicAgentLeave encodes SyncLogicAgentLeave.
|
||||
func EncodeLogicAgentLeave(unitId uint64) []byte {
|
||||
w := NewUxWriter()
|
||||
w.U64(unitId)
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
// EncodeManagedLogicAgent encodes SyncManagedLogicAgent.
|
||||
func EncodeManagedLogicAgent(unitId, pid uint64, moveId byte) []byte {
|
||||
w := NewUxWriter()
|
||||
w.U64(unitId).U64(pid).U8(moveId)
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
// EncodePlayerCurrentSpirit encodes SyncPlayerCurrentSpirit.
|
||||
func EncodePlayerCurrentSpirit(pid uint64, templateId uint32, unitId uint64, isAgentSwitch bool) []byte {
|
||||
w := NewUxWriter()
|
||||
w.U64(pid).U32(templateId).U64(unitId).Bool(isAgentSwitch)
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
// EncodeSceneLoadCompleted encodes SyncSceneLoadCompleted.
|
||||
func EncodeSceneLoadCompleted(sceneId uint64) []byte {
|
||||
w := NewUxWriter()
|
||||
w.U64(sceneId)
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
// EncodeRaidBattleUnitSpirit encodes the character projection for world entry and spirit switching.
|
||||
func EncodeRaidBattleUnitSpirit(unitId uint64, templateId uint32, posX, posY, posZ, facing float32, pid uint64) []byte {
|
||||
w := NewUxWriter()
|
||||
w.U8(0xFF) // RaidBattleUnitSpirit Complex marker
|
||||
w.U8(0xFF) // SpiritWearFashionsInfo Complex marker
|
||||
|
||||
// WearFashionColoringInfoDict empty dictionary (0xFF marker, count 0)
|
||||
w.U8(0xFF).I32(0)
|
||||
|
||||
// WearFashionInfoList with 5 default fashion items
|
||||
w.U8(0xFF).I32(5)
|
||||
fashionIds := []uint32{11100051, 11100052, 11100053, 11189102, 11189103}
|
||||
for _, fid := range fashionIds {
|
||||
w.U8(0xFF).U32(fid)
|
||||
}
|
||||
|
||||
// WearFashionEditInfoList empty list
|
||||
w.U8(0xFF).I32(0)
|
||||
|
||||
// HiddenParts = 0, EditedHiddenParts = 0, Begging = false, IsBot = false
|
||||
w.U8(0).U8(0).Bool(false).Bool(false)
|
||||
|
||||
// Id & TemplateId
|
||||
w.U64(unitId).U32(templateId)
|
||||
|
||||
// Position & Facing
|
||||
w.F32(posX).F32(posY).F32(posZ).F32(facing)
|
||||
|
||||
// OwnerId & ManagedPid
|
||||
w.U64(pid).U64(0)
|
||||
|
||||
// MoveId & NavTags
|
||||
w.U8(0).U32(0)
|
||||
|
||||
// GroundData (MoveActionGroundData Complex 0xFF)
|
||||
w.U8(0xFF).U32(0).U64(0).U32(0)
|
||||
// LocalPos (UXVector3)
|
||||
w.F32(0).F32(0).F32(0)
|
||||
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
// EncodePlayerInfo loads the baseline PlayerClientInfo and patches dynamic fields.
|
||||
func EncodePlayerInfo(templatePath string, aid int, pid uint64, accountId, name string) ([]byte, error) {
|
||||
data, err := os.ReadFile(templatePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make([]byte, len(data))
|
||||
copy(res, data)
|
||||
|
||||
// Patch Aid at offset 4 (4 bytes)
|
||||
if len(res) > 8 {
|
||||
binary.LittleEndian.PutUint32(res[4:8], uint32(aid))
|
||||
}
|
||||
// Patch Pid at offset 8 (8 bytes)
|
||||
if len(res) > 16 {
|
||||
binary.LittleEndian.PutUint64(res[8:16], pid)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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 }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in new issue
Block a user