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:
1 parent
195a1d36f5
commit
dab553d4a2
81 files changed
+8707652
-61
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user