from __future__ import annotations
import json
import os
import re
import struct
import sys
import UnityPy
class Cursor:
def __init__(self, data: bytes):
self.data = data
self.pos = 0
def take(self, n: int) -> bytes:
if self.pos + n > len(self.data):
raise ValueError(f"{n} bytes wanted at {self.pos} of {len(self.data)}")
b = self.data[self.pos : self.pos + n]
self.pos += n
return b
def i32(self) -> int:
return struct.unpack("<i", self.take(4))[0]
def u32(self) -> int:
return struct.unpack("<I", self.take(4))[0]
def i64(self) -> int:
return struct.unpack("<q", self.take(8))[0]
def f32(self) -> float:
return struct.unpack("<f", self.take(4))[0]
def flag(self) -> bool:
v = self.i32()
if v not in (0, 1):
raise ValueError(f"bool {v} at {self.pos - 4}")
return bool(v)
def pptr(self) -> list:
return [self.i32(), self.i64()]
def left(self) -> int:
return len(self.data) - self.pos
def script_bytes(obj) -> tuple[bytes, tuple[int, int]]:
raw = obj.get_raw_data()
sfid, spid = struct.unpack_from("<iq", raw, 16)
n = struct.unpack_from("<i", raw, 28)[0]
at = (32 + n + 3) & ~3
return raw[at:], (sfid, spid)
class Files:
def __init__(self, data_dir: str):
self.dir = data_dir
self.loaded: dict[str, object] = {}
self.bundled = not os.path.exists(os.path.join(data_dir, "globalgamemanagers"))
if self.bundled:
bundle = next(iter(UnityPy.load(os.path.join(data_dir, "data.unity3d")).files.values()))
for name, f in bundle.files.items():
if hasattr(f, "objects"):
self.loaded[name.lower()] = f
def scenes(self) -> list[str]:
names = self.loaded if self.bundled else os.listdir(self.dir)
return sorted((f for f in names if re.fullmatch(r"level\d+", f)), key=lambda f: int(f[5:]))
def file(self, name: str):
name = os.path.basename(name).lower()
if name not in self.loaded:
path = next(
os.path.join(self.dir, f) for f in os.listdir(self.dir) if f.lower() == name
)
self.loaded[name] = next(iter(UnityPy.load(path).files.values()))
return self.loaded[name]
def target(self, sf, fid: int):
if fid == 0:
return sf
return self.file(sf.externals[fid - 1].name)
def name_of(self, sf) -> str:
for k, v in self.loaded.items():
if v is sf:
return k
raise KeyError("file not loaded")
def script_class(self, sf, ref: tuple[int, int]) -> str:
s = self.target(sf, ref[0]).objects[ref[1]].read()
return f"{s.m_Namespace}.{s.m_ClassName}" if s.m_Namespace else s.m_ClassName
def urp_asset(b: bytes) -> dict:
c = Cursor(b)
out = {"version": c.i32()}
c.i32() c.i32() c.pptr() out["renderers"] = [c.pptr() for _ in range(c.i32())]
out["default_renderer"] = c.i32()
out["require_depth_texture"] = c.flag()
out["require_opaque_texture"] = c.flag()
c.i32() c.flag() c.flag() c.i32() out["msaa"] = c.i32()
return out
STENCIL = ("override_stencil", "reference", "compare", "pass", "fail", "z_fail")
def renderer(b: bytes) -> dict:
c = Cursor(b)
c.take(12 * 2 + 12 * 7) out = {"features": [c.pptr() for _ in range(c.i32())]}
out["feature_map"] = [c.i64() for _ in range(c.i32())]
out["native_render_pass"] = c.flag()
if c.left() == 96:
out["xr_system_data"] = c.pptr()
elif c.left() != 84:
return {"layout": "unread", "length": len(b)}
out["post_process_data"] = c.pptr()
out["asset_version"] = c.i32()
out["prepass_layer_mask"] = c.u32()
out["opaque_layer_mask"] = c.u32()
out["transparent_layer_mask"] = c.u32()
out["stencil"] = {k: (c.flag() if k == "override_stencil" else c.i32()) for k in STENCIL}
out["shadow_transparent_receive"] = c.flag()
out["rendering_mode"] = c.i32()
out["depth_priming_mode"] = c.i32()
out["copy_depth_mode"] = c.i32()
out["depth_attachment_format"] = c.i32()
out["depth_texture_format"] = c.i32()
out["accurate_gbuffer_normals"] = c.flag()
out["intermediate_texture_mode"] = c.i32()
assert c.left() == 0
return out
def feature(cls: str, b: bytes) -> dict:
c = Cursor(b)
out = {"class": cls, "active": c.flag()}
if cls == "UnityEngine.Rendering.Universal.DecalRendererFeature":
if len(b) != 24:
return {**out, "layout": "unread", "length": len(b)}
out["decal"] = {
"technique": c.i32(),
"max_draw_distance": c.f32(),
"decal_layers": c.flag(),
"dbuffer_surface_data": c.i32(),
"screen_space_normal_blend": c.i32(),
}
elif cls == "UnityEngine.Rendering.Universal.FullScreenPassRendererFeature":
if len(b) != 40:
return {**out, "layout": "unread", "length": len(b)}
out["full_screen"] = {
"injection_point": c.i32(),
"fetch_color_buffer": c.flag(),
"requirements": c.i32(),
"pass_material": c.pptr(),
"pass_index": c.i32(),
"bind_depth_stencil_attachment": c.flag(),
"version": c.i32(),
}
return out
def camera(b: bytes) -> dict:
c = Cursor(b)
out = {
"render_shadows": c.flag(),
"depth_texture_option": c.i32(),
"opaque_texture_option": c.i32(),
"camera_type": c.i32(),
"stack": [c.pptr() for _ in range(c.i32())],
"renderer_index": c.i32(),
"volume_layer_mask": c.u32(),
"volume_trigger": c.pptr(),
"volume_update_mode": c.i32(),
"render_post_processing": c.flag(),
"antialiasing": c.i32(),
"antialiasing_quality": c.i32(),
"stop_nan": c.flag(),
"dithering": c.flag(),
"clear_depth": c.flag(),
"allow_xr_rendering": c.flag(),
"allow_hdr_output": c.flag(),
"use_screen_coord_override": c.flag(),
"screen_size_override": [c.f32() for _ in range(4)],
"screen_coord_scale_bias": [c.f32() for _ in range(4)],
"requires_depth_texture": c.flag(),
"requires_color_texture": c.flag(),
"taa": {
"quality": c.i32(),
"frame_influence": c.f32(),
"jitter_scale": c.f32(),
"mip_bias": c.f32(),
"variance_clamp_scale": c.f32(),
"contrast_adaptive_sharpening": c.f32(),
},
"version": c.i32(),
}
if c.left() != 0:
return {"layout": "unread", "length": len(b)}
return out
def main(data_dir: str, out_path: str) -> None:
files = Files(data_dir)
ggm = files.file("globalgamemanagers")
graphics = next(o for o in ggm.objects.values() if o.type.name == "GraphicsSettings").read_typetree()
quality = next(o for o in ggm.objects.values() if o.type.name == "QualitySettings").read_typetree()
default = graphics["m_CustomRenderPipeline"]
levels = [
q["customRenderPipeline"] if q["customRenderPipeline"]["m_PathID"] else default
for q in quality["m_QualitySettings"]
] or [default]
assets, renderers, features = {}, {}, {}
for level in levels:
if not level["m_PathID"]:
continue
sf = files.target(ggm, level["m_FileID"])
key = f"{files.name_of(sf)}/{level['m_PathID']}"
if key in assets:
continue
b, script = script_bytes(sf.objects[level["m_PathID"]])
if files.script_class(sf, script) != "UnityEngine.Rendering.Universal.UniversalRenderPipelineAsset":
continue
a = urp_asset(b)
assets[key] = a
for fid, pid in a["renderers"]:
rf = files.target(sf, fid)
rkey = f"{files.name_of(rf)}/{pid}"
rb, rscript = script_bytes(rf.objects[pid])
r = renderer(rb)
r["class"] = files.script_class(rf, rscript)
renderers[rkey] = r
for ffid, fpid in r.get("features", []):
ff = files.target(rf, ffid)
fb, fscript = script_bytes(ff.objects[fpid])
features[f"{files.name_of(ff)}/{fpid}"] = feature(files.script_class(ff, fscript), fb)
cameras = {}
for scene in files.scenes():
sf = files.file(scene)
for o in sf.objects.values():
if o.type.name != "MonoBehaviour":
continue
b, script = script_bytes(o)
try:
cls = files.script_class(sf, script)
except (KeyError, IndexError):
continue
if cls == "UnityEngine.Rendering.Universal.UniversalAdditionalCameraData":
cameras[f"{scene}/{o.path_id}"] = camera(b)
if not files.bundled:
files.loaded.pop(scene, None)
with open(out_path, "w", encoding="utf-8") as f:
json.dump(
{"assets": assets, "renderers": renderers, "features": features, "cameras": cameras},
f,
indent=1,
sort_keys=True,
)
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])