#!/usr/bin/env python3 """Writes tools/blender/export_manifest.json for every model of the Godot reference (U2A batch). The entries come from the Godot manifests (assets/*_manifest.json and assets-source/phase10/ environment-manifest.json): source file, the GLB facts to compare with, and the export rules of the group. Entries already in the manifest keep their hand-checked rules (the six representative assets and the scene buildings of U2A). Run from the Unity project root: python3 tools/blender/make_manifest.py [../saumwacht-godot] """ import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] GODOT = Path(sys.argv[1] if len(sys.argv) > 1 else ROOT.parent / 'saumwacht-godot').resolve() MANIFEST = ROOT / 'tools/blender/export_manifest.json' FIGURE_CLIPS = ['idle', 'walk', 'attack_a', 'attack_b', 'hit', 'ability', 'death'] def entries(relative): data = json.loads((GODOT / relative).read_text()) assets = data['assets'] if isinstance(data, dict) and 'assets' in data else data return assets if isinstance(assets, list) else [assets] def main(): current = json.loads(MANIFEST.read_text()) known = {asset['id'] for asset in current['assets']} added = [] def add(entry): if entry['id'] not in known: known.add(entry['id']) added.append(entry) # Phase 4: troops of the four factions, mercenaries, wave enemies, worker, cores, lane segment. for asset in entries('assets/phase4_manifest.json'): key = asset['id'] group = 'Enemies' if key.startswith('e') else 'Props' if key in ('worker', 'map_lane_segment', 'core_team0', 'core_team1') else 'Units' add({'id': key, 'group': group, 'kind': 'rigid', 'source': 'assets-source/phase4/blend/%s.blend' % key, 'godot_manifest': {'file': 'assets/phase4_manifest.json', 'id': key}, 'rules': {'skip_modifiers': ['HIGH RES'], 'logical_names': True, 'forward_marker': 'Forward'}}) # Figures with skeleton (the .blend keeps the parts; the GLB joined them into one body). figures = [('ringborke', 'phase11', 'w04', 'RingborkeRig', ['assets/materials/phase11/ringborke-cut-albedo-4k.png', 'assets/materials/phase11/ringborke-cut-normal-4k.png', 'assets/materials/phase11/ringborke-cut-roughness-4k.png']), ('arbeiter', 'phase12', 'worker', 'ArbeiterRig', []), ('basaltbastion', 'phase15', 'g04', 'BasaltRig', []), ('quellgeflecht', 'phase16', 'w10', 'QuellRig', []), ('glasballiste', 'phase19', 'g06', 'GlassRig', [])] for key, phase, godot_id, rig, textures in figures: record = next(a for a in entries('assets/%s_manifest.json' % phase) if a['id'] == godot_id) # Fighters need the combat clips; the worker has its own set (checked against Godot as a whole). required = FIGURE_CLIPS if all(c in record['clips'] for c in FIGURE_CLIPS) else ['idle', 'walk'] rules = {'join': {'parent': rig, 'into': record['parts'][0]}, 'rest_action': None, 'root_bone': 'Root', 'required_clips': required} if textures: rules['textures'] = textures add({'id': key, 'group': 'Characters', 'kind': 'skinned', 'source': 'assets-source/%s/%s.blend' % (phase, key), 'godot_manifest': {'file': 'assets/%s_manifest.json' % phase, 'id': godot_id}, 'rules': rules}) # The seven peoples (G1-era representatives). for record in entries('assets/luneraria/peoples_manifest.json'): add({'id': record['id'], 'group': 'Peoples', 'kind': 'skinned', 'source': record['source'], 'godot_manifest': {'file': 'assets/luneraria/peoples_manifest.json', 'id': record['id']}, 'rules': {'join': {'parent': 'PeopleRig', 'into': record['parts'][0]}, 'rest_action': None, 'root_bone': 'Root', 'required_clips': FIGURE_CLIPS}}) # G1 tower and grotto (replaced in the game by the G2 models, kept for completeness). add({'id': 'g1_tower', 'group': 'Buildings', 'kind': 'static', 'source': 'assets-source/luneraria/tower.blend', 'godot_manifest': {'file': 'assets/luneraria/tower_manifest.json', 'id': 'lane_tower'}}) add({'id': 'g1_grotto', 'group': 'Buildings', 'kind': 'static', 'source': 'assets-source/luneraria/grotto.blend', 'godot_manifest': {'file': 'assets/luneraria/grotto_manifest.json', 'id': 'boss_grotto'}}) # Environments: the Verdant Rift (merged into runtime chunks like the GLB) and the menu stage. environment = json.loads((GODOT / 'assets-source/phase10/environment-manifest.json').read_text()) add({'id': 'verdant_rift', 'group': 'Environment', 'kind': 'static', 'source': 'assets-source/phase10/verdant-rift.blend', 'expect': {'triangles': environment['triangles'], 'mesh_count': environment['runtime_chunks']}, # Joining the source objects (as the reference did) leaves zero normals at two corners of # one face in two chunks; Blender cannot store a normal there (degenerate corner space). 'rules': {'chunk': 16, 'allow_bad_normals': {'Chunk_-2_-1_ENV_cliff': 2, 'Chunk_1_0_ENV_cliff': 2}, 'textures': ['assets/materials/phase10/limestone-albedo.png']}}) add({'id': 'menu_stage', 'group': 'Environment', 'kind': 'static', 'source': 'assets-source/phase20/menu-stage.blend', 'godot_manifest': {'file': 'assets/menu_stage_manifest.json', 'id': None}}) # Compare props: generator only (no .blend); the other three compare GLBs are copies of # ringborke, arbeiter and core_team0 and map to those FBX files. for key, prefix in [('compare_rock', 'CompareRock'), ('compare_tree', 'CompareTree'), ('compare_arena_tile', 'CompareArenaTile')]: add({'id': key, 'group': 'Environment', 'kind': 'static', 'source': 'assets-source/phase14/build_compare_props.py', 'generator': {'inputs': ['assets/characters/ringborke.glb', 'assets/characters/arbeiter.glb', 'assets/models/core_team0.glb']}, 'godot_manifest': {'file': 'assets/compare/compare-manifest.json', 'id': key}, 'rules': {'objects_prefix': prefix}}) current['assets'].extend(added) current['aliases'] = {'compare_ringborke': 'ringborke', 'compare_arbeiter': 'arbeiter', 'compare_core': 'core_team0'} MANIFEST.write_text(json.dumps(current, indent=2, ensure_ascii=False) + '\n') print('manifest: %d assets (%d added), %d aliases' % (len(current['assets']), len(added), len(current['aliases']))) main()