Files
ic10emu/www/stationpedia.py
2024-04-28 12:53:47 -07:00

551 lines
17 KiB
Python

import json
import re
from collections import defaultdict
from pathlib import Path
from pprint import pprint
from typing import Any, NotRequired, TypedDict # type: ignore[Any]
try:
import markdown
except ImportError:
markdown = None
class SlotInsert(TypedDict):
SlotIndex: str
SlotName: str
SlotType: str
class LInsert(TypedDict):
LogicName: str
LogicAccessTypes: str
class PediaPageItem(TypedDict):
SlotClass: str
SortingClass: str
MaxQuantity: NotRequired[float]
FilterType: NotRequired[str]
Consumable: NotRequired[bool]
Ingredient: NotRequired[bool]
Reagents: NotRequired[dict[str, float]]
class PediaPageDevice(TypedDict):
ConnectionList: list[list[str]]
HasReagents: bool
HasAtmosphere: bool
HasLockState: bool
HasOpenState: bool
HasOnOffState: bool
HasActivateState: bool
HasModeState: bool
HasColorState: bool
DevicesLength: NotRequired[int]
class MemoryInstruction(TypedDict):
Type: str
Value: int
Description: str
class PediaPageMemory(TypedDict):
MemorySize: int
MemorySizeReadable: str
MemoryAccess: str
Instructions: dict[str, MemoryInstruction] | None
class PediaPageLogicInfo(TypedDict):
LogicSlotTypes: dict[str, dict[str, str]]
LogicTypes: dict[str, str]
class PediaPage(TypedDict):
Key: str
Title: str
Description: str
PrefabName: str
PrefabHash: int
SlotInserts: list[SlotInsert]
LogicInsert: list[LInsert]
LogicSlotInsert: list[LInsert]
ModeInsert: list[LInsert]
ConnectionInsert: list[LInsert]
LogicInfo: PediaPageLogicInfo | None
Item: NotRequired[PediaPageItem]
Device: NotRequired[PediaPageDevice]
WirelessLogic: bool | None
Memory: PediaPageMemory | None
TransmissionReceiver: bool | None
class ScriptCommand(TypedDict):
desc: str
example: str
class PediaReagent(TypedDict):
Hash: int
Unit: str
Sources: dict[str, float] | None
class Pedia(TypedDict):
pages: list[PediaPage]
reagents: dict[str, int]
scriptCommands: dict[str, ScriptCommand]
class DBSlot(TypedDict):
name: str
typ: str
class DBPageStates(TypedDict):
lock: NotRequired[bool]
open: NotRequired[bool]
mode: NotRequired[bool]
onoff: NotRequired[bool]
color: NotRequired[bool]
activate: NotRequired[bool]
class DBPageConnection(TypedDict):
typ: str
role: str
name: str
class DBPageDevice(TypedDict):
states: DBPageStates
reagents: bool
atmosphere: bool
pins: NotRequired[int]
class DBPageItem(TypedDict):
slotclass: str | None
sorting: str | None
filtertype: NotRequired[str]
maxquantity: NotRequired[int]
consumable: NotRequired[bool]
ingredient: NotRequired[bool]
reagents: NotRequired[dict[str, float]]
class DBPageMemoryInstruction(TypedDict):
typ: str
value: int
desc: str
class DBPageMemory(TypedDict):
size: int
sizeDisplay: str
access: str
instructions: dict[str, DBPageMemoryInstruction] | None
class DBPage(TypedDict):
name: str
hash: int
title: str
desc: str
slots: list[DBSlot] | None
logic: dict[str, str] | None
slotlogic: dict[str, dict[str, str]] | None
modes: dict[int, str] | None
conn: dict[int, DBPageConnection] | None
item: NotRequired[DBPageItem]
device: NotRequired[DBPageDevice]
transmitter: bool
receiver: bool
memory: DBPageMemory | None
translation_regex = re.compile(r"<N:([A-Z]{2}):(\w+)>")
translation_keys: set[str] = set()
translation_codes: set[str] = set()
def replace_translation(m: re.Match[str]) -> str:
match m.groups():
case (code, key):
translation_keys.add(key)
translation_codes.add(code)
return key
case _ as g:
print("bad translation match?", g, m.string)
return m.string
def trans(s: str) -> str:
return re.sub(translation_regex, replace_translation, s)
color_regex = re.compile(
r"<color=(#?\w+)>((:?(?!<color=(?:#?\w+)>).)+?)</color>", re.DOTALL
)
link_regex = re.compile(r"<link=(\w+)>(.+?)</link>")
def strip_color(s: str) -> str:
replacemnt = r"\2"
last = s
new = color_regex.sub(replacemnt, last)
while new != last:
last = new
new = color_regex.sub(replacemnt, last)
return new
def color_to_html(s: str) -> str:
replacemnt = r"""<div style="color: \1;">\2</div>"""
last = s
new = color_regex.sub(replacemnt, last)
while new != last:
last = new
new = color_regex.sub(replacemnt, last)
return new
def strip_link(s: str) -> str:
replacemnt = r"\2"
last = s
new = link_regex.sub(replacemnt, last)
while new != last:
last = new
new = link_regex.sub(replacemnt, last)
return new
def extract_all() -> None:
db: dict[str, DBPage] = {}
pedia: Pedia = {"pages": [], "reagents": {}, "scriptCommands": {}}
with (Path("data") / "Stationpedia.json").open("r") as f:
pedia = json.load(f)
for page in pedia["pages"]:
item: DBPage = {
"name": "",
"hash": 0,
"title": "",
"desc": "",
"slots": None,
"logic": None,
"slotlogic": None,
"modes": None,
"conn": None,
"transmitter": False,
"receiver": False,
"memory": None,
}
match page:
case {
"Key": _,
"Title": title,
"Description": desc,
"PrefabName": name,
"PrefabHash": name_hash,
"SlotInserts": slots,
"LogicInsert": logic,
"LogicSlotInsert": slotlogic,
"ModeInsert": modes,
"ConnectionInsert": conninsert,
}:
connNames = {
int(insert["LogicAccessTypes"]): insert["LogicName"]
for insert in conninsert
}
device = page.get("Device", None)
item_props = page.get("Item", None)
logicinfo = page.get("LogicInfo", None)
wireless = page.get("WirelessLogic", False)
receiver = page.get("TransmissionReceiver", False)
memory = page.get("Memory", None)
item["name"] = name
item["hash"] = name_hash
item["title"] = trans(title)
item["desc"] = trans(strip_link(strip_color(desc)))
match slots:
case []:
item["slots"] = None
case _:
item["slots"] = [{}] * len(slots) # type: ignore[reportAssignmentType]
for slot in slots:
item["slots"][int(slot["SlotIndex"])] = {
"name": trans(slot["SlotName"]),
"typ": slot["SlotType"],
}
match logic:
case []:
item["logic"] = None
case _:
item["logic"] = {}
for lat in logic:
item["logic"][strip_link(strip_color(lat["LogicName"]))] = (
lat["LogicAccessTypes"].replace(" ", "")
)
match slotlogic:
case []:
item["slotlogic"] = None
case _:
item["slotlogic"] = {}
for slt in slotlogic:
item["slotlogic"][
strip_link(strip_color(slt["LogicName"]))
] = {s: "Read" for s in slt["LogicAccessTypes"].split(", ")}
match modes:
case []:
item["modes"] = None
case _:
item["modes"] = {}
for mode in modes:
item["modes"][int(mode["LogicAccessTypes"])] = mode[
"LogicName"
]
match device:
case None:
pass
case {
"ConnectionList": connections,
"HasReagents": hasReagents,
"HasAtmosphere": hasAtmosphere,
"HasLockState": hasLockState,
"HasOpenState": hasOpenState,
"HasModeState": hasModeState,
"HasOnOffState": hasOnOffState,
"HasActivateState": hasActivateState,
"HasColorState": hasColorState,
}:
match connections:
case []:
item["conn"] = None
case _:
item["conn"] = {}
for index, [conn_typ, conn_role] in enumerate(
connections
):
item["conn"][index] = {
"typ": conn_typ,
"role": conn_role,
"name": connNames.get(index, "Connection"),
}
states: DBPageStates = {}
states["lock"] = hasLockState
states["open"] = hasOpenState
states["mode"] = hasModeState
states["activate"] = hasActivateState
states["onoff"] = hasOnOffState
states["color"] = hasColorState
deviceslength = device.get("DevicesLength", None)
dbdevice: DBPageDevice = {
"states": states,
"reagents": hasReagents,
"atmosphere": hasAtmosphere,
}
match deviceslength:
case None:
pass
case _:
dbdevice["pins"] = deviceslength
item["device"] = dbdevice
case _:
print("NON-CONFORMING: ")
pprint(device)
return
match item_props:
case None:
pass
case {"SlotClass": slotclass, "SortingClass": sortingclass}:
maxquantity = item_props.get("MaxQuantity", None)
filtertype = item_props.get("FilterType", None)
dbitem: DBPageItem = {
"sorting": sortingclass,
"slotclass": slotclass,
}
match maxquantity:
case None:
pass
case _:
dbitem["maxquantity"] = int(maxquantity)
match filtertype:
case None:
pass
case _:
dbitem["filtertype"] = filtertype
consumable = item_props.get("Consumable", None)
match consumable:
case None:
pass
case _:
dbitem["consumable"] = consumable
ingredient = item_props.get("Ingredient", None)
match ingredient:
case None:
pass
case _:
dbitem["ingredient"] = ingredient
reagents = item_props.get("Reagents", None)
match reagents:
case None:
pass
case _:
dbitem["reagents"] = reagents
item["item"] = dbitem
case _:
print("NON-CONFORMING: ")
pprint(item_props)
return
match logicinfo:
case None:
pass
case _:
for lt, access in logicinfo["LogicTypes"].items():
if item["logic"] is None:
item["logic"] = {}
item["logic"][lt] = access
for slot, slotlogicinfo in logicinfo["LogicSlotTypes"].items():
if item["slotlogic"] is None:
item["slotlogic"] = {}
if slot not in item["slotlogic"]:
item["slotlogic"][slot] = {}
for slt, access in slotlogicinfo.items():
item["slotlogic"][slot][slt] = access
if wireless:
item["transmitter"] = True
if receiver:
item["receiver"] = True
match memory:
case None:
pass
case _:
item["memory"] = {
"size": memory["MemorySize"],
"sizeDisplay": memory["MemorySizeReadable"],
"access": memory["MemoryAccess"],
"instructions": None,
}
instructions = memory.get("Instructions", None)
match instructions:
case None:
pass
case _:
def condense_lines(s: str) -> str:
return "\r\n".join(
[" ".join(line.split()) for line in s.splitlines()]
)
item["memory"]["instructions"] = {
inst: {
"typ": info["Type"],
"value": info["Value"],
"desc": condense_lines(
strip_color(strip_link(info["Description"]))
),
}
for inst, info in instructions.items()
}
case _:
print("NON-CONFORMING: ")
pprint(page)
return
db[name] = item
print("Translation codes:")
pprint(translation_codes)
print("Translations keys:")
pprint(translation_keys)
logicable = [item["name"] for item in db.values() if item["logic"] is not None]
slotlogicable = [
item["name"] for item in db.values() if item["slotlogic"] is not None
]
devices = [item["name"] for item in db.values() if "device" in item]
structures = [
item["name"] for item in db.values() if item["name"].startswith("Structure")
]
items = [item["name"] for item in db.values() if "item" in item]
def clean_nones(value: Any) -> Any: # type: ignore[Any]
if isinstance(value, list):
return [clean_nones(x) for x in value if x is not None] # type: ignore[unknown]
elif isinstance(value, dict):
return {
key: clean_nones(val)
for key, val in value.items() # type:ignore[reportUnknownVariable]
if val is not None
}
else:
return value # type: ignore[Any]
enums: dict[str, dict[str, int]] = {}
with open("data/Enums.json", "r") as f:
exported_enums: dict[str, dict[str, int]] = json.load(f)
for cat, cat_enums in exported_enums.items():
for enum, val in cat_enums.items():
key = cat
if cat == "Enums":
if "." in enum:
key, enum = enum.split(".")
else :
key = "Condition"
if key not in enums:
enums[key] = {}
enums[key][enum] = val
with open("data/database.json", "w") as f:
json.dump(
clean_nones(
{
"logic_enabled": logicable,
"slot_logic_enabled": slotlogicable,
"devices": devices,
"structures": structures,
"items": items,
"db": db,
"names_by_hash": {
page["hash"]: page["name"] for page in db.values()
},
"reagents": pedia["reagents"],
"enums": enums,
}
),
f,
indent=1,
sort_keys=True,
)
if __name__ == "__main__":
# extract_logicable()
extract_all()