unitytk 0.2.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. {unitytk-0.2.1/unitytk.egg-info → unitytk-0.3.0}/PKG-INFO +1 -1
  2. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_articulated_rig_runtime.py +2 -4
  3. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_asset_delivery.py +26 -8
  4. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_audio_events_integration.py +168 -17
  5. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_base_test.py +78 -0
  6. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_blender_render_effects_e2e.py +1 -1
  7. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_blender_shadow_plane_integration.py +1 -1
  8. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_blender_shot_metadata_integration.py +1 -1
  9. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_clip_name_contract_integration.py +1 -1
  10. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_controller_runtime_integration.py +1 -1
  11. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_deploy_templates.py +99 -0
  12. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_embedded_dependencies_integration.py +184 -2
  13. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_emissive_groups_integration.py +1 -1
  14. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_fade_window_preservation.py +1 -1
  15. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_keyed_visibility.py +1 -1
  16. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_lightmap_metadata_integration.py +852 -1
  17. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_maya_surface_materials_integration.py +120 -62
  18. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_multi_section_audio_integration.py +2 -2
  19. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_render_effects_e2e.py +2 -2
  20. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_render_opacity_controller.py +1 -1
  21. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_render_opacity_e2e.py +1 -1
  22. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_render_opacity_integration.py +1 -1
  23. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_shadow_plane_integration.py +1 -1
  24. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_shadow_plane_runtime_integration.py +1 -1
  25. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_shared_material_standalone.py +1 -1
  26. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_shot_metadata_integration.py +1 -1
  27. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_shots_audio_sidebyside_integration.py +1 -1
  28. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_standalone_enduser.py +1 -1
  29. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_temp_projects.py +26 -3
  30. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/__init__.py +1 -1
  31. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/asset_delivery.py +1 -2
  32. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs +5 -2
  33. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/AudioEvent/AudioEventController.cs +95 -21
  34. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/EmissiveGroup/EmissiveGroupsLit.shader +36 -1
  35. unitytk-0.3.0/unitytk/templates/LightmapMetadata/DeliverableCubeUV.hlsl +119 -0
  36. unitytk-0.3.0/unitytk/templates/LightmapMetadata/DeliverableCubeUV.hlsl.meta +7 -0
  37. unitytk-0.3.0/unitytk/templates/LightmapMetadata/DeliverableLit.shader +373 -0
  38. unitytk-0.3.0/unitytk/templates/LightmapMetadata/DeliverableLit.shader.meta +9 -0
  39. unitytk-0.3.0/unitytk/templates/LightmapMetadata/DeliverablePmrem.cs +307 -0
  40. unitytk-0.3.0/unitytk/templates/LightmapMetadata/DeliverablePmrem.cs.meta +11 -0
  41. unitytk-0.3.0/unitytk/templates/LightmapMetadata/DeliverableShading.cs +460 -0
  42. unitytk-0.3.0/unitytk/templates/LightmapMetadata/DeliverableShading.cs.meta +11 -0
  43. unitytk-0.3.0/unitytk/templates/LightmapMetadata/DeliverableToneMap.shader +65 -0
  44. unitytk-0.3.0/unitytk/templates/LightmapMetadata/DeliverableToneMap.shader.meta +9 -0
  45. unitytk-0.3.0/unitytk/templates/LightmapMetadata/DeliverableToneMapFeature.cs +53 -0
  46. unitytk-0.3.0/unitytk/templates/LightmapMetadata/DeliverableToneMapFeature.cs.meta +11 -0
  47. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs +496 -48
  48. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/M3trik.Unitytk.asmdef +8 -1
  49. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/MayaSurface/MayaSurfaceMaterialImporter.cs +69 -16
  50. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/README.md +69 -27
  51. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ShadowPlane/ShadowPlaneController.cs +19 -16
  52. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/UnitytkSettings.cs +138 -21
  53. {unitytk-0.2.1 → unitytk-0.3.0/unitytk.egg-info}/PKG-INFO +1 -1
  54. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk.egg-info/SOURCES.txt +12 -0
  55. {unitytk-0.2.1 → unitytk-0.3.0}/LICENSE +0 -0
  56. {unitytk-0.2.1 → unitytk-0.3.0}/MANIFEST.in +0 -0
  57. {unitytk-0.2.1 → unitytk-0.3.0}/docs/README.md +0 -0
  58. {unitytk-0.2.1 → unitytk-0.3.0}/pyproject.toml +0 -0
  59. {unitytk-0.2.1 → unitytk-0.3.0}/setup.cfg +0 -0
  60. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_bool_to_fade_curve.py +0 -0
  61. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_launcher.py +0 -0
  62. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_run_tests.py +0 -0
  63. {unitytk-0.2.1 → unitytk-0.3.0}/test/test_scene_builder.py +0 -0
  64. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/launcher.py +0 -0
  65. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/scene_builder.py +0 -0
  66. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/template_deployer.py +0 -0
  67. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ArticulatedRig/ArticulatedRigController.cs +0 -0
  68. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ArticulatedRig/ArticulatedRigController.cs.meta +0 -0
  69. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ArticulatedRig/ArticulatedRigPointerGrab.cs +0 -0
  70. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ArticulatedRig/ArticulatedRigPointerGrab.cs.meta +0 -0
  71. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs.meta +0 -0
  72. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRGrab.cs +0 -0
  73. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRGrab.cs.meta +0 -0
  74. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRSetup.cs +0 -0
  75. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRSetup.cs.meta +0 -0
  76. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ArticulatedRig/XR/M3trik.Unitytk.XR.asmdef +0 -0
  77. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ArticulatedRig/XR/M3trik.Unitytk.XR.asmdef.meta +0 -0
  78. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ArticulatedRig/XR.meta +0 -0
  79. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ArticulatedRig.meta +0 -0
  80. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/AudioEvent/AudioEventController.cs.meta +0 -0
  81. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/AudioEvent.meta +0 -0
  82. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/EmissiveGroup/EmissiveGroupController.cs +0 -0
  83. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/EmissiveGroup/EmissiveGroupController.cs.meta +0 -0
  84. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/EmissiveGroup/EmissiveGroups.hlsl +0 -0
  85. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/EmissiveGroup/EmissiveGroups.hlsl.meta +0 -0
  86. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/EmissiveGroup/EmissiveGroupsLit.shader.meta +0 -0
  87. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/EmissiveGroup.meta +0 -0
  88. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs.meta +0 -0
  89. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/LightmapMetadata.meta +0 -0
  90. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/M3trik.Unitytk.asmdef.meta +0 -0
  91. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/MayaSurface/MayaSurfaceMaterialImporter.cs.meta +0 -0
  92. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/MayaSurface.meta +0 -0
  93. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/README.md.meta +0 -0
  94. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/RenderEffects/RenderEffectsController.cs +0 -0
  95. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/RenderEffects/RenderEffectsController.cs.meta +0 -0
  96. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/RenderEffects.meta +0 -0
  97. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ShadowPlane/ShadowPlane.shader +0 -0
  98. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ShadowPlane/ShadowPlane.shader.meta +0 -0
  99. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ShadowPlane/ShadowPlaneController.cs.meta +0 -0
  100. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ShadowPlane/ShadowPlaneHorizon.hlsl +0 -0
  101. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ShadowPlane/ShadowPlaneHorizon.hlsl.meta +0 -0
  102. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ShadowPlane.meta +0 -0
  103. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ShotMetadata/ShotMetadataController.cs +0 -0
  104. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ShotMetadata/ShotMetadataController.cs.meta +0 -0
  105. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/ShotMetadata.meta +0 -0
  106. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/UnitytkSettings.cs.meta +0 -0
  107. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk/templates/__init__.py +0 -0
  108. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk.egg-info/dependency_links.txt +0 -0
  109. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk.egg-info/requires.txt +0 -0
  110. {unitytk-0.2.1 → unitytk-0.3.0}/unitytk.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitytk
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: A modular Python toolkit for Unity interaction.
5
5
  Author-email: Ryan Simpson <m3trik@outlook.com>
