fix(inventory-go): restore GET /inventory/{name} (live Inv window was empty)
The Go cutover omitted get_character_inventory; the React InventoryWindow
fetches GET /inventory/{name} and got 404 -> empty. Port the endpoint:
per-character items with placement (current_wielded_location/container_id/
items_capacity), mana (current_mana/max_mana from original_json IntValues),
icon overlays, and join-table combat/req/enh/rating stats; material-prefixed
name. Returns {character_name,item_count,items}.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4bc51a1f48
commit
db534ea389
3 changed files with 136 additions and 8 deletions
92
go-services/inventory-go/inventory_char.go
Normal file
92
go-services/inventory-go/inventory_char.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GET /inventory/{character_name} — full per-character inventory for the React
|
||||
// Inventory window. Port of inventory-service get_character_inventory +
|
||||
// enrich_db_item (main.py:2622 / 2338). The Go cutover omitted this endpoint
|
||||
// (it was assumed unused), but the React InventoryWindow fetches it, so its
|
||||
// absence (404) made the live inventory render empty.
|
||||
//
|
||||
// Returns {character_name, item_count, items:[...]} with the snake_case fields
|
||||
// the frontend normalizeItem consumes: placement via current_wielded_location /
|
||||
// container_id / items_capacity, the mana panel via current_mana / max_mana,
|
||||
// icon overlays, plus tooltip combat/requirement/enhancement/rating stats. Mana
|
||||
// and icon overlays are pulled straight from original_json IntValues (same keys
|
||||
// the plugin/search path use); the rest come from the normalized join tables.
|
||||
func (s *Server) handleCharacterInventory(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.PathValue("character_name")
|
||||
limit := clampInt(qIntDefault(r.URL.Query(), "limit", 1000), 1, 5000)
|
||||
offset := qIntDefault(r.URL.Query(), "offset", 0)
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
const q = `
|
||||
SELECT
|
||||
i.item_id, i.name, i.icon, i.object_class, i.value, i.burden,
|
||||
i.current_wielded_location, i.container_id, i.items_capacity, i.stack_size,
|
||||
cs.max_damage, cs.armor_level, cs.damage_bonus, cs.attack_bonus,
|
||||
cs.melee_defense_bonus, cs.magic_defense_bonus,
|
||||
r.wield_level, r.skill_level, r.equip_skill, r.lore_requirement,
|
||||
e.material, e.imbue, e.item_set, e.tinks, e.workmanship,
|
||||
rt.damage_rating, rt.crit_rating, rt.crit_damage_rating, rt.heal_boost_rating,
|
||||
NULLIF((rd.original_json->'IntValues'->>'218103815')::int, 0) AS current_mana,
|
||||
NULLIF((rd.original_json->'IntValues'->>'218103814')::int, 0) AS max_mana,
|
||||
NULLIF((rd.original_json->'IntValues'->>'218103849')::int, 0) AS icon_overlay_id,
|
||||
NULLIF((rd.original_json->'IntValues'->>'218103850')::int, 0) AS icon_underlay_id
|
||||
FROM items i
|
||||
LEFT JOIN item_combat_stats cs ON i.id = cs.item_id
|
||||
LEFT JOIN item_requirements r ON i.id = r.item_id
|
||||
LEFT JOIN item_enhancements e ON i.id = e.item_id
|
||||
LEFT JOIN item_ratings rt ON i.id = rt.item_id
|
||||
LEFT JOIN item_raw_data rd ON i.id = rd.item_id
|
||||
WHERE i.character_name = $1
|
||||
ORDER BY i.name
|
||||
LIMIT $2 OFFSET $3`
|
||||
|
||||
rows, err := queryRowsAsMaps(r.Context(), s.pool, q, name, limit, offset)
|
||||
if err != nil {
|
||||
s.dbErr(w, "inventory/"+name, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]map[string]any, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, enrichInventoryRow(row))
|
||||
}
|
||||
|
||||
// Unlike the Python endpoint (404 on no rows), always return 200 with a
|
||||
// possibly-empty list — the window treats both as empty, and 200 avoids the
|
||||
// frontend's catch-all error path.
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"character_name": name,
|
||||
"item_count": len(items),
|
||||
"items": items,
|
||||
})
|
||||
}
|
||||
|
||||
// enrichInventoryRow flattens a joined inventory row into the frontend item
|
||||
// shape: drops NULL columns and applies the material-name prefix to the item
|
||||
// name (enrich_db_item parity, e.g. "Pyreal" + "Chiran Helm" -> "Pyreal Chiran
|
||||
// Helm"), preserving the un-prefixed name in original_name.
|
||||
func enrichInventoryRow(row map[string]any) map[string]any {
|
||||
out := make(map[string]any, len(row)+2)
|
||||
for k, v := range row {
|
||||
if v != nil {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
if mat, ok := out["material"].(string); ok && mat != "" {
|
||||
out["material_name"] = mat
|
||||
if name, ok := out["name"].(string); ok && name != "" &&
|
||||
!strings.HasPrefix(strings.ToLower(name), strings.ToLower(mat)) {
|
||||
out["name"] = mat + " " + name
|
||||
out["original_name"] = name
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
35
go-services/inventory-go/inventory_char_test.go
Normal file
35
go-services/inventory-go/inventory_char_test.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEnrichInventoryRow(t *testing.T) {
|
||||
// NULL columns are dropped.
|
||||
out := enrichInventoryRow(map[string]any{"name": "Helm", "armor_level": nil, "value": 100})
|
||||
if _, ok := out["armor_level"]; ok {
|
||||
t.Errorf("nil column armor_level should be dropped, got %v", out["armor_level"])
|
||||
}
|
||||
if out["value"] != 100 {
|
||||
t.Errorf("value = %v, want 100", out["value"])
|
||||
}
|
||||
|
||||
// Material prefix is applied and original_name preserved.
|
||||
out = enrichInventoryRow(map[string]any{"name": "Chiran Helm", "material": "Pyreal"})
|
||||
if out["name"] != "Pyreal Chiran Helm" {
|
||||
t.Errorf("name = %v, want %q", out["name"], "Pyreal Chiran Helm")
|
||||
}
|
||||
if out["original_name"] != "Chiran Helm" {
|
||||
t.Errorf("original_name = %v, want %q", out["original_name"], "Chiran Helm")
|
||||
}
|
||||
if out["material_name"] != "Pyreal" {
|
||||
t.Errorf("material_name = %v, want Pyreal", out["material_name"])
|
||||
}
|
||||
|
||||
// Already-prefixed name is not doubled.
|
||||
out = enrichInventoryRow(map[string]any{"name": "Pyreal Helm", "material": "Pyreal"})
|
||||
if out["name"] != "Pyreal Helm" {
|
||||
t.Errorf("name = %v, want no double prefix", out["name"])
|
||||
}
|
||||
if _, ok := out["original_name"]; ok {
|
||||
t.Errorf("original_name should be unset when no prefix applied")
|
||||
}
|
||||
}
|
||||
|
|
@ -28,14 +28,14 @@ import (
|
|||
var buildVersion = "dev"
|
||||
|
||||
type Server struct {
|
||||
pool *pgxpool.Pool
|
||||
attributeSets map[string]string // AttributeSetInfo: set-id -> set name
|
||||
objectClasses map[int]string // ObjectClass: id -> name
|
||||
materials map[int]string // MaterialType: id -> name
|
||||
spells map[int]map[string]any // SpellTable: spell-id -> raw spell value object
|
||||
equipMaskMap map[int]string // EquipMask: mask -> technical name (exact lookup)
|
||||
equipMaskOrdered []equipMaskEntry // EquipMask in ascending-mask order (bit-flag decode)
|
||||
log *slog.Logger
|
||||
pool *pgxpool.Pool
|
||||
attributeSets map[string]string // AttributeSetInfo: set-id -> set name
|
||||
objectClasses map[int]string // ObjectClass: id -> name
|
||||
materials map[int]string // MaterialType: id -> name
|
||||
spells map[int]map[string]any // SpellTable: spell-id -> raw spell value object
|
||||
equipMaskMap map[int]string // EquipMask: mask -> technical name (exact lookup)
|
||||
equipMaskOrdered []equipMaskEntry // EquipMask in ascending-mask order (bit-flag decode)
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
@ -93,6 +93,7 @@ func main() {
|
|||
mux.HandleFunc("GET /sets/list", srv.handleSetsList)
|
||||
mux.HandleFunc("GET /characters/list", srv.handleCharactersList)
|
||||
mux.HandleFunc("GET /search/items", srv.handleSearchItems)
|
||||
mux.HandleFunc("GET /inventory/{character_name}", srv.handleCharacterInventory)
|
||||
mux.HandleFunc("POST /debug/process", srv.handleDebugProcess)
|
||||
// Ingestion (works in read-write mode; on the read-only prod instance these
|
||||
// fail the read-only transaction, which is the intended guard).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue