package main import ( "encoding/json" "io" "math" "net/http" "strconv" "strings" ) // Item-processor: a faithful port of inventory-service extract_item_properties + // the process_inventory column population. Given a raw item dict it produces the // normalized rows for the 7 tables, applying the exact per-table sentinel->NULL // rules. Validated against production's stored rows (read-only) via /debug/process. // --- value-bag accessors (JSON object keys are strings) --- func bag(raw map[string]any, name string) map[string]any { if m, ok := raw[name].(map[string]any); ok { return m } return map[string]any{} } func ivI(iv map[string]any, key string, def int) int { if v, ok := iv[key]; ok { if f, ok := v.(float64); ok { return int(f) } } return def } func dvF(dv map[string]any, key string, def float64) float64 { if v, ok := dv[key]; ok { if f, ok := v.(float64); ok { return f } } return def } func rawI(raw map[string]any, key string, def int) int { if v, ok := raw[key]; ok { if f, ok := v.(float64); ok { return int(f) } } return def } func rawF(raw map[string]any, key string, def float64) float64 { if v, ok := raw[key]; ok { if f, ok := v.(float64); ok { return f } } return def } func rawS(raw map[string]any, key string) string { if s, ok := raw[key].(string); ok { return s } return "" } func rawB(raw map[string]any, key string) bool { b, _ := raw[key].(bool) return b } // IV first, else top-level field, else default (e.g. max_damage). func ivElseTopI(iv, raw map[string]any, ivKey, topKey string, def int) int { if v, ok := iv[ivKey]; ok { if f, ok := v.(float64); ok { return int(f) } } return rawI(raw, topKey, def) } func dvElseTopF(dv, raw map[string]any, dvKey, topKey string, def float64) float64 { if v, ok := dv[dvKey]; ok { if f, ok := v.(float64); ok { return f } } return rawF(raw, topKey, def) } // translateMaterial: materials[id] else "Unknown_Material_{id}". func (s *Server) translateMaterial(id int) string { if n, ok := s.materials[id]; ok { return n } return "Unknown_Material_" + strconv.Itoa(id) } func toIntList(v any) []int { arr, ok := v.([]any) if !ok { return nil } out := make([]int, 0, len(arr)) for _, e := range arr { if f, ok := e.(float64); ok { out = append(out, int(f)) } } return out } // processItem produces the normalized columns for all 7 tables, post-null. func (s *Server) processItem(raw map[string]any) map[string]any { iv := bag(raw, "IntValues") dv := bag(raw, "DoubleValues") items := map[string]any{ "item_id": rawValue(raw, "Id"), "name": rawS(raw, "Name"), "icon": rawI(raw, "Icon", 0), "object_class": rawI(raw, "ObjectClass", 0), "value": rawI(raw, "Value", 0), "burden": rawI(raw, "Burden", 0), "has_id_data": rawB(raw, "HasIdData"), "last_id_time": rawI(raw, "LastIdTime", 0), "current_wielded_location": ivI(iv, "10", 0), "container_id": rawI(raw, "ContainerId", 0), "slot": ivI(iv, "231735296", -1), "bonded": ivI(iv, "33", 0), "attuned": ivI(iv, "114", 0), "unique": ivI(iv, "279", 0) != 0, "stack_size": ivI(iv, "12", 1), "max_stack_size": ivI(iv, "11", 1), "items_capacity": nilNeg(ivI(iv, "6", -1)), "containers_capacity": nilNeg(ivI(iv, "7", -1)), "structure": nilNeg(ivI(iv, "92", -1)), "max_structure": nilNeg(ivI(iv, "91", -1)), "rare_id": nilNeg(ivI(iv, "17", -1)), "lifespan": nilNeg(ivI(iv, "267", -1)), "remaining_lifespan": nilNeg(ivI(iv, "268", -1)), } // combat (sentinel defaults), then base values merged. wt := ivI(iv, "218103835", -1) if wt > 100 { wt = 100 } combat := map[string]any{ "max_damage": ivElseTopI(iv, raw, "218103842", "MaxDamage", -1), "damage_type": ivI(iv, "218103832", -1), "damage_bonus": dvElseTopF(dv, raw, "167772174", "DamageBonus", -1.0), "elemental_damage_bonus": ivI(iv, "204", -1), "elemental_damage_vs_monsters": dvF(dv, "152", -1.0), "variance": dvF(dv, "167772171", -1.0), "cleaving": ivI(iv, "292", -1), "crit_damage_rating": ivI(iv, "314", -1), "damage_over_time": ivI(iv, "318", -1), "attack_bonus": dvElseTopF(dv, raw, "167772170", "AttackBonus", -1.0), "weapon_time": wt, "weapon_skill": ivI(iv, "218103840", -1), "armor_level": topElseIvI(raw, iv, "ArmorLevel", "28", -1), "melee_defense_bonus": dvF(dv, "29", -1.0), "missile_defense_bonus": dvF(dv, "149", -1.0), "magic_defense_bonus": dvF(dv, "150", -1.0), "resist_magic": ivI(iv, "36", -1), "crit_resist_rating": ivI(iv, "315", -1), "crit_damage_resist_rating": ivI(iv, "316", -1), "dot_resist_rating": ivI(iv, "350", -1), "life_resist_rating": ivI(iv, "351", -1), "nether_resist_rating": ivI(iv, "331", -1), "heal_over_time": ivI(iv, "312", -1), "healing_resist_rating": ivI(iv, "317", -1), "mana_conversion_bonus": dvF(dv, "144", -1.0), "pk_damage_rating": ivI(iv, "381", -1), "pk_damage_resist_rating": ivI(iv, "382", -1), "gear_pk_damage_rating": ivI(iv, "383", -1), "gear_pk_damage_resist_rating": ivI(iv, "384", -1), } s.mergeBaseValues(raw, combat) requirements := map[string]any{ "wield_level": rawI(raw, "WieldLevel", -1), "skill_level": rawI(raw, "SkillLevel", -1), "lore_requirement": rawI(raw, "LoreRequirement", -1), "equip_skill": rawValueStr(raw, "EquipSkill"), } // material + item_set translated strings. var material any if m := rawS(raw, "Material"); m != "" { material = m } else if v, ok := iv["131"]; ok { if f, ok := v.(float64); ok && int(f) != 0 { name := s.translateMaterial(int(f)) if !strings.HasPrefix(name, "Unknown_Material_") { material = name } } } var itemSet any if v, ok := iv["265"]; ok { if f, ok := v.(float64); ok && int(f) != 0 { id := strconv.Itoa(int(f)) if n, ok := s.attributeSets[id]; ok { itemSet = n } else { itemSet = id } } } enhancements := map[string]any{ "material": material, "imbue": rawValueStr(raw, "Imbue"), "tinks": rawI(raw, "Tinks", -1), "workmanship": rawF(raw, "Workmanship", -1.0), "num_times_tinkered": ivI(iv, "171", -1), "free_tinkers_bitfield": ivI(iv, "264", -1), "num_items_in_material": ivI(iv, "170", -1), "imbue_attempts": ivI(iv, "205", -1), "imbue_successes": ivI(iv, "206", -1), "imbued_effect2": ivI(iv, "303", -1), "imbued_effect3": ivI(iv, "304", -1), "imbued_effect4": ivI(iv, "305", -1), "imbued_effect5": ivI(iv, "306", -1), "imbue_stacking_bits": ivI(iv, "311", -1), "item_set": itemSet, "equipment_set_extra": ivI(iv, "321", -1), "aetheria_bitfield": ivI(iv, "322", -1), "heritage_specific_armor": ivI(iv, "324", -1), "shared_cooldown": ivI(iv, "280", -1), } ratingKeys := map[string]string{ "damage_rating": "307", "damage_resist_rating": "308", "crit_rating": "313", "crit_resist_rating": "315", "crit_damage_rating": "314", "crit_damage_resist_rating": "316", "heal_boost_rating": "323", "vitality_rating": "341", "healing_rating": "342", "weakness_rating": "329", "nether_over_time": "330", "healing_resist_rating": "317", "nether_resist_rating": "331", "dot_resist_rating": "350", "life_resist_rating": "351", "sneak_attack_rating": "356", "recklessness_rating": "357", "deception_rating": "358", "pk_damage_rating": "381", "pk_damage_resist_rating": "382", "gear_pk_damage_rating": "383", "gear_pk_damage_resist_rating": "384", "gear_damage": "370", "gear_damage_resist": "371", "gear_crit": "372", "gear_crit_resist": "373", "gear_crit_damage": "374", "gear_crit_damage_resist": "375", "gear_healing_boost": "376", "gear_max_health": "379", "gear_nether_resist": "377", "gear_life_resist": "378", "gear_overpower": "388", "gear_overpower_resist": "389", } ratings := map[string]any{} for col, k := range ratingKeys { ratings[col] = ivI(iv, k, -1) } // spells: union of Spells + ActiveSpells, is_active = in ActiveSpells. spells := toIntList(raw["Spells"]) active := toIntList(raw["ActiveSpells"]) activeSet := map[int]bool{} for _, id := range active { activeSet[id] = true } seen := map[int]bool{} var spellRows []map[string]any for _, id := range append(append([]int{}, spells...), active...) { if seen[id] { continue } seen[id] = true spellRows = append(spellRows, map[string]any{"spell_id": id, "is_active": activeSet[id]}) } return map[string]any{ "items": items, "combat": nullify(combat, sentinelCombat), "requirements": nullify(requirements, sentinelReq), "enhancements": nullifyKeep(enhancements, sentinelEnh), // ALWAYS inserts a row "ratings": nullify(ratings, sentinelRating), "spells": spellRows, } } func topElseIvI(raw, iv map[string]any, topKey, ivKey string, def int) int { if v, ok := raw[topKey]; ok { if f, ok := v.(float64); ok { return int(f) } } return ivI(iv, ivKey, def) } func rawValue(raw map[string]any, key string) any { if v, ok := raw[key]; ok { if f, ok := v.(float64); ok { return int64(f) } return v } if v, ok := raw[strings.ToLower(key)]; ok { // Id -> id fallback if f, ok := v.(float64); ok { return int64(f) } return v } return nil } func rawValueStr(raw map[string]any, key string) any { if s, ok := raw[key].(string); ok && s != "" { return s } return nil } func nilNeg(v int) any { if v == -1 { return nil } return v } // per-table sentinel predicates: true => value should become NULL. func sentinelCombat(v any) bool { return isNeg1(v) || isNeg1f(v) } func sentinelReq(v any) bool { return isNeg1(v) || v == nil || v == "" } func sentinelEnh(v any) bool { return isNeg1(v) || isNeg1f(v) || v == nil || v == "" } func sentinelRating(v any) bool { return isNeg1(v) || isNeg1f(v) || v == nil } func isNeg1(v any) bool { i, ok := v.(int); return ok && i == -1 } func isNeg1f(v any) bool { f, ok := v.(float64); return ok && f == -1.0 } // nullify replaces sentinel values with nil. Returns nil for the whole table if // every value is sentinel (the per-table "skip insert" guard) — EXCEPT // enhancements, which always inserts; we keep its map even if all-null. func nullify(m map[string]any, isSentinel func(any) bool) map[string]any { any_ := false out := make(map[string]any, len(m)) for k, v := range m { if isSentinel(v) { out[k] = nil } else { out[k] = v any_ = true } } if !any_ { return nil // combat/req/ratings: skip the insert when all-sentinel } return out } // nullifyKeep is like nullify but ALWAYS returns the map (for item_enhancements, // which inserts a row even when every value is NULL). func nullifyKeep(m map[string]any, isSentinel func(any) bool) map[string]any { out := make(map[string]any, len(m)) for k, v := range m { if isSentinel(v) { out[k] = nil } else { out[k] = v } } return out } // mergeBaseValues reverses active-spell buffs into base_* columns (compute_base_values). type spellEffect struct { key int change, bonus float64 } var intEffects = map[int]spellEffect{ 1616: {218103842, 20, 0}, 2096: {218103842, 22, 0}, 5183: {218103842, 24, 0}, 4395: {218103842, 24, 0}, 3688: {218103842, 300, 0}, 2598: {218103842, 2, 2}, 2586: {218103842, 4, 4}, 4661: {218103842, 7, 7}, 6089: {218103842, 10, 10}, 1486: {28, 200, 0}, 2108: {28, 220, 0}, 4407: {28, 240, 0}, 2604: {28, 20, 20}, 2592: {28, 40, 40}, 4667: {28, 60, 60}, 6095: {28, 80, 80}, } var doubleEffects = map[int]spellEffect{ 3258: {152, 0.06, 0}, 3259: {152, 0.07, 0}, 5182: {152, 0.08, 0}, 4414: {152, 0.08, 0}, 3735: {152, 0.15, 0}, 3251: {152, 0.01, 0.01}, 3250: {152, 0.03, 0.03}, 4670: {152, 0.05, 0.05}, 6098: {152, 0.07, 0.07}, 1592: {167772172, 0.15, 0}, 2106: {167772172, 0.17, 0}, 4405: {167772172, 0.20, 0}, 2603: {167772172, 0.03, 0.03}, 2591: {167772172, 0.05, 0.05}, 4666: {167772172, 0.07, 0.07}, 6094: {167772172, 0.09, 0.09}, 1605: {29, 0.15, 0}, 2101: {29, 0.17, 0}, 4400: {29, 0.20, 0}, 3699: {29, 0.25, 0}, 2600: {29, 0.03, 0.03}, 3985: {29, 0.04, 0.04}, 2588: {29, 0.05, 0.05}, 4663: {29, 0.07, 0.07}, 6091: {29, 0.09, 0.09}, 1480: {144, 1.60, 0}, 2117: {144, 1.70, 0}, 4418: {144, 1.80, 0}, 3201: {144, 1.05, 1.05}, 3199: {144, 1.10, 1.10}, 3202: {144, 1.15, 1.15}, 3200: {144, 1.20, 1.20}, 6086: {144, 1.25, 1.25}, 6087: {144, 1.30, 1.30}, } func (s *Server) mergeBaseValues(raw, combat map[string]any) { spells := toIntList(raw["Spells"]) active := toIntList(raw["ActiveSpells"]) for _, p := range []struct { prop string key int }{{"max_damage", 218103842}, {"armor_level", 28}} { val, ok := combat[p.prop].(int) if !ok || val == -1 { continue } for _, sid := range active { if e, ok := intEffects[sid]; ok && e.key == p.key { val -= int(e.change) } } for _, sid := range spells { if e, ok := intEffects[sid]; ok && e.key == p.key && e.bonus != 0 { val += int(e.bonus) } } combat["base_"+p.prop] = val } for _, p := range []struct { prop string key int }{{"attack_bonus", 167772172}, {"melee_defense_bonus", 29}, {"elemental_damage_vs_monsters", 152}, {"mana_conversion_bonus", 144}} { val, ok := combat[p.prop].(float64) if !ok || val == -1.0 { continue } for _, sid := range active { if e, ok := doubleEffects[sid]; ok && e.key == p.key { val -= e.change } } for _, sid := range spells { if e, ok := doubleEffects[sid]; ok && e.key == p.key && e.bonus != 0 { val += e.bonus } } combat["base_"+p.prop] = math.Round(val*10000) / 10000 } } // POST /debug/process — returns the normalized columns for a raw item JSON body // (loopback validation against production's stored rows; never writes). func (s *Server) handleDebugProcess(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(io.LimitReader(r.Body, 8<<20)) var raw map[string]any if json.Unmarshal(body, &raw) != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "invalid JSON"}) return } writeJSON(w, http.StatusOK, s.processItem(raw)) }