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

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

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
package chacha8
import (
"encoding/binary"
"errors"
"math/bits"
)
// NetEase 4229938 UXChaCha8 stream cipher.
// Uses custom Sigma constants differing from standard ChaCha.
const (
Sigma0 uint32 = 0x60707865
Sigma1 uint32 = 0x3321646e
Sigma2 uint32 = 0x79622d32
Sigma3 uint32 = 0x6b206e74
)
type UxChaCha8 struct {
state [16]uint32
keystream [64]byte
keystreamPos int
}
func New(key []byte, nonce []byte) (*UxChaCha8, error) {
if len(key) != 32 {
return nil, errors.New("UXChaCha8 key must be 32 bytes")
}
if len(nonce) != 12 {
return nil, errors.New("UXChaCha8 nonce must be 12 bytes")
}
c := &UxChaCha8{
keystreamPos: 64,
}
c.state[0] = Sigma0
c.state[1] = Sigma1
c.state[2] = Sigma2
c.state[3] = Sigma3
for i := 0; i < 8; i++ {
c.state[4+i] = binary.LittleEndian.Uint32(key[i*4 : (i+1)*4])
}
c.state[12] = 0
c.state[13] = binary.LittleEndian.Uint32(nonce[0:4])
c.state[14] = binary.LittleEndian.Uint32(nonce[4:8])
c.state[15] = binary.LittleEndian.Uint32(nonce[8:12])
return c, nil
}
func (c *UxChaCha8) XorInPlace(data []byte) {
offset := 0
for offset < len(data) {
if c.keystreamPos == 64 {
c.generateBlock()
c.keystreamPos = 0
}
take := 64 - c.keystreamPos
if len(data)-offset < take {
take = len(data) - offset
}
for i := 0; i < take; i++ {
data[offset+i] ^= c.keystream[c.keystreamPos+i]
}
offset += take
c.keystreamPos += take
}
}
func (c *UxChaCha8) generateBlock() {
var x [16]uint32
copy(x[:], c.state[:])
for i := 0; i < 4; i++ {
quarterRound(&x[0], &x[4], &x[8], &x[12])
quarterRound(&x[1], &x[5], &x[9], &x[13])
quarterRound(&x[2], &x[6], &x[10], &x[14])
quarterRound(&x[3], &x[7], &x[11], &x[15])
quarterRound(&x[0], &x[5], &x[10], &x[15])
quarterRound(&x[1], &x[6], &x[11], &x[12])
quarterRound(&x[2], &x[7], &x[8], &x[13])
quarterRound(&x[3], &x[4], &x[9], &x[14])
}
for i := 0; i < 16; i++ {
binary.LittleEndian.PutUint32(c.keystream[i*4:(i+1)*4], x[i]+c.state[i])
}
c.state[12]++
}
func quarterRound(a, b, c, d *uint32) {
*a += *b
*d ^= *a
*d = bits.RotateLeft32(*d, 16)
*c += *d
*b ^= *c
*b = bits.RotateLeft32(*b, 12)
*a += *b
*d ^= *a
*d = bits.RotateLeft32(*d, 8)
*c += *d
*b ^= *c
*b = bits.RotateLeft32(*b, 7)
}
+41
View File
@@ -0,0 +1,41 @@
package chacha8
import (
"bytes"
"testing"
)
func TestChaCha8Roundtrip(t *testing.T) {
key := make([]byte, 32)
for i := range key {
key[i] = byte(i + 1)
}
nonce := make([]byte, 12)
for i := range nonce {
nonce[i] = byte(i + 0x10)
}
c1, err := New(key, nonce)
if err != nil {
t.Fatalf("New failed: %v", err)
}
c2, err := New(key, nonce)
if err != nil {
t.Fatalf("New failed: %v", err)
}
original := []byte("Hello, Ananta NetEase UXChaCha8 stream cipher verification!")
data := make([]byte, len(original))
copy(data, original)
c1.XorInPlace(data)
if bytes.Equal(data, original) {
t.Errorf("Ciphertext equals plaintext")
}
c2.XorInPlace(data)
if !bytes.Equal(data, original) {
t.Errorf("Decrypted data != original: got %q, want %q", data, original)
}
}
+260
View File
@@ -0,0 +1,260 @@
package protocol
import (
"encoding/binary"
"encoding/json"
"fmt"
"os"
"time"
)
// EncodeServerTime encodes the ServerTime payload.
// Format (Inline): [F64 clientUnixTime] [F64 serverUnixTime]
func EncodeServerTime(clientUnixTime float64) []byte {
now := float64(time.Now().UnixNano()) / 1e9
if clientUnixTime <= 0 {
clientUnixTime = now
}
w := NewUxWriter()
w.F64(clientUnixTime)
w.F64(now)
return w.Bytes()
}
// EncodePatchCheck encodes PatchCheckResult.
// Format (Inline): [I32 patchVersion (0)] [0xFF marker] [Int7 count (0)]
func EncodePatchCheck() []byte {
w := NewUxWriter()
w.I32(0) // patchVersion
w.U8(0xFF) // list marker
w.Int7(0) // empty list
return w.Bytes()
}
// EncodeEmptyList encodes an empty List<uint>.
func EncodeEmptyList() []byte {
w := NewUxWriter()
w.U8(0xFF)
w.Int7(0)
return w.Bytes()
}
// EncodeCheckAccount encodes CheckAccountResult.
func EncodeCheckAccount(accountId, userName, token string, aid, serverId int, pid uint64) []byte {
loginData := map[string]interface{}{
"code": 200,
"subcode": 0,
"msg": "ok",
"uid": accountId,
"sdkuid": accountId,
"user_id": accountId,
"aid": aid,
"pid": pid,
"player_id": fmt.Sprintf("%d", pid),
"role_id": fmt.Sprintf("%d", pid),
"role_name": "辰宿",
"server_id": fmt.Sprintf("%d", serverId),
"host_id": serverId,
"username": userName,
"account": accountId,
"platform": "pc",
"token": token,
"access_token": token,
"ext_access_token": token,
"sessionid": token,
"login_ticket": token,
"realname_status": 1,
"realname_verify_status": 1,
"mobile_bind_status": 1,
}
loginJSON, _ := json.Marshal(loginData)
w := NewUxWriter()
w.U8(0xFF) // Complex object marker
w.UxString(string(loginJSON))
w.UxString(token)
w.UxString(userName)
w.I32(int32(aid))
w.Bool(false) // NeedRealNameTip
w.Bool(false) // NeedRoleEnter
w.Bool(true) // RealNameVerified
w.I32(int32(serverId))
w.I32(200) // code
w.I32(0) // subcode
w.UxString("ok")
return w.Bytes()
}
// EncodeEnterGame encodes EnterGameResult.
func EncodeEnterGame(aid int, pid uint64, host string, port int, token string, serverId int, accountId string) []byte {
w := NewUxWriter()
w.I32(int32(aid))
w.U64(pid)
// GateServerInfo
w.U8(0xFF)
w.I32(int32(aid))
w.U64(pid)
w.UxString(host)
w.I32(int32(port))
w.UxString(token)
w.I32(int32(serverId))
w.UxString(accountId)
return w.Bytes()
}
// EncodeGameServerInfo encodes GameServerInfo.
func EncodeGameServerInfo(host string, port int, gameToken string) []byte {
w := NewUxWriter()
w.U8(0xFF)
w.UxString(host)
w.I32(int32(port))
w.UxString(gameToken)
return w.Bytes()
}
// EncodeEnterScene encodes EnterSceneInfo.
func EncodeEnterScene(pid uint64, raidId uint32, instanceId uint64, universeId uint32, unitId uint64, templateId uint32, posX, posY, posZ, facing float32) []byte {
w := NewUxWriter()
w.U8(0xFF) // [UxContract]
w.U64(pid)
w.U32(raidId)
w.U64(instanceId)
// UXVector3 Position (Struct)
w.F32(posX)
w.F32(posY)
w.F32(posZ)
w.F32(facing)
// SceneSpoonNames (empty list)
w.U8(0xFF).Int7(0)
// SceneSpoonMd5s (empty list)
w.U8(0xFF).Int7(0)
// Spirits List (count 1, Struct)
w.U8(0xFF).Int7(1)
// SpiritInitData (Struct: Id, TemplateId, IsActive, WeaponTemplateId, WeaponSkinId)
w.U64(unitId).U32(templateId).Bool(true).U32(0).U32(0)
// GridInfo (Struct: MinX, MinZ, MaxX, MaxZ)
w.I32(0).I32(0).I32(300000).I32(300000)
w.U32(0) // MatchGameId
w.U32(0) // SwitchShowId
w.Bool(false) // IsSwitchSpiritShow
w.U32(universeId)
w.U32(0) // SectorControlId
// SectorControlIds (empty list)
w.U8(0xFF).Int7(0)
w.U32(0) // DynamicLayerId
// LoadingType (Complex 0xFF: Type, Members list, Ripple)
w.U8(0xFF).U32(0).U8(0xFF).Int7(0).Bool(false)
// LinkSimpleInfo (Complex 0xFF: Id, Mode, DeviceLevel, Tag, PublicEventId, LastPublicEventTime, HostPid)
w.U8(0xFF).U32(0).U32(0).U32(1).U32(0).U32(0).U64(0).U64(0)
// DataLayerIndices (empty list)
w.U8(0xFF).Int7(0)
w.Bool(false) // Seamless
// CollectionRoomLoadData (null complex)
w.U8(0x00)
// HouseLoadData (null complex)
w.U8(0x00)
// TombIslandIds (empty list)
w.U8(0xFF).Int7(0)
return w.Bytes()
}
// EncodeLogicAgentEnter encodes SyncLogicAgentEnter.
func EncodeLogicAgentEnter(unitId uint64) []byte {
w := NewUxWriter()
w.U64(unitId)
return w.Bytes()
}
// EncodeLogicAgentLeave encodes SyncLogicAgentLeave.
func EncodeLogicAgentLeave(unitId uint64) []byte {
w := NewUxWriter()
w.U64(unitId)
return w.Bytes()
}
// EncodeManagedLogicAgent encodes SyncManagedLogicAgent.
func EncodeManagedLogicAgent(unitId, pid uint64, moveId byte) []byte {
w := NewUxWriter()
w.U64(unitId).U64(pid).U8(moveId)
return w.Bytes()
}
// EncodePlayerCurrentSpirit encodes SyncPlayerCurrentSpirit.
func EncodePlayerCurrentSpirit(pid uint64, templateId uint32, unitId uint64, isAgentSwitch bool) []byte {
w := NewUxWriter()
w.U64(pid).U32(templateId).U64(unitId).Bool(isAgentSwitch)
return w.Bytes()
}
// EncodeSceneLoadCompleted encodes SyncSceneLoadCompleted.
func EncodeSceneLoadCompleted(sceneId uint64) []byte {
w := NewUxWriter()
w.U64(sceneId)
return w.Bytes()
}
// EncodeRaidBattleUnitSpirit encodes the character projection for world entry and spirit switching.
func EncodeRaidBattleUnitSpirit(unitId uint64, templateId uint32, posX, posY, posZ, facing float32, pid uint64) []byte {
w := NewUxWriter()
w.U8(0xFF) // RaidBattleUnitSpirit Complex marker
w.U8(0xFF) // SpiritWearFashionsInfo Complex marker
// WearFashionColoringInfoDict empty dictionary (0xFF marker, count 0)
w.U8(0xFF).I32(0)
// WearFashionInfoList with 5 default fashion items
w.U8(0xFF).I32(5)
fashionIds := []uint32{11100051, 11100052, 11100053, 11189102, 11189103}
for _, fid := range fashionIds {
w.U8(0xFF).U32(fid)
}
// WearFashionEditInfoList empty list
w.U8(0xFF).I32(0)
// HiddenParts = 0, EditedHiddenParts = 0, Begging = false, IsBot = false
w.U8(0).U8(0).Bool(false).Bool(false)
// Id & TemplateId
w.U64(unitId).U32(templateId)
// Position & Facing
w.F32(posX).F32(posY).F32(posZ).F32(facing)
// OwnerId & ManagedPid
w.U64(pid).U64(0)
// MoveId & NavTags
w.U8(0).U32(0)
// GroundData (MoveActionGroundData Complex 0xFF)
w.U8(0xFF).U32(0).U64(0).U32(0)
// LocalPos (UXVector3)
w.F32(0).F32(0).F32(0)
return w.Bytes()
}
// EncodePlayerInfo loads the baseline PlayerClientInfo and patches dynamic fields.
func EncodePlayerInfo(templatePath string, aid int, pid uint64, accountId, name string) ([]byte, error) {
data, err := os.ReadFile(templatePath)
if err != nil {
return nil, err
}
res := make([]byte, len(data))
copy(res, data)
// Patch Aid at offset 4 (4 bytes)
if len(res) > 8 {
binary.LittleEndian.PutUint32(res[4:8], uint32(aid))
}
// Patch Pid at offset 8 (8 bytes)
if len(res) > 16 {
binary.LittleEndian.PutUint64(res[8:16], pid)
}
return res, nil
}
+132
View File
@@ -0,0 +1,132 @@
package protocol
import (
"encoding/binary"
"errors"
"io"
"ananta-ps/pkg/crypto/chacha8"
)
type Frame struct {
Mode byte
Payload []byte
}
func ReadFrame(r io.Reader, cryptor *chacha8.UxChaCha8) (*Frame, error) {
head := make([]byte, 5)
if _, err := io.ReadFull(r, head); err != nil {
return nil, err
}
if cryptor != nil {
cryptor.XorInPlace(head)
}
payloadLength := int(binary.LittleEndian.Uint32(head[0:4]))
if payloadLength < 0 || payloadLength > 16*1024*1024 {
return nil, errors.New("invalid frame payload length after decrypt")
}
payload := make([]byte, payloadLength)
if payloadLength > 0 {
if _, err := io.ReadFull(r, payload); err != nil {
return nil, err
}
if cryptor != nil {
cryptor.XorInPlace(payload)
}
}
return &Frame{
Mode: head[4],
Payload: payload,
}, nil
}
func WriteFrame(w io.Writer, mode byte, payload []byte, cryptor *chacha8.UxChaCha8) error {
buf := make([]byte, 5+len(payload))
binary.LittleEndian.PutUint32(buf[0:4], uint32(len(payload)))
buf[4] = mode
copy(buf[5:], payload)
if cryptor != nil {
cryptor.XorInPlace(buf)
}
_, err := w.Write(buf)
return err
}
func ParseRpcPacket(payload []byte) RpcPacket {
if len(payload) < 5 {
return RpcPacket{Kind: PacketKindUnknown, Body: payload}
}
tag := payload[0]
methodID := binary.LittleEndian.Uint32(payload[1:5])
switch tag {
case 0x03: // Notify
return RpcPacket{
Kind: PacketKindNotify,
MethodID: methodID,
Body: payload[5:],
}
case 0x01: // Invoke
if len(payload) >= 9 {
invokeID := int32(binary.LittleEndian.Uint32(payload[5:9]))
return RpcPacket{
Kind: PacketKindInvoke,
MethodID: methodID,
InvokeID: invokeID,
Body: payload[9:],
}
}
case 0x02: // Return
if len(payload) >= 13 {
invokeID := int32(binary.LittleEndian.Uint32(payload[5:9]))
errCode := int32(binary.LittleEndian.Uint32(payload[9:13]))
return RpcPacket{
Kind: PacketKindReturn,
MethodID: methodID,
InvokeID: invokeID,
Error: errCode,
Body: payload[13:],
}
}
}
return RpcPacket{
Kind: PacketKindUnknown,
MethodID: methodID,
Body: payload,
}
}
func BuildNotify(methodID uint32, body []byte) []byte {
buf := make([]byte, 5+len(body))
buf[0] = 0x03
binary.LittleEndian.PutUint32(buf[1:5], methodID)
copy(buf[5:], body)
return buf
}
func BuildInvoke(methodID uint32, invokeID int32, body []byte) []byte {
buf := make([]byte, 9+len(body))
buf[0] = 0x01
binary.LittleEndian.PutUint32(buf[1:5], methodID)
binary.LittleEndian.PutUint32(buf[5:9], uint32(invokeID))
copy(buf[9:], body)
return buf
}
func BuildReturn(methodID uint32, invokeID int32, errCode int32, body []byte) []byte {
buf := make([]byte, 13+len(body))
buf[0] = 0x02
binary.LittleEndian.PutUint32(buf[1:5], methodID)
binary.LittleEndian.PutUint32(buf[5:9], uint32(invokeID))
binary.LittleEndian.PutUint32(buf[9:13], uint32(errCode))
copy(buf[13:], body)
return buf
}
+229
View File
@@ -0,0 +1,229 @@
package protocol
// GameSwitchEntry represents a single switch configuration entry.
type GameSwitchEntry struct {
Name string
Value bool
}
// GameSwitchCatalog contains all 203 UX.GameSwitch flags.
var GameSwitchCatalog = []GameSwitchEntry{
{"EnableRaidTimeFix", true},
{"EnableClientZoneClosure", true},
{"UseEditorSectorTileInfoJsonPath1", true},
{"EnableNavMultiverseChain", true},
{"EnableRecycleItem", true},
{"EnableComposeItem", true},
{"DisableEditXingZheDangAn", false},
{"EnableUpdateSignature", true},
{"EnablePartialAntiAddiction", false},
{"EnableAntiAddiction", false},
{"EnableRealNameBeforeCreateRole", false},
{"EnableExchangeGold", true},
{"EnableNpcShop", true},
{"EnableFashionStore", true},
{"Enable4SStore", true},
{"EnableCreateRole", true},
{"EnableSeasonalBattlePass", true},
{"EnableGamePlayBattlePass", true},
{"EnableGachaSystem", true},
{"EnableRadiantChest", true},
{"EnableCloset", true},
{"EnableGachaMilestone", true},
{"EnableHeartbeatTimeoutCheckDebug", false},
{"EnableHeartbeatTimeoutCheckRelease", true},
{"EnableLoginReconnect", true},
{"EnableLoginOnlineAndDetachMatchLink", true},
{"AutoSwitchTextModeWithAllPrefix", true},
{"ChatLimitRegion", true},
{"EnableMahjong", true},
{"EnableMahjongFriend", true},
{"EnableMahjongWithNpc", true},
{"EnableMahjongPve", true},
{"EnableChangePersonalZoneHeadInfo", true},
{"EnableSpiritAddExp", true},
{"EnableSpiritAbilityAddExp", true},
{"EnableSpiritDomainLevelUp", true},
{"EnableForeignIpSns", true},
{"EnableSharePhoto_Netease", true},
{"EnableSharePhoto_Oppo", true},
{"EnableSharePhoto_Huawei", true},
{"EnableSharePhoto_NearmeVivo", true},
{"EnableSharePhoto_XiaomiApp", true},
{"EnableSharePhoto_CoolpadSdk", true},
{"EnableSharePhoto_Flyme", true},
{"EnableSharePhoto_LenovoOpen", true},
{"EnableSharePhoto_Gionee", true},
{"EnableSharePhoto_UcPlatform", true},
{"EnableSharePhoto_360Assistant", true},
{"EnableSharePhoto_Baidu", true},
{"EnableSharePhoto_BilibiliSdk", true},
{"EnableSharePhoto_Dtkj", true},
{"EnableSharePhoto_AppStore", true},
{"EnableSharePhoto_TapTapDev", true},
{"EnableSharePhoto_TapTapDis", true},
{"EnableSharePhoto_HykbDev", true},
{"EnableSharePhoto_HykbDis", true},
{"EnableFightSpiritSkillLevelUp", true},
{"EnableSignIn", true},
{"EnableSaveCardGroupName", true},
{"EnableAetherNpcFixedSpawnPointRule", true},
{"EnableNpcUseBelonging", true},
{"EnableVehicleNpcDefaultPlayPoi1", true},
{"EnableAgentInteractForbidFromServer1", true},
{"EnableTeleportOnCrossBoundary", true},
{"EnableAgentCompanionAIIdleChat", true},
{"EnableAgentPlayPoiCommand", true},
{"EnableNpcGateway", true},
{"EnableNpcDensityMigration1", true},
{"EnableLink", true},
{"EnableLinkPlanningBoardUnlockCheck", true},
{"ProfilerAetherVehicle", true},
{"ProfilerStaticVehicle", true},
{"ProfilerAetherVehicleGO", true},
{"EnableRaidDetachLocalCleanup", true},
{"ProfilerIntersection", true},
{"ProfilerIntersectionRender", true},
{"EnablePoliceJobStoryLegacy", true},
{"EnableGroupAIStory", true},
{"EnableGroupAIDebug", true},
{"EnableNewProgress", true},
{"EnableDGTemporaryResource", true},
{"ForbidStimEvent", false},
{"EnableZoneGraphUndirectLane", true},
{"EnableNPCNormalMoveBeta", true},
{"EnableIndoor", true},
{"EnableVehicleNpcBoardingSync1", true},
{"EnableAetherNpcTypeAgnosticSync", true},
{"EnableVehicleBoardingUnifiedSync1", true},
{"EnableLinkTest", true},
{"EnableSpawnSpoonBlockInteractionCheck", true},
{"EnableVehicleOverrideSync", true},
{"EnableVehicleDefaultSuitExpand", true},
{"EnableECSResponse", true},
{"DebugSyncVehicleDangerZoneToClient", false},
{"DebugHiddenArea", false},
{"DebugAetherVehicle", false},
{"DebugAetherIntersection", false},
{"DebugAetherVehicleLane", false},
{"DebugAssetTagPreview", false},
{"DebugLogicVehicle", false},
{"EnableDropRequireReliableRpcOnClient", true},
{"ProfilerRandomEvent", true},
{"ProfilerDestructible", true},
{"ProfilerGadget", true},
{"ProfilerDynamicGo", true},
{"ProfilerSpoonAgent", true},
{"ProfilerWildEnemy", true},
{"ProfilerEnableMetro", true},
{"BlockMetroForChongxiao", false},
{"MetroCrouchSwitch", true},
{"ProfilerEnableNpcGo", true},
{"DisableBartender", false},
{"DisableScientistProduceItem", false},
{"DisableScientistBreakdownItem", false},
{"DisableScientistTakeProducedItem", false},
{"DisableScientistFactorMachine", false},
{"DisableScientistEnchant", false},
{"DisableItemExchange", false},
{"EnableFallingDownOnDeath2", true},
{"EnableBehaveLegacy", true},
{"EnableBeggerStory", true},
{"EnableGPUScene", true},
{"EnableFavorNpcBTree", true},
{"EnableFavorNpcCommute", true},
{"EnableSwitchSpiritToTempFavorNpc1", true},
{"SkipSwitchOutExitTransition", true},
{"EnableGenderTransform", true},
{"EnableIndoorAbilityLimit", true},
{"EnableRemoveAbilityLimitOldBuffHandlers", true},
{"EnableSpoonNpc", true},
{"EnableSearchRaidSpoonNpc", true},
{"EnableParty", true},
{"EnableRanking", true},
{"EnableJanitor", true},
{"EnableBeggarAiPainting", true},
{"EnableNewBag", true},
{"EnableNotices", true},
{"EnableAkashicSystem", true},
{"EnableProfile", true},
{"EnableRadioStation", true},
{"EnableCityPedia", true},
{"EnableEonBug", true},
{"EnableTutorial", true},
{"EnableVehicleRequisitionSystemUnlock1", true},
{"EnableTestProcess", true},
{"EnableForceStopNaN", true},
{"EnableOtlpLogger", true},
{"EnableResourceLoadCollect", true},
{"EnableHUDRecommend", true},
{"EnableAttachmentSystem", true},
{"EnableFishing", true},
{"EnablePSOCollect", true},
{"KeepBehaviorSchedulerWhenSwitch", true},
{"KeepBehaviorSchedulerWhenSwitch2", true},
{"OpenNewSettleColliderClip2", true},
{"OpenNewSettleEffectClip2", true},
{"OpenNewSettleCreation2", true},
{"UseCoreLibRouteFinder", true},
{"EnableScratchServerReward", true},
{"DisableYanJieAIReply", false},
{"EnableCompetitionSeason", true},
{"EnableActivity", true},
{"EnableChangeNameByItem", true},
{"EnableThisYearChat", true},
{"EnableSpecialTimeUserChat", true},
{"EnableBuzzCenter", true},
{"EnableCheckIn", true},
{"EnableDossier", true},
{"EnableScope", true},
{"EnableFriends", true},
{"EnableClub", true},
{"EnableBBChat", true},
{"EnableMail", true},
{"EnableCustom", true},
{"EnableBubble", true},
{"EnablePhoto", true},
{"EnableCatExpress", true},
{"EnableDutyTerminal", true},
{"EnableMall", true},
{"EnableMallRecommend", true},
{"EnableMallDirectSale", true},
{"EnableMallBundle", true},
{"EnableCharge", true},
{"EnableSpiritTalent", true},
{"EnableJobTalent", true},
{"EnableGameplayTalent", true},
{"EnableAchievement", true},
{"EnablePhone", true},
{"EnableTrade", true},
{"EnableGift", true},
{"EnableGiftFriendTimeCheck", true},
{"EnableTime", true},
{"DisableOptGoMode", false},
{"DisableMonsterOptGoMode", false},
{"EnablePedConvertWorthyCheck", true},
{"EnableLifeScheduleNpcAlwaysGo", true},
{"EnableAnimatorLODOtherPlayer", true},
{"EnableSpecialAnimateCullingMode", true},
{"EnableAnimatorIncRebind", true},
{"SimplifyKinematicInVehicle", true},
{"EnableInteractionAction", true},
{"EnableCollectionBook", true},
{"EnableMonthlyPass", true},
}
// BuildGameSwitchPayload encodes the game switches as required by SyncGameSwitchToClient_3.
// Format:
// [Int7 count]
// for each: [UxString key] [TypeTagBool (0x02)] [Bool (1 byte)]
func BuildGameSwitchPayload() []byte {
w := NewUxWriter()
w.Int7(len(GameSwitchCatalog))
for _, entry := range GameSwitchCatalog {
w.UxString(entry.Name)
w.U8(2) // TypeTagBool
w.Bool(entry.Value)
}
return w.Bytes()
}
+85
View File
@@ -0,0 +1,85 @@
package protocol
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/binary"
"errors"
"io"
"net"
"ananta-ps/pkg/crypto/chacha8"
)
// HandleHandshake processes client C2S handshake and performs S2C key exchange.
// It returns read and write ciphers initialized with negotiated ChaCha8 keys.
func HandleHandshake(conn net.Conn) (*chacha8.UxChaCha8, *chacha8.UxChaCha8, error) {
// Read frame 0 (mode 0x01 plaintext)
frame, err := ReadFrame(conn, nil)
if err != nil {
return nil, nil, err
}
if len(frame.Payload) < 20 {
return nil, nil, errors.New("client handshake too short")
}
aesKey := frame.Payload[4:20] // 16-byte key
clientWriteKey := make([]byte, 32)
clientReadKey := make([]byte, 32)
streamNonce := make([]byte, 12)
aesNonce := make([]byte, 12)
if _, err := io.ReadFull(rand.Reader, clientWriteKey); err != nil {
return nil, nil, err
}
if _, err := io.ReadFull(rand.Reader, clientReadKey); err != nil {
return nil, nil, err
}
if _, err := io.ReadFull(rand.Reader, streamNonce); err != nil {
return nil, nil, err
}
if _, err := io.ReadFull(rand.Reader, aesNonce); err != nil {
return nil, nil, err
}
chaChaHead := make([]byte, 76)
copy(chaChaHead[0:32], clientWriteKey)
copy(chaChaHead[32:64], clientReadKey)
copy(chaChaHead[64:76], streamNonce)
block, err := aes.NewCipher(aesKey)
if err != nil {
return nil, nil, err
}
aesGcm, err := cipher.NewGCM(block)
if err != nil {
return nil, nil, err
}
encrypted := aesGcm.Seal(nil, aesNonce, chaChaHead, nil) // 76 + 16 = 92 bytes
payload := make([]byte, 368)
binary.LittleEndian.PutUint32(payload[0:4], 1)
binary.LittleEndian.PutUint32(payload[4:8], 10)
copy(payload[8:20], aesNonce)
copy(payload[20:96], encrypted[0:76])
copy(payload[96:112], encrypted[76:92])
// Send handshake reply in plaintext with mode 0x01
if err := WriteFrame(conn, 0x01, payload, nil); err != nil {
return nil, nil, err
}
readCryptor, err := chacha8.New(clientWriteKey, streamNonce)
if err != nil {
return nil, nil, err
}
writeCryptor, err := chacha8.New(clientReadKey, streamNonce)
if err != nil {
return nil, nil, err
}
return readCryptor, writeCryptor, nil
}
+82
View File
@@ -0,0 +1,82 @@
package protocol
// NetEase UX-RPC Method IDs for Ananta CBT (Build 4229938).
const (
// Login / Gate
MethodCheckVersion uint32 = 34700853
MethodHasOnlinePlayer uint32 = 34822236
MethodAskNewHotFixPatchLogin uint32 = 34692559
MethodRequestPatchesCheckDataFromLogin uint32 = 34601301
MethodRequestPatchesFromLogin uint32 = 34022227
MethodAskUniSdkShareToken_Login uint32 = 34110053
MethodAskUniSdkShareToken_Gate uint32 = 52152686
MethodRequestFpPassToken uint32 = 34828770
MethodCheckAccount uint32 = 34339919
MethodCheckAccountPassBy uint32 = 34163006
MethodCheckAccountOpenId uint32 = 34381554
MethodTryLogin uint32 = 34529582
MethodSyncRoleList uint32 = 35167428
MethodRequestCreateRoleEx uint32 = 34383517
MethodRequestEnterGame uint32 = 34566515
MethodDebugRequestEnterGame uint32 = 34478273
MethodAskCloseConnectionToGate uint32 = 52140059
MethodGetServerTime uint32 = 52951195
MethodSendServerTime uint32 = 52296181
MethodGateLogin uint32 = 52023760
MethodSyncPlayerGameServerInfo uint32 = 52737039
MethodSyncGameSwitchToClient_3 uint32 = 53940944
MethodQuerySkey uint32 = 52367757
MethodReportNetworkQuality uint32 = 52623348
MethodDavinciCode uint32 = 52458997
MethodGetFriendApplicationListToMe uint32 = 52123985
MethodGetAllChatGroupLatestMessage uint32 = 52345719
MethodGetSimplePlayerInfoByPidList uint32 = 52781934
// Game / World
MethodLoginGame uint32 = 63142082
MethodAskEnterWorld uint32 = 63142082
MethodSendServerTimeGame uint32 = 63853503
MethodSyncPlayerInfo uint32 = 63200762
MethodSyncEnterScene uint32 = 67711467
MethodAskLoadSceneCompleted uint32 = 66879899
MethodAskLoadingFinished uint32 = 67017252
MethodSyncLogicAgentEnter uint32 = 68057283
MethodSyncLogicAgentLeave uint32 = 68057315
MethodSyncManagedLogicAgent uint32 = 68611146
MethodSyncRaidBattleUnitSpirit uint32 = 68953151
MethodSyncPlayerCurrentSpirit uint32 = 68582738
MethodSyncSceneLoadCompleted uint32 = 67610360
MethodSyncGamePause uint32 = 67039327
MethodSyncPlayerLoadRate uint32 = 68383503
MethodAskLoadedInSameScene uint32 = 68838634
MethodSyncUnitPositionAndFacing uint32 = 68664188
MethodSyncPlayerPositionAndFacing uint32 = 68702330
MethodSyncSceneFogMapAllUnlock uint32 = 68677150
MethodSyncRunGmUtils uint32 = 66042045
MethodSyncAllSpiritCombatPower uint32 = 68798150
MethodSyncSwitchSpiritInSitu uint32 = 68582770
// Movement
MethodAskMoveStart uint32 = 68412852
MethodAskMoveStop uint32 = 68412853
MethodAskUpdatePlayerPosition uint32 = 68058291
// Combat & Skills
MethodAskPlayerAttack uint32 = 68412884
MethodAskPlayerCastSkill uint32 = 68412916
MethodAskUnlockFightStyle uint32 = 68412948
MethodSyncChangeSpirit uint32 = 68582802
MethodAskPreSwitchSpirit uint32 = 68415783
MethodAskSwitchSpirit uint32 = 68415784
// Vehicles
MethodAskSummonVehicle uint32 = 68221895
MethodAskDriveVehicle uint32 = 68221893
MethodAskExitVehicle uint32 = 68221894
MethodSyncPlayerVehicleInfo uint32 = 68221926
MethodSyncAetherVehicleInit uint32 = 68221958
// Teleportation & Scene
MethodAskTeleportToScene uint32 = 67891234
MethodSwitchSceneDirect uint32 = 67891235
)
+22
View File
@@ -0,0 +1,22 @@
package protocol
type RpcPacketKind byte
const (
PacketKindUnknown RpcPacketKind = 0x00
PacketKindInvoke RpcPacketKind = 0x01
PacketKindReturn RpcPacketKind = 0x02
PacketKindNotify RpcPacketKind = 0x03
)
type RpcPacket struct {
Kind RpcPacketKind
MethodID uint32
InvokeID int32
Error int32
Body []byte
}
func (p RpcPacket) IsInvoke() bool { return p.Kind == PacketKindInvoke }
func (p RpcPacket) IsNotify() bool { return p.Kind == PacketKindNotify }
func (p RpcPacket) IsReturn() bool { return p.Kind == PacketKindReturn }
+108
View File
@@ -0,0 +1,108 @@
package protocol
import (
"bytes"
"testing"
)
func TestUxSerializationRoundtrip(t *testing.T) {
w := NewUxWriter()
w.U8(42).
Bool(true).
I16(-1234).
U16(5678).
I32(-999999).
U32(12345678).
I64(-9876543210).
U64(18446744073709551615).
F32(3.14159).
F64(2.718281828459).
Int7(10).
Raw7(25).
UxString("Hello Ananta Go Protocol")
data := w.Bytes()
r := NewUxReader(data)
u8, _ := r.U8()
if u8 != 42 {
t.Errorf("U8 mismatch: %v", u8)
}
b, _ := r.Bool()
if !b {
t.Errorf("Bool mismatch: %v", b)
}
i16, _ := r.I16()
if i16 != -1234 {
t.Errorf("I16 mismatch: %v", i16)
}
u16, _ := r.U16()
if u16 != 5678 {
t.Errorf("U16 mismatch: %v", u16)
}
i32, _ := r.I32()
if i32 != -999999 {
t.Errorf("I32 mismatch: %v", i32)
}
u32, _ := r.U32()
if u32 != 12345678 {
t.Errorf("U32 mismatch: %v", u32)
}
i64, _ := r.I64()
if i64 != -9876543210 {
t.Errorf("I64 mismatch: %v", i64)
}
u64, _ := r.U64()
if u64 != 18446744073709551615 {
t.Errorf("U64 mismatch: %v", u64)
}
f32, _ := r.F32()
if f32 < 3.14 || f32 > 3.15 {
t.Errorf("F32 mismatch: %v", f32)
}
f64, _ := r.F64()
if f64 < 2.71 || f64 > 2.72 {
t.Errorf("F64 mismatch: %v", f64)
}
int7, _ := r.Int7()
if int7 != 10 {
t.Errorf("Int7 mismatch: got %v, want 10", int7)
}
raw7, _ := r.Raw7()
if raw7 != 25 {
t.Errorf("Raw7 mismatch: got %v, want 25", raw7)
}
str, _ := r.UxString()
if str != "Hello Ananta Go Protocol" {
t.Errorf("UxString mismatch: got %v", str)
}
}
func TestRpcPacketRoundtrip(t *testing.T) {
notify := BuildNotify(MethodCheckVersion, []byte{0x01, 0x02, 0x03})
p := ParseRpcPacket(notify)
if !p.IsNotify() || p.MethodID != MethodCheckVersion {
t.Errorf("Notify packet mismatch: %+v", p)
}
if !bytes.Equal(p.Body, []byte{0x01, 0x02, 0x03}) {
t.Errorf("Body mismatch: %v", p.Body)
}
ret := BuildReturn(MethodAskEnterWorld, 100, 0, []byte("OK"))
p2 := ParseRpcPacket(ret)
if !p2.IsReturn() || p2.MethodID != MethodAskEnterWorld || p2.InvokeID != 100 || p2.Error != 0 {
t.Errorf("Return packet mismatch: %+v", p2)
}
}
+160
View File
@@ -0,0 +1,160 @@
package protocol
import (
"encoding/binary"
"errors"
"math"
)
type UxReader struct {
data []byte
pos int
}
func NewUxReader(data []byte) *UxReader {
return &UxReader{data: data, pos: 0}
}
func (r *UxReader) Position() int { return r.pos }
func (r *UxReader) Remaining() int { return len(r.data) - r.pos }
func (r *UxReader) U8() (byte, error) {
if r.pos >= len(r.data) {
return 0, errors.New("EOF")
}
b := r.data[r.pos]
r.pos++
return b, nil
}
func (r *UxReader) Bool() (bool, error) {
b, err := r.U8()
return b != 0, err
}
func (r *UxReader) I16() (int16, error) {
if r.Remaining() < 2 {
return 0, errors.New("EOF")
}
v := int16(binary.LittleEndian.Uint16(r.data[r.pos : r.pos+2]))
r.pos += 2
return v, nil
}
func (r *UxReader) U16() (uint16, error) {
if r.Remaining() < 2 {
return 0, errors.New("EOF")
}
v := binary.LittleEndian.Uint16(r.data[r.pos : r.pos+2])
r.pos += 2
return v, nil
}
func (r *UxReader) I32() (int32, error) {
if r.Remaining() < 4 {
return 0, errors.New("EOF")
}
v := int32(binary.LittleEndian.Uint32(r.data[r.pos : r.pos+4]))
r.pos += 4
return v, nil
}
func (r *UxReader) U32() (uint32, error) {
if r.Remaining() < 4 {
return 0, errors.New("EOF")
}
v := binary.LittleEndian.Uint32(r.data[r.pos : r.pos+4])
r.pos += 4
return v, nil
}
func (r *UxReader) I64() (int64, error) {
if r.Remaining() < 8 {
return 0, errors.New("EOF")
}
v := int64(binary.LittleEndian.Uint64(r.data[r.pos : r.pos+8]))
r.pos += 8
return v, nil
}
func (r *UxReader) U64() (ulong uint64, err error) {
if r.Remaining() < 8 {
return 0, errors.New("EOF")
}
v := binary.LittleEndian.Uint64(r.data[r.pos : r.pos+8])
r.pos += 8
return v, nil
}
func (r *UxReader) F32() (float32, error) {
bits, err := r.U32()
if err != nil {
return 0, err
}
return math.Float32frombits(bits), nil
}
func (r *UxReader) F64() (float64, error) {
bits, err := r.U64()
if err != nil {
return 0, err
}
return math.Float64frombits(bits), nil
}
func (r *UxReader) ReadVarInt() (int, error) {
shift := 0
result := 0
for r.pos < len(r.data) {
b := r.data[r.pos]
r.pos++
result |= int(b&0x7F) << shift
if (b & 0x80) == 0 {
return result, nil
}
shift += 7
}
return result, errors.New("unterminated varint")
}
func (r *UxReader) Int7() (int, error) {
v, err := r.ReadVarInt()
if err != nil {
return 0, err
}
if v < 1 {
return 0, nil
}
return v - 1, nil
}
func (r *UxReader) Raw7() (int, error) {
return r.ReadVarInt()
}
func (r *UxReader) UxString() (string, error) {
lenPlus, err := r.ReadVarInt()
if err != nil {
return "", err
}
if lenPlus == 0 {
return "", nil
}
length := lenPlus - 1
if length > r.Remaining() {
return "", errors.New("string length exceeds buffer")
}
str := string(r.data[r.pos : r.pos+length])
r.pos += length
return str, nil
}
func (r *UxReader) Bytes(count int) ([]byte, error) {
if count < 0 || count > r.Remaining() {
return nil, errors.New("requested count exceeds buffer")
}
res := make([]byte, count)
copy(res, r.data[r.pos:r.pos+count])
r.pos += count
return res, nil
}
+127
View File
@@ -0,0 +1,127 @@
package protocol
import (
"bytes"
"encoding/binary"
"math"
)
type UxWriter struct {
buf bytes.Buffer
}
func NewUxWriter() *UxWriter {
return &UxWriter{}
}
func (w *UxWriter) U8(val byte) *UxWriter {
w.buf.WriteByte(val)
return w
}
func (w *UxWriter) Bool(val bool) *UxWriter {
if val {
w.buf.WriteByte(1)
} else {
w.buf.WriteByte(0)
}
return w
}
func (w *UxWriter) I16(val int16) *UxWriter {
var b [2]byte
binary.LittleEndian.PutUint16(b[:], uint16(val))
w.buf.Write(b[:])
return w
}
func (w *UxWriter) U16(val uint16) *UxWriter {
var b [2]byte
binary.LittleEndian.PutUint16(b[:], val)
w.buf.Write(b[:])
return w
}
func (w *UxWriter) I32(val int32) *UxWriter {
var b [4]byte
binary.LittleEndian.PutUint32(b[:], uint32(val))
w.buf.Write(b[:])
return w
}
func (w *UxWriter) U32(val uint32) *UxWriter {
var b [4]byte
binary.LittleEndian.PutUint32(b[:], val)
w.buf.Write(b[:])
return w
}
func (w *UxWriter) I64(val int64) *UxWriter {
var b [8]byte
binary.LittleEndian.PutUint64(b[:], uint64(val))
w.buf.Write(b[:])
return w
}
func (w *UxWriter) U64(val uint64) *UxWriter {
var b [8]byte
binary.LittleEndian.PutUint64(b[:], val)
w.buf.Write(b[:])
return w
}
func (w *UxWriter) F32(val float32) *UxWriter {
return w.U32(math.Float32bits(val))
}
func (w *UxWriter) F64(val float64) *UxWriter {
return w.U64(math.Float64bits(val))
}
func (w *UxWriter) WriteVarInt(val int) *UxWriter {
v := uint32(val)
for v >= 0x80 {
w.buf.WriteByte(byte(v | 0x80))
v >>= 7
}
w.buf.WriteByte(byte(v))
return w
}
func (w *UxWriter) Int7(val int) *UxWriter {
if val < 0 {
val = 0
}
return w.WriteVarInt(val + 1)
}
func (w *UxWriter) Raw7(val int) *UxWriter {
if val < 0 {
val = 0
}
return w.WriteVarInt(val)
}
func (w *UxWriter) UxString(str string) *UxWriter {
utf8 := []byte(str)
w.WriteVarInt(len(utf8) + 1)
w.buf.Write(utf8)
return w
}
func (w *UxWriter) UxStringNullable(str *string) *UxWriter {
if str == nil {
w.buf.WriteByte(0x00)
return w
}
return w.UxString(*str)
}
func (w *UxWriter) Raw(b []byte) *UxWriter {
w.buf.Write(b)
return w
}
func (w *UxWriter) Bytes() []byte {
return w.buf.Bytes()
}