import argparse import os import sys import json from pathlib import Path # Add the utils directory to the Python path # This allows importing modules from the 'utils' subfolder utils_path = Path(__file__).parent / 'utils' sys.path.append(str(utils_path)) from yaml_utils import load_unity_yaml, convert_to_plain_python_types from file_utils import find_files_by_extension, create_guid_to_path_map def parse_physics_settings(input_dir, project_mode): """ Parses the appropriate physics settings file based on the project mode. """ physics_data = {} if project_mode == "3D": asset_path = input_dir / "ProjectSettings" / "DynamicsManager.asset" if asset_path.is_file(): docs = load_unity_yaml(str(asset_path)) if docs: settings = convert_to_plain_python_types(docs[0]).get('PhysicsManager', {}) physics_data['gravity'] = settings.get('m_Gravity') physics_data['sleepThreshold'] = settings.get('m_SleepThreshold') physics_data['solverType'] = settings.get('m_SolverType') physics_data['layerCollisionMatrix'] = settings.get('m_LayerCollisionMatrix') physics_data['autoSimulation'] = settings.get('m_AutoSimulation') physics_data['autoSyncTransforms'] = settings.get('m_AutoSyncTransforms') else: # 2D asset_path = input_dir / "ProjectSettings" / "Physics2DSettings.asset" if asset_path.is_file(): docs = load_unity_yaml(str(asset_path)) if docs: settings = convert_to_plain_python_types(docs[0]).get('Physics2DSettings', {}) physics_data['gravity'] = settings.get('m_Gravity') physics_data['velocityIterations'] = settings.get('m_VelocityIterations') physics_data['positionIterations'] = settings.get('m_PositionIterations') physics_data['layerCollisionMatrix'] = settings.get('m_LayerCollisionMatrix') physics_data['autoSimulation'] = settings.get('m_AutoSimulation') physics_data['autoSyncTransforms'] = settings.get('m_AutoSyncTransforms') return physics_data def parse_project_settings(input_dir, output_dir): """ Parses various project settings files to create a comprehensive manifest. """ print("\n--- Starting Task 2: Comprehensive Project Settings Parser ---") manifest_data = {} guid_map = create_guid_to_path_map(str(input_dir)) # --- ProjectSettings.asset --- project_settings_path = input_dir / "ProjectSettings" / "ProjectSettings.asset" if project_settings_path.is_file(): docs = load_unity_yaml(str(project_settings_path)) if docs: player_settings = convert_to_plain_python_types(docs[0]).get('PlayerSettings', {}) manifest_data['productName'] = player_settings.get('productName') manifest_data['companyName'] = player_settings.get('companyName') manifest_data['bundleVersion'] = player_settings.get('bundleVersion') manifest_data['activeColorSpace'] = player_settings.get('m_ActiveColorSpace') # --- Mappers for human-readable values --- scripting_backend_map = {0: "Mono", 1: "IL2CPP"} api_compatibility_map = {3: ".NET Framework", 6: ".NET Standard 2.1"} # --- Extract and map platform-specific settings --- scripting_backends = player_settings.get('scriptingBackend', {}) manifest_data['scriptingBackend'] = { platform: scripting_backend_map.get(val, f"Unknown ({val})") for platform, val in scripting_backends.items() } api_levels = player_settings.get('apiCompatibilityLevelPerPlatform', {}) manifest_data['apiCompatibilityLevel'] = { platform: api_compatibility_map.get(val, f"Unknown ({val})") for platform, val in api_levels.items() } # Fallback for older Unity versions that use a single key if not api_levels and 'apiCompatibilityLevel' in player_settings: val = player_settings.get('apiCompatibilityLevel') manifest_data['apiCompatibilityLevel']['Standalone'] = api_compatibility_map.get(val, f"Unknown ({val})") manifest_data['activeInputHandler'] = player_settings.get('activeInputHandler') manifest_data['allowUnsafeCode'] = player_settings.get('allowUnsafeCode') manifest_data['managedStrippingLevel'] = player_settings.get('managedStrippingLevel') manifest_data['scriptingDefineSymbols'] = player_settings.get('scriptingDefineSymbols') # --- Deduce configured platforms from various settings --- configured_platforms = set() if 'applicationIdentifier' in player_settings: configured_platforms.update(player_settings['applicationIdentifier'].keys()) if 'scriptingBackend' in player_settings: configured_platforms.update(player_settings['scriptingBackend'].keys()) manifest_data['configuredPlatforms'] = sorted(list(configured_platforms)) # --- Filter managedStrippingLevel based on configured platforms --- managed_stripping_level = player_settings.get('managedStrippingLevel', {}) manifest_data['managedStrippingLevel'] = { platform: level for platform, level in managed_stripping_level.items() if platform in manifest_data['configuredPlatforms'] } # --- Populate all configured platforms for scripting settings --- default_api_level = player_settings.get('apiCompatibilityLevel') final_scripting_backends = {} final_api_levels = {} for platform in manifest_data['configuredPlatforms']: # Scripting Backend (Default to Mono if not specified) backend_val = scripting_backends.get(platform, 0) final_scripting_backends[platform] = scripting_backend_map.get(backend_val, f"Unknown ({backend_val})") # API Compatibility Level (Default to project's global setting if not specified) level_val = api_levels.get(platform, default_api_level) final_api_levels[platform] = api_compatibility_map.get(level_val, f"Unknown ({level_val})") manifest_data['scriptingBackend'] = final_scripting_backends manifest_data['apiCompatibilityLevel'] = final_api_levels # --- EditorSettings.asset for 2D/3D Mode --- editor_settings_path = input_dir / "ProjectSettings" / "EditorSettings.asset" if editor_settings_path.is_file(): docs = load_unity_yaml(str(editor_settings_path)) if docs: editor_settings = convert_to_plain_python_types(docs[0]).get('EditorSettings', {}) manifest_data['projectMode'] = "2D" if editor_settings.get('m_DefaultBehaviorMode') == 1 else "3D" # --- GraphicsSettings.asset for Render Pipeline --- graphics_settings_path = input_dir / "ProjectSettings" / "GraphicsSettings.asset" manifest_data['renderPipeline'] = 'Built-in' if graphics_settings_path.is_file(): docs = load_unity_yaml(str(graphics_settings_path)) if docs: graphics_settings = convert_to_plain_python_types(docs[0]).get('GraphicsSettings', {}) pipeline_ref = graphics_settings.get('m_CustomRenderPipeline') or graphics_settings.get('m_SRPDefaultSettings', {}).get('UnityEngine.Rendering.Universal.UniversalRenderPipeline') if pipeline_ref and pipeline_ref.get('guid'): guid = pipeline_ref['guid'] if guid in guid_map: asset_path = Path(guid_map[guid]).name.upper() if "URP" in asset_path: manifest_data['renderPipeline'] = 'URP' elif "HDRP" in asset_path: manifest_data['renderPipeline'] = 'HDRP' else: manifest_data['renderPipeline'] = 'Scriptable' # --- TagManager.asset --- tag_manager_path = input_dir / "ProjectSettings" / "TagManager.asset" if tag_manager_path.is_file(): docs = load_unity_yaml(str(tag_manager_path)) if docs: tag_manager = convert_to_plain_python_types(docs[0]).get('TagManager', {}) manifest_data['tags'] = tag_manager.get('tags') layers_list = tag_manager.get('layers', []) # Only include layers that have a name, preserving their index manifest_data['layers'] = {i: name for i, name in enumerate(layers_list) if name} # --- EditorBuildSettings.asset --- build_settings_path = input_dir / "ProjectSettings" / "EditorBuildSettings.asset" if build_settings_path.is_file(): docs = load_unity_yaml(str(build_settings_path)) if docs: build_settings = convert_to_plain_python_types(docs[0]).get('EditorBuildSettings', {}) manifest_data['buildScenes'] = [ {'path': scene.get('path'), 'enabled': scene.get('enabled') == 1} for scene in build_settings.get('m_Scenes', []) ] # --- TimeManager.asset --- time_manager_path = input_dir / "ProjectSettings" / "TimeManager.asset" if time_manager_path.is_file(): docs = load_unity_yaml(str(time_manager_path)) if docs: time_manager = convert_to_plain_python_types(docs[0]).get('TimeManager', {}) # Cherry-pick only the useful time settings manifest_data['timeSettings'] = { 'Fixed Timestep': time_manager.get('Fixed Timestep'), 'Maximum Allowed Timestep': time_manager.get('Maximum Allowed Timestep'), 'm_TimeScale': time_manager.get('m_TimeScale'), 'Maximum Particle Timestep': time_manager.get('Maximum Particle Timestep') } # --- Physics Settings --- manifest_data['physicsSettings'] = parse_physics_settings(input_dir, manifest_data.get('projectMode', '3D')) # --- Write manifest.json --- manifest_output_path = output_dir / "manifest.json" try: with open(manifest_output_path, 'w', encoding='utf-8') as f: json.dump(manifest_data, f, separators=(',', ':')) print(f"Successfully created manifest.json at {manifest_output_path}") except IOError as e: print(f"Error writing to {manifest_output_path}. {e}", file=sys.stderr) def parse_package_manifests(input_dir, output_dir): """ Parses the primary package manifest and creates a clean packages.json file. """ print("\n--- Starting Task 3: Package Manifest Extractor ---") manifest_path = input_dir / "Packages" / "manifest.json" if manifest_path.is_file(): try: with open(manifest_path, 'r', encoding='utf-8') as f: packages_data = json.load(f) packages_output_path = output_dir / "packages.json" with open(packages_output_path, 'w', encoding='utf-8') as f: json.dump(packages_data, f, separators=(',', ':')) # Compact output print(f"Successfully created packages.json at {packages_output_path}") except (IOError, json.JSONDecodeError) as e: print(f"Error processing {manifest_path}: {e}", file=sys.stderr) else: print(f"Warning: {manifest_path} not found.") def generate_guid_mappers(input_dir, output_dir): """ Finds all .meta files and generates JSON files mapping GUIDs to asset paths. """ print("\n--- Starting Task 4: GUID Mapper Generator ---") assets_dir = input_dir / "Assets" if not assets_dir.is_dir(): print(f"Error: 'Assets' directory not found in '{input_dir}'", file=sys.stderr) return meta_files = find_files_by_extension(str(assets_dir), '.meta') print(f"Found {len(meta_files)} .meta files to process.") asset_type_map = { '.prefab': 'prefabs', '.unity': 'scenes', '.mat': 'materials', '.cs': 'scripts', '.png': 'textures', '.jpg': 'textures', '.jpeg': 'textures', '.asset': 'scriptable_objects', } guid_maps = {value: {} for value in asset_type_map.values()} guid_maps['others'] = {} for meta_file_path_str in meta_files: meta_file_path = Path(meta_file_path_str) asset_file_path = Path(meta_file_path_str.rsplit('.meta', 1)[0]) # THE FIX: Ensure that the corresponding path is a file, not a directory if not asset_file_path.is_file(): continue guid = None try: with open(meta_file_path, 'r', encoding='utf-8') as f: for line in f: if line.strip().startswith('guid:'): guid = line.strip().split(':')[1].strip() break except Exception as e: print(f"Warning: Could not read or parse guid from {meta_file_path}. {e}", file=sys.stderr) continue if guid: asset_ext = asset_file_path.suffix.lower() asset_type = asset_type_map.get(asset_ext, 'others') relative_path = asset_file_path.relative_to(input_dir).as_posix() guid_maps[asset_type][guid] = relative_path mappers_dir = output_dir / "GuidMappers" try: mappers_dir.mkdir(parents=True, exist_ok=True) for asset_type, guid_map in guid_maps.items(): if guid_map: output_path = mappers_dir / f"{asset_type}.json" with open(output_path, 'w', encoding='utf-8') as f: json.dump(guid_map, f, separators=(',', ':')) # Compact output print(f"Successfully created GUID mappers in {mappers_dir}") except OSError as e: print(f"Error: Could not create GUID mapper directory or files. {e}", file=sys.stderr) def main(): """ Main function to run the high-level data extraction process. """ parser = argparse.ArgumentParser( description="Extracts high-level summary data from a Unity project." ) parser.add_argument("--input", type=str, required=True, help="The root directory of the target Unity project.") parser.add_argument("--output", type=str, required=True, help="The directory where the generated output folder will be saved.") args = parser.parse_args() input_dir = Path(args.input) output_dir = Path(args.output) if not input_dir.is_dir(): print(f"Error: Input path '{input_dir}' is not a valid directory.", file=sys.stderr) sys.exit(1) high_level_output_dir = output_dir / "HighLevel" try: high_level_output_dir.mkdir(parents=True, exist_ok=True) print(f"Output will be saved to: {high_level_output_dir}") except OSError as e: print(f"Error: Could not create output directory '{high_level_output_dir}'. {e}", file=sys.stderr) sys.exit(1) parse_project_settings(input_dir, high_level_output_dir) parse_package_manifests(input_dir, high_level_output_dir) generate_guid_mappers(input_dir, high_level_output_dir) print("\nHigh-level extraction complete.") if __name__ == "__main__": main()