#!/usr/bin/env python3 """Inventory every extracted MD3 occurrence, preserving archive/case variants.""" import hashlib from collections import defaultdict from pathlib import Path from md3 import MD3 from analyze_qvm import write_json def paths():return sorted(p for p in Path('assets/extracted').rglob('*') if p.is_file() and p.suffix.lower()=='.md3') def virtual(p): parts=p.parts;j=parts.index('members');return '/'.join(parts[j+2:]) def main(): rows=[];groups=defaultdict(list) for p in paths(): raw=p.read_bytes();r={'path':str(p),'virtual_path':virtual(p),'sha256':hashlib.sha256(raw).hexdigest(),'bytes':len(raw)} try: m=MD3(raw);surfaces=[];changes=[];first=[m.tag(0,j)['name'] for j in range(m.tags)] if m.frames else [] for frame in range(m.frames): m.frame(frame) for j in range(m.tags): tag=m.tag(frame,j) if tag['name']!=first[j]:changes.append([frame,j,tag['name']]) for s in m.surfaces: uv=[m.uv(s,j) for j in range(s.vertices)];surfaces.append({'name':s.name,'frames':s.frames,'vertices':s.vertices,'triangles':s.triangles,'uv_bounds':[[min(v[j] for v in uv),max(v[j] for v in uv)] for j in range(2)] if uv else None,'shaders':[m.shader(s,j) for j in range(s.shaders)]}) r.update({'frames':m.frames,'tag_names':first,'tag_name_changes':changes,'surfaces':surfaces,'status':'validated'}) except (ValueError,IndexError) as e:r.update({'status':'rejected','error':str(e)}) rows.append(r);groups[r['virtual_path'].casefold()].append({'path':str(p),'sha256':r['sha256'],'stored_virtual_path':r['virtual_path']}) duplicates={k:v for k,v in groups.items() if len(v)>1};write_json(Path('docs/research/md3-inventory.json'),{'models':rows,'totals':{'occurrences':len(rows),'validated':sum(r['status']=='validated' for r in rows),'bytes':sum(r['bytes'] for r in rows),'casefold_paths':len(groups)},'path_variants':duplicates,'limits':'Every extracted occurrence retained; casefold groups are comparisons, not VFS precedence or asset renames. Frames/tags/UVs validated finite, triangle bounds and packed regions validated. Normal interpretation/rendering/runtime resolution remain separate.'}) if __name__=='__main__':main()