Files
Asset_Inspectors/loaders/ikv_loader.py
2025-12-30 00:25:33 -06:00

229 lines
6.7 KiB
Python

class IKVToken:
__slots__ = ("type", "val")
def __init__(self, t, v=None):
self.type = t
self.val = v
def ikv_lex(src):
n = len(src)
i = 0
def is_ws(c):
return c in " \t\r\n"
while i < n:
c = src[i]
if is_ws(c):
i += 1
continue
if c == "/" and i + 1 < n and src[i + 1] == "/":
i += 2
while i < n and src[i] != "\n":
i += 1
continue
if c == "#":
i += 1
while i < n and src[i] != "\n":
i += 1
continue
if c in "{}[],":
i += 1
yield IKVToken(c)
continue
if c == '"':
i += 1
out = []
while i < n:
ch = src[i]
if ch == "\\" and i + 1 < n:
nx = src[i + 1]
if nx == "n":
out.append("\n")
i += 2
continue
if nx == "r":
out.append("\r")
i += 2
continue
if nx == "t":
out.append("\t")
i += 2
continue
if nx == "\\":
out.append("\\")
i += 2
continue
if nx == '"':
out.append('"')
i += 2
continue
out.append(nx)
i += 2
continue
if ch == '"':
i += 1
break
out.append(ch)
i += 1
yield IKVToken("STRING", "".join(out))
continue
j = i
while j < n and (not is_ws(src[j])) and src[j] not in "{}[],":
j += 1
w = src[i:j]
i = j
yield IKVToken("WORD", w)
yield IKVToken("EOF")
class IKVParser:
def __init__(self, src):
self.tokens = ikv_lex(src)
self.cur = next(self.tokens)
def _eat(self, t):
if self.cur.type != t:
raise ValueError(f"IKV parse: expected {t}, got {self.cur.type}")
self.cur = next(self.tokens)
def _maybe(self, t):
if self.cur.type == t:
self.cur = next(self.tokens)
return True
return False
def parse(self):
if self.cur.type == "WORD" and self.cur.val == "ikv1":
self._eat("WORD")
if self.cur.type in ("STRING", "WORD"):
self.cur = next(self.tokens)
if self.cur.type != "{":
raise ValueError("IKV parse: expected { after header")
return self._parse_object()
if self.cur.type == "{":
return self._parse_object()
return self._parse_object_flat()
def _parse_object(self):
self._eat("{")
obj = {}
while True:
if self._maybe("}"):
break
if self._maybe(","):
continue
if self.cur.type != "STRING":
raise ValueError("IKV parse: object key must be string")
key = self.cur.val
self._eat("STRING")
val = self._parse_value()
obj[key] = val
self._maybe(",")
return obj
def _parse_array(self):
self._eat("[")
arr = []
while True:
if self._maybe("]"):
break
if self._maybe(","):
continue
arr.append(self._parse_value())
self._maybe(",")
return arr
def _parse_value(self):
if self.cur.type == "{":
return self._parse_object()
if self.cur.type == "[":
return self._parse_array()
if self.cur.type == "STRING":
s = self.cur.val
self._eat("STRING")
return s
if self.cur.type == "WORD":
w = self.cur.val
self._eat("WORD")
if w == "true":
return True
if w == "false":
return False
if w == "null":
return None
try:
if any(ch in w for ch in ".eE"):
return float(w)
return int(w, 10)
except Exception:
return w
raise ValueError("IKV parse: unexpected token")
def _parse_object_flat(self):
obj = {}
while self.cur.type != "EOF":
if self._maybe(","):
continue
if self.cur.type != "STRING":
raise ValueError("IKV parse: expected string key")
key = self.cur.val
self._eat("STRING")
obj[key] = self._parse_value()
self._maybe(",")
return obj
def parse_ikv_text(txt):
return IKVParser(txt).parse()
def pack_value_from_index_gen(index, generation):
return ((int(generation) & 0xFFFF) << 16) | (int(index) & 0xFFFF)
def extract_handles_recursive(node):
out = []
def maybe_add_value_type_meta(d):
if not isinstance(d, dict):
return
if "value" in d and "type" in d and "meta" in d:
v = d["value"]
t = d["type"]
m = d["meta"]
if isinstance(v, int) and isinstance(t, int) and isinstance(m, int):
out.append((int(v) & 0xFFFFFFFF, int(t) & 0xFFFF, int(m) & 0xFFFF))
def maybe_add_index_gen(d):
if not isinstance(d, dict):
return
if "index" in d and "generation" in d and "type" in d and "meta" in d:
idx = d["index"]
gen = d["generation"]
t = d["type"]
m = d["meta"]
if isinstance(idx, int) and isinstance(gen, int) and isinstance(t, int) and isinstance(m, int):
v = pack_value_from_index_gen(idx, gen)
out.append((int(v) & 0xFFFFFFFF, int(t) & 0xFFFF, int(m) & 0xFFFF))
def visit(x):
if isinstance(x, dict):
if "ihandle" in x and isinstance(x["ihandle"], dict):
maybe_add_value_type_meta(x["ihandle"])
maybe_add_index_gen(x["ihandle"])
maybe_add_value_type_meta(x)
maybe_add_index_gen(x)
for vv in x.values():
visit(vv)
elif isinstance(x, list):
for vv in x:
visit(vv)
visit(node)
seen = set()
uniq = []
for h in out:
k = (h[0] | (h[1] << 32) | (h[2] << 48))
if k not in seen:
seen.add(k)
uniq.append(h)
return uniq