feat(go): Port comprehensive gameplay routers (traffic, police, combat, minigames, racing, teleport, crowd, weather, fashion, admin bridge) into native Go engine

This commit is contained in:
CAtlas0784 committed 2026-10-29 03:53:15 +07:00
1 parent 195a1d36f5
commit dab553d4a2
81 files changed
+8707652 -61

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
package gameserver
import (
"fmt"
"log"
"strings"
"ananta-ps/pkg/protocol"
)
type POILandmark struct {
Name string
X float32
Y float32
Z float32
Facing float32
}
var KnownLandmarks = map[string]POILandmark{
"center": {Name: "City Center", X: 2260.0, Y: 15.0, Z: 1380.0, Facing: 0.0},
"police": {Name: "Lixi Police Station", X: 2285.0, Y: 10.0, Z: 1405.0, Facing: 90.0},
"smokey": {Name: "Smokey Drift Garage", X: 2260.5, Y: 0.0, Z: 1374.3, Facing: 90.0},
"beach": {Name: "Bayside Beach", X: 2800.0, Y: 0.0, Z: 1296.0, Facing: 270.0},
"subway": {Name: "Metro Station", X: 2150.0, Y: -10.0, Z: 1520.0, Facing: 180.0},
"rooftop": {Name: "Skyline Rooftop", X: 2310.0, Y: 85.0, Z: 1350.0, Facing: 45.0},
}
type AdminBridge struct {
server *Server
}
func NewAdminBridge(srv *Server) *AdminBridge {
return &AdminBridge{server: srv}
}
// ExecuteCommand executes a GM command string on active sessions.
func (ab *AdminBridge) ExecuteCommand(cmdLine string) (string, error) {
cmdLine = strings.TrimSpace(cmdLine)
if cmdLine == "" {
return "", fmt.Errorf("empty command")
}
parts := strings.Fields(cmdLine)
cmd := strings.ToLower(parts[0])
sessions := ab.server.GetActiveSessions()
if len(sessions) == 0 {
return "", fmt.Errorf("no active players in the game world")
}
targetSess := sessions[0] // Apply to primary active player
switch cmd {
case "teleport", "tp":
if len(parts) < 2 {
return "", fmt.Errorf("usage: teleport <poi_name> OR teleport <x> <y> <z>")
}
if poi, ok := KnownLandmarks[strings.ToLower(parts[1])]; ok {
_ = targetSess.Teleport(poi.X, poi.Y, poi.Z, poi.Facing)
return fmt.Sprintf("Teleported to %s (%.1f, %.1f, %.1f)", poi.Name, poi.X, poi.Y, poi.Z), nil
}
if len(parts) >= 4 {
var x, y, z float32
_, _ = fmt.Sscanf(parts[1], "%f", &x)
_, _ = fmt.Sscanf(parts[2], "%f", &y)
_, _ = fmt.Sscanf(parts[3], "%f", &z)
_ = targetSess.Teleport(x, y, z, 0)
return fmt.Sprintf("Teleported to coordinates (%.1f, %.1f, %.1f)", x, y, z), nil
}
return "", fmt.Errorf("unknown POI landmark '%s'", parts[1])
case "weather":
if len(parts) < 2 {
return "", fmt.Errorf("usage: weather <id> (e.g. 1=Sunny, 2=Rain, 3=Fog)")
}
var wId uint32
_, _ = fmt.Sscanf(parts[1], "%d", &wId)
ab.server.SetWorldWeather(wId)
return fmt.Sprintf("World weather set to ID %d", wId), nil
case "time":
if len(parts) < 2 {
return "", fmt.Errorf("usage: time <hour> (e.g. 12.0 for noon, 0.0 for midnight)")
}
var hour float32
_, _ = fmt.Sscanf(parts[1], "%f", &hour)
ab.server.SetWorldTimeOfDay(hour)
return fmt.Sprintf("World time set to %.1f:00", hour), nil
case "wanted":
if len(parts) < 2 {
return "", fmt.Errorf("usage: wanted <stars> (0-5)")
}
var stars uint32
_, _ = fmt.Sscanf(parts[1], "%d", &stars)
targetSess.wantedLevel = stars
w := protocol.NewUxWriter()
w.U64(targetSess.pid).U32(stars)
_ = targetSess.NotifyData(protocol.MethodGetMethodIDDefault("SyncWantedLevel", 68582860), w.Bytes())
return fmt.Sprintf("Player wanted level set to %d stars", stars), nil
case "spawn_vehicle", "veh":
var templateId uint32 = 1001
if len(parts) >= 2 {
_, _ = fmt.Sscanf(parts[1], "%d", &templateId)
}
vehUnitId := uint64(0x6000000000000000) | uint64(len(ab.server.sessions)+1)
targetSess.activeVehicleId = vehUnitId
w := protocol.NewUxWriter()
w.U64(vehUnitId).U32(templateId).F32(targetSess.posX + 3.0).F32(targetSess.posY).F32(targetSess.posZ + 3.0).F32(targetSess.facing)
_ = targetSess.NotifyData(protocol.MethodGetMethodIDDefault("SyncSpawnVehicle", 68582851), w.Bytes())
return fmt.Sprintf("Spawned vehicle template %d (unit %d)", templateId, vehUnitId), nil
case "spawn_enemy":
_ = ab.server.handleGmWorldRefreshEnemyGroup(targetSess, &protocol.RpcPacket{Body: nil})
return "Spawned combat enemy squad near player", nil
case "switch", "char":
if len(parts) < 2 {
return "", fmt.Errorf("usage: switch <spirit_template_id>")
}
var newId uint32
_, _ = fmt.Sscanf(parts[1], "%d", &newId)
// Perform clean character swap
newUnitId := targetSess.unitId + 1
_ = targetSess.NotifyData(protocol.MethodSyncLogicAgentLeave, protocol.EncodeLogicAgentLeave(targetSess.unitId))
_ = targetSess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(newUnitId))
_ = targetSess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(newUnitId, targetSess.pid, 0))
newProj := protocol.EncodeRaidBattleUnitSpirit(newUnitId, newId, targetSess.posX, targetSess.posY, targetSess.posZ, targetSess.facing, targetSess.pid)
_ = targetSess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, newProj)
_ = targetSess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(targetSess.pid, newId, newUnitId, true))
targetSess.templateId = newId
targetSess.unitId = newUnitId
return fmt.Sprintf("Switched player character to template %d", newId), nil
case "fog_unlock":
wFog := protocol.NewUxWriter()
wFog.U32(uint32(targetSess.activeScene)).Bool(true)
_ = targetSess.NotifyData(protocol.MethodSyncSceneFogMapAllUnlock, wFog.Bytes())
return "All map fog unlocked", nil
case "dive_unlock":
wDive := protocol.NewUxWriter()
wDive.UxString("EnableDive").U8(0xFF).Int7(1).UxString("true")
_ = targetSess.NotifyData(protocol.MethodSyncRunGmUtils, wDive.Bytes())
return "Diving mechanics enabled", nil
default:
log.Printf("[ADMIN] Unrecognized GM command: %s", cmd)
return "", fmt.Errorf("unknown command '%s'", cmd)
}
}
+113
View File
@@ -0,0 +1,113 @@
package gameserver
import (
"log"
"ananta-ps/pkg/protocol"
)
type EnemyUnit struct {
ID uint64 `json:"id"`
TemplateID uint32 `json:"templateId"`
PosX float32 `json:"posX"`
PosY float32 `json:"posY"`
PosZ float32 `json:"posZ"`
Facing float32 `json:"facing"`
HP float32 `json:"hp"`
MaxHP float32 `json:"maxHp"`
}
func (s *Server) handleGmWorldRefreshEnemyGroup(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
groupId, _ := r.U32()
if groupId == 0 {
groupId = 2001
}
log.Printf("[COMBAT] GmWorldRefreshEnemyGroup groupId=%d near player pos (%.1f,%.1f,%.1f)",
groupId, sess.posX, sess.posY, sess.posZ)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
// Spawn 3 combat enemies near the player
enemyTemplates := []uint32{200101, 200102, 200103}
for i, tId := range enemyTemplates {
enemyUnitId := uint64(0x3000000000000000) | uint64(i+1)
ex := sess.posX + float32(i*4) - 4.0
ey := sess.posY
ez := sess.posZ + 6.0
// SyncRaidBattleUnitSpirit
proj := protocol.EncodeRaidBattleUnitSpirit(enemyUnitId, tId, ex, ey, ez, 180.0, 0)
_ = sess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, proj)
// SyncUnitHp
wHp := protocol.NewUxWriter()
wHp.U64(enemyUnitId).F32(10000.0).F32(10000.0) // currentHp, maxHp
_ = sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncUnitHp", 68582870), wHp.Bytes())
}
return nil
}
func (s *Server) handleReportUnitHitFly(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
targetUnitId, _ := r.U64()
stiffId, _ := r.U32()
speed, _ := r.F32()
log.Printf("[COMBAT] ReportUnitHitFly target=%d stiffId=%d speed=%.2f", targetUnitId, stiffId, speed)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleReportUnitHitFlyEnd(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
targetUnitId, _ := r.U64()
log.Printf("[COMBAT] ReportUnitHitFlyEnd target=%d", targetUnitId)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleReportUnitFallGround(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
targetUnitId, _ := r.U64()
log.Printf("[COMBAT] ReportUnitFallGround target=%d", targetUnitId)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleReportUnitFallGroundEnd(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
targetUnitId, _ := r.U64()
log.Printf("[COMBAT] ReportUnitFallGroundEnd target=%d", targetUnitId)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleAskControlPowerHoldEnemyUp(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
enemyId, _ := r.U64()
log.Printf("[COMBAT] Telekinesis Grab enemy=%d", enemyId)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleAskControlPowerHoldEnemyFallGrou(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
enemyId, _ := r.U64()
log.Printf("[COMBAT] Telekinesis Slam enemy=%d", enemyId)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleAskPlayerAttack(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
comboIdx, _ := r.U32()
targetId, _ := r.U64()
log.Printf("[COMBAT] PlayerAttack combo=%d target=%d", comboIdx, targetId)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleAskPlayerCastSkill(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
skillId, _ := r.U32()
log.Printf("[COMBAT] PlayerCastSkill skill=%d", skillId)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
+128
View File
@@ -0,0 +1,128 @@
package gameserver
import (
"log"
"ananta-ps/pkg/protocol"
)
type UrbanCrowdEngine struct {
server *Server
crowdDensity float32
}
func NewUrbanCrowdEngine(srv *Server) *UrbanCrowdEngine {
return &UrbanCrowdEngine{
server: srv,
crowdDensity: 1.0,
}
}
func (s *Server) handleAskSetCrowdDensity(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
density, _ := r.F32()
log.Printf("[CROWD] Set crowd density = %.2f", density)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
w := protocol.NewUxWriter()
w.F32(density)
_ = sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncSetCrowdDensity", 68996045), w.Bytes())
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncCrowdSpecifiedDensityRefresh", 68195167), w.Bytes())
}
// Dialog RPC Handlers
func (s *Server) handleAskStartDialog(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
npcId, _ := r.U64()
dialogId, _ := r.U32()
log.Printf("[DIALOG] AskStartDialog npc=%d dialog=%d", npcId, dialogId)
// Return dialog session id
wRet := protocol.NewUxWriter()
wRet.U32(dialogId)
return sess.ReturnData(pkt.InvokeID, pkt.MethodID, wRet.Bytes())
}
func (s *Server) handleAskDoDialogAction(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
dialogId, _ := r.U32()
actionId, _ := r.U32()
log.Printf("[DIALOG] AskDoDialogAction dialog=%d action=%d", dialogId, actionId)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleAskForceFinishDialog(sess *Session, pkt *protocol.RpcPacket) error {
log.Printf("[DIALOG] AskForceFinishDialog")
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleGetCharacterRandomDialog(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
charId, _ := r.U32()
log.Printf("[DIALOG] GetCharacterRandomDialog for character=%d", charId)
wRet := protocol.NewUxWriter()
wRet.U32(10001) // Default random dialog ID
return sess.ReturnData(pkt.InvokeID, pkt.MethodID, wRet.Bytes())
}
func (s *Server) handleAskAgentStartNpcSound(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
npcId, _ := r.U64()
soundId, _ := r.U32()
log.Printf("[NPC] AgentStartNpcSound npc=%d sound=%d", npcId, soundId)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleAskDoNpcAction(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
npcId, _ := r.U64()
actionName, _ := r.UxString()
log.Printf("[NPC] DoNpcAction npc=%d action='%s'", npcId, actionName)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
// Phone & Contacts
func (s *Server) handleAskPhoneAddCallRecord(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
contactId, _ := r.U32()
log.Printf("[PHONE] AddCallRecord contactId=%d", contactId)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleAskPhoneContactOptionAction(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
contactId, _ := r.U32()
optionId, _ := r.U32()
log.Printf("[PHONE] ContactOptionAction contact=%d option=%d", contactId, optionId)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
// Tab-Wheel Companion / Boss Summoning
func (s *Server) handleAskSummonGangMember(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
charId, _ := r.U32()
if charId == 0 {
charId = 1002
}
companionUnitId := uint64(0x5500000000000000) | uint64(charId)
log.Printf("[COMPANION] SummonGangMember charId=%d unitId=%d for player=%d", charId, companionUnitId, sess.pid)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
// Spawn companion in the world
proj := protocol.EncodeRaidBattleUnitSpirit(companionUnitId, charId, sess.posX+2.0, sess.posY, sess.posZ+2.0, sess.facing, sess.pid)
return sess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, proj)
}
func (s *Server) handleAskDestroyAllGangMember(sess *Session, pkt *protocol.RpcPacket) error {
log.Printf("[COMPANION] DestroyAllGangMember for player=%d", sess.pid)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
+45
View File
@@ -0,0 +1,45 @@
package gameserver
import (
"log"
"ananta-ps/pkg/protocol"
)
func (s *Server) handleAskSetSpiritFashionsWithSource(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
charId, _ := r.U32()
fashionId, _ := r.U32()
log.Printf("[WARDROBE] SetSpiritFashions char=%d fashion=%d", charId, fashionId)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
w := protocol.NewUxWriter()
w.U32(charId).U32(fashionId)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncSetSpiritFashions", 68582900), w.Bytes())
}
func (s *Server) handleAskApplyFashionColoringSchemeInfos(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
fashionId, _ := r.U32()
slotId, _ := r.U32()
colorHex, _ := r.U32()
log.Printf("[WARDROBE] ApplyFashionColoring fashion=%d slot=%d color=0x%08X", fashionId, slotId, colorHex)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleAskSetWeaponSkins(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
weaponId, _ := r.U32()
skinId, _ := r.U32()
log.Printf("[WEAPON] SetWeaponSkins weapon=%d skin=%d", weaponId, skinId)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleAskSyncWeaponSkinToSpirits(sess *Session, pkt *protocol.RpcPacket) error {
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
+135
View File
@@ -0,0 +1,135 @@
package gameserver
import (
"log"
"ananta-ps/pkg/protocol"
)
// Chinese Chess Board (9x10 Xiangqi)
type ChineseChessGame struct {
Board [10][9]int8
}
func NewChineseChessGame() *ChineseChessGame {
g := &ChineseChessGame{}
g.Reset()
return g
}
func (g *ChineseChessGame) Reset() {
// Standard Xiangqi starting pieces:
// Red: 1-King, 2-Advisor, 3-Elephant, 4-Horse, 5-Chariot, 6-Cannon, 7-Soldier
// Black: -1 to -7
g.Board = [10][9]int8{
{-5, -4, -3, -2, -1, -2, -3, -4, -5},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, -6, 0, 0, 0, 0, 0, -6, 0},
{-7, 0, -7, 0, -7, 0, -7, 0, -7},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{7, 0, 7, 0, 7, 0, 7, 0, 7},
{0, 6, 0, 0, 0, 0, 0, 6, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{5, 4, 3, 2, 1, 2, 3, 4, 5},
}
}
func (s *Server) handleAskChineseChessMove(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
fromX, _ := r.U8()
fromY, _ := r.U8()
toX, _ := r.U8()
toY, _ := r.U8()
log.Printf("[CHESS] ChineseChess move from (%d,%d) to (%d,%d)", fromX, fromY, toX, toY)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
w := protocol.NewUxWriter()
w.U8(fromX).U8(fromY).U8(toX).U8(toY)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncChineseChessMove", 68582880), w.Bytes())
}
// Mahjong Solo
func (s *Server) handleAskMahjongAction(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
actionType, _ := r.U32()
tileId, _ := r.U32()
log.Printf("[MAHJONG] Mahjong action=%d tile=%d", actionType, tileId)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
w := protocol.NewUxWriter()
w.U32(actionType).U32(tileId)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncMahjongAction", 68582881), w.Bytes())
}
// Balloon Shooting Game
func (s *Server) handleEnterBalloonZone(sess *Session, pkt *protocol.RpcPacket) error {
log.Printf("[BALLOON] Player entered balloon shooting zone")
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
// SyncGameGroundZoneInfo for Balloon game
wZone := protocol.NewUxWriter()
wZone.U32(3).U64(0x9000000000000001).Bool(true)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncGameGroundZoneInfo", 68004609), wZone.Bytes())
}
func (s *Server) handleLeaveBalloon(sess *Session, pkt *protocol.RpcPacket) error {
log.Printf("[BALLOON] Player left balloon zone")
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleAskBreakDestructibleObject(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
objId, _ := r.U64()
sess.balloonScore += 10
log.Printf("[BALLOON] BreakDestructibleObject id=%d totalScore=%d", objId, sess.balloonScore)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
wScore := protocol.NewUxWriter()
wScore.U64(objId).U32(sess.balloonScore)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncBalloonScore", 68582882), wScore.Bytes())
}
// Bowling Alley
func (s *Server) handleEnterBowlingZone(sess *Session, pkt *protocol.RpcPacket) error {
log.Printf("[BOWLING] Player entered bowling zone")
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
wZone := protocol.NewUxWriter()
wZone.U32(1).U64(0x9000000000000002).Bool(true)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncGameGroundZoneInfo", 68004609), wZone.Bytes())
}
// Claw Machine
func (s *Server) handleAskClawBuyTicket(sess *Session, pkt *protocol.RpcPacket) error {
log.Printf("[CLAW] Bought claw machine ticket")
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleAskClawSettlement(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
isSuccess, _ := r.Bool()
log.Printf("[CLAW] Claw settlement success=%v", isSuccess)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleAskBuyMiniGameTicket(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
ticketType, _ := r.U32()
log.Printf("[MINIGAME] BuyMiniGameTicket type=%d", ticketType)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
+84
View File
@@ -0,0 +1,84 @@
package gameserver
import (
"log"
"ananta-ps/pkg/protocol"
)
func (s *Server) handleAskSetWantedLevel(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
wantedLevel, _ := r.U32()
sess.wantedLevel = wantedLevel
log.Printf("[POLICE] Wanted level set to %d for player %d", wantedLevel, sess.pid)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
w := protocol.NewUxWriter()
w.U64(sess.pid).U32(wantedLevel)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncWantedLevel", 68582860), w.Bytes())
}
func (s *Server) handleAskPoliceArrest(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
officerId, _ := r.U64()
log.Printf("[POLICE] Officer %d arresting player %d", officerId, sess.pid)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
// Trigger Arrest Cutscene
w := protocol.NewUxWriter()
w.U64(sess.unitId).U64(officerId).Bool(true)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncPoliceArrestCutscene", 68582861), w.Bytes())
}
func (s *Server) handleReportPoliceArrestFinish(sess *Session, pkt *protocol.RpcPacket) error {
log.Printf("[POLICE] ReportPoliceArrestFinish received, teleporting player %d to prison", sess.pid)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
sess.wantedLevel = 0
// Teleport to Lixi Police Department Prison Cell
prisonX, prisonY, prisonZ, facing := float32(2300.0), float32(10.0), float32(1420.0), float32(180.0)
_ = sess.Teleport(prisonX, prisonY, prisonZ, facing)
wWanted := protocol.NewUxWriter()
wWanted.U64(sess.pid).U32(0)
_ = sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncWantedLevel", 68582860), wWanted.Bytes())
wPrison := protocol.NewUxWriter()
wPrison.U64(sess.pid).Bool(true)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncPolicePrisonStatus", 68582862), wPrison.Bytes())
}
func (s *Server) handleAskPayFine(sess *Session, pkt *protocol.RpcPacket) error {
log.Printf("[POLICE] AskPayFine received, clearing criminal record for player %d", sess.pid)
sess.wantedLevel = 0
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
w := protocol.NewUxWriter()
w.U64(sess.pid).U32(0)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncWantedLevel", 68582860), w.Bytes())
}
func (s *Server) handleAskReleaseFromPrison(sess *Session, pkt *protocol.RpcPacket) error {
log.Printf("[POLICE] AskReleaseFromPrison, releasing player %d to police station lobby", sess.pid)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
lobbyX, lobbyY, lobbyZ, facing := float32(2285.0), float32(10.0), float32(1405.0), float32(90.0)
_ = sess.Teleport(lobbyX, lobbyY, lobbyZ, facing)
wPrison := protocol.NewUxWriter()
wPrison.U64(sess.pid).Bool(false)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncPolicePrisonStatus", 68582862), wPrison.Bytes())
}
+77
View File
@@ -0,0 +1,77 @@
package gameserver
import (
"log"
"time"
"ananta-ps/pkg/protocol"
)
type RacingSession struct {
TaskID uint32
VehicleID uint64
CurrentGate uint32
TotalGates uint32
StartTime time.Time
IsActive bool
}
func (s *Server) handleAskAcceptAndSetCurrentTask(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
taskId, _ := r.U32()
log.Printf("[RACING] AskAcceptAndSetCurrentTask taskId=%d", taskId)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
if taskId == 60000147 { // Ace Driver - City Night Race
sess.racing = &RacingSession{
TaskID: taskId,
CurrentGate: 0,
TotalGates: 12,
StartTime: time.Now(),
IsActive: true,
}
log.Printf("[RACING] Ignited City Night Race (12 checkpoint gates) for player %d", sess.pid)
// SyncRaceCountdown (3, 2, 1, GO)
wRace := protocol.NewUxWriter()
wRace.U32(taskId).U32(12).U32(3) // 3 seconds countdown
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncRaceCountdown", 68582890), wRace.Bytes())
}
return nil
}
func (s *Server) handleAskSetRacingCompetitionVehicle(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
vehUnitId, _ := r.U64()
log.Printf("[RACING] Bound competition vehicle %d", vehUnitId)
if sess.racing != nil {
sess.racing.VehicleID = vehUnitId
}
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleAskSetTaskCounterValue(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
taskId, _ := r.U32()
counterVal, _ := r.U32()
log.Printf("[RACING] Checkpoint passed task=%d gate=%d", taskId, counterVal)
if sess.racing != nil {
sess.racing.CurrentGate = counterVal
if counterVal >= sess.racing.TotalGates {
duration := time.Since(sess.racing.StartTime).Seconds()
log.Printf("[RACING] Race finished in %.2f seconds!", duration)
sess.racing.IsActive = false
// SyncRaceFinish
wFin := protocol.NewUxWriter()
wFin.U32(taskId).F32(float32(duration)).Bool(true)
_ = sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncRaceFinish", 68582891), wFin.Bytes())
}
}
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
+222 -45
View File
@@ -15,31 +15,41 @@ import (
)
type Server struct {
cfg *config.Config
templatePath string
ln net.Listener
cancelFunc context.CancelFunc
wg sync.WaitGroup
sessions map[string]*Session
mu sync.RWMutex
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
}
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
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
}
func New(cfg *config.Config, templatePath string) *Server {
@@ -55,17 +65,29 @@ func New(cfg *config.Config, templatePath string) *Server {
}
}
return &Server{
srv := &Server{
cfg: cfg,
templatePath: templatePath,
sessions: make(map[string]*Session),
}
srv.trafficEngine = NewCityTrafficEngine(srv)
srv.crowdEngine = NewUrbanCrowdEngine(srv)
srv.adminBridge = NewAdminBridge(srv)
return srv
}
func (s *Server) GetAdminBridge() *AdminBridge {
return s.adminBridge
}
func (s *Server) Start() error {
ctx, cancel := context.WithCancel(context.Background())
s.cancelFunc = cancel
// Load Road Network for city traffic simulation
_ = s.trafficEngine.LoadRoadNetwork("data/client_data/World/RoadNetwork.json")
s.trafficEngine.Start(ctx)
addr := fmt.Sprintf("%s:%d", s.cfg.Network.BindHost, s.cfg.Network.GamePort)
ln, err := net.Listen("tcp", addr)
if err != nil {
@@ -81,6 +103,9 @@ func (s *Server) Start() error {
}
func (s *Server) Stop() error {
if s.trafficEngine != nil {
s.trafficEngine.Stop()
}
if s.cancelFunc != nil {
s.cancelFunc()
}
@@ -217,7 +242,6 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn) {
return
}
// Frame mode 0x03 is heartbeat
if frame.Mode == 0x03 {
_ = protocol.WriteFrame(conn, 0x03, frame.Payload, sess.sendCipher)
continue
@@ -225,23 +249,23 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn) {
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)
log.Printf("[GAME-SERVER] Dispatch error for method %d (%s): %v",
packet.MethodID, protocol.GetMethodName(packet.MethodID), err)
}
}
}
func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
switch pkt.MethodID {
// ---------------- Login / World Entry ----------------
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)
@@ -249,7 +273,6 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
log.Printf("[WORLD] Warning: player_info_template.bin load error: %v", err)
}
// 3. SyncEnterScene
enterScenePayload := protocol.EncodeEnterScene(
sess.pid,
sess.activeRaid,
@@ -272,18 +295,10 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
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:
@@ -292,25 +307,20 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
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())
@@ -320,6 +330,7 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
w.U64(sess.pid).F64(1.0)
return sess.NotifyData(protocol.MethodSyncPlayerLoadRate, w.Bytes())
// ---------------- Movement & Position ----------------
case protocol.MethodAskMoveStart, protocol.MethodAskMoveStop:
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
@@ -336,11 +347,11 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
}
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
// ---------------- Character Switching ----------------
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 {
@@ -369,13 +380,179 @@ func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error {
sess.unitId = newUnitId
return nil
case protocol.MethodAskSummonVehicle, protocol.MethodAskDriveVehicle, protocol.MethodAskExitVehicle:
// ---------------- 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)
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:
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
case protocol.MethodAskPlayerAttack, protocol.MethodAskPlayerCastSkill, protocol.MethodAskUnlockFightStyle:
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
// ---------------- 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)
default:
// Safe neutral typed fallback for unrouted RPC invokes
if pkt.Kind == protocol.PacketKindInvoke {
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
+222
View File
@@ -0,0 +1,222 @@
package gameserver
import (
"log"
"math"
"strings"
"time"
"ananta-ps/pkg/protocol"
)
const (
CarShopTeleportConfig uint32 = 8
JiguanTeleportConfig uint32 = 2
GarageExitTeleportConfig uint32 = 7
)
type Vec3 struct {
X float32
Y float32
Z float32
}
type ShopAnchor struct {
Entrance Vec3
Interior Vec3
Facing float32
Leave Vec3
LeaveYaw float32
}
var defaultShopInteriors = []ShopAnchor{
{
Entrance: Vec3{X: 2260.5, Y: 0, Z: 1374.3},
Interior: Vec3{X: 2271.5, Y: 0, Z: 1374.3},
Facing: 90.0,
Leave: Vec3{X: 2240.0, Y: 0, Z: 1374.3},
LeaveYaw: 270.0,
},
{
Entrance: Vec3{X: 2800.0, Y: 0, Z: 1296.0},
Interior: Vec3{X: 2817.3, Y: 0, Z: 1296.7},
Facing: 270.0,
Leave: Vec3{X: 2780.0, Y: 0, Z: 1296.0},
LeaveYaw: 90.0,
},
}
type PreTeleportOption struct {
ConfigID uint32
Position Vec3
Facing float32
BeforeResName string
AfterResName string
BeforePosition Vec3
BeforeRot Vec3
AfterPosition Vec3
AfterRot Vec3
CustomBefore bool
CustomAfter bool
}
func (s *Server) handleAskTeleport(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
opt := decodePreTeleportOption(r)
log.Printf("[TELEPORT] AskTeleport config=%d before='%s' after='%s' pos=(%.1f,%.1f,%.1f)",
opt.ConfigID, opt.BeforeResName, opt.AfterResName, opt.Position.X, opt.Position.Y, opt.Position.Z)
now := time.Now().UnixMilli()
switch opt.ConfigID {
case CarShopTeleportConfig:
isExit := strings.Contains(strings.ToLower(opt.BeforeResName), "out")
if !isExit && sess.lastShopExitMs != 0 && (now-sess.lastShopExitMs < 4000) {
log.Printf("[TELEPORT] Suppressed car shop enter debounce (within 4s of exit)")
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
shop := findNearestShop(Vec3{X: sess.posX, Y: sess.posY, Z: sess.posZ})
if opt.Position.X == 0 && opt.Position.Y == 0 && opt.Position.Z == 0 {
if isExit {
opt.Position = shop.Leave
opt.Facing = shop.LeaveYaw
opt.AfterPosition = shop.Leave
opt.AfterRot = Vec3{X: 0, Y: shop.LeaveYaw, Z: 0}
} else {
opt.Position = shop.Interior
opt.Facing = shop.Facing
opt.AfterPosition = shop.Interior
opt.AfterRot = Vec3{X: 0, Y: shop.Facing, Z: 0}
}
}
if isExit {
sess.lastShopExitMs = now
}
case JiguanTeleportConfig:
// Elevator landing
if opt.Position.X == 0 && opt.Position.Y == 0 && opt.Position.Z == 0 {
opt.Position = Vec3{X: sess.posX + 5.0, Y: sess.posY, Z: sess.posZ}
opt.AfterPosition = opt.Position
opt.CustomAfter = true
}
case GarageExitTeleportConfig:
// Garage exit
if opt.Position.X == 0 && opt.Position.Y == 0 && opt.Position.Z == 0 {
opt.Position = Vec3{X: 2280.0, Y: 10.0, Z: 1400.0}
opt.AfterPosition = opt.Position
}
}
sess.pendingTeleport = opt
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
// Echo SyncPreTeleportOption
w := protocol.NewUxWriter()
encodePreTeleportOption(w, opt)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncPreTeleportOption", 68582739), w.Bytes())
}
func (s *Server) handleReportPreTeleportFinish(sess *Session, pkt *protocol.RpcPacket) error {
log.Printf("[TELEPORT] ReportPreTeleportFinish received, applying teleport destination")
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
opt := sess.pendingTeleport
if opt != nil {
dest := opt.Position
if dest.X != 0 || dest.Y != 0 || dest.Z != 0 {
_ = sess.Teleport(dest.X, dest.Y, dest.Z, opt.Facing)
}
}
return nil
}
func (s *Server) handleReportPostTeleportFinish(sess *Session, pkt *protocol.RpcPacket) error {
log.Printf("[TELEPORT] ReportPostTeleportFinish completed")
sess.pendingTeleport = nil
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
func (s *Server) handleAskEnterRaidByMapEntrance(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
entranceID, _ := r.U32()
log.Printf("[TELEPORT] AskEnterRaidByMapEntrance entranceID=%d", entranceID)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
// SyncTeleport to entrance target
w := protocol.NewUxWriter()
w.U64(sess.unitId).U32(entranceID).F32(sess.posX).F32(sess.posY).F32(sess.posZ).F32(sess.facing)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncTeleport", 68582740), w.Bytes())
}
func findNearestShop(pos Vec3) ShopAnchor {
best := defaultShopInteriors[0]
bestDist := float64(math.MaxFloat64)
for _, shop := range defaultShopInteriors {
dx := float64(shop.Entrance.X - pos.X)
dz := float64(shop.Entrance.Z - pos.Z)
d := dx*dx + dz*dz
if d < bestDist {
bestDist = d
best = shop
}
}
return best
}
func decodePreTeleportOption(r *protocol.UxReader) *PreTeleportOption {
opt := &PreTeleportOption{}
if r == nil || r.Remaining() == 0 {
return opt
}
opt.ConfigID, _ = r.U32()
opt.Position.X, _ = r.F32()
opt.Position.Y, _ = r.F32()
opt.Position.Z, _ = r.F32()
opt.Facing, _ = r.F32()
opt.BeforeResName, _ = r.UxString()
opt.AfterResName, _ = r.UxString()
opt.BeforePosition.X, _ = r.F32()
opt.BeforePosition.Y, _ = r.F32()
opt.BeforePosition.Z, _ = r.F32()
opt.BeforeRot.X, _ = r.F32()
opt.BeforeRot.Y, _ = r.F32()
opt.BeforeRot.Z, _ = r.F32()
opt.AfterPosition.X, _ = r.F32()
opt.AfterPosition.Y, _ = r.F32()
opt.AfterPosition.Z, _ = r.F32()
opt.AfterRot.X, _ = r.F32()
opt.AfterRot.Y, _ = r.F32()
opt.AfterRot.Z, _ = r.F32()
opt.CustomBefore, _ = r.Bool()
opt.CustomAfter, _ = r.Bool()
return opt
}
func encodePreTeleportOption(w *protocol.UxWriter, opt *PreTeleportOption) {
if opt == nil {
return
}
w.U32(opt.ConfigID)
w.F32(opt.Position.X).F32(opt.Position.Y).F32(opt.Position.Z)
w.F32(opt.Facing)
w.UxString(opt.BeforeResName)
w.UxString(opt.AfterResName)
w.F32(opt.BeforePosition.X).F32(opt.BeforePosition.Y).F32(opt.BeforePosition.Z)
w.F32(opt.BeforeRot.X).F32(opt.BeforeRot.Y).F32(opt.BeforeRot.Z)
w.F32(opt.AfterPosition.X).F32(opt.AfterPosition.Y).F32(opt.AfterPosition.Z)
w.F32(opt.AfterRot.X).F32(opt.AfterRot.Y).F32(opt.AfterRot.Z)
w.Bool(opt.CustomBefore)
w.Bool(opt.CustomAfter)
}
+254
View File
@@ -0,0 +1,254 @@
package gameserver
import (
"context"
"encoding/json"
"log"
"os"
"sync"
"time"
"ananta-ps/pkg/protocol"
)
type TrafficVehicle struct {
ID uint64 `json:"id"`
TemplateID uint32 `json:"templateId"`
LaneID uint32 `json:"laneId"`
Distance float32 `json:"distance"`
Speed float32 `json:"speed"` // meters / sec
LaneLength float32 `json:"laneLength"`
}
type RoadLane struct {
ID uint32 `json:"id"`
Length float32 `json:"length"`
}
type RoadNetworkData struct {
Lanes []RoadLane `json:"lanes"`
}
type CityTrafficEngine struct {
mu sync.RWMutex
vehicles map[uint64]*TrafficVehicle
lanes []RoadLane
isRunning bool
cancel context.CancelFunc
server *Server
idCounter uint64
maxVehicles int
}
func NewCityTrafficEngine(srv *Server) *CityTrafficEngine {
return &CityTrafficEngine{
vehicles: make(map[uint64]*TrafficVehicle),
server: srv,
idCounter: 0x7000000000000001,
maxVehicles: 20,
}
}
func (te *CityTrafficEngine) LoadRoadNetwork(path string) error {
data, err := os.ReadFile(path)
if err != nil {
// Provide default mock lanes if file is not found
te.lanes = []RoadLane{
{ID: 101, Length: 500.0},
{ID: 102, Length: 650.0},
{ID: 103, Length: 800.0},
{ID: 104, Length: 450.0},
}
log.Printf("[TRAFFIC] Road network file %s not found, initialized %d fallback road lanes", path, len(te.lanes))
return nil
}
var netData RoadNetworkData
if err := json.Unmarshal(data, &netData); err != nil {
return err
}
te.lanes = netData.Lanes
log.Printf("[TRAFFIC] Loaded %d road lanes from %s", len(te.lanes), path)
return nil
}
func (te *CityTrafficEngine) Start(ctx context.Context) {
te.mu.Lock()
if te.isRunning {
te.mu.Unlock()
return
}
ctx, cancel := context.WithCancel(ctx)
te.cancel = cancel
te.isRunning = true
// Seed initial traffic vehicles
templatePool := []uint32{1001, 1002, 1003, 1004, 1005}
for i := 0; i < te.maxVehicles && i < len(te.lanes)*5; i++ {
lane := te.lanes[i%len(te.lanes)]
vID := te.idCounter
te.idCounter++
te.vehicles[vID] = &TrafficVehicle{
ID: vID,
TemplateID: templatePool[i%len(templatePool)],
LaneID: lane.ID,
Distance: float32(i*40) + 10.0,
Speed: 8.0 + float32(i%5)*1.5,
LaneLength: lane.Length,
}
}
te.mu.Unlock()
log.Printf("[TRAFFIC] CityTrafficEngine started with %d simulated AI vehicles", len(te.vehicles))
go te.runLoop(ctx)
}
func (te *CityTrafficEngine) Stop() {
te.mu.Lock()
defer te.mu.Unlock()
if te.cancel != nil {
te.cancel()
}
te.isRunning = false
te.vehicles = make(map[uint64]*TrafficVehicle)
}
func (te *CityTrafficEngine) runLoop(ctx context.Context) {
ticker := time.NewTicker(150 * time.Millisecond) // ~6.7 Hz downsampled traffic lane updates
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
te.tick(0.150)
}
}
}
func (te *CityTrafficEngine) tick(dt float32) {
te.mu.Lock()
if len(te.vehicles) == 0 {
te.mu.Unlock()
return
}
w := protocol.NewUxWriter()
// Encode SyncAetherAIVehicleLaneDatas payload
w.U8(0xFF).Int7(len(te.vehicles))
for _, v := range te.vehicles {
v.Distance += v.Speed * dt
if v.Distance > v.LaneLength {
v.Distance = 0
}
w.U64(v.ID).U32(v.LaneID).F32(v.Distance).F32(v.Speed)
}
payload := w.Bytes()
te.mu.Unlock()
// Broadcast to all active sessions
methodID := protocol.MethodGetMethodIDDefault("SyncAetherAIVehicleLaneDatas", 68582850)
sessions := te.server.GetActiveSessions()
for _, sess := range sessions {
_ = sess.NotifyData(methodID, payload)
}
}
// Vehicle RPC Handlers
func (s *Server) handleAskSummonVehicle(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
vehTemplateId, _ := r.U32()
if vehTemplateId == 0 {
vehTemplateId = 1001
}
vehUnitId := uint64(0x6000000000000000) | uint64(time.Now().UnixNano()&0xFFFFFFFF)
sess.activeVehicleId = vehUnitId
log.Printf("[VEHICLE] AskSummonVehicle template=%d unitId=%d for player=%d", vehTemplateId, vehUnitId, sess.pid)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
// SyncSpawnVehicle
w := protocol.NewUxWriter()
w.U64(vehUnitId).U32(vehTemplateId).F32(sess.posX + 3.0).F32(sess.posY).F32(sess.posZ + 3.0).F32(sess.facing)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncSpawnVehicle", 68582851), w.Bytes())
}
func (s *Server) handleAskDriveVehicle(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
vehUnitId, _ := r.U64()
log.Printf("[VEHICLE] AskDriveVehicle vehUnitId=%d player=%d", vehUnitId, sess.pid)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
sess.isDriving = true
sess.activeVehicleId = vehUnitId
// SyncDriverEnterVehicle
w := protocol.NewUxWriter()
w.U64(vehUnitId).U64(sess.pid).U8(0) // SeatIndex 0 = Driver
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncDriverEnterVehicle", 68582852), w.Bytes())
}
func (s *Server) handleAskExitVehicle(sess *Session, pkt *protocol.RpcPacket) error {
log.Printf("[VEHICLE] AskExitVehicle player=%d vehUnitId=%d", sess.pid, sess.activeVehicleId)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
sess.isDriving = false
// SyncDriverExitVehicle
w := protocol.NewUxWriter()
w.U64(sess.activeVehicleId).U64(sess.pid)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncDriverExitVehicle", 68582853), w.Bytes())
}
func (s *Server) handleAskVehicleMove(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
vehID, _ := r.U64()
x, _ := r.F32()
y, _ := r.F32()
z, _ := r.F32()
facing, _ := r.F32()
sess.posX = x
sess.posY = y
sess.posZ = z
sess.facing = facing
// Echo SyncVehicleTransform to other peers if needed
w := protocol.NewUxWriter()
w.U64(vehID).F32(x).F32(y).F32(z).F32(facing)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncVehicleTransform", 68582854), w.Bytes())
}
func (s *Server) handleChangeVehicleDoorState(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
vehID, _ := r.U64()
doorIdx, _ := r.U8()
isOpen, _ := r.Bool()
log.Printf("[VEHICLE] ChangeVehicleDoorState veh=%d door=%d open=%v", vehID, doorIdx, isOpen)
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
w := protocol.NewUxWriter()
w.U64(vehID).U8(doorIdx).Bool(isOpen)
return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncVehicleDoorState", 68582855), w.Bytes())
}
func (s *Server) handleAskVehicleSendGamePlaySignal(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
vehID, _ := r.U64()
sigType, _ := r.U32()
log.Printf("[VEHICLE] GamePlaySignal veh=%d sigType=%d", vehID, sigType)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
+49
View File
@@ -0,0 +1,49 @@
package gameserver
import (
"log"
"ananta-ps/pkg/protocol"
)
func (s *Server) SetWorldWeather(weatherId uint32) {
log.Printf("[WEATHER] Setting world weather to %d", weatherId)
w := protocol.NewUxWriter()
w.U32(weatherId)
payload := w.Bytes()
methodID := protocol.MethodGetMethodIDDefault("SyncTimeWeather", 68582910)
for _, sess := range s.GetActiveSessions() {
_ = sess.NotifyData(methodID, payload)
}
}
func (s *Server) SetWorldTimeOfDay(hour float32) {
log.Printf("[WEATHER] Setting world time of day to %.1f:00", hour)
w := protocol.NewUxWriter()
w.F32(hour)
payload := w.Bytes()
methodID := protocol.MethodGetMethodIDDefault("SyncTimeOfDay", 68582911)
for _, sess := range s.GetActiveSessions() {
_ = sess.NotifyData(methodID, payload)
}
}
func (s *Server) handleGmUnlockAllFogMap(sess *Session, pkt *protocol.RpcPacket) error {
log.Printf("[FOG] GmUnlockAllFogMap requested")
if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil {
return err
}
wFog := protocol.NewUxWriter()
wFog.U32(uint32(sess.activeScene)).Bool(true)
return sess.NotifyData(protocol.MethodSyncSceneFogMapAllUnlock, wFog.Bytes())
}
func (s *Server) handleGmSetAmbianceEnabled(sess *Session, pkt *protocol.RpcPacket) error {
r := protocol.NewUxReader(pkt.Body)
enabled, _ := r.Bool()
log.Printf("[AMBIENCE] SetAmbianceEnabled = %v", enabled)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}