From 6bd5365cc22f84c047c1585e3f54571af46f53e5 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sun, 5 Jul 2026 18:52:51 -0400 Subject: feat: renameable directories and files in admin gui --- data/.admin_history.json | 2 +- data/objects/adamantite_rock.yaml | 5 -- internal/admin/server.go | 75 ++++++++++++++++++++++- internal/admin/static/admin.css | 11 ++++ internal/admin/static/admin.js | 65 ++++++++++++++++++++ internal/admin/static/cardeditor.js | 6 +- internal/admin/static/dropeditor.js | 5 +- internal/admin/static/editor.js | 108 +++++++++++++++++++++++++++++++--- internal/admin/static/hazardeditor.js | 5 +- internal/admin/static/itemeditor.js | 17 ++++-- internal/admin/static/mobeditor.js | 8 ++- internal/admin/static/moduleeditor.js | 8 ++- internal/admin/static/objecteditor.js | 23 +++++--- internal/admin/static/techeditor.js | 2 +- internal/admin/undo.go | 3 + internal/admin/yaml_util.go | 62 ++++++++++++++++++- 16 files changed, 358 insertions(+), 47 deletions(-) diff --git a/data/.admin_history.json b/data/.admin_history.json index d4e355e..65e86d6 100644 --- a/data/.admin_history.json +++ b/data/.admin_history.json @@ -1 +1 @@ -{"history":[{"time":"2026-07-05T15:49:30-04:00","description":"Update object adamantite_rock","file_path":"data/objects/adamantite_rock.yaml","old_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock\nname: adamantite rock\n","new_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock\nname: adamantite rock\nsteal_level: 1\nsteal_speed: 1\nsteal_table: gem_table\nsteal_xp: 1\n","is_delete":false,"is_create":false}],"redo":null} \ No newline at end of file +{"history":[{"time":"2026-07-05T18:39:14-04:00","description":"move objects adamantite_rock3.yaml to root","file_path":"data/objects/test4/adamantite_rock3.yaml","new_file_path":"data/objects/adamantite_rock3.yaml","old_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock_copy\nname: adamantite rock\n","new_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock_copy\nname: adamantite rock\n","is_delete":false,"is_create":false},{"time":"2026-07-05T18:39:14-04:00","description":"delete directory objects/test4","file_path":"data/objects/test4","old_content":"","new_content":"","is_delete":true,"is_create":false},{"time":"2026-07-05T18:39:15-04:00","description":"move objects adamantite_rock3.yaml to root","file_path":"data/objects/test3/adamantite_rock3.yaml","new_file_path":"data/objects/adamantite_rock3.yaml","old_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock_copy\nname: adamantite rock\n","new_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock_copy\nname: adamantite rock\n","is_delete":false,"is_create":false},{"time":"2026-07-05T18:39:15-04:00","description":"delete directory objects/test3","file_path":"data/objects/test3","old_content":"","new_content":"","is_delete":true,"is_create":false},{"time":"2026-07-05T18:40:55-04:00","description":"rename objects adamantite_rock3 to adamantite_rock4","file_path":"data/objects/adamantite_rock3.yaml","new_file_path":"data/objects/adamantite_rock4.yaml","old_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock_copy\nname: adamantite rock\n","new_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock_copy\nname: adamantite rock\n","is_delete":false,"is_create":false},{"time":"2026-07-05T18:41:13-04:00","description":"move objects adamantite_rock4 to test2/","file_path":"data/objects/adamantite_rock4.yaml","new_file_path":"data/objects/test2/adamantite_rock4.yaml","old_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock_copy\nname: adamantite rock\n","new_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock_copy\nname: adamantite rock\n","is_delete":false,"is_create":false},{"time":"2026-07-05T18:41:15-04:00","description":"move objects adamantite_rock4 to test3/","file_path":"data/objects/test2/adamantite_rock4.yaml","new_file_path":"data/objects/test3/adamantite_rock4.yaml","old_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock_copy\nname: adamantite rock\n","new_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock_copy\nname: adamantite rock\n","is_delete":false,"is_create":false},{"time":"2026-07-05T18:41:36-04:00","description":"move objects adamantite_rock4.yaml to root","file_path":"data/objects/test3/adamantite_rock4.yaml","new_file_path":"data/objects/adamantite_rock4.yaml","old_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock_copy\nname: adamantite rock\n","new_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock_copy\nname: adamantite rock\n","is_delete":false,"is_create":false},{"time":"2026-07-05T18:41:36-04:00","description":"delete directory objects/test3","file_path":"data/objects/test3","old_content":"","new_content":"","is_delete":true,"is_create":false},{"time":"2026-07-05T18:41:37-04:00","description":"delete directory objects/test2","file_path":"data/objects/test2","old_content":"","new_content":"","is_delete":true,"is_create":false},{"time":"2026-07-05T18:42:04-04:00","description":"Create object adamantite_rock4_copy","file_path":"data/objects/adamantite_rock4_copy.yaml","old_content":"","new_content":"id: adamantite_rock4_copy\n","is_delete":false,"is_create":true},{"time":"2026-07-05T18:42:04-04:00","description":"Update object adamantite_rock4_copy","file_path":"data/objects/adamantite_rock4_copy.yaml","old_content":"id: adamantite_rock4_copy\n","new_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock4_copy\nname: adamantite rock\n","is_delete":false,"is_create":false},{"time":"2026-07-05T18:42:31-04:00","description":"Delete object adamantite_rock4","file_path":"data/objects/adamantite_rock4.yaml","old_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock_copy\nname: adamantite rock\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-07-05T18:42:33-04:00","description":"Delete object adamantite_rock4_copy","file_path":"data/objects/adamantite_rock4_copy.yaml","old_content":"color: \"78\"\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: \"\"\n depleted_message: \"\"\n drops:\n - depletes: true\n item_id: adamantite_ore\n level: 70\n message: \"\"\n quantity: 0\n table: \"\"\n weight: 100\n xp: 95\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the adamantite rock...\n nest_chance: \"\"\n respawn_broadcast: A green glint of adamantite catches your eye from some {name}.\n respawn_timer: \"\"\n skill: mining\n success:\n base: 0.15\n cap: 0.85\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: adamantite_rock4_copy\nname: adamantite rock\n","new_content":"","is_delete":true,"is_create":false}],"redo":null} \ No newline at end of file diff --git a/data/objects/adamantite_rock.yaml b/data/objects/adamantite_rock.yaml index 4145efe..83544d5 100644 --- a/data/objects/adamantite_rock.yaml +++ b/data/objects/adamantite_rock.yaml @@ -26,8 +26,3 @@ gather: hidden: false id: adamantite_rock name: adamantite rock -steal: - level: 1 - speed: 1 - table: gem_table - xp: 1 diff --git a/internal/admin/server.go b/internal/admin/server.go index 53dd5c8..e29de9a 100644 --- a/internal/admin/server.go +++ b/internal/admin/server.go @@ -117,30 +117,37 @@ func NewServer(cfg *config.Config, useTLS bool, accountStore *player.AccountStor apiMux.HandleFunc("/api/rooms", s.handleRooms) apiMux.HandleFunc("/api/rooms/new-empty", s.handleCreateEmptyRoom) apiMux.HandleFunc("/api/rooms/", s.handleRoomByID) + apiMux.HandleFunc("/api/objects/rename", s.handleEntityRename("objects")) apiMux.HandleFunc("/api/objects/move", s.handleEntityMove("objects")) apiMux.HandleFunc("/api/objects/dirs", s.handleEntityDirs("objects")) apiMux.HandleFunc("/api/objects", s.handleObjects) apiMux.HandleFunc("/api/objects/", s.handleObjectByID) + apiMux.HandleFunc("/api/items/rename", s.handleEntityRename("items")) apiMux.HandleFunc("/api/items/move", s.handleEntityMove("items")) apiMux.HandleFunc("/api/items/dirs", s.handleEntityDirs("items")) apiMux.HandleFunc("/api/items", s.handleItems) apiMux.HandleFunc("/api/items/", s.handleItemByID) + apiMux.HandleFunc("/api/mobs/rename", s.handleEntityRename("mobs")) apiMux.HandleFunc("/api/mobs/move", s.handleEntityMove("mobs")) apiMux.HandleFunc("/api/mobs/dirs", s.handleEntityDirs("mobs")) apiMux.HandleFunc("/api/mobs", s.handleMobs) apiMux.HandleFunc("/api/mobs/", s.handleMobByID) + apiMux.HandleFunc("/api/drops/rename", s.handleEntityRename("drops")) apiMux.HandleFunc("/api/drops/move", s.handleEntityMove("drops")) apiMux.HandleFunc("/api/drops/dirs", s.handleEntityDirs("drops")) apiMux.HandleFunc("/api/drops", s.handleDrops) apiMux.HandleFunc("/api/drops/", s.handleDropByID) + apiMux.HandleFunc("/api/hazards/rename", s.handleEntityRename("hazards")) apiMux.HandleFunc("/api/hazards/move", s.handleEntityMove("hazards")) apiMux.HandleFunc("/api/hazards/dirs", s.handleEntityDirs("hazards")) apiMux.HandleFunc("/api/hazards", s.handleHazards) apiMux.HandleFunc("/api/hazards/", s.handleHazardByID) + apiMux.HandleFunc("/api/techs/rename", s.handleEntityRename("techs")) apiMux.HandleFunc("/api/techs/move", s.handleEntityMove("techs")) apiMux.HandleFunc("/api/techs/dirs", s.handleEntityDirs("techs")) apiMux.HandleFunc("/api/techs", s.handleTechs) apiMux.HandleFunc("/api/techs/", s.handleTechByID) + apiMux.HandleFunc("/api/modules/rename", s.handleEntityRename("modules")) apiMux.HandleFunc("/api/modules/move", s.handleEntityMove("modules")) apiMux.HandleFunc("/api/modules/dirs", s.handleEntityDirs("modules")) apiMux.HandleFunc("/api/modules", s.handleModules) @@ -481,9 +488,10 @@ func (s *AdminServer) handleEntityDirs(entityType string) http.HandlerFunc { return } var body struct { - Action string `json:"action"` - Name string `json:"name"` - Dir string `json:"dir"` + Action string `json:"action"` + Name string `json:"name"` + Dir string `json:"dir"` + NewName string `json:"new_name"` } if err := readJSON(r, &body); err != nil { writeJSON(w, map[string]any{"error": "invalid"}) @@ -506,12 +514,50 @@ func (s *AdminServer) handleEntityDirs(entityType string) http.HandlerFunc { s.undoStack.Push(ch) } writeJSON(w, map[string]any{"ok": true, "moved": len(changes) - 1}) + case "rename_dir": + if err := renameEntityDir(s.dataDir, entityType, body.Dir, body.NewName); err != nil { + writeJSON(w, map[string]any{"error": err.Error()}) + return + } + writeJSON(w, map[string]any{"ok": true, "name": body.NewName}) default: writeJSON(w, map[string]any{"error": "unknown action"}) } } } +func (s *AdminServer) handleEntityRename(entityType string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var body struct { + ID string `json:"id"` + NewID string `json:"new_id"` + } + if err := readJSON(r, &body); err != nil || body.ID == "" || body.NewID == "" { + writeJSON(w, map[string]any{"error": "invalid"}) + return + } + oldPath, newPath, data, err := renameEntityFile(s.dataDir, entityType, body.ID, body.NewID) + if err != nil { + writeJSON(w, map[string]any{"error": err.Error()}) + return + } + desc := fmt.Sprintf("rename %s %s to %s", entityType, body.ID, body.NewID) + s.undoStack.Push(ChangeDesc{ + Description: desc, + FilePath: oldPath, + NewFilePath: newPath, + OldContent: data, + NewContent: data, + }) + s.invalidateEntityCaches(entityType) + writeJSON(w, map[string]any{"ok": true, "id": body.NewID}) + } +} + func (s *AdminServer) handleEntityMove(entityType string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { @@ -542,6 +588,7 @@ func (s *AdminServer) handleEntityMove(entityType string) http.HandlerFunc { OldContent: data, NewContent: data, }) + s.invalidateEntityCaches(entityType) writeJSON(w, map[string]any{"ok": true}) } } @@ -565,6 +612,28 @@ func (s *AdminServer) rebuildAfterUndo(change *ChangeDesc) { } } } + + for _, p := range changeFiles(change) { + s.invalidateEntityCacheForPath(p) + } +} + +func (s *AdminServer) invalidateEntityCaches(entityType string) { + switch entityType { + case "mobs": + s.mobStore.ReloadDefs(s.dataDir) + case "drops": + behavior.ClearDropIndex() + } +} + +func (s *AdminServer) invalidateEntityCacheForPath(path string) { + for _, subdir := range []string{"mobs", "drops"} { + rel, err := filepath.Rel(filepath.Join(s.dataDir, subdir), path) + if err == nil && !strings.HasPrefix(rel, "..") && rel != "." { + s.invalidateEntityCaches(subdir) + } + } } func changeFiles(change *ChangeDesc) []string { diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index 3c6aaa4..bef3589 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -231,6 +231,17 @@ g.ud-hover:hover text{font-weight:bold} .cm-menu button:hover{background:var(--accent)} .cm-menu button.danger{color:#f55} .cm-menu button.danger:hover{background:var(--danger)} +.rename-header{display:flex;align-items:center;gap:6px} +.rename-header-text{margin-bottom:0!important;padding-bottom:0!important;border-bottom:none!important} +.rename-pencil{font-size:14px;cursor:pointer;color:#888;padding:2px 4px;border-radius:3px;transition:color .15s,transform .15s} +.rename-pencil:hover{color:#fff;transform:scale(1.1)} +.rename-edit-row{display:flex;align-items:center;gap:4px} +.rename-input{flex:1;padding:4px 8px;font-size:14px;font-weight:bold;background:var(--input-bg);color:var(--text);border:1px solid var(--accent);border-radius:3px;font-family:monospace} +.rename-btn-confirm,.rename-btn-cancel{padding:2px 8px;font-size:13px;border-radius:3px;cursor:pointer;font-family:monospace;line-height:1} +.rename-btn-confirm{background:var(--success);color:#fff;border:none} +.rename-btn-cancel{background:var(--danger);color:#fff;border:none} +.item-rename-input{padding:4px 6px;font-size:12px;background:var(--input-bg);color:var(--text);border:1px solid var(--accent);border-radius:3px;font-family:monospace;width:100%} + .panel-left{width:200px;background:var(--panel);border-right:1px solid var(--border);overflow-y:auto;display:flex;flex-direction:column;flex-shrink:0} .panel-left .panel-header{font-size:11px;color:#aaa;text-transform:uppercase;letter-spacing:.5px;padding:12px 12px 8px;border-bottom:1px solid var(--border);flex-shrink:0} .panel-left .dc-item{display:block;padding:6px 12px;font-size:11px;cursor:pointer;color:var(--text);border-bottom:1px solid rgba(255,255,255,.03);font-family:monospace} diff --git a/internal/admin/static/admin.js b/internal/admin/static/admin.js index f8e9d13..a6bb8b2 100644 --- a/internal/admin/static/admin.js +++ b/internal/admin/static/admin.js @@ -52,4 +52,69 @@ window.saveForm = function(url, data, msg) { }).catch(function(e) { notify('Save failed: '+e.message, 'error'); }); }; +window.entityRename = function(type, oldID, newID) { + return API.post('/api/' + type + '/rename', {id: oldID, new_id: newID}).then(function(r) { + notify('Renamed ' + oldID + ' to ' + newID, 'success'); + updateUndoBar(); + return r; + }); +}; + +window.renderRenameableHeader = function(label, id) { + var h = '