6
6
  License: MIT
@@ -1671,7 +1671,7 @@ def _run_unity(test, temp_dir, execute_method, prepare, timeout=600):
1671
1671
  verifier; returns the parsed results (or skips on the license wall)."""
1672
1672
  editor = os.environ.get("UNITYTK_TEST_EDITOR")
1673
1673
  launcher = UnityLauncher(editor) if editor else UnityLauncher()
1674
- project_path = os.path.join(temp_dir, "UnityProject")
1674
+ project_path = TempProjects.project(temp_dir)
1675
1675
  if not launcher.create_project(project_path, batch_mode=True):
1676
1676
  test.skipTest(UnityEnv.project_creation_reason())
1677
1677
  results_path = os.path.join(temp_dir, "unity_results.json")
@@ -1865,9 +1865,7 @@ class TestArticulatedRigRuntime(unittest.TestCase):
1865
1865
  for name in _bundle_assets()
1866
1866
  }
1867
1867
  self.assertEqual(got, expected, "Unity named a deployed asset differently")
1868
- package = TemplateDeployer.package_dir(
1869
- os.path.join(self.temp_dir, "UnityProject")
1870
- )
1868
+ package = TemplateDeployer.package_dir(TempProjects.project(self.temp_dir))
1871
1869
  rewritten = []
1872
1870
  for name in _bundle_assets():
1873
1871
  path = os.path.join(package, *name.split("/")) + ".meta"
@@ -1,6 +1,7 @@
1
1
  # !/usr/bin/python
2
2
  # coding=utf-8
3
3
  """Tests for unitytk.CopyToAssetsDeliverer -- copy-to-Assets hand-off Strategy (no DCC)."""
4
+
4
5
  import json
5
6
  import os
6
7
  import struct
@@ -20,6 +21,7 @@ class _Logger:
20
21
  self.errors = []
21
22
  self.warnings = []
22
23
  self.infos = []
24
+ self.debugs = []
23
25
 
24
26
  def error(self, m, *args):
25
27
  self.errors.append(m % args if args else m)
@@ -30,6 +32,9 @@ class _Logger:
30
32
  def info(self, m, *args):
31
33
  self.infos.append(m % args if args else m)
32
34
 
35
+ def debug(self, m, *args):
36
+ self.debugs.append(m % args if args else m)
37
+
33
38
 
34
39
  def _carrier_fbx(path: str, channels) -> str:
35
40
  """A binary FBX (7700 layout) whose ``data_export`` carrier holds
@@ -108,7 +113,9 @@ def _payload(fbx, default_name="Export"):
108
113
 
109
114
  class SanitizeTest(unittest.TestCase):
110
115
  def test_strips_invalid_and_paths(self):
