255 lines
8.7 KiB
Python
255 lines
8.7 KiB
Python
"""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
|
|
import re
|
|
|
|
from app import config
|
|
|
|
PLACEHOLDER = "<= PLACEHOLDER =>"
|
|
|
|
CATEGORY_SHIPS = "ships"
|
|
CATEGORY_SHIP_ITEMS = "ship-items"
|
|
CATEGORY_FPS_ITEMS = "fps-items"
|
|
CATEGORY_COMMODITIES = "commodities"
|
|
|
|
# Commodities that can carry a mining/refining quality value (1-1000). These are
|
|
# the ores, minerals and gems that players actually mine (and their refined
|
|
# counterparts); crafted alloys, gases, organics, processed goods and junk
|
|
# by-products are deliberately excluded. Keep in sync with the Ore quality table
|
|
# at https://starcitizen.tools/Ore_quality.
|
|
QUALITY_MATERIALS = frozenset(
|
|
name.lower()
|
|
for name in (
|
|
"Agricium",
|
|
"Aluminum",
|
|
"Aphorite",
|
|
"Aslarite",
|
|
"Beradom",
|
|
"Beryl",
|
|
"Bexalite",
|
|
"Borase",
|
|
"Carinite",
|
|
"Copper",
|
|
"Corundum",
|
|
"Diamond",
|
|
"Dolivine",
|
|
"Feynmaline",
|
|
"Glacosite",
|
|
"Gold",
|
|
"Hadanite",
|
|
"Hephaestanite",
|
|
"Iron",
|
|
"Jaclium",
|
|
"Janalite",
|
|
"Laranite",
|
|
"Lindinium",
|
|
"Ouratite",
|
|
"Pressurized Ice",
|
|
"Quantainium",
|
|
"Quartz",
|
|
"Riccite",
|
|
"Sadaryx",
|
|
"Saldynium",
|
|
"Savrilium",
|
|
"Silicon",
|
|
"Stileron",
|
|
"Taranite",
|
|
"Tin",
|
|
"Titanium",
|
|
"Torite",
|
|
"Tungsten",
|
|
)
|
|
)
|
|
|
|
# Variant suffixes are presentation labels, not a different material: "Iron
|
|
# (Ore)" and "Iron" share a quality value, as do "(Raw)", "(Pure)" and "(R)".
|
|
_QUALITY_VARIANT = re.compile(r"\s*\((?:Ore|Raw|Pure|R|\d+\s*SCU)\)\s*$", re.IGNORECASE)
|
|
_RAW_PREFIX = re.compile(r"^Raw\s+", re.IGNORECASE)
|
|
|
|
|
|
def _quality_applicable(name, category):
|
|
if category != CATEGORY_COMMODITIES:
|
|
return False
|
|
base_name = _QUALITY_VARIANT.sub("", name or "").strip()
|
|
base_name = _RAW_PREFIX.sub("", base_name).strip().lower()
|
|
return base_name in QUALITY_MATERIALS
|
|
|
|
|
|
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,
|
|
"quality_applicable": False,
|
|
}, {
|
|
"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,
|
|
"quality_applicable": _quality_applicable(
|
|
_resolve_name(std.get("Name"), item.get("name"), class_name=class_name),
|
|
category,
|
|
),
|
|
}
|
|
if category == CATEGORY_COMMODITIES:
|
|
summary["size"] = None
|
|
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),
|
|
CATEGORY_COMMODITIES: sum(
|
|
1 for i in items if i["category"] == CATEGORY_COMMODITIES
|
|
),
|
|
}
|
|
|
|
|
|
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
|
|
|
|
for entry in _load_json(data_dir, "commodities.json"):
|
|
summary, detail = _normalize_item(entry, CATEGORY_COMMODITIES)
|
|
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)
|