"""Project-specific asset audit; not the official Khronos glTF validator.""" from pathlib import Path import sys,struct,json,hashlib,re import numpy as np from PIL import Image ROOT=Path(sys.argv[1]).resolve();BASE=Path(sys.argv[2]).resolve();OUT=Path(sys.argv[3]) DT={5120:np.dtype('i1'),5121:np.dtype('u1'),5122:np.dtype('=0 and b.get('byteOffset',0)+b['byteLength']<=len(blob) def read(i): a=g['accessors'][i];bv=g['bufferViews'][a['bufferView']];dtype=DT[a['componentType']];nc=NC[a['type']];stride=bv.get('byteStride',dtype.itemsize*nc);rel=a.get('byteOffset',0);assert rel%dtype.itemsize==0 assert rel+(a['count']-1)*stride+nc*dtype.itemsize<=bv['byteLength'] arr=np.ndarray((a['count'],nc),dtype=dtype,buffer=blob,offset=bv.get('byteOffset',0)+rel,strides=(stride,dtype.itemsize)).copy() if dtype.kind=='f':assert np.isfinite(arr).all() return arr for i in range(len(g['accessors'])):read(i) parent={};nodes=g['nodes'] for i,nd in enumerate(nodes): for ch in nd.get('children',[]):assert ch not in parent;assert 0<=ch=0);assert np.max(np.abs(weight.sum(1)-1))<1e-5 nlen=np.linalg.norm(normal,axis=1);nbad=int(np.sum(abs(nlen-1)>1e-3)) if nbad:issues.append({'file':f.name,'issue':'nonunit_normals','count':nbad,'min':float(nlen.min()),'max':float(nlen.max())}) uv=read(at['TEXCOORD_0']);assert uv.min()>=-1e-5 and uv.max()<=1.00001 tris=v[idx.reshape(-1,3)];areas=np.linalg.norm(np.cross(tris[:,1]-tris[:,0],tris[:,2]-tris[:,0]),axis=1);degenerate=int(np.sum(areas<1e-10)) clips=[] for a in g['animations']: assert a['name'] not in clips;clips.append(a['name']) for channel in a['channels']: assert channel['target']['node'] in joints;s=a['samplers'][channel['sampler']];t=read(s['input']).ravel();val=read(s['output']);assert (np.diff(t)>0).all() and t.min()>=0;assert len(val)==len(t) if channel['target']['path']=='rotation':assert np.max(np.abs(np.linalg.norm(val,axis=1)-1))<1e-4 assert len(clips)==17 textures=[] for im in g.get('images',[]): uri=im['uri'];p=(f.parent/uri).resolve();assert p.is_relative_to(ROOT) and p.is_file();assert 'http' not in uri with Image.open(p) as img:assert img.size==(4096,2048);img.verify() textures.append({'path':str(p.relative_to(ROOT)),'sha256':sha(p)});refs.append(str(p.relative_to(ROOT))) assert 'EXT_texture_webp' in g['extensionsRequired'];assert g['materials'][0]['alphaMode']=='MASK' for tex in g['textures']:assert 'EXT_texture_webp' in tex['extensions'] models.append({'file':str(f.relative_to(ROOT)),'bytes':len(raw),'sha256':sha(f),'vertices':len(v),'triangles':len(idx)//3,'bones':len(joints),'clips':clips,'rest_bind_error':resterr,'nonunit_normals':nbad,'zero_area_triangles':degenerate,'textures':textures}) meta=json.loads((ROOT/'CHARACTER_RESOURCES.json').read_text()) for k,a in meta['resources'].items(): assert (ROOT/'assets'/a['file']).is_file() if a.get('colors'): model=next(m for m in models if m['file']=='assets/'+a['file']);assert (ROOT/'assets'/a['colors']).stat().st_size==model['vertices']*3*4 core={} for name in ['game.js','game.css']: core[name]={'baseline':sha(BASE/name),'working':sha(ROOT/name)};assert core[name]['baseline']==core[name]['working'] key=re.search(r"const SAVE_KEY = ['\"]([^'\"]+)",(ROOT/'game.js').read_text()) report={'scope':'Custom structural and numerical asset audit. No artistic, full collision, legal or security certification.','models':models,'logical_keys':len(meta['resources']),'physical_geometry_files':len(models),'core_files':core,'save_key':key[1] if key else None,'issues':issues} OUT.write_text(json.dumps(report,ensure_ascii=False,indent=2));print(json.dumps({'models':len(models),'logical':len(meta['resources']),'issues':issues,'geometry_sha':[m['sha256'] for m in models]},indent=2)) SHADOW HOST R10 - просмотр моделей

SHADOW HOST R10 / TEST

В игру
Отдельный просмотрщик: без автобоя и записи сохранений. Использует тот же рендерер и GLB, что игра.