"""Replay an Afterimage camera recipe in Blender 3.4 using the original Settlers asset.

Blender --background --factory-startup --disable-autoexec --python replay.py --
  --source /path/to/01_desert.lighting.blend --recipe /path/to/recipe.json
  --passage crossing --rule corrected --output /path/to/render-folder

Requires the original source libraries and textures. No scene is overwritten.
The recipe source hash must match. This renders; it does not re-run selection.
"""
import argparse,hashlib,json,sys
from pathlib import Path
import bpy
import numpy as np
from mathutils import Matrix,Vector
p=argparse.ArgumentParser();p.add_argument('--source',required=True);p.add_argument('--recipe',required=True);p.add_argument('--passage',choices=['approach','crossing'],required=True);p.add_argument('--rule',choices=['initial','corrected'],required=True);p.add_argument('--output',required=True);p.add_argument('--single-frame',type=int);p.add_argument('--geometry-reference')
a=p.parse_args(sys.argv[sys.argv.index('--')+1:]);source=Path(a.source);recipe=json.loads(Path(a.recipe).read_text());digest=lambda p:hashlib.sha256(Path(p).read_bytes()).hexdigest()
assert digest(source)==recipe['source_sha256'],'Use the unchanged original asset, not an already-revised scene.'
case=recipe['cases'][a.passage];candidate=case[a.rule];dest=Path(a.output);dest.mkdir(parents=True,exist_ok=True)
bpy.ops.wm.open_mainfile(filepath=str(source),load_ui=False,use_scripts=False);s=bpy.context.scene;s.render.use_simplify=True;s.render.simplify_subdivision=1;s.render.simplify_subdivision_render=1;s.render.resolution_x=640;s.render.resolution_y=360;s.render.resolution_percentage=100;s.render.engine='BLENDER_EEVEE';s.eevee.taa_render_samples=16;s.render.use_sequencer=False;s.render.image_settings.file_format='PNG';s.render.image_settings.color_mode='RGB'
rig=s.objects['RIG-lunte_proxy'];controls={k:rig.pose.bones['IK-MSTR-Foot.'+k] for k in ['L','R']};base={}
for row in candidate['frames']:
 s.frame_set(row['frame']);bpy.context.view_layer.update();base[row['frame']]=(rig.location.copy(),{k:b.matrix_basis.copy() for k,b in controls.items()})
cam=bpy.data.objects.new('Afterimage interpretation replay',s.camera.data.copy());cam.data.animation_data_clear();s.collection.objects.link(cam);s.camera=cam
for tree in [s.node_tree]+list(bpy.data.node_groups):
 if tree:
  for n in tree.nodes:
   if n.bl_idname=='CompositorNodeOutputFile':n.mute=True
receipts=[]
reference=np.load(a.geometry_reference) if a.geometry_reference else None
for row in candidate['frames']:
 f=row['frame']
 if a.single_frame is not None and f!=a.single_frame:continue
 s.frame_set(f);rig.location=base[f][0]+Vector(case['state']['offset'])
 for k,b in controls.items():b.matrix_basis=base[f][1][k].copy()
 cam.matrix_world=Matrix(row['camera_matrix']);cam.data.lens=candidate['lens'];cam.data.sensor_fit='HORIZONTAL';cam.data.sensor_width=36;cam.data.shift_x=candidate['shift_x'];cam.data.shift_y=0
 s.node_tree.nodes['Math'].inputs[0].default_value=recipe['render_policy']['foreground_blur_threshold'];s.node_tree.nodes['Math.001'].inputs[1].default_value=recipe['render_policy']['foreground_blur_gain'];bpy.context.view_layer.update()
 geometry_error=None
 if reference is not None:
  geometry_error=0.0
  for name in ['GEO-head','GEO-hair']:
   e=s.objects[name].evaluated_get(bpy.context.evaluated_depsgraph_get());mesh=e.to_mesh();v=np.array([e.matrix_world@p.co for p in mesh.vertices]);e.to_mesh_clear();expected=reference[f'{f}-{name}']+case['state']['offset'];assert v.shape==expected.shape;geometry_error=max(geometry_error,float(np.abs(v-expected).max()))
  assert geometry_error<.001, ('Source geometry mismatch',f,geometry_error)
 out=dest/f'{f:04d}.png';s.render.filepath=str(out);bpy.ops.render.render(write_still=True);receipts.append(dict(frame=f,sha256=digest(out),head_hair_geometry_max_error_m=geometry_error))
assert receipts, 'Requested frame is outside the passage'
assert digest(source)==recipe['source_sha256']
(dest/'replay-receipt.json').write_text(json.dumps(dict(passage=a.passage,rule=a.rule,recipe_sha256=digest(a.recipe),source_sha256=digest(source),frames=receipts),indent=2))