111
- self.assertEqual(CopyToAssetsDeliverer.sanitize_asset_name("my/model:v2"), "my_model_v2")
116
+ self.assertEqual(
117
+ CopyToAssetsDeliverer.sanitize_asset_name("my/model:v2"), "my_model_v2"
118
+ )
112
119
 
113
120
  def test_collapses_and_trims(self):
114
121
  self.assertEqual(CopyToAssetsDeliverer.sanitize_asset_name(" a b "), "a b")
@@ -193,7 +200,9 @@ class DeliveryTest(unittest.TestCase):
193
200
  )
194
201
 
195
202
  def test_explicit_asset_name_overrides_and_sanitizes(self):
196
- _bridge, result = self._deliver({"ASSET_NAME": "Hero/Mesh:01"}, default_name="Cube")
203
+ _bridge, result = self._deliver(
204
+ {"ASSET_NAME": "Hero/Mesh:01"}, default_name="Cube"
205
+ )
197
206
  self.assertEqual(os.path.basename(result["asset"]), "Hero_Mesh_01.fbx")
198
207
 
199
208
  def test_launch_mode_open_invokes_windowed_launch(self):
@@ -282,6 +291,7 @@ class DeliveryTest(unittest.TestCase):
282
291
  orig = ad.CopyToAssetsDeliverer._launch_editor
283
292
  # Patch UnityLauncher inside the real hook (deferred import target).
284
293
  import unitytk.launcher as launcher_mod
294
+
285
295
  real = launcher_mod.UnityLauncher
286
296
  launcher_mod.UnityLauncher = _FakeLauncher
287
297
  try:
@@ -344,15 +354,14 @@ class DeliveryTest(unittest.TestCase):
344
354
  )
345
355
  # Genuinely under Assets/ (no drive escape).
346
356
  assets = os.path.realpath(os.path.join(self.project, "Assets"))
347
- self.assertTrue(
348
- os.path.realpath(dest).startswith(assets), f"raw={raw!r}"
349
- )
357
+ self.assertTrue(os.path.realpath(dest).startswith(assets), f"raw={raw!r}")
350
358
 
351
359
  def test_explicit_subdir_reducing_to_nothing_drops_into_assets_root(self):
352
360
  for raw in (".", "/", "../.."):
353
361
  _bridge, result = self._deliver({"ASSETS_SUBDIR": raw}, default_name="Cube")
354
362
  self.assertEqual(
355
- result["asset"], os.path.join(self.project, "Assets", "Cube.fbx"),
363
+ result["asset"],
364
+ os.path.join(self.project, "Assets", "Cube.fbx"),
356
365
  f"raw={raw!r}",
357
366
  )
358
367
  self.assertEqual(result["subdir"], "")
@@ -388,7 +397,11 @@ class SelfContainedDeliveryTest(unittest.TestCase):
388
397
  "lightmap_metadata": {
389
398
  "version": 1,
390
399
  "objects": [{"name": "Crate", "map": "Crate_Lightmap.exr", "uvIndex": 1}],
391
- "probe": {"map": "Room_Probe.exr", "position": [0, 0, 0], "unit_scale": 0.01},
400
+ "probe": {
401
+ "map": "Room_Probe.exr",
402
+ "position": [0, 0, 0],
403
+ "unit_scale": 0.01,
404
+ },
392
405
  },
