extract_high_level.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import argparse
  2. import os
  3. import sys
  4. import json
  5. from pathlib import Path
  6. # Add the parent directory to the Python path to find utils
  7. utils_path = Path(__file__).parent.parent / 'utils'
  8. sys.path.append(str(utils_path))
  9. from yaml_utils import load_unity_yaml, convert_to_plain_python_types
  10. from file_utils import find_files_by_extension, create_guid_to_path_map
  11. from json_utils import write_json
  12. from config_utils import load_config
  13. def parse_physics_settings(input_dir, project_mode):
  14. """
  15. Parses the appropriate physics settings file based on the project mode.
  16. """
  17. print(f" -> Analyzing {project_mode} physics settings...")
  18. physics_data = {}
  19. if project_mode == "3D":
  20. asset_path = input_dir / "ProjectSettings" / "DynamicsManager.asset"
  21. if asset_path.is_file():
  22. docs = load_unity_yaml(str(asset_path))
  23. if docs:
  24. settings = convert_to_plain_python_types(docs[0]).get('PhysicsManager', {})
  25. physics_data['gravity'] = settings.get('m_Gravity')
  26. physics_data['sleepThreshold'] = settings.get('m_SleepThreshold')
  27. physics_data['solverType'] = settings.get('m_SolverType')
  28. physics_data['layerCollisionMatrix'] = settings.get('m_LayerCollisionMatrix')
  29. physics_data['autoSimulation'] = settings.get('m_AutoSimulation')
  30. physics_data['autoSyncTransforms'] = settings.get('m_AutoSyncTransforms')
  31. else:
  32. print(" ...DynamicsManager.asset not found.")
  33. else: # 2D
  34. asset_path = input_dir / "ProjectSettings" / "Physics2DSettings.asset"
  35. if asset_path.is_file():
  36. docs = load_unity_yaml(str(asset_path))
  37. if docs:
  38. settings = convert_to_plain_python_types(docs[0]).get('Physics2DSettings', {})
  39. physics_data['gravity'] = settings.get('m_Gravity')
  40. physics_data['velocityIterations'] = settings.get('m_VelocityIterations')
  41. physics_data['positionIterations'] = settings.get('m_PositionIterations')
  42. physics_data['layerCollisionMatrix'] = settings.get('m_LayerCollisionMatrix')
  43. physics_data['autoSimulation'] = settings.get('m_AutoSimulation')
  44. physics_data['autoSyncTransforms'] = settings.get('m_AutoSyncTransforms')
  45. else:
  46. print(" ...Physics2DSettings.asset not found.")
  47. return physics_data
  48. def parse_project_settings(input_dir, output_dir, indent=None, shrink=False, ignored_folders=None):
  49. """
  50. Parses various project settings files to create a comprehensive manifest.
  51. """
  52. manifest_data = {}
  53. print("--> Generating GUID to Path map...")
  54. guid_map = create_guid_to_path_map(str(input_dir), ignored_folders=ignored_folders)
  55. print(" ...GUID map generated.")
  56. # --- ProjectSettings.asset ---
  57. print("--> Parsing ProjectSettings.asset...")
  58. project_settings_path = input_dir / "ProjectSettings" / "ProjectSettings.asset"
  59. if project_settings_path.is_file():
  60. docs = load_unity_yaml(str(project_settings_path))
  61. if docs:
  62. player_settings = convert_to_plain_python_types(docs[0]).get('PlayerSettings', {})
  63. manifest_data['productName'] = player_settings.get('productName')
  64. manifest_data['companyName'] = player_settings.get('companyName')
  65. manifest_data['bundleVersion'] = player_settings.get('bundleVersion')
  66. manifest_data['activeColorSpace'] = player_settings.get('m_ActiveColorSpace')
  67. # --- Mappers for human-readable values ---
  68. scripting_backend_map = {0: "Mono", 1: "IL2CPP"}
  69. api_compatibility_map = {3: ".NET Framework", 6: ".NET Standard 2.1"}
  70. # --- Extract and map platform-specific settings ---
  71. scripting_backends = player_settings.get('scriptingBackend', {})
  72. manifest_data['scriptingBackend'] = {
  73. platform: scripting_backend_map.get(val, f"Unknown ({val})")
  74. for platform, val in scripting_backends.items()
  75. }
  76. api_levels = player_settings.get('apiCompatibilityLevelPerPlatform', {})
  77. manifest_data['apiCompatibilityLevel'] = {
  78. platform: api_compatibility_map.get(val, f"Unknown ({val})")
  79. for platform, val in api_levels.items()
  80. }
  81. # Fallback for older Unity versions that use a single key
  82. if not api_levels and 'apiCompatibilityLevel' in player_settings:
  83. val = player_settings.get('apiCompatibilityLevel')
  84. manifest_data['apiCompatibilityLevel']['Standalone'] = api_compatibility_map.get(val, f"Unknown ({val})")
  85. manifest_data['activeInputHandler'] = player_settings.get('activeInputHandler')
  86. manifest_data['allowUnsafeCode'] = player_settings.get('allowUnsafeCode')
  87. manifest_data['managedStrippingLevel'] = player_settings.get('managedStrippingLevel')
  88. manifest_data['scriptingDefineSymbols'] = player_settings.get('scriptingDefineSymbols')
  89. # --- Deduce configured platforms from various settings ---
  90. configured_platforms = set()
  91. if 'applicationIdentifier' in player_settings:
  92. configured_platforms.update(player_settings['applicationIdentifier'].keys())
  93. if 'scriptingBackend' in player_settings:
  94. configured_platforms.update(player_settings['scriptingBackend'].keys())
  95. manifest_data['configuredPlatforms'] = sorted(list(configured_platforms))
  96. # --- Filter managedStrippingLevel based on configured platforms ---
  97. managed_stripping_level = player_settings.get('managedStrippingLevel', {})
  98. manifest_data['managedStrippingLevel'] = {
  99. platform: level
  100. for platform, level in managed_stripping_level.items()
  101. if platform in manifest_data['configuredPlatforms']
  102. }
  103. # --- Populate all configured platforms for scripting settings ---
  104. default_api_level = player_settings.get('apiCompatibilityLevel')
  105. final_scripting_backends = {}
  106. final_api_levels = {}
  107. for platform in manifest_data['configuredPlatforms']:
  108. # Scripting Backend (Default to Mono if not specified)
  109. backend_val = scripting_backends.get(platform, 0)
  110. final_scripting_backends[platform] = scripting_backend_map.get(backend_val, f"Unknown ({backend_val})")
  111. # API Compatibility Level (Default to project's global setting if not specified)
  112. level_val = api_levels.get(platform, default_api_level)
  113. final_api_levels[platform] = api_compatibility_map.get(level_val, f"Unknown ({level_val})")
  114. manifest_data['scriptingBackend'] = final_scripting_backends
  115. manifest_data['apiCompatibilityLevel'] = final_api_levels
  116. else:
  117. print(" ...ProjectSettings.asset not found.")
  118. # --- EditorSettings.asset for 2D/3D Mode ---
  119. print("--> Parsing EditorSettings.asset...")
  120. editor_settings_path = input_dir / "ProjectSettings" / "EditorSettings.asset"
  121. if editor_settings_path.is_file():
  122. docs = load_unity_yaml(str(editor_settings_path))
  123. if docs:
  124. editor_settings = convert_to_plain_python_types(docs[0]).get('EditorSettings', {})
  125. manifest_data['projectMode'] = "2D" if editor_settings.get('m_DefaultBehaviorMode') == 1 else "3D"
  126. else:
  127. print(" ...EditorSettings.asset not found.")
  128. # --- GraphicsSettings.asset for Render Pipeline ---
  129. print("--> Parsing GraphicsSettings.asset...")
  130. graphics_settings_path = input_dir / "ProjectSettings" / "GraphicsSettings.asset"
  131. manifest_data['renderPipeline'] = 'Built-in'
  132. if graphics_settings_path.is_file():
  133. docs = load_unity_yaml(str(graphics_settings_path))
  134. if docs:
  135. graphics_settings = convert_to_plain_python_types(docs[0]).get('GraphicsSettings', {})
  136. pipeline_ref = graphics_settings.get('m_CustomRenderPipeline') or graphics_settings.get('m_SRPDefaultSettings', {}).get('UnityEngine.Rendering.Universal.UniversalRenderPipeline')
  137. if pipeline_ref and pipeline_ref.get('guid'):
  138. guid = pipeline_ref['guid']
  139. # Use .get() for safer access to the guid_map
  140. asset_path_str = guid_map.get(guid)
  141. if asset_path_str:
  142. asset_path = Path(asset_path_str).name.upper()
  143. if "URP" in asset_path: manifest_data['renderPipeline'] = 'URP'
  144. elif "HDRP" in asset_path: manifest_data['renderPipeline'] = 'HDRP'
  145. else: manifest_data['renderPipeline'] = 'Scriptable'
  146. else:
  147. print(" ...GraphicsSettings.asset not found.")
  148. # --- TagManager.asset ---
  149. print("--> Parsing TagManager.asset...")
  150. tag_manager_path = input_dir / "ProjectSettings" / "TagManager.asset"
  151. if tag_manager_path.is_file():
  152. docs = load_unity_yaml(str(tag_manager_path))
  153. if docs:
  154. tag_manager = convert_to_plain_python_types(docs[0]).get('TagManager', {})
  155. manifest_data['tags'] = tag_manager.get('tags')
  156. layers_list = tag_manager.get('layers', [])
  157. # Only include layers that have a name, preserving their index
  158. manifest_data['layers'] = {i: name for i, name in enumerate(layers_list) if name}
  159. else:
  160. print(" ...TagManager.asset not found.")
  161. # --- EditorBuildSettings.asset ---
  162. print("--> Parsing EditorBuildSettings.asset...")
  163. build_settings_path = input_dir / "ProjectSettings" / "EditorBuildSettings.asset"
  164. if build_settings_path.is_file():
  165. docs = load_unity_yaml(str(build_settings_path))
  166. if docs:
  167. build_settings = convert_to_plain_python_types(docs[0]).get('EditorBuildSettings', {})
  168. manifest_data['buildScenes'] = [
  169. {'path': scene.get('path'), 'enabled': scene.get('enabled') == 1}
  170. for scene in build_settings.get('m_Scenes', [])
  171. ]
  172. else:
  173. print(" ...EditorBuildSettings.asset not found.")
  174. # --- TimeManager.asset ---
  175. print("--> Parsing TimeManager.asset...")
  176. time_manager_path = input_dir / "ProjectSettings" / "TimeManager.asset"
  177. if time_manager_path.is_file():
  178. docs = load_unity_yaml(str(time_manager_path))
  179. if docs:
  180. time_manager = convert_to_plain_python_types(docs[0]).get('TimeManager', {})
  181. # Cherry-pick only the useful time settings
  182. manifest_data['timeSettings'] = {
  183. 'Fixed Timestep': time_manager.get('Fixed Timestep'),
  184. 'Maximum Allowed Timestep': time_manager.get('Maximum Allowed Timestep'),
  185. 'm_TimeScale': time_manager.get('m_TimeScale'),
  186. 'Maximum Particle Timestep': time_manager.get('Maximum Particle Timestep')
  187. }
  188. else:
  189. print(" ...TimeManager.asset not found.")
  190. # --- Physics Settings ---
  191. print("--> Parsing physics settings...")
  192. manifest_data['physicsSettings'] = parse_physics_settings(input_dir, manifest_data.get('projectMode', '3D'))
  193. # --- Write manifest.json ---
  194. manifest_output_path = output_dir / "manifest.json"
  195. try:
  196. write_json(manifest_data, manifest_output_path, indent=indent, shrink=shrink)
  197. print(f"--> Successfully created manifest.json at {manifest_output_path}")
  198. except Exception as e:
  199. print(f"Error writing to {manifest_output_path}. {e}", file=sys.stderr)
  200. def parse_package_manifests(input_dir, output_dir, indent=None, shrink=False):
  201. """
  202. Parses the primary package manifest and creates a clean packages.json file.
  203. """
  204. manifest_path = input_dir / "Packages" / "manifest.json"
  205. if manifest_path.is_file():
  206. try:
  207. print(f"--> Found package manifest at {manifest_path}")
  208. with open(manifest_path, 'r', encoding='utf-8') as f:
  209. packages_data = json.load(f)
  210. packages_output_path = output_dir / "packages.json"
  211. write_json(packages_data, packages_output_path, indent=indent, shrink=shrink)
  212. print(f"--> Successfully created packages.json at {packages_output_path}")
  213. except (IOError, json.JSONDecodeError) as e:
  214. print(f"Error processing {manifest_path}: {e}", file=sys.stderr)
  215. else:
  216. print(f"Warning: {manifest_path} not found.")
  217. def main():
  218. """
  219. Main function to run the high-level data extraction process.
  220. """
  221. parser = argparse.ArgumentParser(
  222. description="Extracts high-level summary data from a Unity project."
  223. )
  224. parser.add_argument("--input", type=str, required=True, help="The root directory of the target Unity project.")
  225. parser.add_argument("--output", type=str, required=True, help="The directory where the generated output folder will be saved.")
  226. args = parser.parse_args()
  227. # --- Load Configuration ---
  228. config = load_config()
  229. ignored_folders = config.get('ignored_folders', [])
  230. shrink_json = config.get('shrink_json', False)
  231. indent_level = config.get('indentation_level', 4)
  232. input_dir = Path(args.input)
  233. output_dir = Path(args.output)
  234. if not input_dir.is_dir():
  235. print(f"Error: Input path '{input_dir}' is not a valid directory.", file=sys.stderr)
  236. sys.exit(1)
  237. high_level_output_dir = output_dir / "HighLevel"
  238. try:
  239. high_level_output_dir.mkdir(parents=True, exist_ok=True)
  240. print(f"Output will be saved to: {high_level_output_dir}")
  241. except OSError as e:
  242. print(f"Error: Could not create output directory '{high_level_output_dir}'. {e}", file=sys.stderr)
  243. sys.exit(1)
  244. print("\n--- Running High-Level Extraction ---")
  245. print("\n[1/2] Parsing project settings...")
  246. parse_project_settings(
  247. input_dir,
  248. high_level_output_dir,
  249. indent=indent_level,
  250. shrink=shrink_json,
  251. ignored_folders=ignored_folders
  252. )
  253. print("\n[2/2] Parsing package manifests...")
  254. parse_package_manifests(
  255. input_dir,
  256. high_level_output_dir,
  257. indent=indent_level,
  258. shrink=shrink_json
  259. )
  260. print("\nHigh-level extraction complete.")
  261. if __name__ == "__main__":
  262. main()