200 lines
6.9 KiB
Go
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)
|
|
}
|
|
}
|