package web
import (
"context"
"encoding/json"
"fmt"
"html/template"
"log"
"net"
"net/http"
"sync"
"time"
"ananta-ps/internal/config"
"ananta-ps/internal/gameserver"
)
type AdminServer struct {
cfg *config.Config
gameServer *gameserver.Server
httpSrv *http.Server
cancelFunc context.CancelFunc
mu sync.RWMutex
}
func NewAdmin(cfg *config.Config, gameServer *gameserver.Server) *AdminServer {
return &AdminServer{
cfg: cfg,
gameServer: gameServer,
}
}
func (a *AdminServer) Start() error {
if !a.cfg.Admin.Enabled {
return nil
}
ctx, cancel := context.WithCancel(context.Background())
a.cancelFunc = cancel
mux := http.NewServeMux()
mux.HandleFunc("/", a.handleDashboard)
mux.HandleFunc("/api/teleport", a.handleTeleport)
mux.HandleFunc("/api/command", a.handleCommand)
mux.HandleFunc("/api/sessions", a.handleSessions)
host := a.cfg.Admin.Host
if host == "" {
host = "127.0.0.1"
}
addr := net.JoinHostPort(host, fmt.Sprint(a.cfg.Admin.Port))
a.httpSrv = &http.Server{
Addr: addr,
Handler: mux,
}
ln, err := net.Listen("tcp", addr)
if err != nil {
cancel()
return err
}
go func() {
log.Printf("[ADMIN-GM] Admin Web Panel running at http://%s:%d/", a.cfg.Network.AdvertisedHost, a.cfg.Admin.Port)
if err := a.httpSrv.Serve(ln); err != nil && err != http.ErrServerClosed {
log.Printf("[ADMIN-GM] Web server error: %v", err)
}
}()
_ = ctx
return nil
}
func (a *AdminServer) Stop() error {
if a.cancelFunc != nil {
a.cancelFunc()
}
if a.httpSrv != nil {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_ = a.httpSrv.Shutdown(ctx)
}
return nil
}
func (a *AdminServer) handleDashboard(w http.ResponseWriter, r *http.Request) {
tmpl := `
Ananta GM Admin Dashboard
Server Status
Active Sessions
{{.ActiveSessions}}
Player UID / Account
{{.PlayerPID}} ({{.AccountId}})
Active Character Template
{{.InitialSpirit}}
Free-Roam & Abilities
`
sessions := a.gameServer.GetActiveSessions()
data := map[string]interface{}{
"ActiveSessions": len(sessions),
"PlayerPID": a.cfg.Player.Pid,
"AccountId": a.cfg.Player.AccountId,
"InitialSpirit": a.cfg.Player.InitialSpiritTemplateId,
}
t := template.Must(template.New("dashboard").Parse(tmpl))
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = t.Execute(w, data)
}
func (a *AdminServer) handleTeleport(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
X float32 `json:"x"`
Y float32 `json:"y"`
Z float32 `json:"z"`
Facing float32 `json:"facing"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "msg": err.Error()})
return
}
sessions := a.gameServer.GetActiveSessions()
if len(sessions) == 0 {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "msg": "No active client connected in game"})
return
}
for _, sess := range sessions {
_ = sess.Teleport(req.X, req.Y, req.Z, req.Facing)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "count": len(sessions)})
}
func (a *AdminServer) handleSessions(w http.ResponseWriter, r *http.Request) {
sessions := a.gameServer.GetActiveSessions()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"count": len(sessions),
"host": a.cfg.Network.AdvertisedHost,
"port": a.cfg.Network.GamePort,
})
}
func (a *AdminServer) handleCommand(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Command string `json:"command"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "msg": err.Error()})
return
}
bridge := a.gameServer.GetAdminBridge()
if bridge == nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "msg": "Admin bridge not initialized"})
return
}
res, err := bridge.ExecuteCommand(req.Command)
w.Header().Set("Content-Type", "application/json")
if err != nil {
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "msg": err.Error()})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "result": res})
}