123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- 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()
|