aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-16 17:05:17 -0400
committerhistoria <[not public]>2026-09-16 17:05:17 -0400
commit1dad15698845d370e998f7cc5feb8aece3fab58f (patch)
tree8d58058782de9f422573e1f8efd5d130ce7d1ce9 /internal
parenta6cf03a6ae98e4fb675da25046d148e92bcf7206 (diff)
downloadthehouseoficarus-1dad15698845d370e998f7cc5feb8aece3fab58f.tar.gz
refactor: remove dead code, consolidate save/error handling, add lint gate
Diffstat (limited to 'internal')
-rw-r--r--internal/admin/api_drops.go10
-rw-r--r--internal/admin/api_files.go38
-rw-r--r--internal/admin/api_hazards.go10
-rw-r--r--internal/admin/api_items.go8
-rw-r--r--internal/admin/api_items_undo_test.go2
-rw-r--r--internal/admin/api_map.go6
-rw-r--r--internal/admin/api_mobs.go10
-rw-r--r--internal/admin/api_modules.go22
-rw-r--r--internal/admin/api_objects.go8
-rw-r--r--internal/admin/api_players.go10
-rw-r--r--internal/admin/api_room_courses.go40
-rw-r--r--internal/admin/api_room_insert_remove.go6
-rw-r--r--internal/admin/api_room_insert_remove_test.go49
-rw-r--r--internal/admin/api_rooms.go112
-rw-r--r--internal/admin/api_rooms_bulk.go26
-rw-r--r--internal/admin/api_rooms_bulk_delete.go14
-rw-r--r--internal/admin/api_rooms_bulk_delete_test.go8
-rw-r--r--internal/admin/api_search.go2
-rw-r--r--internal/admin/api_techs.go22
-rw-r--r--internal/admin/api_triggers.go30
-rw-r--r--internal/admin/id_alloc.go4
-rw-r--r--internal/admin/server.go31
-rw-r--r--internal/admin/xterm_to_css.go27
-rw-r--r--internal/behavior/behavior.go2
-rw-r--r--internal/cardart/store.go2
-rw-r--r--internal/casino/blackjack.go2
-rw-r--r--internal/casino/credits_test.go2
-rw-r--r--internal/casino/machine.go6
-rw-r--r--internal/color/color.go2
-rw-r--r--internal/combat/formulas.go2
-rw-r--r--internal/config/config.go2
-rw-r--r--internal/engine/tick.go2
-rw-r--r--internal/game/account_registration_regression_test.go2
-rw-r--r--internal/game/act_agility.go4
-rw-r--r--internal/game/act_burn.go14
-rw-r--r--internal/game/act_clean.go2
-rw-r--r--internal/game/act_combine.go2
-rw-r--r--internal/game/act_effects.go18
-rw-r--r--internal/game/act_farm.go14
-rw-r--r--internal/game/act_fletch.go6
-rw-r--r--internal/game/act_gather.go4
-rw-r--r--internal/game/act_identify.go2
-rw-r--r--internal/game/act_state_test.go2
-rw-r--r--internal/game/act_steal.go6
-rw-r--r--internal/game/baseline_race_test.go16
-rw-r--r--internal/game/casino.go2
-rw-r--r--internal/game/casino_forfeit_regression_test.go4
-rw-r--r--internal/game/cmd_aps.go8
-rw-r--r--internal/game/cmd_bank.go14
-rw-r--r--internal/game/cmd_color.go2
-rw-r--r--internal/game/cmd_consume.go2
-rw-r--r--internal/game/cmd_craft.go4
-rw-r--r--internal/game/cmd_description.go2
-rw-r--r--internal/game/cmd_dig.go4
-rw-r--r--internal/game/cmd_drop.go10
-rw-r--r--internal/game/cmd_estate_directory.go2
-rw-r--r--internal/game/cmd_farm.go32
-rw-r--r--internal/game/cmd_fletch.go2
-rw-r--r--internal/game/cmd_get.go20
-rw-r--r--internal/game/cmd_god.go2
-rw-r--r--internal/game/cmd_goto.go2
-rw-r--r--internal/game/cmd_hideallexits.go2
-rw-r--r--internal/game/cmd_id.go5
-rw-r--r--internal/game/cmd_look.go2
-rw-r--r--internal/game/cmd_mods.go16
-rw-r--r--internal/game/cmd_move.go6
-rw-r--r--internal/game/cmd_option.go10
-rw-r--r--internal/game/cmd_prompt.go6
-rw-r--r--internal/game/cmd_quit.go4
-rw-r--r--internal/game/cmd_remove.go8
-rw-r--r--internal/game/cmd_room_insert.go2
-rw-r--r--internal/game/cmd_score.go38
-rw-r--r--internal/game/cmd_setplayerflag.go2
-rw-r--r--internal/game/cmd_shop.go4
-rw-r--r--internal/game/cmd_skills.go8
-rw-r--r--internal/game/cmd_smelt.go6
-rw-r--r--internal/game/cmd_stop.go2
-rw-r--r--internal/game/cmd_style.go2
-rw-r--r--internal/game/cmd_summon.go2
-rw-r--r--internal/game/cmd_tech.go12
-rw-r--r--internal/game/cmd_trigger_combat.go9
-rw-r--r--internal/game/cmd_trigger_transport.go2
-rw-r--r--internal/game/cmd_use.go8
-rw-r--r--internal/game/cmd_wear.go6
-rw-r--r--internal/game/combat_attack.go20
-rw-r--r--internal/game/combat_mob.go22
-rw-r--r--internal/game/core_hacking.go2
-rw-r--r--internal/game/core_login_account.go2
-rw-r--r--internal/game/core_production.go1
-rw-r--r--internal/game/core_utils.go11
-rw-r--r--internal/game/credits_pickup_test.go4
-rw-r--r--internal/game/economy_recovery_identity_test.go4
-rw-r--r--internal/game/economy_recovery_regression_test.go28
-rw-r--r--internal/game/economy_shutdown_test.go8
-rw-r--r--internal/game/exit_migrate.go4
-rw-r--r--internal/game/game.go4
-rw-r--r--internal/game/hacking/hacking.go2
-rw-r--r--internal/game/hacking/liars_dice.go4
-rw-r--r--internal/game/harness_test.go13
-rw-r--r--internal/game/inventory_pickup_test.go4
-rw-r--r--internal/game/lifecycle_txn_test.go4
-rw-r--r--internal/game/look_entities.go8
-rw-r--r--internal/game/look_room.go2
-rw-r--r--internal/game/look_target.go2
-rw-r--r--internal/game/migrate_rooms_test.go10
-rw-r--r--internal/game/owner_concurrency_test.go22
-rw-r--r--internal/game/owner_disconnect_test.go2
-rw-r--r--internal/game/production_engine.go8
-rw-r--r--internal/game/production_index.go7
-rw-r--r--internal/game/production_menu.go4
-rw-r--r--internal/game/quit_casino_test.go20
-rw-r--r--internal/game/readiness_regression_test.go14
-rw-r--r--internal/game/render_color.go14
-rw-r--r--internal/game/render_combat.go18
-rw-r--r--internal/game/save.go26
-rw-r--r--internal/game/shutdown_admission_test.go4
-rw-r--r--internal/game/sys_assassin.go2
-rw-r--r--internal/game/sys_combat_test.go9
-rw-r--r--internal/game/sys_escape_test.go6
-rw-r--r--internal/game/sys_hazard.go2
-rw-r--r--internal/game/sys_labor.go2
-rw-r--r--internal/game/sys_science.go31
-rw-r--r--internal/game/sys_technology.go2
-rw-r--r--internal/game/sys_triggers.go6
-rw-r--r--internal/game/tick.go2
-rw-r--r--internal/item/item.go2
-rw-r--r--internal/net/conn.go3
-rw-r--r--internal/net/server.go2
-rw-r--r--internal/object/object.go2
-rw-r--r--internal/persistence/txn.go3
-rw-r--r--internal/player/player.go6
-rw-r--r--internal/ui/bar.go22
-rw-r--r--internal/validate/checks.go128
-rw-r--r--internal/world/grid.go2
134 files changed, 661 insertions, 752 deletions
diff --git a/internal/admin/api_drops.go b/internal/admin/api_drops.go
index 3724a6c..4cb0ae9 100644
--- a/internal/admin/api_drops.go
+++ b/internal/admin/api_drops.go
@@ -15,7 +15,7 @@ func (s *AdminServer) handleDrops(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
tree, err := listYAMLTree(s.dataDir, "drops")
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
if tree == nil {
@@ -44,17 +44,17 @@ func (s *AdminServer) handleDropByID(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
- s.getDrop(w, r, id)
+ s.getDrop(w, id)
case http.MethodPut:
s.updateDrop(w, r, id)
case http.MethodDelete:
- s.deleteDrop(w, r, id)
+ s.deleteDrop(w, id)
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
}
-func (s *AdminServer) getDrop(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) getDrop(w http.ResponseWriter, id string) {
data, path, err := readYAMLFile(s.dataDir, "drops", id)
if err != nil {
http.Error(w, `{"error":"drop not found"}`, http.StatusNotFound)
@@ -108,7 +108,7 @@ func (s *AdminServer) updateDrop(w http.ResponseWriter, r *http.Request, id stri
writeJSON(w, m)
}
-func (s *AdminServer) deleteDrop(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) deleteDrop(w http.ResponseWriter, id string) {
_, path, err := readYAMLFile(s.dataDir, "drops", id)
if err != nil {
http.Error(w, `{"error":"drop not found"}`, http.StatusNotFound)
diff --git a/internal/admin/api_files.go b/internal/admin/api_files.go
index ea75249..d32bda4 100644
--- a/internal/admin/api_files.go
+++ b/internal/admin/api_files.go
@@ -57,18 +57,18 @@ func (s *AdminServer) handleFileRename(w http.ResponseWriter, r *http.Request) {
NewName string `json:"new_name"`
}
if err := readJSON(r, &body); err != nil || body.Path == "" || body.NewName == "" {
- writeJSON(w, map[string]any{"error": "missing path or new_name"})
+ failJSON(w, "missing path or new_name")
return
}
newName := strings.TrimSpace(body.NewName)
if newName == "" || newName == "." || newName == ".." || strings.ContainsAny(newName, "/\\") {
- writeJSON(w, map[string]any{"error": "invalid file name"})
+ failJSON(w, "invalid file name")
return
}
for _, c := range newName {
if c < 32 || c == 127 {
- writeJSON(w, map[string]any{"error": "file name contains invalid characters"})
+ failJSON(w, "file name contains invalid characters")
return
}
}
@@ -77,28 +77,28 @@ func (s *AdminServer) handleFileRename(w http.ResponseWriter, r *http.Request) {
relPath = strings.TrimPrefix(relPath, "data/")
oldAbs, err := resolveDataPath(s.dataDir, relPath)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
oldContent, err := snapshotFile(oldAbs)
if err != nil {
- writeJSON(w, map[string]any{"error": "file not found: " + relPath})
+ failJSON(w, "file not found: "+relPath)
return
}
newAbs := filepath.Join(filepath.Dir(oldAbs), newName)
if _, err := os.Stat(newAbs); err == nil {
- writeJSON(w, map[string]any{"error": "a file named " + newName + " already exists"})
+ failJSON(w, "a file named "+newName+" already exists")
return
}
if err := os.WriteFile(newAbs, oldContent, 0644); err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
if err := os.Remove(oldAbs); err != nil {
- writeJSON(w, map[string]any{"error": "remove error: " + err.Error()})
+ failJSON(w, "remove error: "+err.Error())
return
}
@@ -122,7 +122,7 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) {
relPath = strings.TrimPrefix(relPath, "data/")
fullPath, pathErr := resolveDataPath(s.dataDir, relPath)
if pathErr != nil {
- writeJSON(w, map[string]any{"error": pathErr.Error()})
+ failJSON(w, pathErr.Error())
return
}
@@ -131,7 +131,7 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) {
if relPath == "" {
entries, err := os.ReadDir(fullPath)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
var files []map[string]any
@@ -148,14 +148,14 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) {
fullPath := filepath.Join(s.dataDir, relPath)
info, err := os.Stat(fullPath)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + relPath})
+ failJSON(w, "not found: "+relPath)
return
}
if info.IsDir() {
entries, err := os.ReadDir(fullPath)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
var files []map[string]any
@@ -171,7 +171,7 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) {
data, err := os.ReadFile(fullPath)
if err != nil {
- writeJSON(w, map[string]any{"error": "read error: " + err.Error()})
+ failJSON(w, "read error: "+err.Error())
return
}
writeJSON(w, map[string]any{
@@ -182,12 +182,12 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) {
case http.MethodPut, http.MethodPost:
if relPath == "" {
- writeJSON(w, map[string]any{"error": "missing path"})
+ failJSON(w, "missing path")
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
- writeJSON(w, map[string]any{"error": "read body: " + err.Error()})
+ failJSON(w, "read body: "+err.Error())
return
}
@@ -198,12 +198,12 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) {
dir := filepath.Dir(fullPath)
if err := os.MkdirAll(dir, 0755); err != nil {
- writeJSON(w, map[string]any{"error": "mkdir: " + err.Error()})
+ failJSON(w, "mkdir: "+err.Error())
return
}
if err := os.WriteFile(fullPath, body, 0644); err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
@@ -223,12 +223,12 @@ func (s *AdminServer) handleFiles(w http.ResponseWriter, r *http.Request) {
case http.MethodDelete:
if relPath == "" {
- writeJSON(w, map[string]any{"error": "missing path"})
+ failJSON(w, "missing path")
return
}
oldContent, _ := snapshotFile(fullPath)
if err := os.Remove(fullPath); err != nil {
- writeJSON(w, map[string]any{"error": "delete error: " + err.Error()})
+ failJSON(w, "delete error: "+err.Error())
return
}
s.evictDeletedRoomFile(relPath)
diff --git a/internal/admin/api_hazards.go b/internal/admin/api_hazards.go
index fac68c4..89618e7 100644
--- a/internal/admin/api_hazards.go
+++ b/internal/admin/api_hazards.go
@@ -13,7 +13,7 @@ func (s *AdminServer) handleHazards(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
tree, err := listYAMLTree(s.dataDir, "hazards")
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
if tree == nil {
@@ -42,17 +42,17 @@ func (s *AdminServer) handleHazardByID(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
- s.getHazard(w, r, id)
+ s.getHazard(w, id)
case http.MethodPut:
s.updateHazard(w, r, id)
case http.MethodDelete:
- s.deleteHazard(w, r, id)
+ s.deleteHazard(w, id)
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
}
-func (s *AdminServer) getHazard(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) getHazard(w http.ResponseWriter, id string) {
data, path, err := readYAMLFile(s.dataDir, "hazards", id)
if err != nil {
http.Error(w, `{"error":"hazard not found"}`, http.StatusNotFound)
@@ -106,7 +106,7 @@ func (s *AdminServer) updateHazard(w http.ResponseWriter, r *http.Request, id st
writeJSON(w, m)
}
-func (s *AdminServer) deleteHazard(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) deleteHazard(w http.ResponseWriter, id string) {
_, path, err := readYAMLFile(s.dataDir, "hazards", id)
if err != nil {
http.Error(w, `{"error":"hazard not found"}`, http.StatusNotFound)
diff --git a/internal/admin/api_items.go b/internal/admin/api_items.go
index 749e8ff..8798543 100644
--- a/internal/admin/api_items.go
+++ b/internal/admin/api_items.go
@@ -40,11 +40,11 @@ func (s *AdminServer) handleItemByID(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
- s.getItem(w, r, id)
+ s.getItem(w, id)
case http.MethodPut:
s.updateItem(w, r, id)
case http.MethodDelete:
- s.deleteItem(w, r, id)
+ s.deleteItem(w, id)
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
@@ -66,7 +66,7 @@ func (s *AdminServer) findItemFile(id string) ([]byte, string, error) {
return findYAMLFileInSubdirs(s.dataDir, "items", id)
}
-func (s *AdminServer) getItem(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) getItem(w http.ResponseWriter, id string) {
data, path, err := s.findItemFile(id)
if err != nil {
http.Error(w, `{"error":"item not found"}`, http.StatusNotFound)
@@ -120,7 +120,7 @@ func (s *AdminServer) updateItem(w http.ResponseWriter, r *http.Request, id stri
writeJSON(w, m)
}
-func (s *AdminServer) deleteItem(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) deleteItem(w http.ResponseWriter, id string) {
_, path, err := s.findItemFile(id)
if err != nil {
http.Error(w, `{"error":"item not found"}`, http.StatusNotFound)
diff --git a/internal/admin/api_items_undo_test.go b/internal/admin/api_items_undo_test.go
index 3ccbe10..d554bc6 100644
--- a/internal/admin/api_items_undo_test.go
+++ b/internal/admin/api_items_undo_test.go
@@ -31,7 +31,7 @@ func TestAccountAPIRedactsMultilinePasswordHash(t *testing.T) {
s := &AdminServer{accountStore: player.NewAccountStore(dir)}
rr := httptest.NewRecorder()
- s.getAccount(rr, httptest.NewRequest("GET", "/api/players/account/alice", nil), "alice")
+ s.getAccount(rr, "alice")
if strings.Contains(rr.Body.String(), "sensitive-hash-sentinel") {
t.Fatalf("password hash leaked in response: %s", rr.Body.String())
diff --git a/internal/admin/api_map.go b/internal/admin/api_map.go
index 5b0464d..5eac385 100644
--- a/internal/admin/api_map.go
+++ b/internal/admin/api_map.go
@@ -463,10 +463,6 @@ func (s *AdminServer) handleNextRoomID(w http.ResponseWriter, r *http.Request) {
if fromStr != "" {
fromID, _ = strconv.Atoi(fromStr)
}
- id, _, err := s.nextRoomID(fromID)
- if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
- return
- }
+ id, _ := s.nextRoomID(fromID)
writeJSON(w, map[string]any{"id": id})
}
diff --git a/internal/admin/api_mobs.go b/internal/admin/api_mobs.go
index 8531e40..c3e47bb 100644
--- a/internal/admin/api_mobs.go
+++ b/internal/admin/api_mobs.go
@@ -13,7 +13,7 @@ func (s *AdminServer) handleMobs(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
tree, err := listYAMLTree(s.dataDir, "mobs")
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
if tree == nil {
@@ -42,11 +42,11 @@ func (s *AdminServer) handleMobByID(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
- s.getMob(w, r, id)
+ s.getMob(w, id)
case http.MethodPut:
s.updateMob(w, r, id)
case http.MethodDelete:
- s.deleteMob(w, r, id)
+ s.deleteMob(w, id)
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
@@ -60,7 +60,7 @@ func (s *AdminServer) findMobFile(id string) ([]byte, string, error) {
return findYAMLFileInSubdirs(s.dataDir, "mobs", id)
}
-func (s *AdminServer) getMob(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) getMob(w http.ResponseWriter, id string) {
data, path, err := s.findMobFile(id)
if err != nil {
http.Error(w, `{"error":"mob not found"}`, http.StatusNotFound)
@@ -110,7 +110,7 @@ func (s *AdminServer) updateMob(w http.ResponseWriter, r *http.Request, id strin
writeJSON(w, m)
}
-func (s *AdminServer) deleteMob(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) deleteMob(w http.ResponseWriter, id string) {
_, path, err := s.findMobFile(id)
if err != nil {
http.Error(w, `{"error":"mob not found"}`, http.StatusNotFound)
diff --git a/internal/admin/api_modules.go b/internal/admin/api_modules.go
index 074eeea..1d8c25b 100644
--- a/internal/admin/api_modules.go
+++ b/internal/admin/api_modules.go
@@ -12,7 +12,7 @@ func (s *AdminServer) handleModules(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
tree, err := listYAMLTree(s.dataDir, "modules")
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
if tree == nil {
@@ -22,23 +22,23 @@ func (s *AdminServer) handleModules(w http.ResponseWriter, r *http.Request) {
case http.MethodPost:
var m map[string]any
if err := readJSON(r, &m); err != nil {
- writeJSON(w, map[string]any{"error": "invalid json"})
+ failJSON(w, "invalid json")
return
}
id, ok := m["id"].(string)
if !ok || strings.TrimSpace(id) == "" {
- writeJSON(w, map[string]any{"error": "missing id"})
+ failJSON(w, "missing id")
return
}
delete(m, "id")
path := filepath.Join(s.dataDir, "modules", id+".yaml")
if _, err := os.Stat(path); err == nil {
- writeJSON(w, map[string]any{"error": "module already exists"})
+ failJSON(w, "module already exists")
return
}
newContent, err := writeMapAsYAML(path, m)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
@@ -68,12 +68,12 @@ func (s *AdminServer) handleModuleByID(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
data, path, err := findYAMLFileInSubdirs(s.dataDir, "modules", id)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
raw, err := yamlToMap(data)
if err != nil {
- writeJSON(w, map[string]any{"error": "parse error: " + err.Error()})
+ failJSON(w, "parse error: "+err.Error())
return
}
raw["id"] = id
@@ -84,7 +84,7 @@ func (s *AdminServer) handleModuleByID(w http.ResponseWriter, r *http.Request) {
case http.MethodPost, http.MethodPut:
var body map[string]any
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid JSON: " + err.Error()})
+ failJSON(w, "invalid JSON: "+err.Error())
return
}
delete(body, "id")
@@ -99,7 +99,7 @@ func (s *AdminServer) handleModuleByID(w http.ResponseWriter, r *http.Request) {
newContent, err := writeMapAsYAML(path, body)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
@@ -120,12 +120,12 @@ func (s *AdminServer) handleModuleByID(w http.ResponseWriter, r *http.Request) {
case http.MethodDelete:
_, path, err := findYAMLFileInSubdirs(s.dataDir, "modules", id)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
oldContent, _ := snapshotFile(path)
if err := os.Remove(path); err != nil {
- writeJSON(w, map[string]any{"error": "delete error: " + err.Error()})
+ failJSON(w, "delete error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
diff --git a/internal/admin/api_objects.go b/internal/admin/api_objects.go
index 8f9eac5..4e1e621 100644
--- a/internal/admin/api_objects.go
+++ b/internal/admin/api_objects.go
@@ -40,11 +40,11 @@ func (s *AdminServer) handleObjectByID(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
- s.getObject(w, r, id)
+ s.getObject(w, id)
case http.MethodPut:
s.updateObject(w, r, id)
case http.MethodDelete:
- s.deleteObject(w, r, id)
+ s.deleteObject(w, id)
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
@@ -58,7 +58,7 @@ func (s *AdminServer) findObjectFile(id string) ([]byte, string, error) {
return findYAMLFileInSubdirs(s.dataDir, "objects", id)
}
-func (s *AdminServer) getObject(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) getObject(w http.ResponseWriter, id string) {
data, path, err := s.findObjectFile(id)
if err != nil {
http.Error(w, `{"error":"object not found"}`, http.StatusNotFound)
@@ -111,7 +111,7 @@ func (s *AdminServer) updateObject(w http.ResponseWriter, r *http.Request, id st
writeJSON(w, m)
}
-func (s *AdminServer) deleteObject(w http.ResponseWriter, r *http.Request, id string) {
+func (s *AdminServer) deleteObject(w http.ResponseWriter, id string) {
_, path, err := s.findObjectFile(id)
if err != nil {
http.Error(w, `{"error":"object not found"}`, http.StatusNotFound)
diff --git a/internal/admin/api_players.go b/internal/admin/api_players.go
index f0bbe31..66142c5 100644
--- a/internal/admin/api_players.go
+++ b/internal/admin/api_players.go
@@ -21,7 +21,7 @@ func (s *AdminServer) handlePlayers(w http.ResponseWriter, r *http.Request) {
charsDir := filepath.Join(s.dataDir, "players", "characters")
entries, err := os.ReadDir(charsDir)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
@@ -87,14 +87,14 @@ func (s *AdminServer) handlePlayerByID(w http.ResponseWriter, r *http.Request) {
}
if strings.HasPrefix(path, "account/") {
- s.getAccount(w, r, strings.TrimPrefix(path, "account/"))
+ s.getAccount(w, strings.TrimPrefix(path, "account/"))
return
}
- s.getCharacter(w, r, path)
+ s.getCharacter(w, path)
}
-func (s *AdminServer) getCharacter(w http.ResponseWriter, r *http.Request, name string) {
+func (s *AdminServer) getCharacter(w http.ResponseWriter, name string) {
canonical, err := s.accountStore.FindCharacter(name)
if err != nil {
writeJSONError(w, "character not found: "+name, http.StatusNotFound)
@@ -136,7 +136,7 @@ func (s *AdminServer) getCharacter(w http.ResponseWriter, r *http.Request, name
writeJSON(w, m)
}
-func (s *AdminServer) getAccount(w http.ResponseWriter, r *http.Request, name string) {
+func (s *AdminServer) getAccount(w http.ResponseWriter, name string) {
canonical, err := s.accountStore.FindAccount(name)
if err != nil {
writeJSONError(w, "account not found: "+name, http.StatusNotFound)
diff --git a/internal/admin/api_room_courses.go b/internal/admin/api_room_courses.go
index b8f334e..bebacff 100644
--- a/internal/admin/api_room_courses.go
+++ b/internal/admin/api_room_courses.go
@@ -131,28 +131,28 @@ func (s *AdminServer) handleCourses(w http.ResponseWriter, r *http.Request) {
case http.MethodPost:
var body map[string]any
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid json"})
+ failJSON(w, "invalid json")
return
}
id, _ := body["id"].(string)
id = strings.TrimSpace(id)
if id == "" {
- writeJSON(w, map[string]any{"error": "missing id"})
+ failJSON(w, "missing id")
return
}
if !validEntityID(id) {
- writeJSON(w, map[string]any{"error": "invalid id"})
+ failJSON(w, "invalid id")
return
}
delete(body, "id")
path := s.courseFilePath(id)
if _, err := os.Stat(path); err == nil {
- writeJSON(w, map[string]any{"error": "course already exists"})
+ failJSON(w, "course already exists")
return
}
newContent, err := writeMapAsYAML(path, body)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
@@ -174,11 +174,11 @@ func (s *AdminServer) handleCourses(w http.ResponseWriter, r *http.Request) {
func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/courses/")
if id == "" {
- writeJSON(w, map[string]any{"error": "missing id"})
+ failJSON(w, "missing id")
return
}
if !validEntityID(id) {
- writeJSON(w, map[string]any{"error": "invalid id"})
+ failJSON(w, "invalid id")
return
}
path := s.courseFilePath(id)
@@ -187,7 +187,7 @@ func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
m, data, _, err := s.loadCourseMap(id)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
m["id"] = id
@@ -197,7 +197,7 @@ func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) {
case http.MethodPost, http.MethodPut:
var body map[string]any
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid JSON: " + err.Error()})
+ failJSON(w, "invalid JSON: "+err.Error())
return
}
delete(body, "id")
@@ -210,7 +210,7 @@ func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) {
}
newContent, err := writeMapAsYAML(path, body)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
isCreate := oldContent == nil
@@ -236,11 +236,11 @@ func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) {
case http.MethodDelete:
oldContent, err := snapshotFile(path)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
if err := os.Remove(path); err != nil {
- writeJSON(w, map[string]any{"error": "delete error: " + err.Error()})
+ failJSON(w, "delete error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
@@ -269,7 +269,7 @@ func (s *AdminServer) handleRoomCourse(w http.ResponseWriter, r *http.Request, r
}
ids, err := s.listCourseIDs()
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
for _, id := range ids {
@@ -314,16 +314,16 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ
Obstacle map[string]any `json:"obstacle"`
}
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid json"})
+ failJSON(w, "invalid json")
return
}
if strings.TrimSpace(body.CourseID) == "" {
- writeJSON(w, map[string]any{"error": "missing course_id"})
+ failJSON(w, "missing course_id")
return
}
m, _, path, err := s.loadCourseMap(body.CourseID)
if err != nil {
- writeJSON(w, map[string]any{"error": "course not found: " + body.CourseID})
+ failJSON(w, "course not found: "+body.CourseID)
return
}
oldContent, _ := snapshotFile(path)
@@ -331,7 +331,7 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ
obstacles := asMapList(m["obstacles"])
for i, o := range obstacles {
if obstacleRoomID(o) == roomID {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d is already step %d of course %s", roomID, i+1, body.CourseID)})
+ failJSON(w, fmt.Sprintf("room %d is already step %d of course %s", roomID, i+1, body.CourseID))
return
}
}
@@ -379,7 +379,7 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ
dir = "north"
}
if !isHorizontalDir(dir) {
- writeJSON(w, map[string]any{"error": "invalid direction: " + dir})
+ failJSON(w, "invalid direction: "+dir)
return
}
@@ -396,7 +396,7 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ
newContent, err := writeMapAsYAML(path, m)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
@@ -479,7 +479,7 @@ func (s *AdminServer) handleRoomDetachCourse(w http.ResponseWriter, r *http.Requ
}
courseID, deleted := s.detachRoomFromCourse(roomID)
if courseID == "" && !deleted {
- writeJSON(w, map[string]any{"error": "room is not part of any course"})
+ failJSON(w, "room is not part of any course")
return
}
writeJSON(w, map[string]any{"ok": true, "course_id": courseID, "deleted": deleted})
diff --git a/internal/admin/api_room_insert_remove.go b/internal/admin/api_room_insert_remove.go
index 812a3ae..45537d6 100644
--- a/internal/admin/api_room_insert_remove.go
+++ b/internal/admin/api_room_insert_remove.go
@@ -72,11 +72,7 @@ func (s *AdminServer) handleRoomInsert(w http.ResponseWriter, r *http.Request) {
return
}
- newID, subdir, allocErr := s.nextRoomID(aID)
- if allocErr != nil {
- writeJSONError(w, fmt.Sprintf("could not allocate a room id: %v", allocErr), http.StatusInternalServerError)
- return
- }
+ newID, subdir := s.nextRoomID(aID)
name := strings.TrimSpace(body.Name)
if name == "" {
diff --git a/internal/admin/api_room_insert_remove_test.go b/internal/admin/api_room_insert_remove_test.go
index f613313..9fbc022 100644
--- a/internal/admin/api_room_insert_remove_test.go
+++ b/internal/admin/api_room_insert_remove_test.go
@@ -54,16 +54,18 @@ func roomExists(s *AdminServer, id int) bool {
return err == nil
}
-func roomExitTarget(s *AdminServer, id int, dir world.ExitDir) (int, bool) {
+// roomExitTarget returns the room an exit points at, or 0 when the room or
+// exit does not exist (expected IDs are always >= 1, so 0 fails assertions).
+func roomExitTarget(s *AdminServer, id int, dir world.ExitDir) int {
r, err := s.world.LoadRoom(id)
if err != nil {
- return 0, false
+ return 0
}
e, ok := r.Exits[dir]
if !ok {
- return 0, false
+ return 0
}
- return e.Room, true
+ return e.Room
}
// postInsert drives the insert endpoint with the given body and returns the
@@ -109,16 +111,16 @@ func TestInsertSuccess(t *testing.T) {
}
// A→east→new, new→east→2, new→west→1, B→west→new.
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != newID {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != newID {
t.Errorf("A east: expected %d, got %d", newID, tgt)
}
- if tgt, _ := roomExitTarget(s, newID, world.East); tgt != 2 {
+ if tgt := roomExitTarget(s, newID, world.East); tgt != 2 {
t.Errorf("new east: expected 2, got %d", tgt)
}
- if tgt, _ := roomExitTarget(s, newID, world.West); tgt != 1 {
+ if tgt := roomExitTarget(s, newID, world.West); tgt != 1 {
t.Errorf("new west: expected 1, got %d", tgt)
}
- if tgt, _ := roomExitTarget(s, 2, world.West); tgt != newID {
+ if tgt := roomExitTarget(s, 2, world.West); tgt != newID {
t.Errorf("B west: expected %d, got %d", newID, tgt)
}
@@ -131,10 +133,10 @@ func TestInsertSuccess(t *testing.T) {
if roomExists(s, newID) {
t.Error("undo: new room file should be gone")
}
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 2 {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != 2 {
t.Errorf("undo: A east should be 2, got %d", tgt)
}
- if tgt, _ := roomExitTarget(s, 2, world.West); tgt != 1 {
+ if tgt := roomExitTarget(s, 2, world.West); tgt != 1 {
t.Errorf("undo: B west should be 1, got %d", tgt)
}
@@ -147,10 +149,10 @@ func TestInsertSuccess(t *testing.T) {
if !roomExists(s, newID) {
t.Error("redo: new room file should be back")
}
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != newID {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != newID {
t.Errorf("redo: A east should be %d, got %d", newID, tgt)
}
- if tgt, _ := roomExitTarget(s, 2, world.West); tgt != newID {
+ if tgt := roomExitTarget(s, 2, world.West); tgt != newID {
t.Errorf("redo: B west should be %d, got %d", newID, tgt)
}
}
@@ -202,7 +204,7 @@ func TestInsertGridConflict(t *testing.T) {
t.Errorf("insert conflict: expected both colliding room ids (#3 and #8), got %q", msg)
}
// No partial writes: the world is unchanged.
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 2 {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != 2 {
t.Errorf("insert conflict: A east should still be 2, got %d", tgt)
}
}
@@ -228,10 +230,10 @@ func TestRemoveSuccess(t *testing.T) {
if roomExists(s, 2) {
t.Error("remove: B file should be gone")
}
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 3 {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != 3 {
t.Errorf("remove: A east should be 3, got %d", tgt)
}
- if tgt, _ := roomExitTarget(s, 3, world.West); tgt != 1 {
+ if tgt := roomExitTarget(s, 3, world.West); tgt != 1 {
t.Errorf("remove: C west should be 1, got %d", tgt)
}
@@ -244,16 +246,16 @@ func TestRemoveSuccess(t *testing.T) {
if !roomExists(s, 2) {
t.Error("undo: B file should be back")
}
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 2 {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != 2 {
t.Errorf("undo: A east should be 2, got %d", tgt)
}
- if tgt, _ := roomExitTarget(s, 3, world.West); tgt != 2 {
+ if tgt := roomExitTarget(s, 3, world.West); tgt != 2 {
t.Errorf("undo: C west should be 2, got %d", tgt)
}
- if tgt, _ := roomExitTarget(s, 2, world.West); tgt != 1 {
+ if tgt := roomExitTarget(s, 2, world.West); tgt != 1 {
t.Errorf("undo: B west should be 1, got %d", tgt)
}
- if tgt, _ := roomExitTarget(s, 2, world.East); tgt != 3 {
+ if tgt := roomExitTarget(s, 2, world.East); tgt != 3 {
t.Errorf("undo: B east should be 3, got %d", tgt)
}
@@ -266,7 +268,7 @@ func TestRemoveSuccess(t *testing.T) {
if roomExists(s, 2) {
t.Error("redo: B file should be gone again")
}
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 3 {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != 3 {
t.Errorf("redo: A east should be 3, got %d", tgt)
}
}
@@ -386,7 +388,7 @@ func TestRemoveGridConflict(t *testing.T) {
if !roomExists(s, 2) {
t.Error("remove conflict: B file should still exist")
}
- if tgt, _ := roomExitTarget(s, 1, world.East); tgt != 2 {
+ if tgt := roomExitTarget(s, 1, world.East); tgt != 2 {
t.Errorf("remove conflict: A east should still be 2, got %d", tgt)
}
}
@@ -434,10 +436,7 @@ func TestNextRoomIDNoCrossSubdirCollision(t *testing.T) {
undoStack: NewUndoStack(dir),
}
// Allocating near room 1 (root) must skip 2 (which lives in zone/).
- id, _, err := s.nextRoomID(1)
- if err != nil {
- t.Fatal(err)
- }
+ id, _ := s.nextRoomID(1)
if id == 2 {
t.Fatalf("nextRoomID returned %d, which collides with the zone/ room", id)
}
diff --git a/internal/admin/api_rooms.go b/internal/admin/api_rooms.go
index 8ee16a5..16fc7a0 100644
--- a/internal/admin/api_rooms.go
+++ b/internal/admin/api_rooms.go
@@ -21,7 +21,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
ids, err := listRoomIDs(s.dataDir)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
if ids == nil {
@@ -33,13 +33,13 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
r.Body.Close()
if err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("read body: %v", err)})
+ failJSON(w, fmt.Sprintf("read body: %v", err))
return
}
var m map[string]any
if err := json.Unmarshal(body, &m); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid json: %v", err)})
+ failJSON(w, fmt.Sprintf("invalid json: %v", err))
return
}
if m == nil {
@@ -93,11 +93,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
if linkFrom != nil {
fromID = *linkFrom
}
- id, subdir, allocErr := s.nextRoomID(fromID)
- if allocErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("alloc id: %v", allocErr)})
- return
- }
+ id, subdir := s.nextRoomID(fromID)
m["id"] = id
if linkFrom != nil && linkDir != nil && *linkDir != "" {
@@ -131,11 +127,11 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
conflicts++
})
if conflicts > 0 {
- writeJSON(w, map[string]any{"error": "Add Next Step: that grid spot is already occupied — room would cause map overlap"})
+ failJSON(w, "Add Next Step: that grid spot is already occupied — room would cause map overlap")
return
}
} else {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid direction: %s", *linkDir)})
+ failJSON(w, fmt.Sprintf("invalid direction: %s", *linkDir))
return
}
}
@@ -153,7 +149,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
if opp, ok := world.OppositeExit[dir]; ok {
srcRoom, loadErr := s.world.LoadRoom(*linkFrom)
if loadErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("load link_from room %d: %v", *linkFrom, loadErr)})
+ failJSON(w, fmt.Sprintf("load link_from room %d: %v", *linkFrom, loadErr))
return
}
@@ -166,7 +162,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
oldContent, _ := snapshotFile(srcPath)
newContent, writeErr := writeYAMLFile(srcPath, srcRoom)
if writeErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("write source room: %v", writeErr)})
+ failJSON(w, fmt.Sprintf("write source room: %v", writeErr))
return
}
extraFiles = append(extraFiles, ExtraFile{
@@ -185,7 +181,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) {
path := filepath.Join(subdir, strconv.Itoa(id)+".yaml")
newContent, writeErr := writeMapAsYAML(path, m)
if writeErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("write room: %v", writeErr)})
+ failJSON(w, fmt.Sprintf("write room: %v", writeErr))
return
}
@@ -214,7 +210,7 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
}
id, err := strconv.Atoi(idStr)
if err != nil {
- writeJSON(w, map[string]any{"error": "invalid room id"})
+ failJSON(w, "invalid room id")
return
}
@@ -234,17 +230,17 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
path, ok := s.world.GetRoomPath(id)
if !ok {
- writeJSON(w, map[string]any{"error": "room not found"})
+ failJSON(w, "room not found")
return
}
data, err := os.ReadFile(path)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
m, err := yamlToMap(data)
if err != nil {
- writeJSON(w, map[string]any{"error": "yaml parse error"})
+ failJSON(w, "yaml parse error")
return
}
m["id"] = id
@@ -253,13 +249,13 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
case http.MethodPut:
path, ok := s.world.GetRoomPath(id)
if !ok {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d not found", id)})
+ failJSON(w, fmt.Sprintf("room %d not found", id))
return
}
oldContent, snapErr := snapshotFile(path)
if snapErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("snapshot room: %v", snapErr)})
+ failJSON(w, fmt.Sprintf("snapshot room: %v", snapErr))
return
}
@@ -267,11 +263,11 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
body, readErr := io.ReadAll(r.Body)
r.Body.Close()
if readErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("read body: %v", readErr)})
+ failJSON(w, fmt.Sprintf("read body: %v", readErr))
return
}
if err := json.Unmarshal(body, &m); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid json: %v", err)})
+ failJSON(w, fmt.Sprintf("invalid json: %v", err))
return
}
if m == nil {
@@ -285,7 +281,7 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
newContent, writeErr := writeMapAsYAML(path, m)
if writeErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("write room: %v", writeErr)})
+ failJSON(w, fmt.Sprintf("write room: %v", writeErr))
return
}
@@ -301,20 +297,20 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
case http.MethodDelete:
path, ok := s.world.GetRoomPath(id)
if !ok {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d not found", id)})
+ failJSON(w, fmt.Sprintf("room %d not found", id))
return
}
oldContent, backupErr := snapshotFile(path)
if backupErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("backup room: %v", backupErr)})
+ failJSON(w, fmt.Sprintf("backup room: %v", backupErr))
return
}
courseID, _ := s.detachRoomFromCourse(id)
if err := os.Remove(path); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("delete room: %v", err)})
+ failJSON(w, fmt.Sprintf("delete room: %v", err))
return
}
@@ -433,16 +429,16 @@ func (s *AdminServer) handleRenameRoomDir(w http.ResponseWriter, r *http.Request
NewName string `json:"new_name"`
}
if err := readJSON(r, &body); err != nil || body.NewName == "" {
- writeJSON(w, map[string]any{"error": "invalid"})
+ failJSON(w, "invalid")
return
}
if body.Dir == "" {
- writeJSON(w, map[string]any{"error": "cannot rename root"})
+ failJSON(w, "cannot rename root")
return
}
newName := strings.TrimSpace(body.NewName)
if newName == "" || strings.ContainsAny(newName, "/\\") || newName == "." || newName == ".." {
- writeJSON(w, map[string]any{"error": "invalid directory name"})
+ failJSON(w, "invalid directory name")
return
}
base := filepath.Join(s.dataDir, "rooms")
@@ -451,15 +447,15 @@ func (s *AdminServer) handleRenameRoomDir(w http.ResponseWriter, r *http.Request
newRel := filepath.Join(parent, newName)
newPath := filepath.Join(base, newRel)
if _, err := os.Stat(oldPath); os.IsNotExist(err) {
- writeJSON(w, map[string]any{"error": "directory not found"})
+ failJSON(w, "directory not found")
return
}
if _, err := os.Stat(newPath); err == nil {
- writeJSON(w, map[string]any{"error": "directory already exists: " + newRel})
+ failJSON(w, "directory already exists: "+newRel)
return
}
if err := os.Rename(oldPath, newPath); err != nil {
- writeJSON(w, map[string]any{"error": "rename failed: " + err.Error()})
+ failJSON(w, "rename failed: "+err.Error())
return
}
s.world.RebuildRoomIndex(s.dataDir)
@@ -475,21 +471,21 @@ func (s *AdminServer) handleCreateRoomDir(w http.ResponseWriter, r *http.Request
Name string `json:"name"`
}
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid"})
+ failJSON(w, "invalid")
return
}
name := strings.TrimSpace(body.Name)
if name == "" || strings.ContainsAny(name, "/\\") || name == "." || name == ".." {
- writeJSON(w, map[string]any{"error": "invalid directory name"})
+ failJSON(w, "invalid directory name")
return
}
newPath := filepath.Join(s.dataDir, "rooms", name)
if _, err := os.Stat(newPath); err == nil {
- writeJSON(w, map[string]any{"error": "directory already exists"})
+ failJSON(w, "directory already exists")
return
}
if err := os.Mkdir(newPath, 0755); err != nil {
- writeJSON(w, map[string]any{"error": "create failed: " + err.Error()})
+ failJSON(w, "create failed: "+err.Error())
return
}
writeJSON(w, map[string]any{"ok": true, "name": name})
@@ -802,37 +798,37 @@ func (s *AdminServer) handleRoomMove(w http.ResponseWriter, r *http.Request) {
Dir string `json:"dir"`
}
if err := readJSON(r, &body); err != nil || body.ID <= 0 {
- writeJSON(w, map[string]any{"error": "invalid"})
+ failJSON(w, "invalid")
return
}
oldPath, ok := s.world.GetRoomPath(body.ID)
if !ok {
- writeJSON(w, map[string]any{"error": "room not found"})
+ failJSON(w, "room not found")
return
}
dir := strings.TrimSpace(body.Dir)
destDir := filepath.Join(s.dataDir, "rooms", dir)
if _, err := os.Stat(destDir); os.IsNotExist(err) {
- writeJSON(w, map[string]any{"error": "directory does not exist: " + dir})
+ failJSON(w, "directory does not exist: "+dir)
return
}
fileName := strconv.Itoa(body.ID) + ".yaml"
newPath := filepath.Join(destDir, fileName)
if _, err := os.Stat(newPath); err == nil {
- writeJSON(w, map[string]any{"error": "room already exists in target directory"})
+ failJSON(w, "room already exists in target directory")
return
}
data, err := os.ReadFile(oldPath)
if err != nil {
- writeJSON(w, map[string]any{"error": "read: " + err.Error()})
+ failJSON(w, "read: "+err.Error())
return
}
if err := os.WriteFile(newPath, data, 0644); err != nil {
- writeJSON(w, map[string]any{"error": "write: " + err.Error()})
+ failJSON(w, "write: "+err.Error())
return
}
if err := os.Remove(oldPath); err != nil {
- writeJSON(w, map[string]any{"error": "remove old: " + err.Error()})
+ failJSON(w, "remove old: "+err.Error())
return
}
s.world.RebuildRoomIndex(s.dataDir)
@@ -1137,7 +1133,7 @@ func (s *AdminServer) handleRoomSwap(w http.ResponseWriter, r *http.Request) {
func (s *AdminServer) handleDuplicateRooms(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
- writeJSON(w, map[string]any{"error": "method not allowed"})
+ failJSON(w, "method not allowed")
return
}
dups := behavior.CheckDuplicateIDs(filepath.Join(s.dataDir, "rooms"))
@@ -1154,7 +1150,7 @@ func (s *AdminServer) handleDuplicateRooms(w http.ResponseWriter, r *http.Reques
func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
- writeJSON(w, map[string]any{"error": "method not allowed"})
+ failJSON(w, "method not allowed")
return
}
var body struct {
@@ -1163,7 +1159,7 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ
NewID int `json:"new_id"`
}
if err := readJSON(r, &body); err != nil || body.Path == "" || body.Action == "" {
- writeJSON(w, map[string]any{"error": "invalid body"})
+ failJSON(w, "invalid body")
return
}
@@ -1171,7 +1167,7 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ
absRooms, _ := filepath.Abs(roomsDir)
absPath, err := filepath.Abs(body.Path)
if err != nil || !strings.HasPrefix(absPath, absRooms+string(filepath.Separator)) {
- writeJSON(w, map[string]any{"error": "invalid path"})
+ failJSON(w, "invalid path")
return
}
@@ -1179,7 +1175,7 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ
case "delete":
old, _ := snapshotFile(absPath)
if err := os.Remove(absPath); err != nil {
- writeJSON(w, map[string]any{"error": "delete failed: " + err.Error()})
+ failJSON(w, "delete failed: "+err.Error())
return
}
s.world.RebuildRoomIndex(s.dataDir)
@@ -1196,27 +1192,27 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ
case "rename":
if body.NewID <= 0 {
- writeJSON(w, map[string]any{"error": "new_id must be positive"})
+ failJSON(w, "new_id must be positive")
return
}
if _, ok := s.world.GetRoomPath(body.NewID); ok {
- writeJSON(w, map[string]any{"error": "room ID already exists"})
+ failJSON(w, "room ID already exists")
return
}
newRel := filepath.Join(filepath.Dir(absPath), strconv.Itoa(body.NewID)+".yaml")
if _, err := os.Stat(newRel); err == nil {
- writeJSON(w, map[string]any{"error": "target file already exists"})
+ failJSON(w, "target file already exists")
return
}
oldContent, err := os.ReadFile(absPath)
if err != nil {
- writeJSON(w, map[string]any{"error": "read failed: " + err.Error()})
+ failJSON(w, "read failed: "+err.Error())
return
}
m, err := yamlToMap(oldContent)
if err != nil {
- writeJSON(w, map[string]any{"error": "yaml parse failed: " + err.Error()})
+ failJSON(w, "yaml parse failed: "+err.Error())
return
}
// A duplicate file was never in the room index (only one of the pair
@@ -1228,11 +1224,11 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ
newContent, err := writeMapAsYAML(newRel, m)
if err != nil {
- writeJSON(w, map[string]any{"error": "write failed: " + err.Error()})
+ failJSON(w, "write failed: "+err.Error())
return
}
if err := os.Remove(absPath); err != nil {
- writeJSON(w, map[string]any{"error": "remove old failed: " + err.Error()})
+ failJSON(w, "remove old failed: "+err.Error())
return
}
s.world.RebuildRoomIndex(s.dataDir)
@@ -1246,7 +1242,7 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ
writeJSON(w, map[string]any{"ok": true})
default:
- writeJSON(w, map[string]any{"error": "unknown action: " + body.Action})
+ failJSON(w, "unknown action: "+body.Action)
}
}
@@ -1259,7 +1255,7 @@ func (s *AdminServer) handleCreateEmptyRoom(w http.ResponseWriter, r *http.Reque
Dir string `json:"dir"`
}
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid"})
+ failJSON(w, "invalid")
return
}
base := filepath.Join(s.dataDir, "rooms")
@@ -1267,7 +1263,7 @@ func (s *AdminServer) handleCreateEmptyRoom(w http.ResponseWriter, r *http.Reque
if body.Dir != "" {
targetDir = filepath.Join(base, body.Dir)
if _, err := os.Stat(targetDir); os.IsNotExist(err) {
- writeJSON(w, map[string]any{"error": "directory not found"})
+ failJSON(w, "directory not found")
return
}
}
@@ -1298,7 +1294,7 @@ func (s *AdminServer) handleCreateEmptyRoom(w http.ResponseWriter, r *http.Reque
}
newContent, writeErr := writeMapAsYAML(path, m)
if writeErr != nil {
- writeJSON(w, map[string]any{"error": "write room: " + writeErr.Error()})
+ failJSON(w, "write room: "+writeErr.Error())
return
}
s.world.RebuildRoomIndex(s.dataDir)
diff --git a/internal/admin/api_rooms_bulk.go b/internal/admin/api_rooms_bulk.go
index a7fcee7..d10e0e3 100644
--- a/internal/admin/api_rooms_bulk.go
+++ b/internal/admin/api_rooms_bulk.go
@@ -44,23 +44,23 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) {
body, readErr := io.ReadAll(r.Body)
r.Body.Close()
if readErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("read body: %v", readErr)})
+ failJSON(w, fmt.Sprintf("read body: %v", readErr))
return
}
var req bulkEditRequest
if err := json.Unmarshal(body, &req); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid json: %v", err)})
+ failJSON(w, fmt.Sprintf("invalid json: %v", err))
return
}
if len(req.IDs) == 0 {
- writeJSON(w, map[string]any{"error": "no room ids"})
+ failJSON(w, "no room ids")
return
}
if isEmptyPatch(req.Patch) {
- writeJSON(w, map[string]any{"error": "no patch fields"})
+ failJSON(w, "no patch fields")
return
}
@@ -78,17 +78,17 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) {
for _, id := range req.IDs {
path, ok := s.world.GetRoomPath(id)
if !ok {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d not found", id)})
+ failJSON(w, fmt.Sprintf("room %d not found", id))
return
}
oldContent, err := os.ReadFile(path)
if err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("read room %d: %v", id, err)})
+ failJSON(w, fmt.Sprintf("read room %d: %v", id, err))
return
}
m, yamlErr := yamlToMap(oldContent)
if yamlErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("parse room %d: %v", id, yamlErr)})
+ failJSON(w, fmt.Sprintf("parse room %d: %v", id, yamlErr))
return
}
@@ -110,13 +110,13 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) {
if patch.Dir != nil && *patch.Dir != "" {
destDir := filepath.Join(s.dataDir, "rooms", *patch.Dir)
if _, statErr := os.Stat(destDir); os.IsNotExist(statErr) {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("directory does not exist: %s", *patch.Dir)})
+ failJSON(w, fmt.Sprintf("directory does not exist: %s", *patch.Dir))
return
}
newPath = filepath.Join(destDir, strconv.Itoa(id)+".yaml")
if newPath != path {
if _, statErr := os.Stat(newPath); statErr == nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d already exists in target directory", id)})
+ failJSON(w, fmt.Sprintf("room %d already exists in target directory", id))
return
}
}
@@ -127,7 +127,7 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) {
newContent, marshalErr := marshalMapYAML(m)
if marshalErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("marshal room %d: %v", id, marshalErr)})
+ failJSON(w, fmt.Sprintf("marshal room %d: %v", id, marshalErr))
return
}
writes = append(writes, roomWrite{id: id, oldPath: path, newPath: newPath, oldContent: oldContent, newContent: newContent})
@@ -138,17 +138,17 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) {
for _, wr := range writes {
if wr.newPath != wr.oldPath {
if err := txn.Move(wr.oldPath, wr.newPath); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("move room %d: %v", wr.id, err)})
+ failJSON(w, fmt.Sprintf("move room %d: %v", wr.id, err))
return
}
}
if err := txn.Replace(wr.newPath, wr.newContent, 0644); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("write room %d: %v", wr.id, err)})
+ failJSON(w, fmt.Sprintf("write room %d: %v", wr.id, err))
return
}
}
if err := txn.Commit(); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("commit bulk edit: %v", err)})
+ failJSON(w, fmt.Sprintf("commit bulk edit: %v", err))
return
}
diff --git a/internal/admin/api_rooms_bulk_delete.go b/internal/admin/api_rooms_bulk_delete.go
index 70c2eb6..5b525f6 100644
--- a/internal/admin/api_rooms_bulk_delete.go
+++ b/internal/admin/api_rooms_bulk_delete.go
@@ -24,18 +24,18 @@ func (s *AdminServer) handleBulkDelete(w http.ResponseWriter, r *http.Request) {
body, readErr := io.ReadAll(r.Body)
r.Body.Close()
if readErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("read body: %v", readErr)})
+ failJSON(w, fmt.Sprintf("read body: %v", readErr))
return
}
var req bulkDeleteRequest
if err := json.Unmarshal(body, &req); err != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid json: %v", err)})
+ failJSON(w, fmt.Sprintf("invalid json: %v", err))
return
}
if len(req.IDs) == 0 {
- writeJSON(w, map[string]any{"error": "no room ids"})
+ failJSON(w, "no room ids")
return
}
@@ -45,7 +45,7 @@ func (s *AdminServer) handleBulkDelete(w http.ResponseWriter, r *http.Request) {
seenIDs := make(map[int]bool, len(req.IDs))
for _, id := range req.IDs {
if seenIDs[id] {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("duplicate room id %d in request", id)})
+ failJSON(w, fmt.Sprintf("duplicate room id %d in request", id))
return
}
seenIDs[id] = true
@@ -61,12 +61,12 @@ func (s *AdminServer) handleBulkDelete(w http.ResponseWriter, r *http.Request) {
for _, id := range req.IDs {
path, ok := s.world.GetRoomPath(id)
if !ok {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d not found", id)})
+ failJSON(w, fmt.Sprintf("room %d not found", id))
return
}
oldContent, snapErr := snapshotFile(path)
if snapErr != nil {
- writeJSON(w, map[string]any{"error": fmt.Sprintf("snapshot room %d: %v", id, snapErr)})
+ failJSON(w, fmt.Sprintf("snapshot room %d: %v", id, snapErr))
return
}
snapshots = append(snapshots, roomSnapshot{id: id, path: path, oldContent: oldContent})
@@ -85,7 +85,7 @@ func (s *AdminServer) handleBulkDelete(w http.ResponseWriter, r *http.Request) {
}
}
}
- writeJSON(w, map[string]any{"error": fmt.Sprintf("delete room %d: %v", snap.id, err)})
+ failJSON(w, fmt.Sprintf("delete room %d: %v", snap.id, err))
return
}
deletedSet[snap.id] = true
diff --git a/internal/admin/api_rooms_bulk_delete_test.go b/internal/admin/api_rooms_bulk_delete_test.go
index 083b890..66464a5 100644
--- a/internal/admin/api_rooms_bulk_delete_test.go
+++ b/internal/admin/api_rooms_bulk_delete_test.go
@@ -14,20 +14,20 @@ import (
"testing"
)
-func postBulkDelete(t *testing.T, s *AdminServer, body string) (map[string]any, int) {
+func postBulkDelete(t *testing.T, s *AdminServer, body string) map[string]any {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/api/rooms/bulk-delete", strings.NewReader(body))
rec := httptest.NewRecorder()
s.handleBulkDelete(rec, req)
var resp map[string]any
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
- return resp, rec.Code
+ return resp
}
func TestBulkDeleteRejectsDuplicateIDs(t *testing.T) {
s := newTestAdminServer(t, map[int]string{1: "id: 1\nname: Room\n"})
path, _ := s.world.GetRoomPath(1)
- resp, _ := postBulkDelete(t, s, `{"ids":[1,1]}`)
+ resp := postBulkDelete(t, s, `{"ids":[1,1]}`)
if errMsg, _ := resp["error"].(string); !strings.Contains(errMsg, "duplicate room id 1") {
t.Fatalf("duplicate IDs not rejected: %v", resp)
}
@@ -60,7 +60,7 @@ func TestBulkDeleteFailureRollsBackDeletions(t *testing.T) {
t.Cleanup(func() { _ = os.Chmod(sub, 0o755); os.RemoveAll(sub) })
path1, _ := s.world.GetRoomPath(1)
- resp, _ := postBulkDelete(t, s, `{"ids":[1,3]}`)
+ resp := postBulkDelete(t, s, `{"ids":[1,3]}`)
if errMsg, _ := resp["error"].(string); !strings.Contains(errMsg, "delete room 3") {
t.Fatalf("expected room-3 delete failure: %v", resp)
}
diff --git a/internal/admin/api_search.go b/internal/admin/api_search.go
index d54e7f1..b97f97e 100644
--- a/internal/admin/api_search.go
+++ b/internal/admin/api_search.go
@@ -18,7 +18,7 @@ func (s *AdminServer) handleSearch(w http.ResponseWriter, r *http.Request) {
q := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
if q == "" {
- writeJSON(w, map[string]any{"error": "missing query parameter 'q'"})
+ failJSON(w, "missing query parameter 'q'")
return
}
diff --git a/internal/admin/api_techs.go b/internal/admin/api_techs.go
index ba3288a..91eb929 100644
--- a/internal/admin/api_techs.go
+++ b/internal/admin/api_techs.go
@@ -20,7 +20,7 @@ func (s *AdminServer) handleTechs(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
tree, err := listYAMLTree(s.dataDir, "techs")
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
if tree == nil {
@@ -30,23 +30,23 @@ func (s *AdminServer) handleTechs(w http.ResponseWriter, r *http.Request) {
case http.MethodPost:
var m map[string]any
if err := readJSON(r, &m); err != nil {
- writeJSON(w, map[string]any{"error": "invalid json"})
+ failJSON(w, "invalid json")
return
}
id, ok := m["id"].(string)
if !ok || strings.TrimSpace(id) == "" {
- writeJSON(w, map[string]any{"error": "missing id"})
+ failJSON(w, "missing id")
return
}
delete(m, "id")
if _, _, err := s.findTechFile(id); err == nil {
- writeJSON(w, map[string]any{"error": "tech already exists"})
+ failJSON(w, "tech already exists")
return
}
path := filepath.Join(s.dataDir, "techs", id+".yaml")
newContent, err := writeMapAsYAML(path, m)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
@@ -76,12 +76,12 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) {
case http.MethodGet:
data, path, err := s.findTechFile(id)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
raw, err := yamlToMap(data)
if err != nil {
- writeJSON(w, map[string]any{"error": "parse error: " + err.Error()})
+ failJSON(w, "parse error: "+err.Error())
return
}
raw["id"] = id
@@ -92,7 +92,7 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) {
case http.MethodPost, http.MethodPut:
var body map[string]any
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid JSON: " + err.Error()})
+ failJSON(w, "invalid JSON: "+err.Error())
return
}
delete(body, "id")
@@ -114,7 +114,7 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) {
newContent, err := writeMapAsYAML(path, body)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
@@ -134,12 +134,12 @@ func (s *AdminServer) handleTechByID(w http.ResponseWriter, r *http.Request) {
case http.MethodDelete:
_, path, err := s.findTechFile(id)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
oldContent, _ := snapshotFile(path)
if err := os.Remove(path); err != nil {
- writeJSON(w, map[string]any{"error": "delete error: " + err.Error()})
+ failJSON(w, "delete error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
diff --git a/internal/admin/api_triggers.go b/internal/admin/api_triggers.go
index 87eb82e..5b82465 100644
--- a/internal/admin/api_triggers.go
+++ b/internal/admin/api_triggers.go
@@ -22,7 +22,7 @@ func (s *AdminServer) handleTriggers(w http.ResponseWriter, r *http.Request) {
writeJSON(w, &YAMLTree{Root: []string{}, Dirs: map[string][]string{}})
return
}
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
if tree == nil {
@@ -32,16 +32,16 @@ func (s *AdminServer) handleTriggers(w http.ResponseWriter, r *http.Request) {
case http.MethodPost:
var m map[string]any
if err := readJSON(r, &m); err != nil {
- writeJSON(w, map[string]any{"error": "invalid json"})
+ failJSON(w, "invalid json")
return
}
id, ok := m["id"].(string)
if !ok || strings.TrimSpace(id) == "" {
- writeJSON(w, map[string]any{"error": "missing id"})
+ failJSON(w, "missing id")
return
}
if !validEntityID(id) {
- writeJSON(w, map[string]any{"error": "invalid id"})
+ failJSON(w, "invalid id")
return
}
delete(m, "id")
@@ -51,13 +51,13 @@ func (s *AdminServer) handleTriggers(w http.ResponseWriter, r *http.Request) {
path := filepath.Join(s.dataDir, "triggers", subdir, id+".yaml")
os.MkdirAll(filepath.Dir(path), 0755)
if _, err := os.Stat(path); err == nil {
- writeJSON(w, map[string]any{"error": "trigger already exists"})
+ failJSON(w, "trigger already exists")
return
}
newContent, err := writeMapAsYAML(path, m)
if err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
@@ -87,17 +87,17 @@ func (s *AdminServer) handleTriggerByID(w http.ResponseWriter, r *http.Request)
case http.MethodGet:
path, _, err := findTriggerFile(s.dataDir, id)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
data, err := os.ReadFile(path)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
raw, err := yamlToMap(data)
if err != nil {
- writeJSON(w, map[string]any{"error": "parse error: " + err.Error()})
+ failJSON(w, "parse error: "+err.Error())
return
}
raw["id"] = id
@@ -109,7 +109,7 @@ func (s *AdminServer) handleTriggerByID(w http.ResponseWriter, r *http.Request)
case http.MethodPut:
var body map[string]any
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid JSON: " + err.Error()})
+ failJSON(w, "invalid JSON: "+err.Error())
return
}
delete(body, "id")
@@ -134,12 +134,12 @@ func (s *AdminServer) handleTriggerByID(w http.ResponseWriter, r *http.Request)
triggerData, err := triggerMapToYAML(body)
if err != nil {
- writeJSON(w, map[string]any{"error": "serialize error: " + err.Error()})
+ failJSON(w, "serialize error: "+err.Error())
return
}
if err := os.WriteFile(newPath, triggerData, 0644); err != nil {
- writeJSON(w, map[string]any{"error": "write error: " + err.Error()})
+ failJSON(w, "write error: "+err.Error())
return
}
@@ -159,16 +159,16 @@ func (s *AdminServer) handleTriggerByID(w http.ResponseWriter, r *http.Request)
case http.MethodDelete:
path, _, err := findTriggerFile(s.dataDir, id)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
oldContent, err := snapshotFile(path)
if err != nil {
- writeJSON(w, map[string]any{"error": "not found: " + id})
+ failJSON(w, "not found: "+id)
return
}
if err := os.Remove(path); err != nil {
- writeJSON(w, map[string]any{"error": "delete error: " + err.Error()})
+ failJSON(w, "delete error: "+err.Error())
return
}
s.undoStack.Push(ChangeDesc{
diff --git a/internal/admin/id_alloc.go b/internal/admin/id_alloc.go
index 3a3c973..83ca89c 100644
--- a/internal/admin/id_alloc.go
+++ b/internal/admin/id_alloc.go
@@ -13,7 +13,7 @@ import (
// collide with an ID that lives in a different subdirectory. The starting point
// is the local subdir's minimum existing ID (clustering), then the first
// globally-free ID scanning upward.
-func (s *AdminServer) nextRoomID(fromRoomID int) (int, string, error) {
+func (s *AdminServer) nextRoomID(fromRoomID int) (int, string) {
base := filepath.Join(s.dataDir, "rooms")
subdir, err := findRoomSubdir(base, fromRoomID)
if err != nil {
@@ -46,7 +46,7 @@ func (s *AdminServer) nextRoomID(fromRoomID int) (int, string, error) {
for used[id] {
id++
}
- return id, subdir, nil
+ return id, subdir
}
func findRoomSubdir(base string, roomID int) (string, error) {
diff --git a/internal/admin/server.go b/internal/admin/server.go
index fb0c53b..2c8681a 100644
--- a/internal/admin/server.go
+++ b/internal/admin/server.go
@@ -1,3 +1,6 @@
+// Package admin implements the HTTP admin server: authenticated routes for
+// editing game data (rooms, items, mobs, drops, triggers, colors, …), the
+// embedded web UI, and the undo stack backing it.
package admin
import (
@@ -516,6 +519,16 @@ func writeJSONError(w http.ResponseWriter, msg string, code int) {
json.NewEncoder(w).Encode(map[string]any{"error": msg})
}
+// failJSON writes a JSON error body without changing the HTTP status. This
+// is the long-standing wire behavior most admin editors depend on: they read
+// res.error out of a 200 response, while api wrappers in admin.js treat any
+// non-200 as a thrown raw-text error. Unifying status codes requires a
+// coordinated change across the static JS clients; do not silently switch
+// call sites to non-200 codes here.
+func failJSON(w http.ResponseWriter, msg string) {
+ writeJSONError(w, msg, http.StatusOK)
+}
+
func readJSON(r *http.Request, v any) error {
body, err := io.ReadAll(r.Body)
if err != nil {
@@ -580,20 +593,20 @@ func (s *AdminServer) handleEntityDirs(entityType string) http.HandlerFunc {
NewName string `json:"new_name"`
}
if err := readJSON(r, &body); err != nil {
- writeJSON(w, map[string]any{"error": "invalid"})
+ failJSON(w, "invalid")
return
}
switch body.Action {
case "create":
if err := createEntityDir(s.dataDir, entityType, body.Name); err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
writeJSON(w, map[string]any{"ok": true, "name": body.Name})
case "delete":
changes, err := deleteEntityDir(s.dataDir, entityType, body.Dir)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
for _, ch := range changes {
@@ -602,12 +615,12 @@ func (s *AdminServer) handleEntityDirs(entityType string) http.HandlerFunc {
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()})
+ failJSON(w, err.Error())
return
}
writeJSON(w, map[string]any{"ok": true, "name": body.NewName})
default:
- writeJSON(w, map[string]any{"error": "unknown action"})
+ failJSON(w, "unknown action")
}
}
}
@@ -624,12 +637,12 @@ func (s *AdminServer) handleEntityRename(entityType string) http.HandlerFunc {
}
if err := readJSON(r, &body); err != nil || body.ID == "" || body.NewID == "" ||
!validEntityID(body.NewID) {
- writeJSON(w, map[string]any{"error": "invalid"})
+ failJSON(w, "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()})
+ failJSON(w, err.Error())
return
}
desc := fmt.Sprintf("rename %s %s to %s", entityType, body.ID, body.NewID)
@@ -656,12 +669,12 @@ func (s *AdminServer) handleEntityMove(entityType string) http.HandlerFunc {
Dir string `json:"dir"`
}
if err := readJSON(r, &body); err != nil || body.ID == "" {
- writeJSON(w, map[string]any{"error": "invalid"})
+ failJSON(w, "invalid")
return
}
oldPath, newPath, data, err := moveEntityFile(s.dataDir, entityType, body.ID, body.Dir)
if err != nil {
- writeJSON(w, map[string]any{"error": err.Error()})
+ failJSON(w, err.Error())
return
}
desc := fmt.Sprintf("move %s %s to %s/", entityType, body.ID, body.Dir)
diff --git a/internal/admin/xterm_to_css.go b/internal/admin/xterm_to_css.go
deleted file mode 100644
index 6c1fda4..0000000
--- a/internal/admin/xterm_to_css.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package admin
-
-import (
- "fmt"
- "strconv"
- "strings"
-
- "thehouseoficarus/internal/color"
-)
-
-func XtermToCSS(xtermHex string) string {
- xtermHex = strings.TrimPrefix(xtermHex, "#")
- idx, err := strconv.ParseUint(xtermHex, 16, 8)
- if err != nil || idx > 255 {
- return "#808080"
- }
- r, g, b := color.Xterm256ToRGB(int(idx))
- return fmt.Sprintf("#%02x%02x%02x", r, g, b)
-}
-
-func XtermColorForCSS(idx int) string {
- if idx < 0 || idx > 255 {
- return "#808080"
- }
- r, g, b := color.Xterm256ToRGB(idx)
- return fmt.Sprintf("#%02x%02x%02x", r, g, b)
-}
diff --git a/internal/behavior/behavior.go b/internal/behavior/behavior.go
index 116918e..e5039af 100644
--- a/internal/behavior/behavior.go
+++ b/internal/behavior/behavior.go
@@ -1,3 +1,5 @@
+// Package behavior defines the data-driven trigger/sequence system (yaml
+// trigger blocks, steps, conditions) and shared yaml directory walking.
package behavior
type GatherConfig struct {
diff --git a/internal/cardart/store.go b/internal/cardart/store.go
index 0763e46..7e9b263 100644
--- a/internal/cardart/store.go
+++ b/internal/cardart/store.go
@@ -1,3 +1,5 @@
+// Package cardart renders playing-card style ASCII art used by the admin
+// dashboard.
package cardart
import (
diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go
index d3b26af..bc038c7 100644
--- a/internal/casino/blackjack.go
+++ b/internal/casino/blackjack.go
@@ -1,3 +1,5 @@
+// Package casino implements the in-game casino: chip/credit wallets, table
+// games (blackjack, baccarat), slot machines, and their shared state machine.
package casino
import (
diff --git a/internal/casino/credits_test.go b/internal/casino/credits_test.go
index 4094d72..cff19c9 100644
--- a/internal/casino/credits_test.go
+++ b/internal/casino/credits_test.go
@@ -55,7 +55,7 @@ func TestWalletErrorMessages(t *testing.T) {
}
func inf() float64 {
- var max float64 = 1e308
+ max := 1e308
return max * 10
}
func nan() float64 { return inf() - inf() }
diff --git a/internal/casino/machine.go b/internal/casino/machine.go
index f156fe7..757588b 100644
--- a/internal/casino/machine.go
+++ b/internal/casino/machine.go
@@ -79,7 +79,7 @@ func (m *Machine) setBet(name string, amount int, wallet Wallet) []Event {
if amount < m.Config.MinBet || amount > m.Config.MaxBet {
return m.private(fmt.Sprintf("Slot machine bets must be %s.", betRangeText(m.Config.MinBet, m.Config.MaxBet)))
}
- return m.beginSpinLocked(name, amount, wallet,
+ return m.beginSpinLocked(amount, wallet,
fmt.Sprintf("%s sets the bet to %d and pulls the lever.", name, amount),
fmt.Sprintf("Bet set to %d. You pull the lever on the slots machine.", amount))
}
@@ -99,12 +99,12 @@ func (m *Machine) start(name string, wallet Wallet) []Event {
if m.bet <= 0 {
return m.private("Set a wager first with 'bet <amount>'.")
}
- return m.beginSpinLocked(name, m.bet, wallet,
+ return m.beginSpinLocked(m.bet, wallet,
fmt.Sprintf("%s pulls the lever on the slots machine.", name),
"You pull the lever on the slots machine.")
}
-func (m *Machine) beginSpinLocked(name string, amount int, wallet Wallet, publicMessage, privateMessage string) []Event {
+func (m *Machine) beginSpinLocked(amount int, wallet Wallet, publicMessage, privateMessage string) []Event {
if _, err := wallet.Wager(amount); err != nil {
return m.private(walletErrorMessage(err, "You don't have enough chips and credits for that bet."))
}
diff --git a/internal/color/color.go b/internal/color/color.go
index e89acc8..ff47a47 100644
--- a/internal/color/color.go
+++ b/internal/color/color.go
@@ -1,3 +1,5 @@
+// Package color implements the game's ANSI/xterm256 color engine: palette
+// parsing, named tag expansion ({red}, {mob}, …), and render modes.
package color
import (
diff --git a/internal/combat/formulas.go b/internal/combat/formulas.go
index f165923..7b979e2 100644
--- a/internal/combat/formulas.go
+++ b/internal/combat/formulas.go
@@ -1,3 +1,5 @@
+// Package combat holds the pure combat formulas (attack roll, max hit,
+// damage, hit checks) shared by player and mob combat.
package combat
import "math/rand"
diff --git a/internal/config/config.go b/internal/config/config.go
index ffc4e50..362c9c1 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -1,3 +1,5 @@
+// Package config loads config.yaml (ports, TLS, tick length, data dir) with
+// defaults and self-generated certificates.
package config
import (
diff --git a/internal/engine/tick.go b/internal/engine/tick.go
index 63ee6af..aa10a73 100644
--- a/internal/engine/tick.go
+++ b/internal/engine/tick.go
@@ -1,3 +1,5 @@
+// Package engine implements the fixed-step game tick loop and subscription
+// dispatch that the MUD systems run on.
package engine
import (
diff --git a/internal/game/account_registration_regression_test.go b/internal/game/account_registration_regression_test.go
index 5ecd254..42e7ad1 100644
--- a/internal/game/account_registration_regression_test.go
+++ b/internal/game/account_registration_regression_test.go
@@ -14,7 +14,7 @@ import (
func TestPendingRegistrationCannotOverwriteExistingAccount(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
a := &net.Session{Conn: newChanConn(), State: net.StateAccountName}
diff --git a/internal/game/act_agility.go b/internal/game/act_agility.go
index 68ecaab..fd16102 100644
--- a/internal/game/act_agility.go
+++ b/internal/game/act_agility.go
@@ -140,7 +140,7 @@ func (g *Game) completeObstacle(sess *net.Session, p *player.Player, d *behavior
g.teleportPlayer(sess, p, nextRoom)
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
func (g *Game) obstacleFail(sess *net.Session, p *player.Player, d *behavior.ObstacleData) {
@@ -162,7 +162,7 @@ func (g *Game) obstacleFail(sess *net.Session, p *player.Player, d *behavior.Obs
sess.WriteLine(g.colorize(sess, "damage_taken",
fmt.Sprintf("You take %s damage. HP: %d/%d", g.dmgDisplay(sess, damage), p.HP, p.MaxHP())))
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
g.cancelAction(p)
g.teleportPlayer(sess, p, onFailRoom)
}
diff --git a/internal/game/act_burn.go b/internal/game/act_burn.go
index b310145..2d03ac4 100644
--- a/internal/game/act_burn.go
+++ b/internal/game/act_burn.go
@@ -191,7 +191,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) {
}
p.RemoveItem(itemID, 1)
g.World.AddGroundItem(p.RoomID, itemID, 1)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You drop the %s and begin to make a fire...", g.itemColorize(sess, logDef, logDef.Name))))
g.broadcastDrop(p, itemID, logDef.Name)
@@ -234,7 +234,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) {
g.awardSkillXP(sess, p, player.Firemaking, xp)
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
g.cancelAction(p)
msg := g.colorize(sess, "fire", "You manage to get a fire going!")
@@ -286,7 +286,7 @@ func (g *Game) advanceStoke(sess *net.Session, p *player.Player) {
g.awardSkillXP(sess, p, player.Firemaking, xp)
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
msg := g.colorize(sess, "fire", fmt.Sprintf("You throw %s onto the fire.", g.itemColorize(sess, logDef, name)))
if xp > 0 && p.OptionBool("xp_drops") {
@@ -353,7 +353,7 @@ func (g *Game) consumeFireTool(sess *net.Session, p *player.Player, toolItemID s
return false
}
slot.Quality--
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
return true
}
}
@@ -363,7 +363,7 @@ func (g *Game) consumeFireTool(sess *net.Session, p *player.Player, toolItemID s
return false
}
p.EquipQuality[toolItemID] = q - 1
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
return true
}
}
@@ -371,13 +371,13 @@ func (g *Game) consumeFireTool(sess *net.Session, p *player.Player, toolItemID s
if def != nil && def.Stackable {
if p.HasItem(toolItemID) {
p.RemoveItem(toolItemID, 1)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
return true
}
for slot, itemID := range p.Equipment {
if itemID == toolItemID {
delete(p.Equipment, slot)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
return true
}
}
diff --git a/internal/game/act_clean.go b/internal/game/act_clean.go
index f5f86d6..f995ae1 100644
--- a/internal/game/act_clean.go
+++ b/internal/game/act_clean.go
@@ -69,7 +69,7 @@ func (g *Game) advanceClean(sess *net.Session, p *player.Player) {
if craft.XP > 0 {
g.awardSkillXP(sess, p, player.Pharmacy, craft.XP)
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
msg := g.resolveCraftMessage(sess, craft.SuccessMessage, "You clean the %i1.", craft, matchItem.ID, nil, p.HasItem)
if p.OptionBool("xp_drops") && craft.XP > 0 {
diff --git a/internal/game/act_combine.go b/internal/game/act_combine.go
index 3d744db..26d85c6 100644
--- a/internal/game/act_combine.go
+++ b/internal/game/act_combine.go
@@ -129,7 +129,7 @@ func (g *Game) advanceCombine(sess *net.Session, p *player.Player) {
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: item.ID, Quantity: outputQty})
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if d.Remaining > 0 {
d.Remaining--
diff --git a/internal/game/act_effects.go b/internal/game/act_effects.go
index a49bd84..4438eb6 100644
--- a/internal/game/act_effects.go
+++ b/internal/game/act_effects.go
@@ -113,14 +113,14 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste
// 5. set_player_flags (and globals, folded in so the SetAll happens once).
if len(step.SetGlobalFlags) > 0 || len(step.SetPlayerFlags) > 0 {
g.applyFlagMutations(p, step.SetGlobalFlags, step.SetPlayerFlags)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
// 6. take_item
if step.TakeItem != "" {
if p.HasItem(step.TakeItem) {
p.RemoveItem(step.TakeItem, 1)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
}
@@ -133,7 +133,7 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste
}
} else {
p.SetInvSlot(slot, &player.InventorySlot{ItemID: step.GiveItem, Quantity: 1})
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
}
@@ -153,7 +153,7 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste
if maxHP := p.MaxHP(); p.HP > maxHP {
p.HP = maxHP
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if sess != nil {
sess.WriteLine(fmt.Sprintf("You regain %d hitpoints.", step.Heal))
}
@@ -164,13 +164,13 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste
if step.Credits < 0 {
if p.Credits >= -step.Credits {
p.Credits += step.Credits
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
} else if sess != nil {
sess.WriteLine(fmt.Sprintf("You don't have enough credits. (Need %d, have %d)", -step.Credits, p.Credits))
}
} else {
p.Credits += step.Credits
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
}
@@ -195,7 +195,7 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste
// 14. aps_node — dynamic room-id flag, kept as a dedicated effect.
if step.ApsNode {
g.setPlayerFlag(p, "aps_node_"+strconv.Itoa(p.RoomID), true)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
}
@@ -230,7 +230,7 @@ func (g *Game) giveTriggerDrop(sess *net.Session, p *player.Player, drop *behavi
if sess != nil {
sess.WriteLine(fmt.Sprintf("You receive %d credits.", qty))
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
return
}
@@ -249,7 +249,7 @@ func (g *Game) giveTriggerDrop(sess *net.Session, p *player.Player, drop *behavi
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty})
sess.WriteLine(fmt.Sprintf("You receive %s.", g.itemColorize(sess, lootDef, name)))
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
// applyTalkStep fires a talk node/option's Action (a single Step)
diff --git a/internal/game/act_farm.go b/internal/game/act_farm.go
index cc7ddc4..91384a0 100644
--- a/internal/game/act_farm.go
+++ b/internal/game/act_farm.go
@@ -115,7 +115,7 @@ func (g *Game) advanceFarmGrowth(sess *net.Session, p *player.Player) {
g.setPlayerFlag(p, prefix+"_diseased", false)
sess.WriteLine(g.colorize(sess, "farm_disease",
fmt.Sprintf("Your %s has died from disease!", g.seedDisplayName(sess, seedID))))
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
continue
}
@@ -157,7 +157,7 @@ func (g *Game) advanceFarmGrowth(sess *net.Session, p *player.Player) {
g.seedDisplayName(sess, seedID), stage, maxStages)))
}
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
}
@@ -194,7 +194,7 @@ func (g *Game) advancePlant(sess *net.Session, p *player.Player) {
g.awardSkillXP(sess, p, player.Farming, intXP)
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
seedName := seedID
if def, err := g.ItemStore.Load(seedID); err == nil {
@@ -288,7 +288,7 @@ func (g *Game) advanceHarvest(sess *net.Session, p *player.Player) {
g.setPlayerFlag(p, prefix+"_diseased", false)
g.setPlayerFlag(p, prefix+"_dead", false)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
productName := product
if productDef != nil {
@@ -322,7 +322,7 @@ func (g *Game) advanceRake(sess *net.Session, p *player.Player) {
g.setPlayerFlag(p, prefix+"_diseased", false)
g.setPlayerFlag(p, prefix+"_watered", false)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
patchName := patchDisplayName(prefix)
sess.WriteLine(fmt.Sprintf("You rake the %s clean.", patchName))
@@ -342,7 +342,7 @@ func (g *Game) advanceWater(sess *net.Session, p *player.Player) {
g.setPlayerFlag(p, prefix+"_watered", true)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
patchName := patchDisplayName(prefix)
sess.WriteLine(fmt.Sprintf("You water the %s.", patchName))
@@ -363,7 +363,7 @@ func (g *Game) advanceCure(sess *net.Session, p *player.Player) {
p.RemoveItem("plant_cure", 1)
g.setPlayerFlag(p, prefix+"_diseased", false)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine("You apply the plant cure. The patch looks healthy again.")
diff --git a/internal/game/act_fletch.go b/internal/game/act_fletch.go
index c4dcef0..96ba736 100644
--- a/internal/game/act_fletch.go
+++ b/internal/game/act_fletch.go
@@ -13,7 +13,7 @@ import (
func (g *Game) startFletchAction(sess *net.Session, p *player.Player, def *item.ItemDef, count int) {
p.EnsureFlags()
g.setPlayerFlag(p, "last_fletch", def.ID)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if def.FirstCraft().TicksPerCycle <= 0 {
g.doInstantFletch(sess, p, def, count)
@@ -80,7 +80,7 @@ func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, def *item.It
return
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
outDef, _ := g.ItemStore.Load(def.ID)
outputName := def.Name
@@ -186,7 +186,7 @@ func (g *Game) advanceFletch(sess *net.Session, p *player.Player) {
if c.XP > 0 {
g.awardSkillXP(sess, p, skill, c.XP)
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
msg := g.resolveCraftMessage(sess, c.SuccessMessage, "You fletch a %n.", c, def.ID, nil, p.HasItem)
if p.OptionBool("xp_drops") && c.XP > 0 {
diff --git a/internal/game/act_gather.go b/internal/game/act_gather.go
index 447e23a..a1c87b9 100644
--- a/internal/game/act_gather.go
+++ b/internal/game/act_gather.go
@@ -239,7 +239,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
if xp > 0 {
g.awardSkillXP(sess, p, player.SkillName(cfg.Skill), xp)
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
msg := drop.SuccessMessage
coloredName := g.itemColorize(sess, itemDef, itemName)
@@ -452,6 +452,6 @@ func (g *Game) checkBirdNest(sess *net.Session, p *player.Player, cfg *behavior.
}
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: birdsNestItemID, Quantity: 1})
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine(fmt.Sprintf("A %s falls out of the tree!", nestName))
}
diff --git a/internal/game/act_identify.go b/internal/game/act_identify.go
index b905494..6ff4d61 100644
--- a/internal/game/act_identify.go
+++ b/internal/game/act_identify.go
@@ -78,6 +78,6 @@ func (g *Game) advanceIdentify(sess *net.Session, p *player.Player) {
}
sess.WriteLine(msg)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
g.cancelAction(p)
}
diff --git a/internal/game/act_state_test.go b/internal/game/act_state_test.go
index 0ec139f..4182c61 100644
--- a/internal/game/act_state_test.go
+++ b/internal/game/act_state_test.go
@@ -8,7 +8,7 @@ import (
func TestPlayerActionDisplayFightingInCombat(t *testing.T) {
g := escapeTestGame()
- inst := spawnTestMob(g, "skeleton", 100)
+ inst := spawnTestMob(g, 100)
p := player.New("combatant")
p.EscapeDir = "north"
diff --git a/internal/game/act_steal.go b/internal/game/act_steal.go
index 880acff..f9ca095 100644
--- a/internal/game/act_steal.go
+++ b/internal/game/act_steal.go
@@ -69,7 +69,7 @@ func (g *Game) doSteal(sess *net.Session, input string) {
g.cancelAction(p)
- targetType, mob, objDef, errMsg := g.resolveStealTarget(sess, p, input)
+ targetType, mob, objDef, errMsg := g.resolveStealTarget(p, input)
if errMsg != "" {
sess.WriteLine(errMsg)
return
@@ -78,7 +78,7 @@ func (g *Game) doSteal(sess *net.Session, input string) {
g.startSteal(sess, p, targetType, mob, objDef)
}
-func (g *Game) resolveStealTarget(sess *net.Session, p *player.Player, input string) (targetType string, mob *world.MobInstance, objDef *object.ObjectDef, errMsg string) {
+func (g *Game) resolveStealTarget(p *player.Player, input string) (targetType string, mob *world.MobInstance, objDef *object.ObjectDef, errMsg string) {
input = strings.ToLower(strings.TrimSpace(input))
instanceIdx := -1
searchName := input
@@ -377,7 +377,7 @@ func (g *Game) advanceSteal(sess *net.Session, p *player.Player) {
g.awardSkillXP(sess, p, player.Thieving, stealXP)
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if p.FirstFreeSlot() == -1 {
sess.WriteLine("Your inventory is full.")
diff --git a/internal/game/baseline_race_test.go b/internal/game/baseline_race_test.go
index fc07c5e..4efc907 100644
--- a/internal/game/baseline_race_test.go
+++ b/internal/game/baseline_race_test.go
@@ -16,16 +16,16 @@ import (
// ticker goroutine, and hub disconnect callbacks.
func TestBaselineRaceSessionInputTickDisconnect(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
- h.seedAccount("bob", "pw", false)
+ h.seedAccount("alice")
+ h.seedAccount("bob")
h.seedCharacter("alice", "alice")
h.seedCharacter("bob", "bob")
aSess, _ := h.connect("alice")
bSess, _ := h.connect("bob")
- aLines := h.goInput(aSess, 1)
- bLines := h.goInput(bSess, 1)
+ aLines := h.goInput(aSess)
+ bLines := h.goInput(bSess)
var wg sync.WaitGroup
wg.Add(2)
@@ -61,10 +61,10 @@ func TestBaselineRaceSessionInputTickDisconnect(t *testing.T) {
// admin entry point) while players keep moving and ticking.
func TestBaselineRaceAdminRewriteWhilePlaying(t *testing.T) {
h := newHarness(t)
- h.seedAccount("carol", "pw", false)
+ h.seedAccount("carol")
h.seedCharacter("carol", "carol")
sess, _ := h.connect("carol")
- lines := h.goInput(sess, 1)
+ lines := h.goInput(sess)
var wg sync.WaitGroup
wg.Add(1)
@@ -90,7 +90,7 @@ func TestBaselineRaceAdminRewriteWhilePlaying(t *testing.T) {
func TestBaselineRaceCasinoWallet(t *testing.T) {
h := newHarness(t)
for _, n := range []string{"dave", "erin", "frank"} {
- h.seedAccount(n, "pw", false)
+ h.seedAccount(n)
p := h.seedCharacter(n, n)
p.Credits = 100000
}
@@ -115,7 +115,7 @@ func TestBaselineRaceCasinoWallet(t *testing.T) {
}()
// Session input competes with the wallet operations.
- lines := h.goInput(s, 1)
+ lines := h.goInput(s)
wg.Add(1)
go func() {
defer wg.Done()
diff --git a/internal/game/casino.go b/internal/game/casino.go
index 5268d34..9af9156 100644
--- a/internal/game/casino.go
+++ b/internal/game/casino.go
@@ -189,7 +189,7 @@ func casinoAffordable(p *player.Player) int { return p.Credits + countChips(p) }
// the player is now clear of all casino games; false means a departure is
// blocked mid-round and the caller (quit) must abort. Nil-safe for test
// scaffolding without a Casino manager.
-func (g *Game) leaveCasinoGames(sess *net.Session, p *player.Player) bool {
+func (g *Game) leaveCasinoGames(p *player.Player) bool {
if g.Casino == nil {
return true
}
diff --git a/internal/game/casino_forfeit_regression_test.go b/internal/game/casino_forfeit_regression_test.go
index db5ba31..d29187d 100644
--- a/internal/game/casino_forfeit_regression_test.go
+++ b/internal/game/casino_forfeit_regression_test.go
@@ -15,7 +15,7 @@ import (
// it on the next startup.
func TestDisconnectForfeitsMachineStake(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 100
@@ -40,7 +40,7 @@ func TestDisconnectForfeitsMachineStake(t *testing.T) {
// frees up and the stake is not refunded by recovery.
func TestDisconnectForfeitsBlackjackStake(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
// A natural opening hand (player or dealer blackjack) settles the round
// inside Bet() itself, before any disconnect can forfeit anything. Deal
diff --git a/internal/game/cmd_aps.go b/internal/game/cmd_aps.go
index 56820ad..88befa5 100644
--- a/internal/game/cmd_aps.go
+++ b/internal/game/cmd_aps.go
@@ -125,14 +125,14 @@ func displayApsNodes(g *Game, sess *net.Session, p *player.Player, known []int)
})
mode := g.colorMode(sess)
- hLabel := g.resolveConstant(sess, "table_header_label")
- hDetail := g.resolveConstant(sess, "table_header_detail")
- hValue := g.resolveConstant(sess, "table_header_value")
+ hLabel := g.resolveConstant("table_header_label")
+ hDetail := g.resolveConstant("table_header_detail")
+ hValue := g.resolveConstant("table_header_value")
rows := make([][]string, len(infos))
for i, info := range infos {
distStr := strconv.Itoa(info.Distance)
if info.Distance == 0 {
- distStr = color.Render(mode, g.resolveConstant(sess, "aps_here"), "here")
+ distStr = color.Render(mode, g.resolveConstant("aps_here"), "here")
}
rows[i] = []string{
color.Render(mode, hDetail, fmt.Sprintf("#%d", info.ID)),
diff --git a/internal/game/cmd_bank.go b/internal/game/cmd_bank.go
index 86057cc..8da6e89 100644
--- a/internal/game/cmd_bank.go
+++ b/internal/game/cmd_bank.go
@@ -163,7 +163,7 @@ func (g *Game) doBankDeposit(sess *net.Session, input string) {
MaxQuality: slot.MaxQuality,
})
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
displayName := itemID
if def != nil {
@@ -202,7 +202,7 @@ func (g *Game) doBankDepositAll(sess *net.Session) {
return
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine(fmt.Sprintf("You deposit %d item%s.", deposited, plural(deposited)))
}
@@ -300,7 +300,7 @@ func (g *Game) withdrawBankItem(sess *net.Session, p *player.Player, bankSlotIdx
slot.Quality == bs.Quality && slot.MaxQuality == bs.MaxQuality {
slot.Quantity += bs.Quantity
p.SetBankSlot(bankSlotIdx, nil)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
displayName := bs.ItemID
if def != nil {
displayName = def.Name
@@ -327,7 +327,7 @@ func (g *Game) withdrawBankItem(sess *net.Session, p *player.Player, bankSlotIdx
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: itemID, Quantity: 1, Quality: bs.Quality, MaxQuality: bs.MaxQuality})
if qty > 1 {
bs.Quantity--
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
displayName := itemID
if def != nil {
displayName = def.Name
@@ -339,7 +339,7 @@ func (g *Game) withdrawBankItem(sess *net.Session, p *player.Player, bankSlotIdx
}
p.SetBankSlot(bankSlotIdx, nil)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
displayName := itemID
if def != nil {
@@ -370,7 +370,7 @@ func (g *Game) doBankDepositCredits(sess *net.Session, parts []string) {
}
p.Credits -= amount
p.BankedCredits += amount
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine(g.colorize(sess, "currency_pickup", fmt.Sprintf("You deposit %s credits. (Banked: %s)", formatInt(amount), formatInt(p.BankedCredits))))
g.showBankBrowse(sess)
}
@@ -392,7 +392,7 @@ func (g *Game) doBankWithdrawCredits(sess *net.Session, parts []string) {
}
p.BankedCredits -= amount
p.Credits += amount
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine(g.colorize(sess, "currency_pickup", fmt.Sprintf("You withdraw %s credits.", formatInt(amount))))
g.showBankBrowse(sess)
}
diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go
index b659795..614583f 100644
--- a/internal/game/cmd_color.go
+++ b/internal/game/cmd_color.go
@@ -93,7 +93,7 @@ func (g *Game) saveAccountColor(sess *net.Session, target, value string) {
}
acc.Colors[target] = value
}
- g.AccountStore.SaveAccount(acc)
+ g.saveAccount(acc)
}
func (g *Game) getCurrentColor(sess *net.Session, category string) string {
diff --git a/internal/game/cmd_consume.go b/internal/game/cmd_consume.go
index aec33c5..a49d897 100644
--- a/internal/game/cmd_consume.go
+++ b/internal/game/cmd_consume.go
@@ -101,7 +101,7 @@ func (g *Game) doEatNow(sess *net.Session, p *player.Player, itemID string, def
}
p.ConsumeCooldown = 3
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine(g.colorize(sess, "eat_food", def.EatMessage))
diff --git a/internal/game/cmd_craft.go b/internal/game/cmd_craft.go
index 37038c0..7091e39 100644
--- a/internal/game/cmd_craft.go
+++ b/internal/game/cmd_craft.go
@@ -165,7 +165,7 @@ func (g *Game) availableItems(p *player.Player, allItems []*item.ItemDef) []*ite
func (g *Game) startItem(sess *net.Session, p *player.Player, def *item.ItemDef, count int, flagKey string) {
p.EnsureFlags()
g.setPlayerFlag(p, flagKey, def.ID)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
g.startProductionFromItem(sess, p, def, count)
}
@@ -182,7 +182,7 @@ func (g *Game) hasGrimyHerbs(p *player.Player) bool {
return false
}
-func (g *Game) findCraftItems(p *player.Player, itemAID, itemBID string) []*item.ItemDef {
+func (g *Game) findCraftItems(itemAID, itemBID string) []*item.ItemDef {
items := g.CraftIndex.ByType("crafting")
toolTypeA := ""
diff --git a/internal/game/cmd_description.go b/internal/game/cmd_description.go
index 1fc24ae..c8a13d1 100644
--- a/internal/game/cmd_description.go
+++ b/internal/game/cmd_description.go
@@ -27,7 +27,7 @@ func (g *Game) handleDescChange(sess *net.Session, input string) {
if input != "" {
p := sess.Player
p.Description = input
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine(fmt.Sprintf("Description set to: %s", input))
} else {
sess.WriteLine("Description left unchanged.")
diff --git a/internal/game/cmd_dig.go b/internal/game/cmd_dig.go
index 3fda55e..1497c34 100644
--- a/internal/game/cmd_dig.go
+++ b/internal/game/cmd_dig.go
@@ -98,7 +98,7 @@ func (g *Game) executeDig(sess *net.Session, args []string, rawInput string) {
p.RoomID = conflictID
p.HazardTimer = 0
p.Stats.RecordRoomVisit(conflictID)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
g.World.SeedGroundItems(conflictID)
g.seedRoomMobs(conflictID)
@@ -194,7 +194,7 @@ func (g *Game) executeDig(sess *net.Session, args []string, rawInput string) {
p.RoomID = newID
p.HazardTimer = 0
p.Stats.RecordRoomVisit(newID)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
g.World.SeedGroundItems(newID)
g.seedRoomMobs(newID)
diff --git a/internal/game/cmd_drop.go b/internal/game/cmd_drop.go
index 674f3da..072f54a 100644
--- a/internal/game/cmd_drop.go
+++ b/internal/game/cmd_drop.go
@@ -55,7 +55,7 @@ func (g *Game) doDropAllNamed(sess *net.Session, input string) {
qty := slot.Quantity
p.SetInvSlot(matches[0].Slot, nil)
g.World.AddGroundItem(p.RoomID, itemID, qty)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if qty == 1 {
sess.WriteLine(fmt.Sprintf("You drop a %s.", coloredName))
} else {
@@ -74,7 +74,7 @@ func (g *Game) doDropAllNamed(sess *net.Session, input string) {
p.SetInvSlot(match.Slot, nil)
g.World.AddGroundItem(p.RoomID, itemID, slot.Quantity)
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if total == 1 {
sess.WriteLine(fmt.Sprintf("You drop a %s.", coloredName))
} else {
@@ -138,7 +138,7 @@ func (g *Game) doDrop(sess *net.Session, input string) {
p.SetInvSlot(matches[0].Slot, nil)
}
g.World.AddGroundItem(p.RoomID, itemID, qty)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if qty == 1 {
sess.WriteLine(fmt.Sprintf("You drop a %s.", g.itemColorize(sess, def, name)))
} else {
@@ -167,7 +167,7 @@ func (g *Game) doDrop(sess *net.Session, input string) {
}
dropped := qty - remaining
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if dropped == 0 {
sess.WriteLine(fmt.Sprintf("You don't have any %s.", g.itemColorize(sess, def, name)))
} else if dropped == 1 {
@@ -223,7 +223,7 @@ func (g *Game) handleDropAll(sess *net.Session, input string) {
p.SetInvSlot(i, nil)
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.State = net.StateGame
if len(dropped) == 0 {
diff --git a/internal/game/cmd_estate_directory.go b/internal/game/cmd_estate_directory.go
index 2441157..fc79e6c 100644
--- a/internal/game/cmd_estate_directory.go
+++ b/internal/game/cmd_estate_directory.go
@@ -206,7 +206,7 @@ func (g *Game) teleportPlayer(sess *net.Session, p *player.Player, roomID int) {
g.World.SeedGroundItems(p.RoomID)
g.seedRoomMobs(p.RoomID)
g.seedRoomObjects(p.RoomID)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
g.doLook(sess)
g.runEnterSteps(sess, p.RoomID)
g.writePrompt(sess)
diff --git a/internal/game/cmd_farm.go b/internal/game/cmd_farm.go
index aaa3779..dec16c0 100644
--- a/internal/game/cmd_farm.go
+++ b/internal/game/cmd_farm.go
@@ -138,13 +138,13 @@ func (g *Game) doPlant(sess *net.Session, input string) {
},
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
func (g *Game) doHarvest(sess *net.Session, input string) {
p := sess.Player
- prefix, _, _ := g.findFarmPatch(sess, p, input, func(flags map[string]any, prefix string) bool {
+ prefix := g.findFarmPatch(p, input, func(flags map[string]any, prefix string) bool {
ready, _ := flags[prefix+"_ready"].(bool)
return ready
})
@@ -223,7 +223,7 @@ func (g *Game) doHarvest(sess *net.Session, input string) {
},
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
func (g *Game) doRake(sess *net.Session, input string) {
@@ -234,7 +234,7 @@ func (g *Game) doRake(sess *net.Session, input string) {
return
}
- prefix, _, _ := g.findFarmPatch(sess, p, input, func(flags map[string]any, prefix string) bool {
+ prefix := g.findFarmPatch(p, input, func(flags map[string]any, prefix string) bool {
weeds, _ := flags[prefix+"_weeds"].(bool)
dead, _ := flags[prefix+"_dead"].(bool)
return weeds || dead
@@ -268,7 +268,7 @@ func (g *Game) doRake(sess *net.Session, input string) {
},
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
func (g *Game) doWater(sess *net.Session, input string) {
@@ -279,7 +279,7 @@ func (g *Game) doWater(sess *net.Session, input string) {
return
}
- prefix, _, _ := g.findFarmPatch(sess, p, input, func(flags map[string]any, prefix string) bool {
+ prefix := g.findFarmPatch(p, input, func(flags map[string]any, prefix string) bool {
seed, _ := flags[prefix+"_seed"].(string)
if seed == "" {
return false
@@ -327,7 +327,7 @@ func (g *Game) doWater(sess *net.Session, input string) {
},
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
func (g *Game) doCure(sess *net.Session, input string) {
@@ -338,7 +338,7 @@ func (g *Game) doCure(sess *net.Session, input string) {
return
}
- prefix, _, _ := g.findFarmPatch(sess, p, input, func(flags map[string]any, prefix string) bool {
+ prefix := g.findFarmPatch(p, input, func(flags map[string]any, prefix string) bool {
diseased, _ := flags[prefix+"_diseased"].(bool)
return diseased
})
@@ -370,7 +370,7 @@ func (g *Game) doCure(sess *net.Session, input string) {
},
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
func (g *Game) ShowFarmPatchDetail(sess *net.Session, p *player.Player, prefix, defID string, index int) {
@@ -436,10 +436,10 @@ func boolToYes(b bool) string {
return "No"
}
-func (g *Game) findFarmPatch(sess *net.Session, p *player.Player, input string, matcher func(map[string]any, string) bool) (prefix string, defID string, index int) {
+func (g *Game) findFarmPatch(p *player.Player, input string, matcher func(map[string]any, string) bool) (prefix string) {
objInstances := g.World.AllObjInstances(p.RoomID)
if len(objInstances) == 0 {
- return "", "", -1
+ return ""
}
type patchInfo struct {
@@ -478,24 +478,24 @@ func (g *Game) findFarmPatch(sess *net.Session, p *player.Player, input string,
}
if len(patches) == 0 {
- return "", "", -1
+ return ""
}
if input == "" {
- return patches[0].prefix, patches[0].defID, patches[0].index
+ return patches[0].prefix
}
lower := strings.ToLower(input)
for _, pi := range patches {
if strings.Contains(lower, strings.ToLower(pi.name)) {
- return pi.prefix, pi.defID, pi.index
+ return pi.prefix
}
}
for _, pi := range patches {
if strings.Contains(lower, pi.prefix) {
- return pi.prefix, pi.defID, pi.index
+ return pi.prefix
}
}
- return "", "", -1
+ return ""
}
diff --git a/internal/game/cmd_fletch.go b/internal/game/cmd_fletch.go
index d638bc0..af27897 100644
--- a/internal/game/cmd_fletch.go
+++ b/internal/game/cmd_fletch.go
@@ -173,7 +173,7 @@ func (g *Game) availableFletchItems(p *player.Player, allItems []*item.ItemDef)
return result
}
-func (g *Game) findFletchItems(p *player.Player, itemAID, itemBID string) []*item.ItemDef {
+func (g *Game) findFletchItems(itemAID, itemBID string) []*item.ItemDef {
items := g.CraftIndex.ByType("fletching")
toolTypeA := ""
diff --git a/internal/game/cmd_get.go b/internal/game/cmd_get.go
index 7a5181f..f2776a3 100644
--- a/internal/game/cmd_get.go
+++ b/internal/game/cmd_get.go
@@ -83,7 +83,7 @@ func (g *Game) doGet(sess *net.Session, input string) {
return
}
slot.Quantity += removed
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine(fmt.Sprintf("You pick up %d x %s. (now %d)", qty, g.itemColorize(sess, def, def.Name), slot.Quantity))
return
}
@@ -98,7 +98,7 @@ func (g *Game) doGet(sess *net.Session, input string) {
return
}
p.SetInvSlot(freeSlot, g.newInventorySlot(itemID, qty))
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if qty == 1 {
sess.WriteLine(fmt.Sprintf("You pick up a %s.", g.itemColorize(sess, def, def.Name)))
} else {
@@ -120,7 +120,7 @@ func (g *Game) doGet(sess *net.Session, input string) {
picked++
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if picked == 0 {
sess.WriteLine("Your inventory is full.")
} else if picked == 1 {
@@ -201,7 +201,7 @@ func (g *Game) doGetAllNamed(sess *net.Session, input string) {
return
}
slot.Quantity += removed
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine(fmt.Sprintf("You pick up %d x %s. (now %d)", removed, coloredName, slot.Quantity))
return
}
@@ -217,7 +217,7 @@ func (g *Game) doGetAllNamed(sess *net.Session, input string) {
return
}
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: itemID, Quantity: removed})
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if removed == 1 {
sess.WriteLine(fmt.Sprintf("You pick up a %s.", coloredName))
} else {
@@ -240,7 +240,7 @@ func (g *Game) doGetAllNamed(sess *net.Session, input string) {
picked++
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if picked == 0 {
sess.WriteLine("Your inventory is full.")
} else if picked == 1 {
@@ -316,7 +316,7 @@ func (g *Game) doGetAll(sess *net.Session) {
} else {
sess.WriteLine("Your inventory is full!")
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
return
}
_, ok := g.World.RemoveReservedItem(p.RoomID, itemID, qty, p.Name)
@@ -340,7 +340,7 @@ func (g *Game) doGetAll(sess *net.Session) {
} else {
sess.WriteLine("Your inventory is full!")
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
return
}
@@ -359,7 +359,7 @@ func (g *Game) doGetAll(sess *net.Session) {
}
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if len(picked) == 0 {
sess.WriteLine("Your inventory is full.")
@@ -379,7 +379,7 @@ func (g *Game) pickupCredits(sess *net.Session, p *player.Player, itemID string)
return
}
p.Credits += qty
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if qty == 1 {
sess.WriteLine(g.colorize(sess, "currency_pickup", "You pick up 1 credit."))
} else {
diff --git a/internal/game/cmd_god.go b/internal/game/cmd_god.go
index 56311cb..b819fbd 100644
--- a/internal/game/cmd_god.go
+++ b/internal/game/cmd_god.go
@@ -61,7 +61,7 @@ func (g *Game) executeUnGod(sess *net.Session, args []string, rawInput string) {
return
}
g.restoreGodPlayer(p)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine("God mode deactivated. Skills restored.")
}
diff --git a/internal/game/cmd_goto.go b/internal/game/cmd_goto.go
index 1771f8d..96984b9 100644
--- a/internal/game/cmd_goto.go
+++ b/internal/game/cmd_goto.go
@@ -45,7 +45,7 @@ func (g *Game) executeGoto(sess *net.Session, args []string, rawInput string) {
p.RoomID = roomID
p.HazardTimer = 0
p.Stats.RecordRoomVisit(roomID)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
g.World.SeedGroundItems(roomID)
g.seedRoomMobs(roomID)
diff --git a/internal/game/cmd_hideallexits.go b/internal/game/cmd_hideallexits.go
index d9747c9..3d0c5e9 100644
--- a/internal/game/cmd_hideallexits.go
+++ b/internal/game/cmd_hideallexits.go
@@ -33,6 +33,6 @@ func (g *Game) executeHideAllExits(sess *net.Session, args []string, rawInput st
for _, k := range cleared {
delete(p.Flags, k)
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine(fmt.Sprintf("Cleared %d discovered exit(s).", len(cleared)))
}
diff --git a/internal/game/cmd_id.go b/internal/game/cmd_id.go
index 87adce7..bf64bbe 100644
--- a/internal/game/cmd_id.go
+++ b/internal/game/cmd_id.go
@@ -2,7 +2,6 @@ package game
import (
"fmt"
- "strings"
"thehouseoficarus/internal/behavior"
"thehouseoficarus/internal/net"
@@ -83,10 +82,10 @@ func junkMultiplier(junkID string, level int) int {
}
func (g *Game) executeIdentify(sess *net.Session, args []string, rawInput string) {
- g.doIdentify(sess, strings.Join(args, " "))
+ g.doIdentify(sess)
}
-func (g *Game) doIdentify(sess *net.Session, input string) {
+func (g *Game) doIdentify(sess *net.Session) {
p := sess.Player
g.cancelAction(p)
diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go
index f36fd9e..3e0bbc4 100644
--- a/internal/game/cmd_look.go
+++ b/internal/game/cmd_look.go
@@ -43,7 +43,7 @@ func (g *Game) doLook(sess *net.Session) {
}
var content []string
- content = append(content, g.showRoomDescription(sess, p, room, descWidth)...)
+ content = append(content, g.showRoomDescription(sess, room, descWidth)...)
content = append(content, g.showRoomObjects(sess, p, room)...)
content = append(content, g.showRoomMobs(sess, p, room)...)
content = append(content, g.showGroundItems(sess, p, room)...)
diff --git a/internal/game/cmd_mods.go b/internal/game/cmd_mods.go
index 0126b5f..de49adc 100644
--- a/internal/game/cmd_mods.go
+++ b/internal/game/cmd_mods.go
@@ -55,10 +55,10 @@ func (g *Game) doMods(sess *net.Session, showAll bool) {
t := &ui.Table{
Title: cat.Name,
Columns: []string{
- color.Render(mode, g.resolveConstant(sess, "table_header_label"), "Module"),
- color.Render(mode, g.resolveConstant(sess, "table_header_detail"), "Level"),
- color.Render(mode, g.resolveConstant(sess, "table_header_xp"), "Cost"),
- color.Render(mode, g.resolveConstant(sess, "table_header_xp"), "XP"),
+ color.Render(mode, g.resolveConstant("table_header_label"), "Module"),
+ color.Render(mode, g.resolveConstant("table_header_detail"), "Level"),
+ color.Render(mode, g.resolveConstant("table_header_xp"), "Cost"),
+ color.Render(mode, g.resolveConstant("table_header_xp"), "XP"),
},
}
@@ -77,10 +77,10 @@ func (g *Game) doMods(sess *net.Session, showAll bool) {
})
} else if unlocked {
t.Rows = append(t.Rows, []string{
- color.Render(mode, g.resolveConstant(sess, "table_header_label"), m.Name),
- color.Render(mode, g.resolveConstant(sess, "table_header_detail"), levelStr),
- color.Render(mode, g.resolveConstant(sess, "table_header_xp"), costStr),
- color.Render(mode, g.resolveConstant(sess, "table_header_xp"), xpStr),
+ color.Render(mode, g.resolveConstant("table_header_label"), m.Name),
+ color.Render(mode, g.resolveConstant("table_header_detail"), levelStr),
+ color.Render(mode, g.resolveConstant("table_header_xp"), costStr),
+ color.Render(mode, g.resolveConstant("table_header_xp"), xpStr),
})
} else {
t.Rows = append(t.Rows, []string{
diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go
index 4772a2c..72c3236 100644
--- a/internal/game/cmd_move.go
+++ b/internal/game/cmd_move.go
@@ -75,7 +75,7 @@ func (g *Game) doMove(sess *net.Session, dir string, isWalk bool) {
// fire in completeMove once the player arrives. ExitDef.Condition (above,
// evaluated by exitDisplayState) gates whether the exit is passable at
// all; each on_traverse entry has its own additional Condition gate.
- p.MovePendingTrigger = g.matchExitTrigger(sess, p, exitDef)
+ p.MovePendingTrigger = g.matchExitTrigger(sess, exitDef)
inCombat := g.Combat.Get(p.Name) != nil
@@ -130,7 +130,7 @@ func (g *Game) doMove(sess *net.Session, dir string, isWalk bool) {
// Condition (and item filter, n/a for traverse) passes for this player, or nil
// if the exit has no matching on_traverse entry. The matched trigger's Steps
// run as a sequence once the player arrives in the target room.
-func (g *Game) matchExitTrigger(sess *net.Session, p *player.Player, exitDef world.ExitDef) *behavior.Trigger {
+func (g *Game) matchExitTrigger(sess *net.Session, exitDef world.ExitDef) *behavior.Trigger {
for i := range exitDef.OnTraverse {
t := &exitDef.OnTraverse[i]
if t.Condition != nil && !g.checkCondition(sess, t.Condition) {
@@ -195,7 +195,7 @@ func (g *Game) completeMove(sess *net.Session, p *player.Player) {
if pendingTrigger != nil {
g.runTrigger(sess, p, []behavior.Trigger{*pendingTrigger}, p.RoomID, nil, false)
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
g.World.SeedGroundItems(p.RoomID)
g.seedRoomMobs(p.RoomID)
diff --git a/internal/game/cmd_option.go b/internal/game/cmd_option.go
index d35ca12..294ed5e 100644
--- a/internal/game/cmd_option.go
+++ b/internal/game/cmd_option.go
@@ -16,10 +16,10 @@ func (g *Game) doOption(sess *net.Session, input string) {
mode := g.colorMode(sess)
if input == "" {
- hLabel := g.resolveConstant(sess, "table_header_label")
- hValue := g.resolveConstant(sess, "table_header_value")
- hDetail := g.resolveConstant(sess, "table_header_detail")
- hDesc := g.resolveConstant(sess, "table_header_desc")
+ hLabel := g.resolveConstant("table_header_label")
+ hValue := g.resolveConstant("table_header_value")
+ hDetail := g.resolveConstant("table_header_detail")
+ hDesc := g.resolveConstant("table_header_desc")
table := &ui.Table{
Columns: []string{
color.Render(mode, hLabel, "Option"),
@@ -102,7 +102,7 @@ func (g *Game) doOption(sess *net.Session, input string) {
if def.Name == "unicode" && parsed == false {
acc.Options["card_size"] = 5
}
- g.AccountStore.SaveAccount(acc)
+ g.saveAccount(acc)
}
sess.WriteLine(fmt.Sprintf("%s set to %s.", def.Name, formatOptionValue(p, def)))
}
diff --git a/internal/game/cmd_prompt.go b/internal/game/cmd_prompt.go
index 3596ebf..b613f44 100644
--- a/internal/game/cmd_prompt.go
+++ b/internal/game/cmd_prompt.go
@@ -25,19 +25,19 @@ func (g *Game) doPrompt(sess *net.Session, input string) {
if input == "reset" {
p.Prompt = "> "
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine("Prompt reset to default.")
return
}
if input == "none" {
p.Prompt = ""
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine("Prompt set to blank.")
return
}
p.Prompt = input
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine(fmt.Sprintf("Prompt set to: %s", input))
}
diff --git a/internal/game/cmd_quit.go b/internal/game/cmd_quit.go
index 85e3510..e564c7e 100644
--- a/internal/game/cmd_quit.go
+++ b/internal/game/cmd_quit.go
@@ -29,7 +29,7 @@ func (g *Game) doQuit(sess *net.Session) {
// to the round in flight refuses and the quit is aborted, so deliberate
// exits never forfeit a stake or reveal cards mid-hand. Forfeit-on-leave
// stays reserved for genuine disconnects and "quit now".
- if !g.leaveCasinoGames(sess, p) {
+ if !g.leaveCasinoGames(p) {
sess.WriteLine(g.colorize(sess, "warning", "Type \"quit now\" to log out anyway — your bet will be forfeited."))
return
}
@@ -82,7 +82,7 @@ func (g *Game) beginQuitCountdown(sess *net.Session) {
// now — immediately before the disconnect. Settling games only
// pay seated bettors, so this forfeits a mid-round stake.
g.emitCasinoEvents(g.Casino.MarkDisconnected(p.Name))
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
g.charsMu.Lock()
delete(g.loggedInChars, p.Name)
g.charsMu.Unlock()
diff --git a/internal/game/cmd_remove.go b/internal/game/cmd_remove.go
index 38f17a3..349617d 100644
--- a/internal/game/cmd_remove.go
+++ b/internal/game/cmd_remove.go
@@ -80,7 +80,7 @@ func (g *Game) doRemove(sess *net.Session, input string) {
delete(p.EquipQuality, foundItemID)
}
p.SetInvSlot(freeSlot, invSlot)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
name := foundItemID
def, _ := g.ItemStore.Load(foundItemID)
@@ -106,7 +106,7 @@ func (g *Game) removeAmmo(sess *net.Session, p *player.Player, qty int) {
delete(p.Equipment, item.SlotAmmo)
p.AmmoQty = 0
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
def, _ := g.ItemStore.Load(ammoID)
name := ammoID
if def != nil {
@@ -129,7 +129,7 @@ func (g *Game) removeAmmo(sess *net.Session, p *player.Player, qty int) {
delete(p.Equipment, item.SlotAmmo)
p.AmmoQty = 0
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
def, _ := g.ItemStore.Load(ammoID)
name := ammoID
@@ -190,7 +190,7 @@ func (g *Game) doRemoveAll(sess *net.Session) {
sess.WriteLine(fmt.Sprintf("You remove %s.", g.itemColorize(sess, def, name)))
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
func (g *Game) removeAllAmmo(sess *net.Session, p *player.Player, itemID string) {
diff --git a/internal/game/cmd_room_insert.go b/internal/game/cmd_room_insert.go
index 132469d..92ef38b 100644
--- a/internal/game/cmd_room_insert.go
+++ b/internal/game/cmd_room_insert.go
@@ -185,7 +185,7 @@ func (g *Game) roomInsert(sess *net.Session, args []string) {
p.RoomID = newID
p.HazardTimer = 0
p.Stats.RecordRoomVisit(newID)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
g.World.SeedGroundItems(newID)
g.seedRoomMobs(newID)
diff --git a/internal/game/cmd_score.go b/internal/game/cmd_score.go
index 426dc08..6f00de0 100644
--- a/internal/game/cmd_score.go
+++ b/internal/game/cmd_score.go
@@ -99,16 +99,16 @@ func (g *Game) doScore(sess *net.Session) {
creditsStr := g.colorize(sess, "currency_pickup", formatInt(p.Credits))
chipsStr := g.colorize(sess, "currency_pickup", formatInt(countChips(p)))
- styleStr := color.Render(mode, g.resolveConstant(sess, "table_header_label"), fmt.Sprintf("Style: %s", p.AttackStyle))
+ styleStr := color.Render(mode, g.resolveConstant("table_header_label"), fmt.Sprintf("Style: %s", p.AttackStyle))
invStr := fmt.Sprintf("Inv: %d/28", 28-p.FreeSlots())
row2 := padded("Credits: "+creditsStr, 19) + padded("Chips: "+chipsStr, 16) + padded(styleStr, 22) + invStr
content(row2)
gap()
- hpStyle := g.hpBarStyle(sess, p.HP, p.MaxHP())
+ hpStyle := g.hpBarStyle(p.HP, p.MaxHP())
hpBarStr := ui.RenderColoredBar(p.HP, p.MaxHP(), 26, unicode, hpStyle, mode)
- hpFg := g.hpBarStyle(sess, p.HP, p.MaxHP()).FilledColor
+ hpFg := g.hpBarStyle(p.HP, p.MaxHP()).FilledColor
hpLine := fmt.Sprintf("HP [%s] %s / %s",
hpBarStr,
color.Render(mode, color.ColorSpec{Fg: hpFg}, fmt.Sprint(p.HP)),
@@ -116,16 +116,16 @@ func (g *Game) doScore(sess *net.Session) {
content(hpLine)
batBarRaw := ui.RenderBarF(p.Battery, p.MaxBattery(), 26, unicode)
- batBarStr := color.Render(mode, g.resolveConstant(sess, "battery_bar"), batBarRaw)
- batFg := g.resolveConstant(sess, "battery_bar").Fg
+ batBarStr := color.Render(mode, g.resolveConstant("battery_bar"), batBarRaw)
+ batFg := g.resolveConstant("battery_bar").Fg
batLine := fmt.Sprintf("Battery [%s] %s / %s",
batBarStr,
color.Render(mode, color.ColorSpec{Fg: batFg}, fmt.Sprintf("%.1f", p.Battery)),
g.colorizeConstant(sess, "score_max_value", fmt.Sprintf("%.0f", p.MaxBattery())))
content(batLine)
- engSpec := g.resolveConstant(sess, "run_energy_bar")
- engEmpty := g.resolveConstant(sess, "hp_bar_empty")
+ engSpec := g.resolveConstant("run_energy_bar")
+ engEmpty := g.resolveConstant("hp_bar_empty")
engStyle := &ui.BarStyle{FilledColor: engSpec.Fg, EmptyColor: engEmpty.Fg, EmptyDim: engEmpty.Dim}
engBarStr := ui.RenderColoredBar(p.RunEnergy, p.MaxRunEnergy(), 26, unicode, engStyle, mode)
engLine := fmt.Sprintf("Run En. [%s] %s / %s",
@@ -158,8 +158,8 @@ func (g *Game) doScore(sess *net.Session) {
buildSkillCell := func(s player.SkillName) string {
level := p.Level(s)
- lvlColor := g.skillLevelColor(sess, level)
- name := padded(color.Render(mode, g.resolveConstant(sess, "active_tech_stat"), skillDisplay[s]), 12)
+ lvlColor := g.skillLevelColor(level)
+ name := padded(color.Render(mode, g.resolveConstant("active_tech_stat"), skillDisplay[s]), 12)
lvl := color.Render(mode, color.ColorSpec{Fg: lvlColor}, fmt.Sprintf("%2d", level))
xpBar := xpProgressBar(p.Skills[s], 12, unicode)
return fmt.Sprintf("%s %s [%s]", name, lvl, xpBar)
@@ -198,8 +198,8 @@ func (g *Game) doScore(sess *net.Session) {
for _, id := range p.ActiveTechList() {
def := GetTechDef(id)
if def != nil {
- on := color.Render(mode, g.resolveConstant(sess, "active_tech_on"), "[ON]")
- name := color.Render(mode, g.resolveConstant(sess, "active_tech_stat"), def.Name)
+ on := color.Render(mode, g.resolveConstant("active_tech_on"), "[ON]")
+ name := color.Render(mode, g.resolveConstant("active_tech_stat"), def.Name)
techParts = append(techParts, on+" "+name)
}
}
@@ -208,9 +208,9 @@ func (g *Game) doScore(sess *net.Session) {
if len(p.ActiveBuffs) > 0 {
for _, buff := range p.ActiveBuffs {
buffLine := fmt.Sprintf("%s +%d%% %s (%d ticks remaining)",
- color.Render(mode, g.resolveConstant(sess, "active_tech_status"), "[BUFF]"),
+ color.Render(mode, g.resolveConstant("active_tech_status"), "[BUFF]"),
buff.BonusPercent,
- color.Render(mode, g.resolveConstant(sess, "active_tech_stat"), buff.Stat),
+ color.Render(mode, g.resolveConstant("active_tech_stat"), buff.Stat),
buff.TicksLeft)
content(buffLine)
}
@@ -257,17 +257,17 @@ func formatInt(n int) string {
// skillLevelColor returns the xterm256 palette index for a skill level cell
// on the datapad score panel, sourcing it from the constant_colors section.
-func (g *Game) skillLevelColor(sess *net.Session, level int) int {
- spec := g.resolveConstant(sess, "skill_lvl_base")
+func (g *Game) skillLevelColor(level int) int {
+ spec := g.resolveConstant("skill_lvl_base")
switch {
case level >= 99:
- spec = g.resolveConstant(sess, "skill_lvl_max")
+ spec = g.resolveConstant("skill_lvl_max")
case level >= 80:
- spec = g.resolveConstant(sess, "skill_lvl_high")
+ spec = g.resolveConstant("skill_lvl_high")
case level >= 60:
- spec = g.resolveConstant(sess, "skill_lvl_mid")
+ spec = g.resolveConstant("skill_lvl_mid")
case level >= 40:
- spec = g.resolveConstant(sess, "skill_lvl_low")
+ spec = g.resolveConstant("skill_lvl_low")
}
return spec.Fg
}
diff --git a/internal/game/cmd_setplayerflag.go b/internal/game/cmd_setplayerflag.go
index 3d0bdbf..ec99cda 100644
--- a/internal/game/cmd_setplayerflag.go
+++ b/internal/game/cmd_setplayerflag.go
@@ -55,6 +55,6 @@ func (g *Game) executeSetPlayerFlag(sess *net.Session, args []string, rawInput s
}
g.setPlayerFlag(tp, flagName, value)
- g.AccountStore.SaveCharacter(tp)
+ g.saveCharacter(tp)
sess.WriteLine(fmt.Sprintf("Player flag '%s' set to %v for %s.", flagName, value, tp.Name))
}
diff --git a/internal/game/cmd_shop.go b/internal/game/cmd_shop.go
index e9f16b8..ca1bf53 100644
--- a/internal/game/cmd_shop.go
+++ b/internal/game/cmd_shop.go
@@ -289,7 +289,7 @@ func (g *Game) doShopBuy(sess *net.Session, mob *world.MobInstance, input string
total := price * taken
p.Credits -= total
g.giveShopItems(p, c.itemID, taken, c.def)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
itemColor := g.itemColorize(sess, c.def, c.name)
credits := g.colorize(sess, "currency_pickup", fmt.Sprintf("%d", total))
@@ -353,7 +353,7 @@ func (g *Game) doShopSell(sess *net.Session, mob *world.MobInstance, input strin
p.RemoveItem(match.ID, actualQty)
p.Credits += total
g.MobStore.ShopAdd(mob.InstanceID, match.ID, actualQty)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
credits := g.colorize(sess, "currency_pickup", fmt.Sprintf("%d", total))
if actualQty == 1 {
diff --git a/internal/game/cmd_skills.go b/internal/game/cmd_skills.go
index 2f9f52e..725286a 100644
--- a/internal/game/cmd_skills.go
+++ b/internal/game/cmd_skills.go
@@ -16,10 +16,10 @@ func (g *Game) executeSkills(sess *net.Session, args []string, rawInput string)
func (g *Game) doSkills(sess *net.Session) {
p := sess.Player
mode := g.colorMode(sess)
- hLabel := g.resolveConstant(sess, "table_header_label")
- hDetail := g.resolveConstant(sess, "table_header_detail")
- hValue := g.resolveConstant(sess, "table_header_value")
- hXP := g.resolveConstant(sess, "table_header_xp")
+ hLabel := g.resolveConstant("table_header_label")
+ hDetail := g.resolveConstant("table_header_detail")
+ hValue := g.resolveConstant("table_header_value")
+ hXP := g.resolveConstant("table_header_xp")
t := &ui.Table{Title: "Skills", Columns: []string{
color.Render(mode, hLabel, "Skill"),
color.Render(mode, hDetail, "Abbr"),
diff --git a/internal/game/cmd_smelt.go b/internal/game/cmd_smelt.go
index 5f7a7b3..97d9371 100644
--- a/internal/game/cmd_smelt.go
+++ b/internal/game/cmd_smelt.go
@@ -17,7 +17,7 @@ func (g *Game) doSmelt(sess *net.Session, input string) {
p := sess.Player
g.cancelAction(p)
- stationDefID, stationName := g.findStation(p.RoomID, []string{"furnace"})
+ stationDefID, _ := g.findStation(p.RoomID, []string{"furnace"})
if stationDefID == "" {
sess.WriteLine("You need a furnace to smelt.")
return
@@ -26,7 +26,7 @@ func (g *Game) doSmelt(sess *net.Session, input string) {
items := g.CraftIndex.BySubtype("smelting")
if input == "" {
- g.showSmeltMenu(sess, p, items, stationDefID, stationName)
+ g.showSmeltMenu(sess, p, items, stationDefID)
return
}
@@ -59,7 +59,7 @@ func (g *Game) doSmelt(sess *net.Session, input string) {
g.showRecipeChoice(sess, p, recipes, "You can't smelt that.", "What would you like to smelt?", "")
}
-func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, items []*item.ItemDef, stationDefID, stationName string) {
+func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, items []*item.ItemDef, stationDefID string) {
seen := make(map[string]bool)
var recipes []*item.ItemDef
diff --git a/internal/game/cmd_stop.go b/internal/game/cmd_stop.go
index b05e1fc..4b287ce 100644
--- a/internal/game/cmd_stop.go
+++ b/internal/game/cmd_stop.go
@@ -10,7 +10,7 @@ func (g *Game) executeStop(sess *net.Session, args []string, rawInput string) {
// queue; cancel a pending quit countdown explicitly.
g.cancelRest(p.Name)
if _, playing := g.Casino.Participation(p.Name); playing {
- g.leaveCasinoGames(sess, p)
+ g.leaveCasinoGames(p)
return
}
ss, _ := g.safespot.Get(p.Name)
diff --git a/internal/game/cmd_style.go b/internal/game/cmd_style.go
index 043c65f..d475b4f 100644
--- a/internal/game/cmd_style.go
+++ b/internal/game/cmd_style.go
@@ -33,7 +33,7 @@ func (g *Game) doStyle(sess *net.Session, input string) {
}
if len(matches) == 1 {
p.AttackStyle = player.AttackStyle(matches[0])
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine(fmt.Sprintf("Combat style set to %s.", matches[0]))
return
}
diff --git a/internal/game/cmd_summon.go b/internal/game/cmd_summon.go
index 9383e31..6477b11 100644
--- a/internal/game/cmd_summon.go
+++ b/internal/game/cmd_summon.go
@@ -60,7 +60,7 @@ func (g *Game) executeSummon(sess *net.Session, args []string, rawInput string)
tp.RoomID = adminRoom
tp.HazardTimer = 0
tp.Stats.RecordRoomVisit(adminRoom)
- g.AccountStore.SaveCharacter(tp)
+ g.saveCharacter(tp)
g.World.SeedGroundItems(adminRoom)
g.seedRoomMobs(adminRoom)
diff --git a/internal/game/cmd_tech.go b/internal/game/cmd_tech.go
index 1431228..587df60 100644
--- a/internal/game/cmd_tech.go
+++ b/internal/game/cmd_tech.go
@@ -129,14 +129,14 @@ func (g *Game) doTechList(sess *net.Session) {
var nameStr string
if tech.Level <= techLevel {
if p.HasActiveTech(tech.ID) {
- status = color.Render(mode, g.resolveConstant(sess, "tech_status_on"), "ON")
+ status = color.Render(mode, g.resolveConstant("tech_status_on"), "ON")
} else {
- status = color.Render(mode, g.resolveConstant(sess, "tech_status_off"), "off")
+ status = color.Render(mode, g.resolveConstant("tech_status_off"), "off")
}
- nameStr = color.Render(mode, g.resolveConstant(sess, "tech_name_unlocked"), tech.Name)
+ nameStr = color.Render(mode, g.resolveConstant("tech_name_unlocked"), tech.Name)
} else {
- status = color.Render(mode, g.resolveConstant(sess, "tech_status_locked"), "locked")
- nameStr = color.Render(mode, g.resolveConstant(sess, "tech_name_locked"), tech.Name)
+ status = color.Render(mode, g.resolveConstant("tech_status_locked"), "locked")
+ nameStr = color.Render(mode, g.resolveConstant("tech_name_locked"), tech.Name)
}
effect := techEffectString(tech)
@@ -192,7 +192,7 @@ func (g *Game) doTechQuick(sess *net.Session, p *player.Player, input string) {
}
p.QuickTech = matches[0].ID
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine(fmt.Sprintf("Quick tech set to %s.", matches[0].Name))
}
diff --git a/internal/game/cmd_trigger_combat.go b/internal/game/cmd_trigger_combat.go
index 2f51057..f9a6bf1 100644
--- a/internal/game/cmd_trigger_combat.go
+++ b/internal/game/cmd_trigger_combat.go
@@ -18,14 +18,14 @@ import (
// level and the mob's Science stat (plus ScienceDefense) respectively — the
// mob's Defense stat is not involved. Science has no attack styles, so no style
// bonus applies.
-func (g *Game) scienceAttack(sess *net.Session, p *player.Player, mob *world.MobInstance, mod *ModDef) bool {
+func (g *Game) scienceAttack(sess *net.Session, p *player.Player, mob *world.MobInstance, mod *ModDef) {
if p.Level(player.Science) < mod.Level {
sess.WriteLine(fmt.Sprintf("You need level %d science to trigger %s.", mod.Level, mod.Name))
- return false
+ return
}
if g.processConsumeQueue(p, sess) {
- return true
+ return
}
g.consumeJunkCost(p, mod)
@@ -78,7 +78,7 @@ func (g *Game) scienceAttack(sess *net.Session, p *player.Player, mob *world.Mob
g.awardSkillXP(sess, p, player.Hitpoints, hpXP)
gains = append(gains, xpGain{string(player.Hitpoints), hpXP})
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
mobName := mobDisplayName(mob, true)
prefix := fmt.Sprintf("%s hits %s for %s damage.",
@@ -91,7 +91,6 @@ func (g *Game) scienceAttack(sess *net.Session, p *player.Player, mob *world.Mob
} else {
sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf("%s fails to connect.", mod.Name)))
}
- return true
}
func (g *Game) findInventoryItem(p *player.Player, input string) (int, *player.InventorySlot) {
diff --git a/internal/game/cmd_trigger_transport.go b/internal/game/cmd_trigger_transport.go
index dbb0d16..f6c9c77 100644
--- a/internal/game/cmd_trigger_transport.go
+++ b/internal/game/cmd_trigger_transport.go
@@ -59,6 +59,6 @@ func (g *Game) executeTransportEffect(sess *net.Session, p *player.Player, mod *
destName = room.Name
}
sess.WriteLine(fmt.Sprintf("You arrive at %s.", destName))
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
g.doLook(sess)
}
diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go
index 12dbc73..b975469 100644
--- a/internal/game/cmd_use.go
+++ b/internal/game/cmd_use.go
@@ -82,7 +82,7 @@ func (g *Game) useRoomObject(sess *net.Session, p *player.Player, input string)
}
recipes := g.stationRecipes(p, objSt.DefID)
if len(recipes) > 0 {
- g.showRecipeMenu(sess, p, recipes, def.Name)
+ g.showRecipeMenu(sess, recipes, def.Name)
return true
}
bt := def.BehaviorType()
@@ -140,7 +140,7 @@ func craftLabel(c *item.CraftDef) string {
return "combine"
}
-func (g *Game) showRecipeMenu(sess *net.Session, p *player.Player, items []*item.ItemDef, stationName string) {
+func (g *Game) showRecipeMenu(sess *net.Session, items []*item.ItemDef, stationName string) {
if len(items) == 0 {
sess.WriteLine(fmt.Sprintf("You don't have anything you can make at the %s.", stationName))
return
@@ -299,8 +299,8 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName,
itemBID := matchesB[0].ID
- craftItems := g.findCraftItems(p, itemAID, itemBID)
- fletchItems := g.findFletchItems(p, itemAID, itemBID)
+ craftItems := g.findCraftItems(itemAID, itemBID)
+ fletchItems := g.findFletchItems(itemAID, itemBID)
if len(craftItems) > 0 && len(fletchItems) > 0 {
var menu []map[string]string
diff --git a/internal/game/cmd_wear.go b/internal/game/cmd_wear.go
index c52321c..cea4d67 100644
--- a/internal/game/cmd_wear.go
+++ b/internal/game/cmd_wear.go
@@ -117,7 +117,7 @@ func (g *Game) doWear(sess *net.Session, input string) {
}
p.Equipment[def.EquipSlot()] = slot.ItemID
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
verb := "wear"
if def.EquipmentType() != "" {
@@ -150,7 +150,7 @@ func (g *Game) equipAmmo(sess *net.Session, p *player.Player, invIdx int, slot *
p.AmmoQty = transferQty
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
displayName := g.itemColorize(sess, def, def.Name)
sess.WriteLine(fmt.Sprintf("You equip %s (x%d).", displayName, p.AmmoQty))
}
@@ -284,7 +284,7 @@ func (g *Game) doWearAll(sess *net.Session) {
if len(equipped) == 0 {
sess.WriteLine("Nothing new to wear.")
} else {
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.Write("You wear: ")
formatPickupList(sess, equipped)
}
diff --git a/internal/game/combat_attack.go b/internal/game/combat_attack.go
index bd16999..80994e1 100644
--- a/internal/game/combat_attack.go
+++ b/internal/game/combat_attack.go
@@ -288,7 +288,7 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI
}
if combat.HitCheck(attRoll, defRoll) {
- g.applyPlayerHit(sess, p, mob, attackType, weaponType, maxHit)
+ g.applyPlayerHit(sess, p, mob, weaponType, maxHit)
} else {
g.applyPlayerMiss(sess, p, mob, weaponType)
}
@@ -354,10 +354,10 @@ func (g *Game) playerAttackUnarmed(sess *net.Session, p *player.Player, mob *wor
return
}
- attackType, _, attRoll, defRoll, maxHit := g.calculateUnarmedAttackRoll(p, mob)
+ _, _, attRoll, defRoll, maxHit := g.calculateUnarmedAttackRoll(p, mob)
if combat.HitCheck(attRoll, defRoll) {
- g.applyPlayerHit(sess, p, mob, attackType, item.EquipMeleeWeapon, maxHit)
+ g.applyPlayerHit(sess, p, mob, item.EquipMeleeWeapon, maxHit)
} else {
g.applyPlayerMiss(sess, p, mob, item.EquipMeleeWeapon)
}
@@ -379,7 +379,7 @@ func (g *Game) calculateUnarmedAttackRoll(p *player.Player, mob *world.MobInstan
return
}
-func (g *Game) applyPlayerHit(sess *net.Session, p *player.Player, mob *world.MobInstance, attackType string, weaponType item.EquipmentType, maxHit int) {
+func (g *Game) applyPlayerHit(sess *net.Session, p *player.Player, mob *world.MobInstance, weaponType item.EquipmentType, maxHit int) {
dmg := combat.RollDamage(maxHit)
mob.HP -= dmg
@@ -403,14 +403,14 @@ func (g *Game) applyPlayerHit(sess *net.Session, p *player.Player, mob *world.Mo
gains := g.awardCombatXP(sess, p, dmg, isRanged)
if isRanged {
- g.consumeAmmo(sess, p)
+ g.consumeAmmo(p)
}
if mob.IsTask() {
- g.writeTaskProgress(sess, p, mob, dmg, gains)
+ g.writeTaskProgress(sess, p, mob, gains)
} else {
mobName := mobDisplayName(mob, true)
- attacker := mobDisplayNameCap(mob, true)
+ attacker := mobDisplayNameCap(mob)
prefix := fmt.Sprintf("You hit %s for %s damage.", g.colorize(sess, "mob", mobName), g.colorize(sess, "damage_dealt", g.dmgDisplay(sess, dmg)))
prefix = padCombatPrefix(prefix,
fmt.Sprintf("You hit %s for > %d < damage.", mobName, 999),
@@ -430,7 +430,7 @@ func (g *Game) applyPlayerMiss(sess *net.Session, p *player.Player, mob *world.M
sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf("You miss %s.", mobDisplayName(mob, true))))
if weaponType == item.EquipRangedWeapon {
- g.consumeAmmo(sess, p)
+ g.consumeAmmo(p)
if p.AmmoQty <= 0 {
sess.WriteLine("You've run out of ammo!")
g.Combat.Leave(p.Name)
@@ -438,7 +438,7 @@ func (g *Game) applyPlayerMiss(sess *net.Session, p *player.Player, mob *world.M
}
}
-func (g *Game) consumeAmmo(sess *net.Session, p *player.Player) {
+func (g *Game) consumeAmmo(p *player.Player) {
ammoID := p.Equipment[item.SlotAmmo]
p.AmmoQty--
@@ -455,5 +455,5 @@ func (g *Game) consumeAmmo(sess *net.Session, p *player.Player) {
}
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
diff --git a/internal/game/combat_mob.go b/internal/game/combat_mob.go
index 7d60a11..c53d847 100644
--- a/internal/game/combat_mob.go
+++ b/internal/game/combat_mob.go
@@ -139,7 +139,7 @@ func (g *Game) applyMobHit(sess *net.Session, p *player.Player, mob *world.MobIn
if cs != nil && !cs.DamageWarningShown {
cs.DamageWarningShown = true
fbName := g.itemDisplayName(mob.DamageWithout)
- attacker := mobDisplayNameCap(mob, true)
+ attacker := mobDisplayNameCap(mob)
sess.WriteLine(g.colorize(sess, "warning",
fmt.Sprintf(" %s's attack is extra effective! Equip %s for protection.",
attacker, fbName)))
@@ -152,9 +152,9 @@ func (g *Game) applyMobHit(sess *net.Session, p *player.Player, mob *world.MobIn
p.HP = 0
}
p.StartRegen()
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
- attacker := mobDisplayNameCap(mob, true)
+ attacker := mobDisplayNameCap(mob)
mobName := mobDisplayName(mob, true)
prefix := fmt.Sprintf("%s hits you for %s damage.", g.colorize(sess, "mob", attacker), g.colorize(sess, "damage_taken", g.dmgDisplay(sess, dmg)))
prefix = padCombatPrefix(prefix,
@@ -174,7 +174,7 @@ func (g *Game) applyMobHit(sess *net.Session, p *player.Player, mob *world.MobIn
sess.WriteLine(g.colorize(sess, "miss",
fmt.Sprintf("You power through %s's relentless attacks and just flee!",
mobDisplayName(mob, false))))
- g.beginEscapeMove(sess, p)
+ g.beginEscapeMove(p)
} else {
p.ClearEscape()
p.WalkSequence = nil
@@ -205,11 +205,11 @@ func padCombatPrefix(prefix, plainSelf, plainOther string) string {
}
func (g *Game) applyMobMiss(sess *net.Session, p *player.Player, mob *world.MobInstance) {
- attacker := mobDisplayNameCap(mob, true)
+ attacker := mobDisplayNameCap(mob)
sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf("%s misses you.", attacker)))
if p.EscapeDir != "" {
- g.beginEscapeMove(sess, p)
+ g.beginEscapeMove(p)
}
}
@@ -238,7 +238,7 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
g.scheduleMobRespawn(mob)
if p.EscapeDir != "" {
- g.beginEscapeMove(sess, p)
+ g.beginEscapeMove(p)
return
}
@@ -280,7 +280,7 @@ func (g *Game) announceKill(sess *net.Session, p *player.Player, mob *world.MobI
func (g *Game) awardKillDrops(sess *net.Session, p *player.Player, mob *world.MobInstance, isTask bool) {
dropLabel := "You receive:"
if !isTask {
- dropLabel = mobDisplayNameCap(mob, true) + " drops:"
+ dropLabel = mobDisplayNameCap(mob) + " drops:"
}
writeDrop := func(itemID string, qty int) {
@@ -373,7 +373,7 @@ func (g *Game) killPlayer(sess *net.Session, p *player.Player, mob *world.MobIns
p.HP = p.MaxHP()
p.RunEnergy = p.MaxRunEnergy()
p.RoomID = g.StartingRoom
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if g.Hub != nil {
g.Hub.EnterRoom(sess, p.RoomID)
}
@@ -412,7 +412,7 @@ func (g *Game) awardCombatXP(sess *net.Session, p *player.Player, dmg int, isRan
for _, gain := range gains {
g.awardSkillXP(sess, p, player.SkillName(gain.Skill), gain.XP)
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
return gains
}
@@ -432,7 +432,7 @@ func (g *Game) stopCombat(playerName string) {
// escape, and that branch is itself gated on moveTicks returning > 0; the
// Cape-of-Agility / GodMode 0-tick bypass never enters the stash path, so there
// is no 0-tick branch to handle here.
-func (g *Game) beginEscapeMove(sess *net.Session, p *player.Player) {
+func (g *Game) beginEscapeMove(p *player.Player) {
p.MoveDirection = p.EscapeDir
p.MoveTarget = p.EscapeTarget
p.MovePendingTrigger = p.EscapeTrigger
diff --git a/internal/game/core_hacking.go b/internal/game/core_hacking.go
index a537057..af7a211 100644
--- a/internal/game/core_hacking.go
+++ b/internal/game/core_hacking.go
@@ -83,7 +83,7 @@ func (g *Game) endHacking(sess *net.Session, p *player.Player, completed bool, w
sess.WriteLine(g.colorize(sess, "xp",
fmt.Sprintf("(+%dxp %s)", xp, player.SkillAbbr[player.Hacking])))
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
delete(g.hackingStates, p.Name)
diff --git a/internal/game/core_login_account.go b/internal/game/core_login_account.go
index 4c05373..47421e9 100644
--- a/internal/game/core_login_account.go
+++ b/internal/game/core_login_account.go
@@ -194,7 +194,7 @@ func (g *Game) handleColorChoice(sess *net.Session, input string) {
acc.Options = make(map[string]any)
}
acc.Options["color"] = "none"
- g.AccountStore.SaveAccount(acc)
+ g.saveAccount(acc)
}
}
g.showMenu(sess)
diff --git a/internal/game/core_production.go b/internal/game/core_production.go
index 192f8ec..34ad8a9 100644
--- a/internal/game/core_production.go
+++ b/internal/game/core_production.go
@@ -7,7 +7,6 @@ import (
)
func (g *Game) findTool(p *player.Player, toolTypes []string) (toolSpeed float64, toolName string, toolType string, found bool) {
- toolSpeed = -1
if itemID, ok := p.Equipment[item.SlotMainHand]; ok {
if def, err := g.ItemStore.Load(itemID); err == nil {
for _, t := range toolTypes {
diff --git a/internal/game/core_utils.go b/internal/game/core_utils.go
index a07e789..de997cb 100644
--- a/internal/game/core_utils.go
+++ b/internal/game/core_utils.go
@@ -44,16 +44,13 @@ func mobDisplayName(m *world.MobInstance, definite bool) string {
return "a " + m.Name
}
-// mobDisplayNameCap is like mobDisplayName but capitalizes the article for use
-// at the start of a sentence (e.g. "The goblin hits you.").
-func mobDisplayNameCap(m *world.MobInstance, definite bool) string {
+// mobDisplayNameCap is like mobDisplayName but always uses the definite
+// article, for use at the start of a sentence (e.g. "The goblin hits you.").
+func mobDisplayNameCap(m *world.MobInstance) string {
if m.Unique {
return m.Name
}
- if definite {
- return "The " + m.Name
- }
- return "A " + m.Name
+ return "The " + m.Name
}
// itemDisplayName returns an item's display name for the given id, falling back
diff --git a/internal/game/credits_pickup_test.go b/internal/game/credits_pickup_test.go
index 61509e4..721c550 100644
--- a/internal/game/credits_pickup_test.go
+++ b/internal/game/credits_pickup_test.go
@@ -13,7 +13,7 @@ import (
// "That's not yours!".
func TestPickupCreditsSentinelQuantity(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 100
@@ -30,7 +30,7 @@ func TestPickupCreditsSentinelQuantity(t *testing.T) {
// the "get all" path.
func TestGetAllPicksUpCreditsWithSentinelQuantity(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 0
diff --git a/internal/game/economy_recovery_identity_test.go b/internal/game/economy_recovery_identity_test.go
index eceeecc..c2d3d0a 100644
--- a/internal/game/economy_recovery_identity_test.go
+++ b/internal/game/economy_recovery_identity_test.go
@@ -21,7 +21,7 @@ import (
// inspection instead of being silently settled against B's balance.
func TestRecoveryDoesNotPayReusedCharacterName(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.ID = "original-character"
@@ -63,7 +63,7 @@ func TestRecoveryDoesNotPayReusedCharacterName(t *testing.T) {
// gone and the rollback silently erases a payout that was already applied.
func TestEconomyRecoveryRunsAfterLifecycleRollback(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.ID = "stable-character"
diff --git a/internal/game/economy_recovery_regression_test.go b/internal/game/economy_recovery_regression_test.go
index f818037..f9d3b37 100644
--- a/internal/game/economy_recovery_regression_test.go
+++ b/internal/game/economy_recovery_regression_test.go
@@ -17,7 +17,7 @@ import (
// startup recovery — the documented refund-on-crash policy.
func TestRecoveryRefundsInFlightMachineWager(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 100
@@ -46,7 +46,7 @@ func TestRecoveryRefundsInFlightMachineWager(t *testing.T) {
// identical before and after a chip-only wager).
func TestRecoveryRefundsChipOnlyWager(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 100
@@ -80,7 +80,7 @@ func TestRecoveryRefundsChipOnlyWager(t *testing.T) {
// settles the stake from that evidence — exactly once, no double refund.
func TestRecoveryReappliesLostPayoutWithEvidence(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 90 // post-debit balance: the 10-credit stake was charged
@@ -113,7 +113,7 @@ func TestRecoveryReappliesLostPayoutWithEvidence(t *testing.T) {
// records, newest processed first) both refund after a crash.
func TestRecoveryRefundsSequentialStakes(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 80 // bet 10 then doubled: 100 -> 90 -> 80
@@ -145,7 +145,7 @@ func TestRecoveryRefundsSequentialStakes(t *testing.T) {
// the balance to `after` would silently drop 20 owed credits.
func TestRecoveryReappliesIdenticalFailedPayouts(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 90 // post-debit balance: both payouts failed to save
@@ -177,7 +177,7 @@ func TestRecoveryReappliesIdenticalFailedPayouts(t *testing.T) {
// player frees a slot, the same outstanding stake refunds successfully.
func TestRefundOutstandingKeepsStakesOpenWithoutInventoryRoom(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.SetInvSlot(0, &player.InventorySlot{ItemID: "chips", Quantity: 10})
@@ -222,7 +222,7 @@ func TestRefundOutstandingKeepsStakesOpenWithoutInventoryRoom(t *testing.T) {
// chips — the record stays open for operator inspection.
func TestRecoveryKeepsWagerOpenWhenChipsDoNotFit(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
// Durable state at crash time: the 10-chip debit landed (10 -> 0
@@ -257,7 +257,7 @@ func TestRecoveryKeepsWagerOpenWhenChipsDoNotFit(t *testing.T) {
// a liability is never silently dropped.
func TestRecoveryPreservesAmbiguousPayout(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
// A payout owed at balance 100 failed to save; the player then
@@ -298,7 +298,7 @@ func TestRecoveryPreservesAmbiguousPayout(t *testing.T) {
// liability is never erased: its record stays open for operator inspection.
func TestRecoveryKeepsUnpaidSiblingsOpenAtCreditCap(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = casino.MaxCredits - 30
@@ -332,7 +332,7 @@ func TestRecoveryKeepsUnpaidSiblingsOpenAtCreditCap(t *testing.T) {
// may be settled only once nothing is owed.
func TestRecoveryRestoresChipsFromMixedRefundCrash(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 100
@@ -379,7 +379,7 @@ func TestRecoveryRestoresChipsFromMixedRefundCrash(t *testing.T) {
// leaves the chip side outstanding.
func TestRecoveryDoesNotReturnPaidChipStakeInKind(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 100
@@ -414,7 +414,7 @@ func TestRecoveryDoesNotReturnPaidChipStakeInKind(t *testing.T) {
// and silently erases the owed amount.
func TestRecoveryUnpaidLiabilitySurvivesLaterRecoveries(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = casino.MaxCredits - 30
@@ -453,7 +453,7 @@ func TestRecoveryUnpaidLiabilitySurvivesLaterRecoveries(t *testing.T) {
// applied — a reported win is never silently dropped.
func TestCapBlockedPayoutIsJournaled(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = casino.MaxCredits
@@ -499,7 +499,7 @@ func TestCapBlockedPayoutIsJournaled(t *testing.T) {
// the open record is still owed.
func TestRecoveryReappliesPartiallyPaidIdenticalPayouts(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 90
diff --git a/internal/game/economy_shutdown_test.go b/internal/game/economy_shutdown_test.go
index a3e6adb..efdd5fd 100644
--- a/internal/game/economy_shutdown_test.go
+++ b/internal/game/economy_shutdown_test.go
@@ -83,7 +83,7 @@ func writeEconomyJournal(t *testing.T, dir string, records []economyRecord) {
func TestEconomyWagerDebitsDurallyAndSettles(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 100
@@ -119,7 +119,7 @@ func TestEconomyWagerDebitsDurallyAndSettles(t *testing.T) {
func TestEconomyFailedSaveRevertsAndVoids(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 100
@@ -224,8 +224,8 @@ func TestEconomyRecoveryAppliesLostPayout(t *testing.T) {
func TestShutdownSavesOnlineCharactersAndRefundsWagers(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
- h.seedAccount("bob", "pw", false)
+ h.seedAccount("alice")
+ h.seedAccount("bob")
h.seedCharacter("alice", "alice")
h.seedCharacter("bob", "bob")
sA, _ := h.connect("alice")
diff --git a/internal/game/exit_migrate.go b/internal/game/exit_migrate.go
index c678591..1fceca9 100644
--- a/internal/game/exit_migrate.go
+++ b/internal/game/exit_migrate.go
@@ -128,7 +128,7 @@ func (g *Game) RewriteRoomIDs(idMap map[int]int) {
continue
}
if rewritePlayerRoomIDs(sess.Player, idMap) {
- g.AccountStore.SaveCharacter(sess.Player)
+ g.saveCharacter(sess.Player)
}
online[sess.Player.Name] = true
}
@@ -146,7 +146,7 @@ func (g *Game) RewriteRoomIDs(idMap map[int]int) {
continue
}
if rewritePlayerRoomIDs(p, idMap) {
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
}
}
diff --git a/internal/game/game.go b/internal/game/game.go
index ca38e27..096e070 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -1,3 +1,5 @@
+// Package game contains the MUD itself: commands, skilling actions, combat,
+// triggers, rendering, economy, and persistence glue around the world.
package game
import (
@@ -173,7 +175,7 @@ func (g *Game) SetHub(hub *net.Hub) {
if p := sess.Player; p != nil {
g.restoreGodPlayer(p)
g.persistLockedOnDisconnect(p)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
p.DeactivateAllTechs()
g.detachSession(p.Name)
delete(g.hackingStates, p.Name)
diff --git a/internal/game/hacking/hacking.go b/internal/game/hacking/hacking.go
index f022ee1..fe6089b 100644
--- a/internal/game/hacking/hacking.go
+++ b/internal/game/hacking/hacking.go
@@ -1,3 +1,5 @@
+// Package hacking implements the Hacking skill minigames (liar's dice,
+// mastermind, …) played against terminals around Vesta.
package hacking
type Minigame interface {
diff --git a/internal/game/hacking/liars_dice.go b/internal/game/hacking/liars_dice.go
index 8e21b94..3c7ac31 100644
--- a/internal/game/hacking/liars_dice.go
+++ b/internal/game/hacking/liars_dice.go
@@ -250,11 +250,9 @@ func (l *LiarsDiceGame) doStatus() string {
sb.WriteString(fmt.Sprintf("Your fragments: %s (You: %d, System: %d)\n",
l.diceStr(l.playerDice), len(l.playerDice), len(l.aiDice)))
if l.hasBid {
- who := "System"
+ who := "you"
if !l.playerTurn {
who = "System"
- } else {
- who = "you"
}
sb.WriteString(fmt.Sprintf("Current bid: %d %s (by %s)\n", l.currentBid.Quantity, faceNamePlural(l.currentBid.Face), who))
}
diff --git a/internal/game/harness_test.go b/internal/game/harness_test.go
index a368634..c80f437 100644
--- a/internal/game/harness_test.go
+++ b/internal/game/harness_test.go
@@ -147,13 +147,14 @@ func seedWorld(t *testing.T, dir string) {
}
// seedAccount writes an account file directly (bypassing the login menu).
-func (h *harness) seedAccount(name, password string, admin bool) {
+func (h *harness) seedAccount(name string) {
+ const password = "pw"
t := h.t
hash, err := player.HashPassword(password)
if err != nil {
t.Fatalf("hash: %v", err)
}
- acc := &player.Account{Name: name, PasswordHash: hash, Admin: admin}
+ acc := &player.Account{Name: name, PasswordHash: hash}
if err := h.g.AccountStore.SaveAccount(acc); err != nil {
t.Fatalf("seed account %s: %v", name, err)
}
@@ -239,11 +240,9 @@ func (h *harness) input(sess *net.Session, lines <-chan string) {
}
}
-// goInput starts input goroutine(s) for a session, returning the send channel.
-func (h *harness) goInput(sess *net.Session, workers int) chan<- string {
+// goInput starts the input goroutine for a session, returning the send channel.
+func (h *harness) goInput(sess *net.Session) chan<- string {
lines := make(chan string, 64)
- for i := 0; i < workers; i++ {
- go h.input(sess, lines)
- }
+ go h.input(sess, lines)
return lines
}
diff --git a/internal/game/inventory_pickup_test.go b/internal/game/inventory_pickup_test.go
index a0d267e..2fc89b7 100644
--- a/internal/game/inventory_pickup_test.go
+++ b/internal/game/inventory_pickup_test.go
@@ -14,7 +14,7 @@ import (
func TestPickupMergesIntoFullInventory(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
writeSeedFile(t, filepath.Join(h.dir, "items", "chips.yaml"), []byte("id: chips\nname: chips\nstackable: true\n"))
sess := &net.Session{Conn: newChanConn(), Player: p, State: net.StateGame}
@@ -34,7 +34,7 @@ func TestPickupMergesIntoFullInventory(t *testing.T) {
func TestPickupFullInventoryWithoutStackStillRefused(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
writeSeedFile(t, filepath.Join(h.dir, "items", "chips.yaml"), []byte("id: chips\nname: chips\nstackable: true\n"))
sess := &net.Session{Conn: newChanConn(), Player: p, State: net.StateGame}
diff --git a/internal/game/lifecycle_txn_test.go b/internal/game/lifecycle_txn_test.go
index f575298..ea1cd65 100644
--- a/internal/game/lifecycle_txn_test.go
+++ b/internal/game/lifecycle_txn_test.go
@@ -17,7 +17,7 @@ import (
// sessions authenticated to it.
func setupAcct(t *testing.T, h *harness) (*net.Session, *net.Session) {
t.Helper()
- h.seedAccount("acct", "pw", false)
+ h.seedAccount("acct")
h.seedCharacter("alice", "acct")
h.seedCharacter("bob", "acct")
s1 := h.connectMenu("acct")
@@ -110,7 +110,7 @@ func TestLifecycleStaleMenuAfterDelete(t *testing.T) {
// through).
func TestLifecycleRenameBeatsConnect(t *testing.T) {
h := newHarness(t)
- h.seedAccount("carl", "pw", false)
+ h.seedAccount("carl")
h.seedCharacter("carl", "carl")
sess, conn := h.connect("carl")
diff --git a/internal/game/look_entities.go b/internal/game/look_entities.go
index 0d08551..70077a3 100644
--- a/internal/game/look_entities.go
+++ b/internal/game/look_entities.go
@@ -13,7 +13,7 @@ import (
)
func (g *Game) showRoomMobs(sess *net.Session, p *player.Player, room *world.Room) []string {
- mobs := g.MobStore.MobsInRoom(p.RoomID)
+ mobs := g.MobStore.MobsInRoom(room.ID)
if len(mobs) == 0 {
return nil
}
@@ -105,7 +105,7 @@ func (g *Game) showRoomObjects(sess *net.Session, p *player.Player, room *world.
if p.GodMode {
var tags []string
tagLabel := func(t string) string {
- return color.Render(g.colorMode(sess), g.resolveConstant(sess, "god_tag_label"), t)
+ return color.Render(g.colorMode(sess), g.resolveConstant("god_tag_label"), t)
}
if isLocal {
tags = append(tags, tagLabel("[LOCAL]"))
@@ -259,7 +259,7 @@ func casinoGameLabel(game casino.GameName) string {
}
func (g *Game) showGroundItems(sess *net.Session, p *player.Player, room *world.Room) []string {
- ground := g.World.GroundItemsDetailed(p.RoomID)
+ ground := g.World.GroundItemsDetailed(room.ID)
if len(ground) == 0 {
return nil
}
@@ -381,7 +381,7 @@ func (g *Game) showGroundItems(sess *net.Session, p *player.Player, room *world.
}
func (g *Game) showRoomPlayers(sess *net.Session, p *player.Player, room *world.Room) {
- others := g.Hub.PlayersInRoom(p.RoomID)
+ others := g.Hub.PlayersInRoom(room.ID)
for _, other := range others {
if other != sess && other.Player != nil {
op := other.Player
diff --git a/internal/game/look_room.go b/internal/game/look_room.go
index 9d77ebe..3208f40 100644
--- a/internal/game/look_room.go
+++ b/internal/game/look_room.go
@@ -53,7 +53,7 @@ func (g *Game) roomSymbolBracket(sess *net.Session, p *player.Player, room *worl
color.Render(mode, dimSpec, "]")
}
-func (g *Game) showRoomDescription(sess *net.Session, p *player.Player, room *world.Room, descWidth int) []string {
+func (g *Game) showRoomDescription(sess *net.Session, room *world.Room, descWidth int) []string {
if descWidth <= 0 {
descWidth = 70
}
diff --git a/internal/game/look_target.go b/internal/game/look_target.go
index 8368946..5f3818d 100644
--- a/internal/game/look_target.go
+++ b/internal/game/look_target.go
@@ -160,7 +160,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) {
if def.Safespot != nil {
realSt := g.World.GetObjState(p.RoomID, st.DefID, st.Index)
if realSt != nil {
- levelStr := "intact"
+ var levelStr string
if realSt.SafespotLevel <= 0 {
levelStr = fmt.Sprintf("intact (level %d/%d)", len(def.Safespot.Levels), len(def.Safespot.Levels))
} else if realSt.SafespotLevel < len(def.Safespot.Levels) {
diff --git a/internal/game/migrate_rooms_test.go b/internal/game/migrate_rooms_test.go
index eb4a44c..57eb46a 100644
--- a/internal/game/migrate_rooms_test.go
+++ b/internal/game/migrate_rooms_test.go
@@ -15,10 +15,10 @@ import (
// in one owner task; nothing stays attached to room 2.
func TestMigrateRoomIDsMovesEverythingTogether(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
- p := h.seedCharacter("alice", "alice")
+ h.seedAccount("alice")
+ h.seedCharacter("alice", "alice")
sess, _ := h.connect("alice")
- p = sess.Player
+ p := sess.Player
h.g.RunOnOwner(func() {
p.RoomID = 2
@@ -59,7 +59,7 @@ func TestMigrateRoomIDsMovesEverythingTogether(t *testing.T) {
// TestMigrateRoomIDsRoundTrip preserves participation both directions.
func TestMigrateRoomIDsRoundTrip(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
h.seedCharacter("alice", "alice")
sess, _ := h.connect("alice")
p := sess.Player
@@ -89,7 +89,7 @@ func TestMigrateRoomIDsRoundTrip(t *testing.T) {
// saved to disk (RoomID rewritten in the character file).
func TestMigrateRoomIDsPersistsOnlinePlayer(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
h.seedCharacter("alice", "alice")
sess, _ := h.connect("alice")
p := sess.Player
diff --git a/internal/game/owner_concurrency_test.go b/internal/game/owner_concurrency_test.go
index 58e426b..72cf878 100644
--- a/internal/game/owner_concurrency_test.go
+++ b/internal/game/owner_concurrency_test.go
@@ -16,16 +16,16 @@ import (
// and hub disconnects concurrently.
func TestOwnerSerializesEntryPoints(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
- h.seedAccount("bob", "pw", false)
+ h.seedAccount("alice")
+ h.seedAccount("bob")
h.seedCharacter("alice", "alice")
h.seedCharacter("bob", "bob")
aSess, _ := h.connect("alice")
bSess, _ := h.connect("bob")
- aLines := h.goInput(aSess, 1)
- bLines := h.goInput(bSess, 1)
+ aLines := h.goInput(aSess)
+ bLines := h.goInput(bSess)
var wg sync.WaitGroup
wg.Add(2)
@@ -61,10 +61,10 @@ func TestOwnerSerializesEntryPoints(t *testing.T) {
// concurrently with live movement and ticking.
func TestOwnerAdminRewriteWhilePlaying(t *testing.T) {
h := newHarness(t)
- h.seedAccount("carol", "pw", false)
+ h.seedAccount("carol")
h.seedCharacter("carol", "carol")
sess, _ := h.connect("carol")
- lines := h.goInput(sess, 1)
+ lines := h.goInput(sess)
var wg sync.WaitGroup
wg.Add(2)
@@ -93,8 +93,8 @@ func TestOwnerAdminRewriteWhilePlaying(t *testing.T) {
// players from receiving output and progressing.
func TestSlowClientDoesNotStallTheWorld(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
- h.seedAccount("bob", "pw", false)
+ h.seedAccount("alice")
+ h.seedAccount("bob")
h.seedCharacter("alice", "alice")
h.seedCharacter("bob", "bob")
@@ -105,7 +105,7 @@ func TestSlowClientDoesNotStallTheWorld(t *testing.T) {
bSess, bConn := h.connect("bob")
aConn.writeGate = make(chan struct{})
- bLines := h.goInput(bSess, 1)
+ bLines := h.goInput(bSess)
for i := 0; i < 300; i++ {
aSess.WriteLine("spam for the stalled client")
@@ -127,11 +127,11 @@ func TestSlowClientDoesNotStallTheWorld(t *testing.T) {
// (save + registry cleanup) serializes with session input on the owner.
func TestOwnerDisconnectDuringInput(t *testing.T) {
h := newHarness(t)
- h.seedAccount("dave", "pw", false)
+ h.seedAccount("dave")
h.seedCharacter("dave", "dave")
sess, conn := h.connect("dave")
- lines := h.goInput(sess, 1)
+ lines := h.goInput(sess)
done := make(chan struct{})
go func() {
defer close(done)
diff --git a/internal/game/owner_disconnect_test.go b/internal/game/owner_disconnect_test.go
index d19b194..9140a40 100644
--- a/internal/game/owner_disconnect_test.go
+++ b/internal/game/owner_disconnect_test.go
@@ -17,7 +17,7 @@ import (
// callback must not block on the owner from the owner.
func TestDisconnectTickHardRemoveCompletesOnOwner(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
h.seedCharacter("alice", "alice")
sess, _ := h.connect("alice")
diff --git a/internal/game/production_engine.go b/internal/game/production_engine.go
index 260cef0..65c01b9 100644
--- a/internal/game/production_engine.go
+++ b/internal/game/production_engine.go
@@ -258,7 +258,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool {
sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, skill)))
}
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
var defaultSuccess string
if info.SuccessMessage != "" {
@@ -280,7 +280,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool {
p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: craft.Fail, Quantity: 1})
}
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
msg := g.resolveCraftMessage(sess, craft.FailMessage, info.FailMessage, craft, item.ID, nil, p.HasItem)
if msg != "" {
@@ -340,10 +340,6 @@ func (g *Game) canContinueProduction(p *player.Player, item *item.ItemDef) bool
if !craftHasAllItemsQty(craft, p.CountItem) {
return false
}
- outputQty := craft.OutputQty
- if outputQty <= 0 {
- outputQty = 1
- }
outDef, _ := g.ItemStore.Load(item.ID)
if outDef != nil && outDef.Stackable {
for i := 0; i < 28; i++ {
diff --git a/internal/game/production_index.go b/internal/game/production_index.go
index 672dcae..2646a43 100644
--- a/internal/game/production_index.go
+++ b/internal/game/production_index.go
@@ -220,9 +220,9 @@ func craftHasAllItemsQty(c *item.CraftDef, countItem func(string) int) bool {
return true
}
-func consumeIngredients(c *item.CraftDef, hasItem func(string) bool, removeItem func(string, int) bool) bool {
+func consumeIngredients(c *item.CraftDef, hasItem func(string) bool, removeItem func(string, int) bool) {
if c == nil {
- return false
+ return
}
for _, e := range c.Ingredients {
found := false
@@ -234,8 +234,7 @@ func consumeIngredients(c *item.CraftDef, hasItem func(string) bool, removeItem
}
}
if !found {
- return false
+ return
}
}
- return true
}
diff --git a/internal/game/production_menu.go b/internal/game/production_menu.go
index 10b7eb0..090214f 100644
--- a/internal/game/production_menu.go
+++ b/internal/game/production_menu.go
@@ -127,7 +127,7 @@ func (g *Game) showProductionTable(sess *net.Session, p *player.Player, items []
mode := g.colorMode(sess)
skillLevel := p.Level(player.SkillName(skill))
- dimSpec := g.resolveConstant(sess, "production_locked")
+ dimSpec := g.resolveConstant("production_locked")
tbl := &ui.Table{
Title: title,
@@ -259,7 +259,7 @@ func (g *Game) handleProductChoice(sess *net.Session, input string) {
p.EnsureFlags()
g.setPlayerFlag(p, lastFlagKey, sel.Item.ID)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if background {
g.startFletchAction(sess, p, sel.Item, sel.Count)
diff --git a/internal/game/quit_casino_test.go b/internal/game/quit_casino_test.go
index f055e07..f905fca 100644
--- a/internal/game/quit_casino_test.go
+++ b/internal/game/quit_casino_test.go
@@ -37,8 +37,8 @@ func quitTestGame(t *testing.T) (*Game, *net.Session) {
return g, sess
}
-func (g *Game) quitResting(name string) bool {
- _, ok := g.restTimers[name]
+func (g *Game) quitResting() bool {
+ _, ok := g.restTimers["alice"]
return ok
}
@@ -63,7 +63,7 @@ func TestQuitBlockedMidRoundKeepsWager(t *testing.T) {
if _, playing := g.Casino.Participation("alice"); !playing {
t.Fatal("quit mid-round removed the player from the table")
}
- if g.quitResting("alice") {
+ if g.quitResting() {
t.Fatal("blocked quit started the rest countdown")
}
// The stake is still committed to the round, not refunded.
@@ -90,7 +90,7 @@ func TestQuitBlockedMidRoundBaccarat(t *testing.T) {
if _, playing := g.Casino.Participation("alice"); !playing {
t.Fatal("quit mid-round removed the player from the baccarat table")
}
- if g.quitResting("alice") {
+ if g.quitResting() {
t.Fatal("blocked quit started the rest countdown")
}
}
@@ -111,7 +111,7 @@ func TestQuitLeavesTableBetweenRounds(t *testing.T) {
if _, playing := g.Casino.Participation("alice"); playing {
t.Fatal("quit did not free the table seat")
}
- if !g.quitResting("alice") {
+ if !g.quitResting() {
t.Fatal("quit did not start the rest countdown")
}
}
@@ -129,7 +129,7 @@ func TestQuitLeavesMachine(t *testing.T) {
if _, playing := g.Casino.Participation("alice"); playing {
t.Fatal("quit did not free the machine")
}
- if !g.quitResting("alice") {
+ if !g.quitResting() {
t.Fatal("quit did not start the rest countdown")
}
if strings.Contains(string(conn.data), "WARNING") {
@@ -145,7 +145,7 @@ func TestStopCancelsQuit(t *testing.T) {
g.executeStop(sess, nil, "stop")
- if g.quitResting("alice") {
+ if g.quitResting() {
t.Fatal("stop did not cancel the pending quit")
}
}
@@ -185,7 +185,7 @@ func TestQuitNowForfeitsOnlyAtLogout(t *testing.T) {
if _, playing := g.Casino.Participation("alice"); !playing {
t.Fatal("quit now freed the seat before the logout tick")
}
- if !g.quitResting("alice") {
+ if !g.quitResting() {
t.Fatal("quit now did not start the rest countdown")
}
@@ -210,7 +210,7 @@ func TestQuitNowForfeitsOnlyAtLogout(t *testing.T) {
if _, stillLoggedIn := g.loggedInChars["alice"]; stillLoggedIn {
t.Fatal("quit now did not remove the session from the logged-in map")
}
- if g.quitResting("alice") {
+ if g.quitResting() {
t.Fatal("countdown timer was not cleared after logout")
}
}
@@ -227,7 +227,7 @@ func TestQuitNowCancelledByStopMidRound(t *testing.T) {
g.doQuitNow(sess)
g.executeStop(sess, nil, "stop")
- if g.quitResting("alice") {
+ if g.quitResting() {
t.Fatal("stop did not cancel the pending quit-now countdown")
}
if _, playing := g.Casino.Participation("alice"); !playing {
diff --git a/internal/game/readiness_regression_test.go b/internal/game/readiness_regression_test.go
index 752b755..7fa2b2e 100644
--- a/internal/game/readiness_regression_test.go
+++ b/internal/game/readiness_regression_test.go
@@ -21,7 +21,7 @@ import (
// bool/int pair is a data race and can lose the disconnect entirely.
func TestDisconnectRaceIsFreeAndNeverLost(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
h.seedCharacter("alice", "alice")
sess, _ := h.connect("alice")
@@ -49,7 +49,7 @@ func TestDisconnectRaceIsFreeAndNeverLost(t *testing.T) {
// takeable stock — never half-remove the ground with nothing delivered.
func TestReservedPickupDoesNotDestroyItems(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
writeSeedFile(t, filepath.Join(h.dir, "items", "chips.yaml"), []byte("id: chips\nname: chips\nstackable: true\n"))
sess := &net.Session{Conn: newChanConn(), Player: p, State: net.StateGame}
@@ -76,7 +76,7 @@ func TestReservedPickupDoesNotDestroyItems(t *testing.T) {
// the crash and recovery still delivers the owed payout.
func TestOwedPayoutSurvivesCharacterRename(t *testing.T) {
h := newHarness(t)
- h.seedAccount("acct", "pw", false)
+ h.seedAccount("acct")
p := h.seedCharacter("alice", "acct")
h.g.RunOnOwner(func() {
p.ID = "stable-char-id" // production characters carry a stable ID
@@ -132,7 +132,7 @@ func TestAccountRenamePreservesAccessAndOwnership(t *testing.T) {
// round resolved, so recovery must not ALSO return the chip stake in kind.
func TestSettledSurrenderDoesNotRefundChipStake(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 100
@@ -166,7 +166,7 @@ func TestSettledSurrenderDoesNotRefundChipStake(t *testing.T) {
// recovery (after the disk is fixed) re-applies the owed payout.
func TestRecoverySaveFailureRetriesOnNextStartup(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
charDir := filepath.Join(h.dir, "players", "characters")
t.Cleanup(func() { os.Chmod(charDir, 0755) })
@@ -208,7 +208,7 @@ func TestRecoverySaveFailureRetriesOnNextStartup(t *testing.T) {
// marker).
func TestLostResolutionMarkerSurfacesAndRefunds(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 100
@@ -247,7 +247,7 @@ func TestLostResolutionMarkerSurfacesAndRefunds(t *testing.T) {
// is not durable.
func TestUnjournalablePayoutRefundsStake(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
p := h.seedCharacter("alice", "alice")
h.g.RunOnOwner(func() {
p.Credits = 100
diff --git a/internal/game/render_color.go b/internal/game/render_color.go
index ab3692e..ccce05e 100644
--- a/internal/game/render_color.go
+++ b/internal/game/render_color.go
@@ -47,7 +47,7 @@ func (g *Game) colorize(sess *net.Session, category, text string) string {
// per-player override map — constants are admin-tunable only — so players
// have no way to change them. Falls back through the file's constant_colors
// map, then to the built-in DefaultConstantColors.
-func (g *Game) resolveConstant(sess *net.Session, category string) color.ColorSpec {
+func (g *Game) resolveConstant(category string) color.ColorSpec {
if g.ConstantColorConfig != nil {
if val, ok := (*g.ConstantColorConfig)[category]; ok && val != "" {
return color.Parse(val)
@@ -62,14 +62,14 @@ func (g *Game) resolveConstant(sess *net.Session, category string) color.ColorSp
// colorizeConstant paints text with a constant color spec, bypassing the
// player override tier entirely.
func (g *Game) colorizeConstant(sess *net.Session, category, text string) string {
- spec := g.resolveConstant(sess, category)
+ spec := g.resolveConstant(category)
return color.Render(g.colorMode(sess), spec, text)
}
// levelColorSpec returns the color spec for the level-difference band shown
// next to mob/player levels (green/red/white by how far the target is from
// the viewer). Uses admin-tunable constant_colors; falls back to built-ins.
-func (g *Game) levelColorSpec(sess *net.Session, myLevel, theirLevel int) color.ColorSpec {
+func (g *Game) levelColorSpec(myLevel, theirLevel int) color.ColorSpec {
diff := theirLevel - myLevel
var name string
switch {
@@ -84,11 +84,11 @@ func (g *Game) levelColorSpec(sess *net.Session, myLevel, theirLevel int) color.
default:
name = "level_diff_down_big"
}
- return g.resolveConstant(sess, name)
+ return g.resolveConstant(name)
}
func (g *Game) levelColorize(sess *net.Session, myLevel, theirLevel int, text string) string {
- spec := g.levelColorSpec(sess, myLevel, theirLevel)
+ spec := g.levelColorSpec(myLevel, theirLevel)
return color.Render(g.colorMode(sess), spec, text)
}
@@ -130,7 +130,7 @@ func (g *Game) namedColorSpecs(sess *net.Session) map[string]color.ColorSpec {
}
}
for name := range config.DefaultConstantColors() {
- if spec := g.resolveConstant(sess, name); !spec.Empty() {
+ if spec := g.resolveConstant(name); !spec.Empty() {
names[name] = spec
}
}
@@ -139,7 +139,7 @@ func (g *Game) namedColorSpecs(sess *net.Session) map[string]color.ColorSpec {
if _, ok := names[name]; ok {
continue
}
- if spec := g.resolveConstant(sess, name); !spec.Empty() {
+ if spec := g.resolveConstant(name); !spec.Empty() {
names[name] = spec
}
}
diff --git a/internal/game/render_combat.go b/internal/game/render_combat.go
index 4055b4d..24e5d01 100644
--- a/internal/game/render_combat.go
+++ b/internal/game/render_combat.go
@@ -33,13 +33,13 @@ func (g *Game) renderBarGeneric(sess *net.Session, current, max int, prefix stri
var spec color.ColorSpec
switch {
case pct >= 0.7:
- spec = g.resolveConstant(sess, prefix+"_high")
+ spec = g.resolveConstant(prefix + "_high")
case pct >= 0.4:
- spec = g.resolveConstant(sess, prefix+"_med")
+ spec = g.resolveConstant(prefix + "_med")
default:
- spec = g.resolveConstant(sess, prefix+"_low")
+ spec = g.resolveConstant(prefix + "_low")
}
- empty := g.resolveConstant(sess, "hp_bar_empty")
+ empty := g.resolveConstant("hp_bar_empty")
unicode := sess.Player != nil && sess.Player.OptionBool("unicode")
style := &ui.BarStyle{FilledColor: spec.Fg, EmptyColor: empty.Fg, EmptyDim: empty.Dim}
@@ -50,19 +50,19 @@ func (g *Game) renderBarGeneric(sess *net.Session, current, max int, prefix stri
// hpBarStyle returns the per-tier fill/empty color spec for an HP bar at the
// given ratio (high>=70%, med>=40%, low<40%). Colors come from the
// constant_colors map (hp_bar_high/med/low/empty).
-func (g *Game) hpBarStyle(sess *net.Session, current, max int) *ui.BarStyle {
+func (g *Game) hpBarStyle(current, max int) *ui.BarStyle {
if max <= 0 {
max = 1
}
pct := float64(current) / float64(max)
- spec := g.resolveConstant(sess, "hp_bar_low")
+ spec := g.resolveConstant("hp_bar_low")
switch {
case pct >= 0.7:
- spec = g.resolveConstant(sess, "hp_bar_high")
+ spec = g.resolveConstant("hp_bar_high")
case pct >= 0.4:
- spec = g.resolveConstant(sess, "hp_bar_med")
+ spec = g.resolveConstant("hp_bar_med")
}
- empty := g.resolveConstant(sess, "hp_bar_empty")
+ empty := g.resolveConstant("hp_bar_empty")
return &ui.BarStyle{FilledColor: spec.Fg, EmptyColor: empty.Fg, EmptyDim: empty.Dim}
}
diff --git a/internal/game/save.go b/internal/game/save.go
new file mode 100644
index 0000000..8e1af3d
--- /dev/null
+++ b/internal/game/save.go
@@ -0,0 +1,26 @@
+package game
+
+import (
+ "log"
+
+ "thehouseoficarus/internal/player"
+)
+
+// Best-effort persistence helpers. Gameplay code re-saves a character or
+// account after most state changes; a save failure there is not recoverable
+// by the caller, so these helpers log it once in one place instead of
+// silently discarding it at ~100 call sites. Lifecycle transactions (create,
+// delete, rename, migration) intentionally do NOT use these: they must
+// propagate save errors to the caller.
+
+func (g *Game) saveCharacter(p *player.Player) {
+ if err := g.AccountStore.SaveCharacter(p); err != nil {
+ log.Printf("save character %s: %v", p.Name, err)
+ }
+}
+
+func (g *Game) saveAccount(acc *player.Account) {
+ if err := g.AccountStore.SaveAccount(acc); err != nil {
+ log.Printf("save account %s: %v", acc.Name, err)
+ }
+}
diff --git a/internal/game/shutdown_admission_test.go b/internal/game/shutdown_admission_test.go
index 6aa4be5..a7e7ee2 100644
--- a/internal/game/shutdown_admission_test.go
+++ b/internal/game/shutdown_admission_test.go
@@ -14,7 +14,7 @@ import (
func TestShutdownRejectsSessionInput(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
h.seedCharacter("alice", "alice")
sess, _ := h.connect("alice")
sess.State = net.StateGame
@@ -37,7 +37,7 @@ func TestShutdownRejectsSessionInput(t *testing.T) {
func TestShutdownRejectsInputDuringDrain(t *testing.T) {
h := newHarness(t)
- h.seedAccount("alice", "pw", false)
+ h.seedAccount("alice")
h.seedCharacter("alice", "alice")
sess, _ := h.connect("alice")
sess.State = net.StateGame
diff --git a/internal/game/sys_assassin.go b/internal/game/sys_assassin.go
index fc814ec..0fd929e 100644
--- a/internal/game/sys_assassin.go
+++ b/internal/game/sys_assassin.go
@@ -51,7 +51,7 @@ func (g *Game) onAssassinKill(sess *net.Session, p *player.Player, mob *world.Mo
}
sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf(" Assassin task: %d of %d %ss remaining.", remaining, total, name)))
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
func streakBonus(streak int) int {
diff --git a/internal/game/sys_combat_test.go b/internal/game/sys_combat_test.go
index 788e62c..dcd8e5e 100644
--- a/internal/game/sys_combat_test.go
+++ b/internal/game/sys_combat_test.go
@@ -8,7 +8,8 @@ import (
"thehouseoficarus/internal/world"
)
-func spawnTestMob(g *Game, defID string, roomID int) *world.MobInstance {
+func spawnTestMob(g *Game, roomID int) *world.MobInstance {
+ const defID = "skeleton"
def, err := g.MobStore.LoadDef(defID)
if err != nil {
panic("spawnTestMob: " + err.Error())
@@ -27,7 +28,7 @@ func TestAggroInlineRollFiresOnArrival(t *testing.T) {
defer patchAggroRand(0.0)()
g := escapeTestGame()
- spawnTestMob(g, "skeleton", 100)
+ spawnTestMob(g, 100)
p := player.New("agro_fire")
p.RoomID = 100
sess := escapeTestSession(p)
@@ -44,7 +45,7 @@ func TestAggroPerTickRetrySubscriber(t *testing.T) {
defer patchAggroRand(0.5)()
g := escapeTestGame()
- spawnTestMob(g, "skeleton", 200)
+ spawnTestMob(g, 200)
p := player.New("agro_retry")
p.RoomID = 200
sess := escapeTestSession(p)
@@ -67,7 +68,7 @@ func TestAggroPerTickRetrySubscriber(t *testing.T) {
defer patchAggroRand(0.5)()
g := escapeTestGame()
- spawnTestMob(g, "skeleton", 300)
+ spawnTestMob(g, 300)
p := player.New("agro_leave")
p.RoomID = 300
sess := escapeTestSession(p)
diff --git a/internal/game/sys_escape_test.go b/internal/game/sys_escape_test.go
index 0a64666..1fce4f4 100644
--- a/internal/game/sys_escape_test.go
+++ b/internal/game/sys_escape_test.go
@@ -89,8 +89,7 @@ func TestBeginEscapeMoveWithEnergy(t *testing.T) {
p.EscapeTarget = 10
p.EscapeIsWalk = false
- sess := escapeTestSession(p)
- g.beginEscapeMove(sess, p)
+ g.beginEscapeMove(p)
if p.EscapeDir != "" {
t.Error("beginEscapeMove should clear EscapeDir")
@@ -115,8 +114,7 @@ func TestBeginEscapeMoveWithoutEnergy(t *testing.T) {
p.EscapeTarget = 5
p.EscapeIsWalk = false
- sess := escapeTestSession(p)
- g.beginEscapeMove(sess, p)
+ g.beginEscapeMove(p)
if p.MoveTicks != 2 {
t.Errorf("expected MoveTicks=2 without energy, got %d", p.MoveTicks)
diff --git a/internal/game/sys_hazard.go b/internal/game/sys_hazard.go
index 3d51b6d..3a8247b 100644
--- a/internal/game/sys_hazard.go
+++ b/internal/game/sys_hazard.go
@@ -123,7 +123,7 @@ func (g *Game) applyHazardHit(sess *net.Session, p *player.Player, hz *world.Haz
p.HP = 0
}
p.StartRegen()
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
msg := hz.HitMessage
switch {
diff --git a/internal/game/sys_labor.go b/internal/game/sys_labor.go
index f0e1d01..5bd04f5 100644
--- a/internal/game/sys_labor.go
+++ b/internal/game/sys_labor.go
@@ -27,7 +27,7 @@ func (g *Game) progressBar(sess *net.Session, progress, max int) string {
// writeTaskProgress prints the per-tick progress line for a task worksite,
// mirroring the combat hit line but framed as labor instead of damage.
-func (g *Game) writeTaskProgress(sess *net.Session, p *player.Player, mob *world.MobInstance, dmg int, gains []xpGain) {
+func (g *Game) writeTaskProgress(sess *net.Session, p *player.Player, mob *world.MobInstance, gains []xpGain) {
progress := mob.MaxHP - mob.HP
if progress < 0 {
progress = 0
diff --git a/internal/game/sys_science.go b/internal/game/sys_science.go
index a616b42..58e2db0 100644
--- a/internal/game/sys_science.go
+++ b/internal/game/sys_science.go
@@ -66,25 +66,6 @@ func GetMod(id string) *ModDef {
return modByID[id]
}
-func FindMod(input string) *ModDef {
- if m, ok := modByID[input]; ok {
- return m
- }
- lower := strings.ToLower(strings.ReplaceAll(input, " ", "_"))
- for _, m := range AllMods {
- if strings.HasPrefix(m.ID, lower) {
- return m
- }
- }
- lowerSpace := strings.ToLower(input)
- for _, m := range AllMods {
- if strings.HasPrefix(strings.ToLower(m.Name), lowerSpace) {
- return m
- }
- }
- return nil
-}
-
func findAllModMatches(input string) []*ModDef {
inputWords := strings.Fields(strings.ToLower(input))
if len(inputWords) == 0 {
@@ -228,18 +209,17 @@ func (g *Game) hasJunkCost(p *player.Player, mod *ModDef) bool {
return true
}
-func (g *Game) consumeJunkCost(p *player.Player, mod *ModDef) bool {
+func (g *Game) consumeJunkCost(p *player.Player, mod *ModDef) {
cost := g.effectiveJunkCost(p, mod)
for itemID, qty := range cost {
if !p.RemoveItem(itemID, qty) {
- return false
+ return
}
}
- g.AccountStore.SaveCharacter(p)
- return true
+ g.saveCharacter(p)
}
-func (g *Game) triggerModReward(sess *net.Session, p *player.Player, mod *ModDef, multiplier float64, extraGains ...xpGain) int {
+func (g *Game) triggerModReward(sess *net.Session, p *player.Player, mod *ModDef, multiplier float64, extraGains ...xpGain) {
g.consumeJunkCost(p, mod)
sciXP := int(float64(mod.BaseXP) * multiplier)
@@ -249,7 +229,7 @@ func (g *Game) triggerModReward(sess *net.Session, p *player.Player, mod *ModDef
g.awardSkillXP(sess, p, player.SkillName(gain.Skill), gain.XP)
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
if p.OptionBool("xp_drops") {
var parts []string
@@ -265,7 +245,6 @@ func (g *Game) triggerModReward(sess *net.Session, p *player.Player, mod *ModDef
sess.WriteLine(g.colorize(sess, "xp", "("+strings.Join(parts, ", ")+")"))
}
}
- return sciXP
}
func (g *Game) junkCostString(p *player.Player, mod *ModDef) string {
diff --git a/internal/game/sys_technology.go b/internal/game/sys_technology.go
index e285ba8..6f03276 100644
--- a/internal/game/sys_technology.go
+++ b/internal/game/sys_technology.go
@@ -189,7 +189,7 @@ func (g *Game) tryRecharge(sess *net.Session, p *player.Player, input string) bo
sess.WriteLine("Your battery is already full.")
} else {
p.Battery = p.MaxBattery()
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
sess.WriteLine(g.colorize(sess, "battery",
"You connect to the charging station. Your battery is fully recharged."))
}
diff --git a/internal/game/sys_triggers.go b/internal/game/sys_triggers.go
index cae774e..038d3fd 100644
--- a/internal/game/sys_triggers.go
+++ b/internal/game/sys_triggers.go
@@ -87,7 +87,7 @@ func (g *Game) startSequence(sess *net.Session, p *player.Player, steps []behavi
Steps: append([]behavior.Step(nil), steps...),
Wait: seq.wait,
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
}
@@ -204,7 +204,7 @@ func (g *Game) completePlayerSequence(seq *sequence, p *player.Player) {
g.removeSequence(seq)
if p != nil && p.PendingSequence != nil && p.PendingSequence.Key == seq.key {
p.PendingSequence = nil
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
if sess := seq.sess; sess != nil && sess.State == net.StateGame {
g.writePrompt(sess)
@@ -322,7 +322,7 @@ func (g *Game) persistLockedOnDisconnect(p *player.Player) {
Steps: append([]behavior.Step(nil), locked.steps...),
Wait: locked.wait,
}
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
}
}
diff --git a/internal/game/tick.go b/internal/game/tick.go
index 3f6a8b2..cd9b2fd 100644
--- a/internal/game/tick.go
+++ b/internal/game/tick.go
@@ -27,7 +27,7 @@ func (g *Game) DisconnectTick() {
}
if sess.TickDisconnectCountdown() {
g.restoreGodPlayer(p)
- g.AccountStore.SaveCharacter(p)
+ g.saveCharacter(p)
g.Hub.HardRemove(sess)
}
}
diff --git a/internal/item/item.go b/internal/item/item.go
index 227eac5..809959e 100644
--- a/internal/item/item.go
+++ b/internal/item/item.go
@@ -1,3 +1,5 @@
+// Package item defines item definitions (crafting, equip, tools) and the
+// item store backed by yaml files.
package item
import (
diff --git a/internal/net/conn.go b/internal/net/conn.go
index a7c53ec..2a250d8 100644
--- a/internal/net/conn.go
+++ b/internal/net/conn.go
@@ -1,3 +1,6 @@
+// Package net implements the server transports: telnet, telnet-over-TLS,
+// and the WebSocket bridge for the web client, plus the shared Session type.
+// It must not be confused with the standard library net package.
package net
import (
diff --git a/internal/net/server.go b/internal/net/server.go
index dd65f6e..eed5486 100644
--- a/internal/net/server.go
+++ b/internal/net/server.go
@@ -146,7 +146,7 @@ const (
closeFlushTimeout = 2 * time.Second
)
-// startWriter puts the session into queued mode: writes are enqueued and a
+// StartWriter puts the session into queued mode: writes are enqueued and a
// dedicated goroutine drains the queue to the network with per-message write
// deadlines. The goroutine exits after Close() — draining whatever was
// queued first — and closes the transport, so a Close() during shutdown
diff --git a/internal/object/object.go b/internal/object/object.go
index 5dbe1cd..fc34e7b 100644
--- a/internal/object/object.go
+++ b/internal/object/object.go
@@ -1,3 +1,5 @@
+// Package object defines world object definitions (stations, farm patches,
+// safespots, steal targets) and the object store backed by yaml files.
package object
import "thehouseoficarus/internal/behavior"
diff --git a/internal/persistence/txn.go b/internal/persistence/txn.go
index 93d186a..3016a19 100644
--- a/internal/persistence/txn.go
+++ b/internal/persistence/txn.go
@@ -1,3 +1,6 @@
+// Package persistence implements crash-safe multi-file mutations: write-ahead
+// journal transactions with undo on recovery, used for account/character
+// lifecycle changes.
package persistence
// Multi-file journal transactions. A Transaction collects file operations
diff --git a/internal/player/player.go b/internal/player/player.go
index daa7799..708588c 100644
--- a/internal/player/player.go
+++ b/internal/player/player.go
@@ -1,3 +1,5 @@
+// Package player defines the Player/Account model, skills and XP curve, and
+// the account/character store backed by yaml files.
package player
import (
@@ -81,10 +83,6 @@ type Skill struct {
XP int
}
-func (s Skill) Level() int {
- return LevelForXP(s.XP)
-}
-
type InventorySlot struct {
ItemID string `yaml:"item_id"`
Quantity int `yaml:"quantity"`
diff --git a/internal/ui/bar.go b/internal/ui/bar.go
index c1abce9..6904dbc 100644
--- a/internal/ui/bar.go
+++ b/internal/ui/bar.go
@@ -13,28 +13,6 @@ type BarStyle struct {
EmptyDim bool
}
-func RenderBar(current, max, width int, unicode bool) string {
- if max <= 0 {
- max = 1
- }
- if current < 0 {
- current = 0
- }
- if current > max {
- current = max
- }
- filled := int(math.Round(float64(current) * float64(width) / float64(max)))
- if filled > width {
- filled = width
- }
- if filled < 0 {
- filled = 0
- }
-
- fillRune, emptyRune := barRunes(unicode)
- return strings.Repeat(fillRune, filled) + strings.Repeat(emptyRune, width-filled)
-}
-
func RenderBarF(current, max float64, width int, unicode bool) string {
if max <= 0 {
max = 1
diff --git a/internal/validate/checks.go b/internal/validate/checks.go
index 6994654..9d09eef 100644
--- a/internal/validate/checks.go
+++ b/internal/validate/checks.go
@@ -1,3 +1,5 @@
+// Package validate runs semantic/reference checks over the game data
+// directory (dangling ids, bad triggers, unreachable wiring) and reports issues.
package validate
import (
@@ -78,7 +80,7 @@ func validateRooms(s Source) []Issue {
}
issues = append(issues, validateTriggerBlock(
fmt.Sprintf("Room %d: exit %q on_traverse", id, dir),
- exit.OnTraverse, false, itemIDs, roomIndex, mobIDs, nil)...)
+ exit.OnTraverse, itemIDs, roomIndex, mobIDs, nil)...)
}
for _, rm := range room.Mobs {
@@ -230,7 +232,7 @@ func validateLocalObject(roomID int, robj world.RoomObject, itemIDs map[string]b
}
if len(def.OnLook) > 0 {
- issues = append(issues, validateTriggerBlock(prefix+": on_look", def.OnLook, false, itemIDs, roomIndex, mobIDs, nil)...)
+ issues = append(issues, validateTriggerBlock(prefix+": on_look", def.OnLook, itemIDs, roomIndex, mobIDs, nil)...)
}
return issues
@@ -495,7 +497,7 @@ func validateMobs(s Source) []Issue {
}
issues = append(issues, validateTriggerBlock(
- fmt.Sprintf("Mob %q: on_kill", id), def.OnKill, false, itemIDs, roomIndex, mobIDs, dropIDs)...)
+ fmt.Sprintf("Mob %q: on_kill", id), def.OnKill, itemIDs, roomIndex, mobIDs, dropIDs)...)
}
return issues
@@ -604,7 +606,7 @@ func validateObjects(s Source) []Issue {
}
issues = append(issues, validateTriggerBlock(
- fmt.Sprintf("Object %q: on_use", id), obj.OnUse, false, itemIDs, roomIndex, mobIDs, dropIDs)...)
+ fmt.Sprintf("Object %q: on_use", id), obj.OnUse, itemIDs, roomIndex, mobIDs, dropIDs)...)
if obj.Gather != nil {
issues = append(issues, validateGather(fmt.Sprintf("Object %q: gather", id), obj.Gather, itemIDs, dropIDs)...)
@@ -615,7 +617,7 @@ func validateObjects(s Source) []Issue {
}
if len(obj.OnLook) > 0 {
- issues = append(issues, validateTriggerBlock(fmt.Sprintf("Object %q: on_look", id), obj.OnLook, false, itemIDs, roomIndex, mobIDs, dropIDs)...)
+ issues = append(issues, validateTriggerBlock(fmt.Sprintf("Object %q: on_look", id), obj.OnLook, itemIDs, roomIndex, mobIDs, dropIDs)...)
}
}
@@ -649,7 +651,7 @@ func validateItems(s Source) []Issue {
if len(def.Search) > 0 {
issues = append(issues, validateTriggerBlock(
- fmt.Sprintf("Item %q: search", id), def.Search, false, itemIDs, roomIndex, mobIDs, dropIDs)...)
+ fmt.Sprintf("Item %q: search", id), def.Search, itemIDs, roomIndex, mobIDs, dropIDs)...)
}
if def.FarmProduct != "" && !itemIDs[def.FarmProduct] {
@@ -866,9 +868,8 @@ func validateCourses(s Source) []Issue {
return issues
}
-// validateRoomTriggers validates every trigger-shaped block on every room:
-// on_enter, on_exit (verb blocks), and on_flag_change / on_global_flag_change
-// (flag blocks, which require on_player_flag/on_global_flag).
+// validateRoomTriggers validates the on_enter and on_exit trigger blocks on
+// every room. Flag-change triggers (data/triggers) are not validated here.
func validateRoomTriggers(s Source) []Issue {
var issues []Issue
roomIndex := s.World.RoomIndex()
@@ -882,11 +883,11 @@ func validateRoomTriggers(s Source) []Issue {
}
if len(room.OnEnter) > 0 {
issues = append(issues, validateTriggerBlock(
- fmt.Sprintf("Room %d: on_enter", id), room.OnEnter, false, itemIDs, roomIndex, mobIDs, nil)...)
+ fmt.Sprintf("Room %d: on_enter", id), room.OnEnter, itemIDs, roomIndex, mobIDs, nil)...)
}
if len(room.OnExit) > 0 {
issues = append(issues, validateTriggerBlock(
- fmt.Sprintf("Room %d: on_exit", id), room.OnExit, false, itemIDs, roomIndex, mobIDs, nil)...)
+ fmt.Sprintf("Room %d: on_exit", id), room.OnExit, itemIDs, roomIndex, mobIDs, nil)...)
}
}
@@ -1379,87 +1380,30 @@ func validateCondition(prefix string, c *behavior.Condition, roomIndex map[int]b
return issues
}
-// checkPlayerOnlyInCondition warns when a global-scope condition uses
-// player-dependent predicates that cannot be evaluated without a player.
-func checkPlayerOnlyInCondition(prefix string, c *behavior.Condition) []Issue {
- if c == nil {
- return nil
- }
- var issues []Issue
- if c.PlayerFlag != "" {
- issues = append(issues, Issue{
- Level: "WARN",
- Type: "semantic",
- Message: fmt.Sprintf("%s: player_flag %q has no effect on global-flag triggers (no player)", prefix, c.PlayerFlag),
- })
- }
- if c.HasItem != "" {
- issues = append(issues, Issue{
- Level: "WARN",
- Type: "semantic",
- Message: fmt.Sprintf("%s: has_item %q has no effect on global-flag triggers (no player)", prefix, c.HasItem),
- })
- }
- if c.MinCredits > 0 {
- issues = append(issues, Issue{
- Level: "WARN",
- Type: "semantic",
- Message: fmt.Sprintf("%s: min_credits has no effect on global-flag triggers (no player)", prefix),
- })
- }
- for i := range c.AllOf {
- issues = append(issues, checkPlayerOnlyInCondition(prefix, &c.AllOf[i])...)
- }
- for i := range c.AnyOf {
- issues = append(issues, checkPlayerOnlyInCondition(prefix, &c.AnyOf[i])...)
- }
- return issues
-}
-
-// validateTriggerBlock validates a list of Trigger entries for one event block.
-// isFlag is true for on_flag_change / on_global_flag_change blocks (which
-// require on_player_flag/on_global_flag and forbid item_id) and false for verb
-// blocks (on_use/on_look/on_kill/on_enter/on_exit/on_traverse), where item_id
-// is checked against itemIDs.
-func validateTriggerBlock(prefix string, block []behavior.Trigger, isFlag bool, itemIDs map[string]bool, roomIndex map[int]bool, mobIDs map[string]bool, dropIDs map[string]bool) []Issue {
+// validateTriggerBlock validates a list of Trigger entries for one verb event
+// block (on_use/on_look/on_kill/on_enter/on_exit/on_traverse), where item_id
+// is checked against itemIDs. Flag-change blocks (data/triggers) are not
+// validated here.
+func validateTriggerBlock(prefix string, block []behavior.Trigger, itemIDs map[string]bool, roomIndex map[int]bool, mobIDs map[string]bool, dropIDs map[string]bool) []Issue {
var issues []Issue
for i := range block {
t := &block[i]
entryPrefix := fmt.Sprintf("%s[%d]", prefix, i)
- if isFlag {
- if t.OnPlayerFlag == "" && t.OnGlobalFlag == "" {
- issues = append(issues, Issue{
- Level: "WARN",
- Type: "semantic",
- Message: fmt.Sprintf("%s: flag-change trigger has no on_player_flag/on_global_flag — it can never fire",
- entryPrefix),
- })
- }
- if t.ItemID != "" {
- issues = append(issues, Issue{
- Level: "WARN",
- Type: "semantic",
- Message: fmt.Sprintf("%s: item_id is ignored on flag-change triggers",
- entryPrefix),
- })
- }
- } else {
- if t.OnPlayerFlag != "" || t.OnGlobalFlag != "" {
- issues = append(issues, Issue{
- Level: "WARN",
- Type: "semantic",
- Message: fmt.Sprintf("%s: on_player_flag/on_global_flag are ignored on verb triggers",
- entryPrefix),
- })
- }
- if t.ItemID != "" && !itemIDs[t.ItemID] {
- issues = append(issues, Issue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: item_id %q does not exist",
- entryPrefix, t.ItemID),
- })
- }
+ if t.OnPlayerFlag != "" || t.OnGlobalFlag != "" {
+ issues = append(issues, Issue{
+ Level: "WARN",
+ Type: "semantic",
+ Message: fmt.Sprintf("%s: on_player_flag/on_global_flag are ignored on verb triggers",
+ entryPrefix),
+ })
+ }
+ if t.ItemID != "" && !itemIDs[t.ItemID] {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("%s: item_id %q does not exist",
+ entryPrefix, t.ItemID),
+ })
}
if t.Lock && len(t.Steps) == 0 {
issues = append(issues, Issue{
@@ -1471,14 +1415,10 @@ func validateTriggerBlock(prefix string, block []behavior.Trigger, isFlag bool,
if t.Condition != nil {
issues = append(issues, validateCondition(entryPrefix, t.Condition, roomIndex)...)
}
- if isFlag && t.OnGlobalFlag != "" && t.Condition != nil {
- issues = append(issues, checkPlayerOnlyInCondition(entryPrefix, t.Condition)...)
- }
+
for si := range t.Steps {
issues = append(issues, validateStep(fmt.Sprintf("%s: step[%d]", entryPrefix, si), &t.Steps[si], itemIDs, roomIndex, mobIDs, dropIDs)...)
- if isFlag && t.OnGlobalFlag != "" && t.Steps[si].Condition != nil {
- issues = append(issues, checkPlayerOnlyInCondition(fmt.Sprintf("%s: step[%d]", entryPrefix, si), t.Steps[si].Condition)...)
- }
+
}
}
return issues
diff --git a/internal/world/grid.go b/internal/world/grid.go
index ac80476..9ee08d1 100644
--- a/internal/world/grid.go
+++ b/internal/world/grid.go
@@ -1,3 +1,5 @@
+// Package world implements rooms, the zone grid, the world index, mobs and
+// their spawns, and ground items — the spatial state of Vesta.
package world
// GridConflict describes a layout conflict found while embedding rooms onto a