From 98bdef072c843fdd8ccdf85a9b54ab9d32d34187 Mon Sep 17 00:00:00 2001
From: historia <[not public]>
Date: Tue, 30 Jun 2026 04:43:54 -0400
Subject: slop refactor
---
cmd/mud/main.go | 19 ++++-
config.yaml | 16 ++--
data/.admin_history.json | 1 +
internal/admin/api_map.go | 48 +++--------
internal/admin/api_rooms.go | 70 ++++++----------
internal/admin/server.go | 39 ++++++---
internal/admin/static/map.js | 184 +++++++++++++++++++++++--------------------
internal/admin/undo.go | 2 +-
internal/config/config.go | 10 +++
9 files changed, 202 insertions(+), 187 deletions(-)
create mode 100644 data/.admin_history.json
diff --git a/cmd/mud/main.go b/cmd/mud/main.go
index 12b842c..990164e 100644
--- a/cmd/mud/main.go
+++ b/cmd/mud/main.go
@@ -78,14 +78,27 @@ func main() {
g.World.AddGroundItem(1, j, 500)
}
+ if cfg.AdminHTTP.Enabled {
+ adminSrv, err := admin.NewServer(cfg, false, g.AccountStore, g.World, g.ItemStore, g.ObjectStore, g.MobStore, dataDir)
+ if err != nil {
+ log.Fatalf("failed to start admin HTTP server: %v", err)
+ }
+ go func() {
+ if err := adminSrv.ListenAndServe(); err != nil {
+ log.Printf("admin HTTP server error: %v", err)
+ }
+ }()
+ log.Printf("Admin HTTP listening on :%d", cfg.AdminHTTP.Port)
+ }
+
if cfg.AdminHTTPS.Enabled {
- adminSrv, err := admin.NewServer(cfg, g.AccountStore, g.World, g.ItemStore, g.ObjectStore, g.MobStore, dataDir)
+ adminSrv, err := admin.NewServer(cfg, true, g.AccountStore, g.World, g.ItemStore, g.ObjectStore, g.MobStore, dataDir)
if err != nil {
- log.Fatalf("failed to start admin server: %v", err)
+ log.Fatalf("failed to start admin HTTPS server: %v", err)
}
go func() {
if err := adminSrv.ListenAndServe(); err != nil {
- log.Printf("admin server error: %v", err)
+ log.Printf("admin HTTPS server error: %v", err)
}
}()
log.Printf("Admin HTTPS listening on :%d", cfg.AdminHTTPS.Port)
diff --git a/config.yaml b/config.yaml
index 29056c1..db6bc0c 100644
--- a/config.yaml
+++ b/config.yaml
@@ -13,21 +13,25 @@ telnet:
enabled: true
port: 4000
-http:
- enabled: true
- port: 8888
-
telnet_tls:
- enabled: false
+ enabled: true
port: 4001
+http:
+ enabled: true
+ port: 8000
+
https:
enabled: false
port: 8443
+admin_http:
+ enabled: false
+ port: 8080
+
admin_https:
enabled: true
- port: 9090
+ port: 8081
default_colors:
room_name: "51 bold"
diff --git a/data/.admin_history.json b/data/.admin_history.json
new file mode 100644
index 0000000..a04a2cb
--- /dev/null
+++ b/data/.admin_history.json
@@ -0,0 +1 @@
+{"history":[{"time":"2026-06-30T04:34:14-04:00","description":"update room 1007 (add exit south to 1021)","file_path":"data/rooms/intro/1007.yaml","old_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1013\n west:\n room: 2004\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1013\n south:\n room: 1021\n west:\n room: 2004\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T04:34:14-04:00","description":"create room 1021","file_path":"data/rooms/intro/1021.yaml","old_content":"","new_content":"exits:\n north: 1007\nid: 1021\nname: 'Room #1021'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T04:34:16-04:00","description":"update room 1021 (add exit south to 1022)","file_path":"data/rooms/intro/1021.yaml","old_content":"exits:\n north: 1007\nid: 1021\nname: 'Room #1021'\n","new_content":"id: 1021\nname: 'Room #1021'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1007\n south:\n room: 1022\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T04:34:16-04:00","description":"create room 1022","file_path":"data/rooms/intro/1022.yaml","old_content":"","new_content":"exits:\n north: 1021\nid: 1022\nname: 'Room #1022'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T04:34:17-04:00","description":"update room 1022 (add exit south to 1023)","file_path":"data/rooms/intro/1022.yaml","old_content":"exits:\n north: 1021\nid: 1022\nname: 'Room #1022'\n","new_content":"id: 1022\nname: 'Room #1022'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1021\n south:\n room: 1023\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T04:34:17-04:00","description":"create room 1023","file_path":"data/rooms/intro/1023.yaml","old_content":"","new_content":"exits:\n north: 1022\nid: 1023\nname: 'Room #1023'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T04:34:20-04:00","description":"delete room 1023 (cleaned 1 exits)","file_path":"data/rooms/intro/1023.yaml","old_content":"exits:\n north: 1022\nid: 1023\nname: 'Room #1023'\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T04:34:22-04:00","description":"delete room 1022 (cleaned 1 exits)","file_path":"data/rooms/intro/1022.yaml","old_content":"id: 1022\nname: 'Room #1022'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1021\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T04:34:24-04:00","description":"delete room 1021 (cleaned 1 exits)","file_path":"data/rooms/intro/1021.yaml","old_content":"id: 1021\nname: 'Room #1021'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1007\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false}],"redo":null}
\ No newline at end of file
diff --git a/internal/admin/api_map.go b/internal/admin/api_map.go
index 5de41a7..7298f1e 100644
--- a/internal/admin/api_map.go
+++ b/internal/admin/api_map.go
@@ -44,7 +44,6 @@ func (s *AdminServer) handleMap(w http.ResponseWriter, r *http.Request) {
Y int `json:"y"`
Name string `json:"name"`
Color string `json:"color"`
- Symbol string `json:"symbol"`
}
type LinkEntry struct {
@@ -249,47 +248,22 @@ func findDisconnectedRooms(s *AdminServer, dir string, seed int, g world.RoomGri
func listRoomIDsInDir(s *AdminServer, dir string) []int {
base := filepath.Join(s.dataDir, "rooms")
- var dirs []string
- if dir == "" {
- dirs = append(dirs, base)
- entries, err := os.ReadDir(base)
- if err != nil {
- return nil
- }
- for _, e := range entries {
- if e.IsDir() {
- dirs = append(dirs, filepath.Join(base, e.Name()))
- subE, err := os.ReadDir(filepath.Join(base, e.Name()))
- if err != nil {
- continue
- }
- for _, se := range subE {
- if se.IsDir() {
- dirs = append(dirs, filepath.Join(base, e.Name(), se.Name()))
- }
- }
- }
- }
- } else {
- dirs = append(dirs, filepath.Join(base, dir))
+ walkRoot := base
+ if dir != "" {
+ walkRoot = filepath.Join(base, dir)
}
var ids []int
- for _, d := range dirs {
- entries, err := os.ReadDir(d)
- if err != nil {
- continue
+ filepath.WalkDir(walkRoot, func(path string, d os.DirEntry, err error) error {
+ if err != nil || d.IsDir() || filepath.Ext(d.Name()) != ".yaml" {
+ return nil
}
- for _, e := range entries {
- if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" {
- continue
- }
- id, err := strconv.Atoi(e.Name()[:len(e.Name())-5])
- if err != nil {
- continue
- }
+ name := d.Name()
+ id, convErr := strconv.Atoi(name[:len(name)-5])
+ if convErr == nil && id > 0 {
ids = append(ids, id)
}
- }
+ return nil
+ })
return ids
}
diff --git a/internal/admin/api_rooms.go b/internal/admin/api_rooms.go
index b60aefc..a477cc3 100644
--- a/internal/admin/api_rooms.go
+++ b/internal/admin/api_rooms.go
@@ -106,7 +106,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
})
}
- exits[string(opp)] = float64(*linkFrom)
+ exits[string(opp)] = *linkFrom
}
}
@@ -161,7 +161,7 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
return
}
m["id"] = id
- writeJSON(w, map[string]any{"room": m, "path": path, "file": path, "raw": string(data)})
+ writeJSON(w, map[string]any{"room": m, "path": path, "file": path})
case http.MethodPut:
path, ok := s.world.GetRoomPath(id)
@@ -298,45 +298,27 @@ func (s *AdminServer) handleRoomDirs(w http.ResponseWriter, r *http.Request) {
return
}
base := filepath.Join(s.dataDir, "rooms")
- entries, err := os.ReadDir(base)
- if err != nil {
- writeJSON(w, []string{""})
- return
- }
dirs := []string{""}
- for _, e := range entries {
- if !e.IsDir() {
- continue
- }
- if hasYAMLFiles(filepath.Join(base, e.Name())) {
- dirs = append(dirs, e.Name())
- }
- subEntries, err := os.ReadDir(filepath.Join(base, e.Name()))
- if err != nil {
- continue
- }
- for _, se := range subEntries {
- if se.IsDir() && hasYAMLFiles(filepath.Join(base, e.Name(), se.Name())) {
- dirs = append(dirs, e.Name()+"/"+se.Name())
+ filepath.WalkDir(base, func(path string, d os.DirEntry, err error) error {
+ if err != nil || !d.IsDir() || path == base {
+ return nil
+ }
+ entries, rdErr := os.ReadDir(path)
+ if rdErr != nil {
+ return nil
+ }
+ for _, e := range entries {
+ if !e.IsDir() && filepath.Ext(e.Name()) == ".yaml" {
+ rel, _ := filepath.Rel(base, path)
+ dirs = append(dirs, rel)
+ return nil
}
}
- }
+ return nil
+ })
writeJSON(w, dirs)
}
-func hasYAMLFiles(dir string) bool {
- entries, err := os.ReadDir(dir)
- if err != nil {
- return false
- }
- for _, e := range entries {
- if !e.IsDir() && filepath.Ext(e.Name()) == ".yaml" {
- return true
- }
- }
- return false
-}
-
func (s *AdminServer) handleRoomLink(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
@@ -377,10 +359,14 @@ func (s *AdminServer) handleRoomLink(w http.ResponseWriter, r *http.Request) {
cA, okA := grid.Coord[body.From]
cB, okB := grid.Coord[body.To]
- if !okA || !okB || cA[2] != cB[2] {
- writeJSON(w, map[string]any{"error": "rooms must be on same z-level"})
- return
- }
+ if !okA || !okB {
+ writeJSON(w, map[string]any{"error": "one or both rooms are not reachable from the seed room"})
+ return
+ }
+ if cA[2] != cB[2] {
+ writeJSON(w, map[string]any{"error": "rooms must be on same z-level"})
+ return
+ }
dx, dy := cB[0]-cA[0], cB[1]-cA[1]
for d, delta := range world.DirectionDeltas3D {
if delta[0] == dx && delta[1] == dy && delta[2] == 0 {
@@ -510,12 +496,6 @@ func (s *AdminServer) handleRoomLink(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]any{"error": "both rooms not found"})
return
}
- if (errA != nil && roomA == nil) {
- roomA = nil
- }
- if (errB != nil && roomB == nil) {
- roomB = nil
- }
var dir, oppDir world.ExitDir
if roomA != nil {
diff --git a/internal/admin/server.go b/internal/admin/server.go
index 8facc4a..823d7ba 100644
--- a/internal/admin/server.go
+++ b/internal/admin/server.go
@@ -34,6 +34,7 @@ var embedded embed.FS
type AdminServer struct {
cfg *config.Config
+ useTLS bool
accountStore *player.AccountStore
world *world.World
itemStore *item.ItemStore
@@ -46,7 +47,7 @@ type AdminServer struct {
cookieSecret []byte
}
-func NewServer(cfg *config.Config, accountStore *player.AccountStore, w *world.World, is *item.ItemStore, os *object.ObjectStore, ms *world.MobStore, dataDir string) (*AdminServer, error) {
+func NewServer(cfg *config.Config, useTLS bool, accountStore *player.AccountStore, w *world.World, is *item.ItemStore, os *object.ObjectStore, ms *world.MobStore, dataDir string) (*AdminServer, error) {
secret := make([]byte, 32)
if _, err := rand.Read(secret); err != nil {
return nil, fmt.Errorf("cookie secret: %w", err)
@@ -62,13 +63,24 @@ func NewServer(cfg *config.Config, accountStore *player.AccountStore, w *world.W
return nil, fmt.Errorf("parse templates: %w", err)
}
- cert, err := loadOrGenerateCert(cfg)
- if err != nil {
- return nil, fmt.Errorf("admin cert: %w", err)
+ var port int
+ if useTLS {
+ port = cfg.AdminHTTPS.Port
+ } else {
+ port = cfg.AdminHTTP.Port
+ }
+
+ var cert tls.Certificate
+ if useTLS {
+ cert, err = loadOrGenerateCert(cfg)
+ if err != nil {
+ return nil, fmt.Errorf("admin cert: %w", err)
+ }
}
s := &AdminServer{
cfg: cfg,
+ useTLS: useTLS,
accountStore: accountStore,
world: w,
itemStore: is,
@@ -159,12 +171,14 @@ func NewServer(cfg *config.Config, accountStore *player.AccountStore, w *world.W
})
s.httpServer = &http.Server{
- Addr: fmt.Sprintf(":%d", cfg.AdminHTTPS.Port),
+ Addr: fmt.Sprintf(":%d", port),
Handler: mux,
- TLSConfig: &tls.Config{
+ }
+ if useTLS {
+ s.httpServer.TLSConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
- },
+ }
}
return s, nil
@@ -175,8 +189,11 @@ func (s *AdminServer) ListenAndServe() error {
if err != nil {
return fmt.Errorf("admin listen: %w", err)
}
- tlsLn := tls.NewListener(ln, s.httpServer.TLSConfig)
- return s.httpServer.Serve(tlsLn)
+ if s.useTLS {
+ tlsLn := tls.NewListener(ln, s.httpServer.TLSConfig)
+ return s.httpServer.Serve(tlsLn)
+ }
+ return s.httpServer.Serve(ln)
}
func (s *AdminServer) authMiddleware(next http.Handler) http.Handler {
@@ -228,7 +245,7 @@ func (s *AdminServer) setAuthCookie(w http.ResponseWriter, account string) {
Value: account + ":" + sig,
Path: "/",
HttpOnly: true,
- Secure: true,
+ Secure: s.useTLS,
SameSite: http.SameSiteStrictMode,
MaxAge: 86400,
})
@@ -270,7 +287,7 @@ func (s *AdminServer) handleLogout(w http.ResponseWriter, r *http.Request) {
Path: "/",
MaxAge: -1,
HttpOnly: true,
- Secure: true,
+ Secure: s.useTLS,
})
http.Redirect(w, r, "/login", http.StatusFound)
}
diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js
index cd53bed..85364d5 100644
--- a/internal/admin/static/map.js
+++ b/internal/admin/static/map.js
@@ -47,7 +47,7 @@ function loadMap() {
if (currentDir) url += '&dir=' + encodeURIComponent(currentDir);
var wasDirChange = dirChangedByUser;
dirChangedByUser = false;
- API.get(url).then(function(data) {
+ return API.get(url).then(function(data) {
mapData = data;
if (!currentDir && data.dir) {
currentDir = data.dir;
@@ -64,11 +64,11 @@ function loadMap() {
}
function blendColors(colorA, colorB) {
+ if (!colorA && !colorB) return '#666666';
+ if (!colorB) return xtermToCss(parseXtermIdx(colorA));
+ if (!colorA) return xtermToCss(parseXtermIdx(colorB));
var idxA = parseXtermIdx(colorA);
var idxB = parseXtermIdx(colorB);
- if (colorA && !colorB) return xtermToCss(idxA);
- if (!colorA && colorB) return xtermToCss(idxB);
- if (!colorA && !colorB) return '#666666';
var rgbA = xtermToRGB(idxA);
var rgbB = xtermToRGB(idxB);
var r = Math.round((rgbA[0] + rgbB[0]) / 2);
@@ -90,17 +90,21 @@ function renderMap(data) {
var W = container.clientWidth;
var H = container.clientHeight;
- roomMap = {};
- if (data.rooms) data.rooms.forEach(function(r) { roomMap[r.id] = r; });
-
- occupied = {};
- if (data.rooms) data.rooms.forEach(function(r) { occupied[r.x+','+r.y] = r.id; });
-
+ roomMap = {}; occupied = {};
upTarget = {}; downTarget = {};
- if (data.upLinks) data.upLinks.forEach(function(l) { upTarget[l.from] = l.to; });
- if (data.downLinks) data.downLinks.forEach(function(l) { downTarget[l.from] = l.to; });
+ var hasUp = {}, hasDown = {};
+
+ if (data.rooms) data.rooms.forEach(function(r) {
+ roomMap[r.id] = r;
+ occupied[r.x+','+r.y] = r.id;
+ });
+ if (data.upLinks) data.upLinks.forEach(function(l) {
+ upTarget[l.from] = l.to; hasUp[l.from] = true;
+ });
+ if (data.downLinks) data.downLinks.forEach(function(l) {
+ downTarget[l.from] = l.to; hasDown[l.from] = true;
+ });
- var svgNS = 'http://www.w3.org/2000/svg';
var g = '';
if (data.links) {
@@ -109,16 +113,11 @@ function renderMap(data) {
if (!fr || !tr) return;
var x1 = fr.x * CELL + CELL/2, y1 = fr.y * CELL + CELL/2;
var x2 = tr.x * CELL + CELL/2, y2 = tr.y * CELL + CELL/2;
- var c = blendColors(fr.color, tr.color);
- c = c.replace('#','');
+ var c = blendColors(fr.color, tr.color).replace('#','');
g += '