"""Load and normalize scunpacked JSON data into the inventory store. Source data: https://github.com/richardthombs/scunpacked (mirrored at https://github.com/StarCitizenWiki/scunpacked-data) Real schemas this loader consumes: - ships.json: {"UUID","ClassName","Name","Description","Career","Role", "Manufacturer":{"UUID","Code","Name"},"Size","Length","Width", "Height","Crew","Mass","IsVehicle","IsGravlev","IsSpaceship",...} - ship-items.json / fps-items.json: {"className","reference","itemName","type","subType","size", "grade","name","tags","classification", "stdItem":{"UUID","ClassName","Size","Grade","Width","Height", "Length","Mass","Type","Name","Description","DescriptionText", "Manufacturer":{"Name","Code","UUID"},"Tags":[...],"Ports":[...]}} - manufacturers.json:{"Code","Name","Reference","Description"} """ import json import os from app import config PLACEHOLDER = "<= PLACEHOLDER =>" CATEGORY_SHIPS = "ships" CATEGORY_SHIP_ITEMS = "ship-items" CATEGORY_FPS_ITEMS = "fps-items" def _load_json(data_dir, filename): path = os.path.join(data_dir, filename) if not os.path.exists(path): return [] with open(path, encoding="utf-8") as f: return json.load(f) def _resolve_name(*candidates, class_name=None): """Pick the first real display name; fall back to a prettified ClassName. Many scunpacked entries have names literally set to "<= PLACEHOLDER =>" (untranslated labels), so the fallback rule is mandatory. """ for candidate in candidates: if candidate and str(candidate).strip() and str(candidate).strip() != PLACEHOLDER: return str(candidate).strip() if class_name: return str(class_name).replace("_", " ") return "Unknown" def _manufacturer(raw): if not raw: return {"code": "UNKN", "name": "Unknown Manufacturer"} code = raw.get("Code") or raw.get("code") or "UNKN" name = _resolve_name(raw.get("Name"), raw.get("name"), class_name=code) return {"code": code, "name": name} def _normalize_ship(ship): class_name = ship.get("ClassName") or "" manufacturer = ship.get("Manufacturer") or {} flags = { "is_spaceship": bool(ship.get("IsSpaceship")), "is_vehicle": bool(ship.get("IsVehicle")), "is_gravlev": bool(ship.get("IsGravlev")), } tags = [flag for flag, value in flags.items() if value] return { "uuid": ship.get("UUID") or "", "class_name": class_name, "name": _resolve_name(ship.get("Name"), class_name=class_name), "category": CATEGORY_SHIPS, "type": ship.get("Career") or "Ship", "sub_type": ship.get("Role") or "", "size": ship.get("Size") or 0, "grade": None, "manufacturer": _manufacturer(manufacturer), "classification": "Ship", "description": ship.get("Description") or ship.get("DescriptionText") or "", "tags": tags, }, { "career": ship.get("Career") or "", "role": ship.get("Role") or "", "length": ship.get("Length"), "width": ship.get("Width"), "height": ship.get("Height"), "crew": ship.get("Crew"), "mass": ship.get("Mass"), **flags, } def _normalize_item(item, category): class_name = item.get("className") or "" std = item.get("stdItem") or {} manufacturer = std.get("Manufacturer") or {} raw_tags = std.get("Tags") tags = list(raw_tags) if isinstance(raw_tags, list) else [] summary = { "uuid": std.get("UUID") or item.get("reference") or "", "class_name": class_name, "name": _resolve_name(std.get("Name"), item.get("name"), class_name=class_name), "category": category, "type": item.get("type") or std.get("Type") or "", "sub_type": item.get("subType") or "", "size": item.get("size") if item.get("size") is not None else std.get("Size"), "grade": item.get("grade") if item.get("grade") is not None else std.get("Grade"), "manufacturer": _manufacturer(manufacturer), "classification": item.get("classification") or "", "description": std.get("Description") or std.get("DescriptionText") or "", "tags": tags, } details = { "ports": std.get("Ports") or [], "mass": std.get("Mass"), "width": std.get("Width"), "height": std.get("Height"), "length": std.get("Length"), } return summary, details class Store: """In-memory, read-only inventory store built from the scunpacked JSON.""" def __init__(self, items, details, manufacturers): self.items = items self.details = details self.manufacturers = manufacturers self.by_class_name = {item["class_name"]: item for item in items} self.counts = { CATEGORY_SHIPS: sum(1 for i in items if i["category"] == CATEGORY_SHIPS), CATEGORY_SHIP_ITEMS: sum(1 for i in items if i["category"] == CATEGORY_SHIP_ITEMS), CATEGORY_FPS_ITEMS: sum(1 for i in items if i["category"] == CATEGORY_FPS_ITEMS), } def load_store(data_dir=None): data_dir = data_dir or config.DATA_DIR items = [] details = {} for ship in _load_json(data_dir, "ships.json"): summary, detail = _normalize_ship(ship) items.append(summary) details[summary["class_name"]] = detail for entry in _load_json(data_dir, "ship-items.json"): summary, detail = _normalize_item(entry, CATEGORY_SHIP_ITEMS) items.append(summary) details[summary["class_name"]] = detail for entry in _load_json(data_dir, "fps-items.json"): summary, detail = _normalize_item(entry, CATEGORY_FPS_ITEMS) items.append(summary) details[summary["class_name"]] = detail # Deterministic order so pagination is stable. items.sort(key=lambda i: (i["category"], i["name"].lower(), i["class_name"])) manufacturers = sorted( ( { "code": m.get("Code") or "", "name": _resolve_name(m.get("Name"), class_name=m.get("Code") or ""), "description": m.get("Description") or "", } for m in _load_json(data_dir, "manufacturers.json") ), key=lambda m: m["name"].lower(), ) return Store(items, details, manufacturers)