Files
Ananta-ps/internal/gameserver/admin_bridge.go
T

200 lines
6.9 KiB
Go

package gameserver
import (
"fmt"
"log"
"math"
"strings"
"time"
"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
targetSess.stateMu.Lock()
defer targetSess.stateMu.Unlock()
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 {
if err := targetSess.teleport(poi.X, poi.Y, poi.Z, poi.Facing); err != nil {
return "", err
}
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
if _, err := fmt.Sscanf(parts[1], "%f", &x); err != nil {
return "", fmt.Errorf("invalid x coordinate")
}
if _, err := fmt.Sscanf(parts[2], "%f", &y); err != nil {
return "", fmt.Errorf("invalid y coordinate")
}
if _, err := fmt.Sscanf(parts[3], "%f", &z); err != nil {
return "", fmt.Errorf("invalid z coordinate")
}
if math.IsNaN(float64(x)) || math.IsInf(float64(x), 0) || math.IsNaN(float64(y)) || math.IsInf(float64(y), 0) || math.IsNaN(float64(z)) || math.IsInf(float64(z), 0) {
return "", fmt.Errorf("coordinates must be finite")
}
if err := targetSess.teleport(x, y, z, 0); err != nil {
return "", err
}
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
if _, err := fmt.Sscanf(parts[1], "%d", &wId); err != nil {
return "", fmt.Errorf("invalid weather id")
}
if err := ab.server.SetWorldWeather(wId); err != nil {
return "", err
}
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
if _, err := fmt.Sscanf(parts[1], "%f", &hour); err != nil {
return "", fmt.Errorf("invalid hour")
}
if err := ab.server.SetWorldTimeOfDay(hour); err != nil {
return "", err
}
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)
if err := targetSess.NotifyData(protocol.MethodGetMethodIDDefault("SyncWantedLevel", 68582860), w.Bytes()); err != nil {
return "", err
}
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(time.Now().UnixNano())
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)
if err := targetSess.NotifyData(protocol.MethodGetMethodIDDefault("SyncSpawnVehicle", 68582851), w.Bytes()); err != nil {
return "", err
}
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
if _, err := fmt.Sscanf(parts[1], "%d", &newId); err != nil || newId == 0 {
return "", fmt.Errorf("invalid spirit template")
}
// Perform clean character swap
newUnitId := targetSess.unitId + 1
if err := targetSess.NotifyData(protocol.MethodSyncLogicAgentLeave, protocol.EncodeLogicAgentLeave(targetSess.unitId)); err != nil {
return "", err
}
if err := targetSess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(targetSess.pid, newId, newUnitId, true)); err != nil {
return "", err
}
if err := targetSess.NotifyData(protocol.MethodSyncLogicAgentEnter, protocol.EncodeLogicAgentEnter(newUnitId)); err != nil {
return "", err
}
if err := targetSess.NotifyData(protocol.MethodSyncManagedLogicAgent, protocol.EncodeManagedLogicAgent(newUnitId, targetSess.pid, 0)); err != nil {
return "", err
}
newProj := protocol.EncodeRaidBattleUnitSpirit(newUnitId, newId, targetSess.posX, targetSess.posY, targetSess.posZ, targetSess.facing, targetSess.pid)
if err := targetSess.NotifyData(protocol.MethodSyncRaidBattleUnitSpirit, newProj); err != nil {
return "", err
}
if err := targetSess.NotifyData(protocol.MethodSyncPlayerCurrentSpirit, protocol.EncodePlayerCurrentSpirit(targetSess.pid, newId, newUnitId, true)); err != nil {
return "", err
}
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)
if err := targetSess.NotifyData(protocol.MethodSyncSceneFogMapAllUnlock, wFog.Bytes()); err != nil {
return "", err
}
return "All map fog unlocked", nil
case "dive_unlock":
wDive := protocol.NewUxWriter()
wDive.UxString("EnableDive").U8(0xFF).Int7(1).UxString("true")
if err := targetSess.NotifyData(protocol.MethodSyncRunGmUtils, wDive.Bytes()); err != nil {
return "", err
}
return "Diving mechanics enabled", nil
default:
log.Printf("[ADMIN] Unrecognized GM command: %s", cmd)
return "", fmt.Errorf("unknown command '%s'", cmd)
}
}