393
406
  "shadow_metadata": {
394
407
  "version": 2,
@@ -411,7 +424,12 @@ class SelfContainedDeliveryTest(unittest.TestCase):
411
424
  self.maps = os.path.join(self.tmp, "sourceimages")
412
425
  os.makedirs(self.maps)
413
426
  self.bytes = {}
414
- for name in ("Crate_Lightmap.exr", "Room_Probe.exr", "Crate_shadow.png", "Crate_horizon.png"):
427
+ for name in (
428
+ "Crate_Lightmap.exr",
429
+ "Room_Probe.exr",
430
+ "Crate_shadow.png",
431
+ "Crate_horizon.png",
432
+ ):
415
433
  self.bytes[name] = name.encode() * 8
416
434
  with open(os.path.join(self.maps, name), "wb") as fh:
417
435
  fh.write(self.bytes[name])
@@ -24,6 +24,7 @@ Requires:
24
24
  - Maya 2025 installed (mayapy.exe)
25
25
  - Unity Hub with at least one editor installed
26
26
  """
27
+
27
28
  import os
28
29
  import json
29
30
  import shutil
@@ -34,7 +35,15 @@ import logging
34
35
  logger = logging.getLogger(__name__)
35
36
 
36
37
  import base_test # noqa: F401 — sys.path bootstrap for the sibling repos
37
- from base_test import EnvSkip, MayaEnv, TempProjects, UnityEnv, UserPropsProbe
38
+ from base_test import (
39
+ BlenderEnv,
40
+ CarrierFbx,
41
+ EnvSkip,
42
+ MayaEnv,
43
+ TempProjects,
44
+ UnityEnv,
45
+ UserPropsProbe,
46
+ )
38
47
 
39
48
  scripts_dir = base_test.SCRIPTS_ROOT
40
49
 
@@ -471,9 +480,7 @@ class TestAudioEventsIntegration(unittest.TestCase):
471
480
  events,
472
481
  {("", 9, "footstep"), ("", 29, "jump"), ("", 44, "land")},
473
482
  )
474
- self.assertEqual(
475
- sorted(r["tracks"]), ["footstep", "jump", "land"]
476
- )
483
+ self.assertEqual(sorted(r["tracks"]), ["footstep", "jump", "land"])
477
484
 
478
485
  # ------------------------------------------------------------------
479
486
  # Step 2: Unity import and readback
@@ -492,7 +499,7 @@ class TestAudioEventsIntegration(unittest.TestCase):
492
499
  # as the sibling integration tests.
493
500
  editor = os.environ.get("UNITYTK_TEST_EDITOR")
494
501
  launcher = UnityLauncher(editor) if editor else UnityLauncher()
495
- project_path = os.path.join(self.temp_dir, "UnityProject")
502
+ project_path = TempProjects.project(self.temp_dir)
496
503
 
497
504
  logger.info(f"Creating Unity project: {project_path}")
498
505
  if not launcher.create_project(project_path, batch_mode=True):
@@ -552,17 +559,26 @@ class TestAudioEventsIntegration(unittest.TestCase):
552
559
  full_log = f.read()
553
560
  # Surface AudioEvent / error / warning lines for analysis
554
561
  relevant = [
555
- ln for ln in full_log.splitlines()
556
- if any(s in ln for s in (
557
- "AudioEvent", "ERROR", "Error", "Warning",
558
- "NullReferenceException", "Failed",
559
- ))
562
+ ln
563
+ for ln in full_log.splitlines()
564
+ if any(
565
+ s in ln
566
+ for s in (
567
+ "AudioEvent",
568
+ "ERROR",
569
+ "Error",
570
+ "Warning",
571
+ "NullReferenceException",
572
+ "Failed",
573
+ )
574
+ )
560
575
  ]
561
576
  self.__class__.unity_log_relevant = relevant
562
577
  if relevant:
563
578
  logger.info(
564
579
  "Unity log relevant lines (%d):\n %s",
565
- len(relevant), "\n ".join(relevant[-40:])
580
+ len(relevant),
581
+ "\n ".join(relevant[-40:]),
566
582
  )
567
583
  else:
568
584
  logger.info("Unity log: no AudioEvent/Warning/Error lines.")
@@ -571,8 +587,7 @@ class TestAudioEventsIntegration(unittest.TestCase):
571
587
  with open(results_path) as f:
572
588
  self.__class__.unity_results = json.load(f)
573
589
 
574
- postproc_path = os.path.join(
575
- self.temp_dir, UserPropsProbe.RESULTS_FILENAME)
590
+ postproc_path = os.path.join(self.temp_dir, UserPropsProbe.RESULTS_FILENAME)
576
591
  if os.path.exists(postproc_path):
577
592
  with open(postproc_path) as f:
578
593
  self.__class__.postproc = json.load(f)
@@ -608,7 +623,8 @@ class TestAudioEventsIntegration(unittest.TestCase):
608
623
  f"in postprocessor results: {list(postproc)}",
609
624
  )
610
625
  self.assertIn(
611
- "audio_manifest", props,
626
+ "audio_manifest",
627
+ props,
612
628
  f"audio_manifest user-property missing on {name}; saw: {list(props)}",
613
629
  )
614
630
  manifest = props["audio_manifest"]
@@ -655,7 +671,8 @@ class TestAudioEventsIntegration(unittest.TestCase):
655
671
 
656
672
  # (d) AnimationEvents injected on the imported clip
657
673
  self.assertGreaterEqual(
658
- unity.get("clip_count", 0), 1,
674
+ unity.get("clip_count", 0),
675
+ 1,
659
676
  "Expected at least one AnimationClip in the imported FBX",
660
677
  )
661
678
  events = unity.get("animation_events", [])
@@ -676,8 +693,8 @@ class TestAudioEventsIntegration(unittest.TestCase):
676
693
  # In Unity-frame terms the events are at clip-relative frames 9,
677
694
  # 29, 44 — which is what AnimationEvent.time * frameRate yields.
678
695
  self.assertEqual(events_by_name.get("footstep"), 9)
679
- self.assertEqual(events_by_name.get("jump"), 29)
680
- self.assertEqual(events_by_name.get("land"), 44)
696
+ self.assertEqual(events_by_name.get("jump"), 29)
697
+ self.assertEqual(events_by_name.get("land"), 44)
681
698
 
682
699
  # (e) Reimport idempotency. Two probes injected before the
683
700
  # reimport tell us about both correctness and Unity behavior:
@@ -721,6 +738,140 @@ class TestAudioEventsIntegration(unittest.TestCase):
721
738
  )
722
739
 
723
740
 
741
+ # ======================================================================
742
+ # Carrier precedence: one audio_manifest per model, the scene's own first
743
+ # ======================================================================
744
+
745
+ CARRIER_VERIFIER_CS = r"""
746
+ using UnityEngine;
747
+ using UnityEditor;
748
+ using System;
749
+ using System.IO;
750
+ using System.Linq;
751
+ using System.Collections.Generic;
752
+
753
+ public class AudioCarrierVerifier
754
+ {
755
+ static string Q(string s) { return "\"" + (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; }
756
+
757
+ public static void Verify()
758
+ {
759
+ var o = new List<string>();
760
+ try
761
+ {
762
+ // Again, now the clips are in the database: the startup import
763
+ // may have met the model before the audio.
764
+ AssetDatabase.ImportAsset("__FBX__",
765
+ ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
766
+ AssetDatabase.Refresh();
767
+ var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("__FBX__");
768
+ o.Add("\"prefab\":" + (prefab != null ? "true" : "false"));
769
+ var nodes = prefab != null
770
+ ? prefab.GetComponentsInChildren<Transform>(true).Select(t => Q(t.name))
771
+ : Enumerable.Empty<string>();
772
+ o.Add("\"nodes\":[" + string.Join(",", nodes) + "]");
773
+ var ctrl = prefab != null ? prefab.GetComponent<AudioEventController>() : null;
774
+ o.Add("\"controller\":" + (ctrl != null ? "true" : "false"));
775
+ var clips = ctrl != null ? ctrl.clips : new List<AudioClip>();
776
+ o.Add("\"clips\":[" + string.Join(",", clips.Select(c => Q(c != null ? c.name : ""))) + "]");
777
+ }
778
+ catch (Exception e) { o.Add("\"error\":" + Q(e.GetType().Name + ": " + e.Message)); }
779
+ File.WriteAllText(@"__RESULTS__", "{" + string.Join(",\n", o) + "}");
780
+ EditorApplication.Exit(0);
781
+ }
782
+ }
783
+ """
784
+
785
+
786
+ class TestAudioCarrierPrecedence(unittest.TestCase):
787
+ """An FBX carrying ``audio_manifest`` on several carriers plays the
788
+ scene's own only.
789
+
790
+ An assembly ships a stray ``data_export1`` (and a module's
791
+ ``NS:data_export``) beside the scene's own, each holding the events as
792
+ last exported alone -- measured on an assembly, 2026-10-13. The scene's
793
+ own record is re-derived from the whole merged scene, so every reader
794
+ takes ONE carrier per channel, the scene's own first: the other
795
+ importers (``CarrierImport.Collect``), the GLB applier, and pythontk's
796
+ ``FbxMedia.audio_events``. The audio importer concatenated every
797
+ carrier's events instead, so the stray's stale event was bound and fired.
798
+
799
+ The fixture is a real FBX with both carriers (``base_test.CarrierFbx``,
800
+ written by a fresh Blender); pythontk's ``FbxMedia.audio_events`` pins the
801
+ same rule on the same shape (``test_fbx_media``).
802
+ """
803
+
804
+ FBX = "Assets/Delivered/assembly.fbx"
805
+
806
+ @classmethod
807
+ def setUpClass(cls):
808
+ cls.temp_dir = TempProjects.allocate("carrier_precedence")
809
+
810
+ @BlenderEnv.skip_unless_available()
811
+ @EnvSkip.unless(_unity_available(), "no Unity editor found")
812
+ def test_the_scenes_own_carrier_wins(self):
813
+ own = {"version": 2, "events": [{"clip": "", "frame": 0, "name": "footstep"}]}
814
+ stray = {"version": 2, "events": [{"clip": "", "frame": 3, "name": "door"}]}
815
+ fixture = CarrierFbx.build(
816
+ os.path.join(self.temp_dir, "assembly.fbx"),
817
+ {
818
+ "data_export": {"audio_manifest": own},
819
+ "data_export1": {"audio_manifest": stray},
820
+ },
821
+ )
822
+
823
+ launcher = UnityLauncher()
824
+ project = TempProjects.project(self.temp_dir)
825
+ if not launcher.create_project(project, batch_mode=True):
826
+ self.skipTest(UnityEnv.project_creation_reason())
827
+ delivered = os.path.join(project, "Assets", "Delivered")
828
+ os.makedirs(delivered, exist_ok=True)
829
+ shutil.copy2(fixture, os.path.join(delivered, "assembly.fbx"))
830
+ # Both tracks resolve, so only the carrier rule decides what binds.
831
+ for name in ("footstep", "door"):
832
+ CarrierFbx.wav(
833
+ os.path.join(project, "Assets", "Audio", f"{name}.wav"), name.encode()
834
+ )
835
+ TemplateDeployer.deploy_package(project, components=["audio_event"])
836
+ results = os.path.join(self.temp_dir, "unity_results.json")
837
+ editor_dir = os.path.join(project, "Assets", "Editor")
838
+ os.makedirs(editor_dir, exist_ok=True)
839
+ with open(
840
+ os.path.join(editor_dir, "AudioCarrierVerifier.cs"), "w", encoding="utf-8"
841
+ ) as fh:
842
+ fh.write(
843
+ CARRIER_VERIFIER_CS.replace("__FBX__", self.FBX).replace(
844
+ "__RESULTS__", results
845
+ )
846
+ )
847
+ log = os.path.join(self.temp_dir, "unity_log.txt")
848
+ launcher.project_path = project
849
+ proc = launcher.launch_editor(
850
+ batch_mode=True,
851
+ execute_method="AudioCarrierVerifier.Verify",
852
+ log_file=log,
853
+ extra_args=["-quit"],
854
+ detached=False,
855
+ )
856
+ returncode = None
857
+ if proc:
858
+ try:
859
+ returncode = proc.wait(timeout=900)
860
+ except subprocess.TimeoutExpired:
861
+ proc.kill()
862
+ raise TimeoutError("Unity timed out")
863
+ UnityEnv.require_results(self, results, returncode, log)
864
+ with open(results) as fh:
865
+ unity = json.load(fh)
866
+ logger.info(f"Unity results:\n{json.dumps(unity, indent=2)}")
867
+ self.assertNotIn("error", unity, unity.get("error"))
868
+ self.assertTrue(unity["prefab"], unity)
869
+ # Premise: both carriers reached the import.
870
+ self.assertTrue({"data_export", "data_export1"} <= set(unity["nodes"]), unity)
871
+ self.assertTrue(unity["controller"], unity)
872
+ self.assertEqual(unity["clips"], ["footstep"], unity)
873
+
874
+
724
875
  if __name__ == "__main__":
725
876
  logging.basicConfig(level=logging.INFO)
726
877
  unittest.main()
@@ -42,6 +42,7 @@ from base_test import (
42
42
  MayaEnv,
43
43
  SuiteOptions,
44
44
  TemplateSources,
45
+ TempProjects,
45
46
  TestAssets,
46
47
  UnityEnv,
47
48
  UserPropsProbe,
@@ -486,6 +487,83 @@ class UnityEnvTest(unittest.TestCase):
486
487
  self.assertEqual(result.errors, [])
487
488
  self.assertEqual(result.testsRun, 3)
488
489
 
490
+ def test_make_urp_fails_fast_on_a_project_too_deep_for_the_editor(self):
491
+ """The editor is not long-path aware: past MAX_PATH its package imports
492
+ fail, URP's Autodesk Interactive graph resolves to nothing, and the
493
+ material suite read the fallback pick as a product bug (2026-10-19)."""
494
+ deep = "C:\\" + "d" * (260 - UnityEnv.URP_IMPORT_DEPTH - 3) # one past the limit
495
+
496
+ class Case(unittest.TestCase):
497
+ def test_deep(self):
498
+ UnityEnv.make_urp(self, None, deep, "unused.log")
499
+
500
+ result = _run_case(Case)
501
+ self.assertEqual(result.errors, [])
502
+ self.assertEqual(len(result.failures), 1)
503
+ self.assertIn("UNITYTK_TEMP_PROJECTS", result.failures[0][1])
504
+
505
+ def test_the_depth_gate_covers_every_shader_graph_urp_ships(self):
506
+ """The gate measured the Autodesk Interactive graph; URP's Arnold
507
+ transparent graph sits 25 characters deeper, so a 107-character project
508
+ passed it, that graph failed to import, URP's Arnold mapper left
509
+ aiStandardSurface on plain Lit and the material suite failed on every
510
+ run from the in-repo base (2026-10-19). Measured against each installed
511
+ editor's bundled URP, not a list of the graphs the suites use today."""
512
+ from unitytk import UnityFinder
513
+
514
+ packages = [
515
+ p
516
+ for p in map(UnityEnv.bundled_urp, UnityFinder.find_editors().values())
517
+ if os.path.isdir(p)
518
+ ]
519
+ if not packages:
520
+ self.skipTest(EnvSkip.reason(UnityEnv.NO_BUNDLED_URP))
521
+ deepest = UnityEnv.URP_IMPORT_DEPTH - len(UnityEnv.URP_PACKAGE_CACHE)
522
+ for package in packages:
523
+ for dirpath, dirnames, filenames in os.walk(package):
524
+ # A folder ending "~" (samples, docs) or hidden is never imported.
525
+ dirnames[:] = [d for d in dirnames if not d.endswith("~") and not d.startswith(".")]
526
+ for name in filenames:
527
+ if name.endswith((".shadergraph", ".shadersubgraph")):
528
+ below = os.path.join(dirpath, name)[len(package):]
529
+ self.assertLessEqual(len(below), deepest, below)
530
+
531
+ @unittest.skipUnless(
532
+ os.name == "nt", "MAX_PATH is the Windows editor's limit; no Unity runs here"
533
+ )
534
+ def test_every_suite_fits_a_urp_project_under_the_default_base(self):
535
+ """A plain ``run_tests.py --all`` builds its projects under the in-repo
536
+ base, where ``<run>\\urp\\UnityProject`` put the material suite's URP
537
+ project at 107 characters: past the gate, so every URP suite failed at
538
+ once (2026-10-19). Every suite's prefix, not only today's URP suites:
539
+ any of them may call ``make_urp`` on its project, or add a ``"urp"`` one."""
540
+ prefixes = set()
541
+ # The allocator's own tests allocate into a sandboxed base, retired prefixes included.
542
+ for path in _source_files(".py", exclude=("test_temp_projects.py", "test_run_tests.py")):
543
+ with open(path, encoding="utf-8", errors="replace") as fh:
544
+ tree = ast.parse(fh.read(), filename=path)
545
+ for node in ast.walk(tree):
546
+ if (
547
+ isinstance(node, ast.Call)
548
+ and isinstance(node.func, ast.Attribute)
549
+ and node.func.attr == "allocate"
550
+ and getattr(node.func.value, "id", None) == "TempProjects"
551
+ and node.args
552
+ and isinstance(node.args[0], ast.Constant)
553
+ and isinstance(node.args[0].value, str)
554
+ ):
555
+ prefixes.add(node.args[0].value)
556
+ self.assertGreater(len(prefixes), 10, "the suites' allocations were not found")
557
+ # TempArtifacts' tag: the clock (16 hex digits), the pid in hex (6: Linux's
558
+ # pid_max ceiling; Windows ids run below it) and 4 random digits.
559
+ tag = "f" * (16 + 6 + 4)
560
+ budget = 259 - UnityEnv.URP_IMPORT_DEPTH
561
+ for prefix in sorted(prefixes):
562
+ run = os.path.join(TempProjects.DEFAULT_BASE, f"{prefix}_{tag}")
563
+ for project in (TempProjects.project(run), TempProjects.project(run, "urp")):
564
+ with self.subTest(project=project):
565
+ self.assertLessEqual(len(project), budget, project)
566
+
489
567
 
490
568
  class SuiteOptionsTest(unittest.TestCase):
491
569
  """The opt-in switches are named once, neutrally."""
@@ -351,7 +351,7 @@ class TestBlenderRenderEffectsE2E(unittest.TestCase):
351
351
  self.skipTest("Blender results not available -- test_01 must run first")
352
352
  editor = os.environ.get("UNITYTK_TEST_EDITOR")
353
353
  launcher = UnityLauncher(editor) if editor else UnityLauncher()
354
- project_path = os.path.join(self.temp_dir, "UnityProject")
354
+ project_path = TempProjects.project(self.temp_dir)
355
355
  if not launcher.create_project(project_path, batch_mode=True):
356
356
  self.skipTest(UnityEnv.project_creation_reason())
357
357
  assets_path = os.path.join(project_path, "Assets")
@@ -606,7 +606,7 @@ class TestBlenderShadowPlaneIntegration(unittest.TestCase):
606
606
  blender = self._blender()
607
607
  editor = os.environ.get("UNITYTK_TEST_EDITOR")
608
608
  launcher = UnityLauncher(editor) if editor else UnityLauncher()
609
- project_path = os.path.join(self.temp_dir, "UnityProject")
609
+ project_path = TempProjects.project(self.temp_dir)
610
610
  if not launcher.create_project(project_path, batch_mode=True):
611
611
  self.skipTest(UnityEnv.project_creation_reason())
612
612
 
@@ -232,7 +232,7 @@ class TestBlenderShotMetadataIntegration(unittest.TestCase):
232
232
  self.skipTest("Blender results not available -- test_01 must run first")
233
233
  editor = os.environ.get("UNITYTK_TEST_EDITOR")
234
234
  launcher = UnityLauncher(editor) if editor else UnityLauncher()
235
- project_path = os.path.join(self.temp_dir, "UnityProject")
235
+ project_path = TempProjects.project(self.temp_dir)
236
236
  if not launcher.create_project(project_path, batch_mode=True):
237
237
  self.skipTest(UnityEnv.project_creation_reason())
238
238
 
@@ -264,7 +264,7 @@ class TestClipNameContract(unittest.TestCase):
264
264
 
265
265
  editor = os.environ.get("UNITYTK_TEST_EDITOR")
266
266
  launcher = UnityLauncher(editor) if editor else UnityLauncher()
267
- project_path = os.path.join(self.temp_dir, "UnityProject")
267
+ project_path = TempProjects.project(self.temp_dir)
268
268
  if not launcher.create_project(project_path, batch_mode=True):
269
269
  self.skipTest(UnityEnv.project_creation_reason())
270
270
 
@@ -181,7 +181,7 @@ class TestControllerRuntimeIntegration(unittest.TestCase):
181
181
  """Drive both controllers in a real Unity; capture the readback."""
182
182
  editor = os.environ.get("UNITYTK_TEST_EDITOR")
183
183
  launcher = UnityLauncher(editor) if editor else UnityLauncher()
184
- project_path = os.path.join(self.temp_dir, "UnityProject")
184
+ project_path = TempProjects.project(self.temp_dir)
185
185
  if not launcher.create_project(project_path, batch_mode=True):
186
186
  self.skipTest(UnityEnv.project_creation_reason())
187
187
 
@@ -127,6 +127,12 @@ class DeployTemplateTest(unittest.TestCase):
127
127
  "EmissiveGroup/EmissiveGroupController.cs",
128
128
  "EmissiveGroup/EmissiveGroups.hlsl",
129
129
  "EmissiveGroup/EmissiveGroupsLit.shader",
130
+ "LightmapMetadata/DeliverableCubeUV.hlsl",
131
+ "LightmapMetadata/DeliverableLit.shader",
132
+ "LightmapMetadata/DeliverablePmrem.cs",
133
+ "LightmapMetadata/DeliverableShading.cs",
134
+ "LightmapMetadata/DeliverableToneMap.shader",
135
+ "LightmapMetadata/DeliverableToneMapFeature.cs",
130
136
  "LightmapMetadata/LightmapMetadataController.cs",
131
137
  "M3trik.Unitytk.asmdef",
132
138
  "MayaSurface/MayaSurfaceMaterialImporter.cs",
@@ -182,6 +188,63 @@ class DeployTemplateTest(unittest.TestCase):
182
188
  f"{include} is not beside {shader}",
183
189
  )
184
190
 
191
+ @staticmethod
192
+ def _shader_pass(text, name):
193
+ """The ``Pass { ... }`` block of shader *text* named *name*, or None."""
194
+ at = text.find(f'Name "{name}"')
195
+ if at < 0:
196
+ return None
197
+ start = [m.start() for m in re.finditer(r"\bPass\s*\{", text[:at])][-1]
198
+ depth = 0
199
+ for i in range(text.index("{", start), len(text)):
200
+ depth += {"{": 1, "}": -1}.get(text[i], 0)
201
+ if depth == 0:
202
+ return text[start : i + 1]
203
+ return None
204
+
205
+ def test_forward_only_urp_shaders_draw_in_both_prepasses(self):
206
+ """A URP shader drawn by a ``UniversalForwardOnly`` pass also draws in
207
+ the depth prepass and the depth-normals prepass (SSAO, any feature
208
+ asking for normals), as URP's Unlit does: under depth priming the
209
+ forward pass tests depth for equality, so a shader missing the prepass's
210
+ pass drew nothing there. Each carries the instance and eye macros
211
+ single-pass instanced XR needs (its own, or those of URP's own pass it
212
+ includes); the normals pass writes the packed normals a Deferred
213
+ renderer asks for."""
214
+ def walk(ref, prefix=""):
215
+ for child in ref.iterdir():
216
+ if child.is_dir():
217
+ yield from walk(child, prefix + child.name + "/")
218
+ elif child.name.endswith(".shader"):
219
+ yield prefix + child.name, child.read_text(encoding="utf-8")
220
+
221
+ forward_only = {
222
+ name: text
223
+ for name, text in walk(resources.files(TEMPLATE_PACKAGE))
224
+ if re.search(r'"LightMode"\s*=\s*"UniversalForwardOnly"', text)
225
+ }
226
+ self.assertEqual(
227
+ sorted(forward_only),
228
+ ["EmissiveGroup/EmissiveGroupsLit.shader", "LightmapMetadata/DeliverableLit.shader"],
229
+ )
230
+ for name, text in forward_only.items():
231
+ for prepass in ("DepthOnly", "DepthNormalsOnly"):
232
+ block = self._shader_pass(text, prepass)
233
+ self.assertIsNotNone(block, f"{name} has no {prepass} pass")
234
+ self.assertRegex(block, rf'"LightMode"\s*=\s*"{prepass}"', f"{name} {prepass}")
235
+ self.assertRegex(block, r"ZWrite\s+On", f"{name} {prepass}")
236
+ if "universal/Shaders/UnlitDepthNormalsPass.hlsl" in block:
237
+ continue # URP's own pass, macros and all
238
+ for macro in (
239
+ "UNITY_VERTEX_INPUT_INSTANCE_ID",
240
+ "UNITY_VERTEX_OUTPUT_STEREO",
241
+ "UNITY_SETUP_INSTANCE_ID",
242
+ "UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO",
243
+ ):
244
+ self.assertIn(macro, block, f"{name} {prepass}: no {macro}")
245
+ normals = self._shader_pass(text, "DepthNormalsOnly")
246
+ self.assertIn("_GBUFFER_NORMALS_OCT", normals, f"{name} DepthNormalsOnly")
247
+
185
248
  # ---------------------------------------------------------------- components
186
249
  def test_components_derive_from_the_bundle(self):
187
250
  """One required core plus one optional component per top-level folder,
@@ -366,6 +429,42 @@ class DeployTemplateTest(unittest.TestCase):
366
429
  self.assertIn("M3trik.Unitytk", data["references"])
367
430
  self.assertEqual(data["defineConstraints"], ["UNITYTK_XRI"])
368
431
 
432
+ def test_the_urp_define_starts_where_the_full_screen_pass_can_run_the_look(self):
433
+ """``UNITYTK_URP`` compiles ``DeliverableLook``, which sets
434
+ ``FullScreenPassRendererFeature.fetchColorBuffer`` and draws through
435
+ core's ``Blit.hlsl`` ``Vert``. Both arrived with URP 2022.3's backport
436
+ of the feature's fixes (in 14.0.9; 14.0.0-14.0.7 have no such field and
437
+ never set the ``_BlitScaleBias`` that vertex reads). A lower floor fails
438
+ the whole package's compile on those versions, not just the look."""
439
+ data = json.loads(bundle_ref("M3trik.Unitytk.asmdef").read_text(encoding="utf-8"))
440
+ urp = [d for d in data["versionDefines"] if d["define"] == "UNITYTK_URP"]
441
+ self.assertEqual(len(urp), 1, data["versionDefines"])
442
+ self.assertEqual(urp[0]["name"], "com.unity.render-pipelines.universal")
443
+ self.assertEqual(urp[0]["expression"], "14.0.9")
444
+ # A reference URP does not have is skipped: a Built-in project compiles.
445
+ self.assertIn("Unity.RenderPipelines.Universal.Runtime", data["references"])
446
+
447
+ def test_the_full_screen_pass_is_named_as_both_urps_declare_it(self):
448
+ """URP 14 declares ``FullScreenPassRendererFeature`` in the global
449
+ namespace, URP 17 in ``UnityEngine.Rendering.Universal`` (its
450
+ ``[MovedFrom("")]``). Only the short name, under a ``using`` of that
451
+ namespace, compiles on both: the qualified name failed the whole
452
+ package's compile on URP 14, where ``UNITYTK_URP`` starts."""
453
+ def walk(ref, prefix=""):
454
+ for child in ref.iterdir():
455
+ if child.is_dir():
456
+ yield from walk(child, prefix + child.name + "/")
457
+ elif child.name.endswith(".cs"):
458
+ yield prefix + child.name, child.read_text(encoding="utf-8")
459
+
460
+ named = []
461
+ for name, text in walk(resources.files(TEMPLATE_PACKAGE)):
462
+ self.assertNotIn("Universal.FullScreenPassRendererFeature", text, name)
463
+ if re.search(r"\bFullScreenPassRendererFeature\b", text):
464
+ named.append(name)
465
+ self.assertIn("using UnityEngine.Rendering.Universal;", text, name)
466
+ self.assertEqual(named, ["LightmapMetadata/DeliverableToneMapFeature.cs"])
467
+
369
468
  def test_status_sees_subfolder_files(self):
370
469
  """A missing file inside a component's subfolder is a partial deploy
371
470
  of that component, named by its bundle-relative name."""