This repository has been archived on 2026-10-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Ananta-ps/internal/gameserver/admin_bridge.go
T

152 lines
5.5 KiB
Go

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)
}
}