2026-10-29 03:12:00 +07:00
|
|
|
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 {
|
2026-10-29 03:53:15 +07:00
|
|
|
cfg *config.Config
|
|
|
|
|
templatePath string
|
|
|
|
|
ln net.Listener
|
|
|
|
|
cancelFunc context.CancelFunc
|
|
|
|
|
wg sync.WaitGroup
|
|
|
|
|
sessions map[string]*Session
|
|
|
|
|
mu sync.RWMutex
|
|
|
|
|
trafficEngine *CityTrafficEngine
|
|
|
|
|
crowdEngine *UrbanCrowdEngine
|
|
|
|
|
adminBridge *AdminBridge
|
2026-10-29 03:12:00 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Session struct {
|
2026-10-29 03:53:15 +07:00
|
|
|
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
|
|
|
|
|
lastShopExitMs int64
|
|
|
|
|
pendingTeleport *PreTeleportOption
|
|
|
|
|
activeVehicleId uint64
|
|
|
|
|
isDriving bool
|
|
|
|
|
wantedLevel uint32
|
|
|
|
|
balloonScore uint32
|
|
|
|
|
racing *RacingSession
|
2026-10-29 03:12:00 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-29 03:53:15 +07:00
|
|
|
srv := &Server{
|
2026-10-29 03:12:00 +07:00
|
|
|
cfg: cfg,
|
|
|
|
|
templatePath: templatePath,
|
|
|
|
|
sessions: make(map[string]*Session),
|
|
|
|
|
}
|
2026-10-29 03:53:15 +07:00
|
|
|
srv.trafficEngine = NewCityTrafficEngine(srv)
|
|
|
|
|
srv.crowdEngine = NewUrbanCrowdEngine(srv)
|
|
|
|
|
srv.adminBridge = NewAdminBridge(srv)
|
|
|
|
|
return srv
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) GetAdminBridge() *AdminBridge {
|
|
|
|
|
return s.adminBridge
|
2026-10-29 03:12:00 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) Start() error {
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
s.cancelFunc = cancel
|
|
|
|
|
|
2026-10-29 03:53:15 +07:00
|
|
|
// Load Road Network for city traffic simulation
|
|
|
|
|
_ = s.trafficEngine.LoadRoadNetwork("data/client_data/World/RoadNetwork.json")
|
|
|
|
|
s.trafficEngine.Start(ctx)
|
|
|
|
|
|
2026-10-29 03:12:00 +07:00
|
|
|
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 {
|
2026-10-29 03:53:15 +07:00
|
|
|
if s.trafficEngine != nil {
|
|
|
|
|
s.trafficEngine.Stop()
|
|
|
|
|
}
|
2026-10-29 03:12:00 +07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-10-29 03:53:15 +07:00
|
|
|
log.Printf("[GAME-SERVER] Dispatch error for method %d (%s): %v",
|
|
|
|
|
packet.MethodID, protocol.GetMethodName(packet.MethodID), err)
|
2026-10-29 03:12:00 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
|
|
|
|
|
switch pkt.MethodID {
|
2026-10-29 03:53:15 +07:00
|
|
|
// ---------------- Login / World Entry ----------------
|
2026-10-29 03:12:00 +07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_ = sess.NotifyData(protocol.MethodSendServerTimeGame, protocol.EncodeServerTime(0))
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_ = sess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(sess.unitId))
|
|
|
|
|
_ = sess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(sess.unitId, sess.pid, 0))
|
|
|
|
|
unitProjection := protocol.EncodeRaidBattleUnitSpirit(sess.unitId, sess.templateId, sess.posX, sess.posY, sess.posZ, sess.facing, sess.pid)
|
|
|
|
|
_ = sess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, unitProjection)
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_ = sess.NotifyData(protocol.MethodSyncSceneLoadCompleted, protocol.EncodeSceneLoadCompleted(sess.activeScene))
|
|
|
|
|
|
|
|
|
|
wPause := protocol.NewUxWriter()
|
|
|
|
|
wPause.Bool(false)
|
|
|
|
|
_ = sess.NotifyData(protocol.MethodSyncGamePause, wPause.Bytes())
|
|
|
|
|
|
|
|
|
|
wFog := protocol.NewUxWriter()
|
|
|
|
|
wFog.U32(uint32(sess.activeScene)).Bool(true)
|
|
|
|
|
_ = sess.NotifyData(protocol.MethodSyncSceneFogMapAllUnlock, wFog.Bytes())
|
|
|
|
|
|
|
|
|
|
wPower := protocol.NewUxWriter()
|
|
|
|
|
wPower.U8(0xFF).Int7(1).U32(sess.templateId).F32(1000.0)
|
|
|
|
|
_ = sess.NotifyData(protocol.MethodSyncAllSpiritCombatPower, wPower.Bytes())
|
|
|
|
|
|
|
|
|
|
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())
|
|
|
|
|
|
2026-10-29 03:53:15 +07:00
|
|
|
// ---------------- Movement & Position ----------------
|
2026-10-29 03:12:00 +07:00
|
|
|
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)
|
|
|
|
|
|
2026-10-29 03:53:15 +07:00
|
|
|
// ---------------- Character Switching ----------------
|
2026-10-29 03:12:00 +07:00
|
|
|
case protocol.MethodAskPreSwitchSpirit:
|
|
|
|
|
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskSwitchSpirit:
|
|
|
|
|
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
|
|
|
|
|
|
2026-10-29 03:53:15 +07:00
|
|
|
// ---------------- Teleport & Map Entrance ----------------
|
|
|
|
|
case protocol.MethodAskTeleport:
|
|
|
|
|
return s.handleAskTeleport(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodReportPreTeleportFinish:
|
|
|
|
|
return s.handleReportPreTeleportFinish(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodReportPostTeleportFinish:
|
|
|
|
|
return s.handleReportPostTeleportFinish(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskEnterRaidByMapEntrance:
|
|
|
|
|
return s.handleAskEnterRaidByMapEntrance(sess, pkt)
|
|
|
|
|
|
|
|
|
|
// ---------------- Vehicles ----------------
|
|
|
|
|
case protocol.MethodAskSummonVehicle:
|
|
|
|
|
return s.handleAskSummonVehicle(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskDriveVehicle:
|
|
|
|
|
return s.handleAskDriveVehicle(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskExitVehicle:
|
|
|
|
|
return s.handleAskExitVehicle(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskVehicleMove:
|
|
|
|
|
return s.handleAskVehicleMove(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodChangeVehicleDoorState:
|
|
|
|
|
return s.handleChangeVehicleDoorState(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskVehicleSendGamePlaySignal:
|
|
|
|
|
return s.handleAskVehicleSendGamePlaySignal(sess, pkt)
|
|
|
|
|
|
|
|
|
|
// ---------------- Police & Crime ----------------
|
|
|
|
|
case protocol.MethodAskSetWantedLevel:
|
|
|
|
|
return s.handleAskSetWantedLevel(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskPoliceArrest:
|
|
|
|
|
return s.handleAskPoliceArrest(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodReportPoliceArrestFinish:
|
|
|
|
|
return s.handleReportPoliceArrestFinish(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskPayFine:
|
|
|
|
|
return s.handleAskPayFine(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskReleaseFromPrison:
|
|
|
|
|
return s.handleAskReleaseFromPrison(sess, pkt)
|
|
|
|
|
|
|
|
|
|
// ---------------- Dialog & NPC & Crowd ----------------
|
|
|
|
|
case protocol.MethodAskStartDialog:
|
|
|
|
|
return s.handleAskStartDialog(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskDoDialogAction:
|
|
|
|
|
return s.handleAskDoDialogAction(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskForceFinishDialog:
|
|
|
|
|
return s.handleAskForceFinishDialog(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodGetCharacterRandomDialog:
|
|
|
|
|
return s.handleGetCharacterRandomDialog(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskAgentStartNpcSound:
|
|
|
|
|
return s.handleAskAgentStartNpcSound(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskDoNpcAction:
|
|
|
|
|
return s.handleAskDoNpcAction(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskSetCrowdDensity:
|
|
|
|
|
return s.handleAskSetCrowdDensity(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskPhoneAddCallRecord:
|
|
|
|
|
return s.handleAskPhoneAddCallRecord(sess, pkt)
|
2026-10-29 03:12:00 +07:00
|
|
|
|
2026-10-29 03:53:15 +07:00
|
|
|
case protocol.MethodAskPhoneContactOptionAction:
|
|
|
|
|
return s.handleAskPhoneContactOptionAction(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskSummonGangMember:
|
|
|
|
|
return s.handleAskSummonGangMember(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskDestroyAllGangMember:
|
|
|
|
|
return s.handleAskDestroyAllGangMember(sess, pkt)
|
|
|
|
|
|
|
|
|
|
// ---------------- Combat ----------------
|
|
|
|
|
case protocol.MethodGmWorldRefreshEnemyGroup:
|
|
|
|
|
return s.handleGmWorldRefreshEnemyGroup(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodReportUnitHitFly:
|
|
|
|
|
return s.handleReportUnitHitFly(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodReportUnitHitFlyEnd:
|
|
|
|
|
return s.handleReportUnitHitFlyEnd(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodReportUnitFallGround:
|
|
|
|
|
return s.handleReportUnitFallGround(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodReportUnitFallGroundEnd:
|
|
|
|
|
return s.handleReportUnitFallGroundEnd(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskControlPowerHoldEnemyUp:
|
|
|
|
|
return s.handleAskControlPowerHoldEnemyUp(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskControlPowerHoldEnemyFallGrou:
|
|
|
|
|
return s.handleAskControlPowerHoldEnemyFallGrou(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskPlayerAttack:
|
|
|
|
|
return s.handleAskPlayerAttack(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskPlayerCastSkill:
|
|
|
|
|
return s.handleAskPlayerCastSkill(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskUnlockFightStyle:
|
2026-10-29 03:12:00 +07:00
|
|
|
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
|
|
|
|
|
|
2026-10-29 03:53:15 +07:00
|
|
|
// ---------------- Mini-games ----------------
|
|
|
|
|
case protocol.MethodAskChineseChessMove:
|
|
|
|
|
return s.handleAskChineseChessMove(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskMahjongAction:
|
|
|
|
|
return s.handleAskMahjongAction(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodEnterBalloonZone:
|
|
|
|
|
return s.handleEnterBalloonZone(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodLeaveBalloon:
|
|
|
|
|
return s.handleLeaveBalloon(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskBreakDestructibleObject:
|
|
|
|
|
return s.handleAskBreakDestructibleObject(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodEnterBowlingZone:
|
|
|
|
|
return s.handleEnterBowlingZone(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskClawBuyTicket:
|
|
|
|
|
return s.handleAskClawBuyTicket(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskClawSettlement:
|
|
|
|
|
return s.handleAskClawSettlement(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskBuyMiniGameTicket:
|
|
|
|
|
return s.handleAskBuyMiniGameTicket(sess, pkt)
|
|
|
|
|
|
|
|
|
|
// ---------------- Racing ----------------
|
|
|
|
|
case protocol.MethodAskAcceptAndSetCurrentTask:
|
|
|
|
|
return s.handleAskAcceptAndSetCurrentTask(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskSetRacingCompetitionVehicle:
|
|
|
|
|
return s.handleAskSetRacingCompetitionVehicle(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskSetTaskCounterValue:
|
|
|
|
|
return s.handleAskSetTaskCounterValue(sess, pkt)
|
|
|
|
|
|
|
|
|
|
// ---------------- Fashion & Wardrobe ----------------
|
|
|
|
|
case protocol.MethodAskSetSpiritFashionsWithSource:
|
|
|
|
|
return s.handleAskSetSpiritFashionsWithSource(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskApplyFashionColoringSchemeInfos:
|
|
|
|
|
return s.handleAskApplyFashionColoringSchemeInfos(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskSetWeaponSkins:
|
|
|
|
|
return s.handleAskSetWeaponSkins(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodAskSyncWeaponSkinToSpirits:
|
|
|
|
|
return s.handleAskSyncWeaponSkinToSpirits(sess, pkt)
|
|
|
|
|
|
|
|
|
|
// ---------------- Weather & Fog ----------------
|
|
|
|
|
case protocol.MethodGmUnlockAllFogMap:
|
|
|
|
|
return s.handleGmUnlockAllFogMap(sess, pkt)
|
|
|
|
|
|
|
|
|
|
case protocol.MethodGmSetAmbianceEnabled:
|
|
|
|
|
return s.handleGmSetAmbianceEnabled(sess, pkt)
|
|
|
|
|
|
2026-10-29 03:12:00 +07:00
|
|
|
default:
|
2026-10-29 03:53:15 +07:00
|
|
|
// Safe neutral typed fallback for unrouted RPC invokes
|
2026-10-29 03:12:00 +07:00
|
|
|
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())
|
|
|
|
|
}
|