unitytk 0.0.1__tar.gz → 0.0.2__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.
- {unitytk-0.0.1 → unitytk-0.0.2}/LICENSE +21 -21
- unitytk-0.0.2/MANIFEST.in +4 -0
- unitytk-0.0.2/PKG-INFO +29 -0
- unitytk-0.0.2/docs/README.md +12 -0
- {unitytk-0.0.1 → unitytk-0.0.2}/pyproject.toml +32 -32
- {unitytk-0.0.1 → unitytk-0.0.2}/setup.cfg +4 -4
- unitytk-0.0.2/test/test_asset_delivery.py +343 -0
- unitytk-0.0.2/test/test_audio_events_integration.py +804 -0
- unitytk-0.0.2/test/test_bool_to_fade_curve.py +286 -0
- {unitytk-0.0.1 → unitytk-0.0.2}/test/test_c130_fcr_integration.py +1295 -1283
- unitytk-0.0.2/test/test_c5m_visibility.py +636 -0
- unitytk-0.0.2/test/test_clip_name_contract_integration.py +328 -0
- unitytk-0.0.2/test/test_deploy_templates.py +126 -0
- unitytk-0.0.2/test/test_fade_window_preservation.py +317 -0
- {unitytk-0.0.1 → unitytk-0.0.2}/test/test_launcher.py +159 -95
- unitytk-0.0.2/test/test_lightmap_metadata_integration.py +571 -0
- {unitytk-0.0.1 → unitytk-0.0.2}/test/test_render_opacity_controller.py +357 -357
- unitytk-0.0.2/test/test_render_opacity_e2e.py +657 -0
- {unitytk-0.0.1 → unitytk-0.0.2}/test/test_render_opacity_integration.py +595 -595
- unitytk-0.0.2/test/test_scene_builder.py +87 -0
- unitytk-0.0.2/test/test_shadow_plane_integration.py +418 -0
- unitytk-0.0.2/test/test_shared_material_standalone.py +684 -0
- unitytk-0.0.2/test/test_shot_metadata_integration.py +526 -0
- unitytk-0.0.2/test/test_shots_audio_sidebyside_integration.py +556 -0
- {unitytk-0.0.1 → unitytk-0.0.2}/test/test_standalone_enduser.py +1511 -1492
- unitytk-0.0.2/unitytk/__init__.py +15 -0
- unitytk-0.0.2/unitytk/asset_delivery.py +280 -0
- {unitytk-0.0.1 → unitytk-0.0.2}/unitytk/launcher.py +291 -231
- {unitytk-0.0.1 → unitytk-0.0.2}/unitytk/scene_builder.py +131 -86
- unitytk-0.0.2/unitytk/template_deployer.py +92 -0
- {unitytk-0.0.1 → unitytk-0.0.2}/unitytk/templates/AudioEventController.cs +587 -471
- unitytk-0.0.2/unitytk/templates/EmissiveGroupController.cs +447 -0
- unitytk-0.0.2/unitytk/templates/EmissiveGroups.hlsl +55 -0
- unitytk-0.0.2/unitytk/templates/EmissiveGroupsLit.shader +252 -0
- unitytk-0.0.2/unitytk/templates/LightmapMetadataController.cs +418 -0
- unitytk-0.0.2/unitytk/templates/README.md +148 -0
- {unitytk-0.0.1 → unitytk-0.0.2}/unitytk/templates/RenderOpacityController.cs +732 -602
- unitytk-0.0.2/unitytk/templates/ShadowPlaneController.cs +300 -0
- unitytk-0.0.2/unitytk/templates/ShotMetadataController.cs +171 -0
- {unitytk-0.0.1 → unitytk-0.0.2}/unitytk/templates/__init__.py +2 -2
- {unitytk-0.0.1 → unitytk-0.0.2}/unitytk/templates/maya_fbx_import_config.json +6 -6
- unitytk-0.0.2/unitytk.egg-info/PKG-INFO +29 -0
- unitytk-0.0.2/unitytk.egg-info/SOURCES.txt +44 -0
- unitytk-0.0.1/MANIFEST.in +0 -4
- unitytk-0.0.1/PKG-INFO +0 -20
- unitytk-0.0.1/docs/README.md +0 -3
- unitytk-0.0.1/test/test_audio_events_integration.py +0 -600
- unitytk-0.0.1/test/test_audio_trigger_roundtrip.py +0 -790
- unitytk-0.0.1/unitytk/__init__.py +0 -30
- unitytk-0.0.1/unitytk/templates/README.md +0 -97
- unitytk-0.0.1/unitytk.egg-info/PKG-INFO +0 -20
- unitytk-0.0.1/unitytk.egg-info/SOURCES.txt +0 -24
- {unitytk-0.0.1 → unitytk-0.0.2}/unitytk.egg-info/dependency_links.txt +0 -0
- {unitytk-0.0.1 → unitytk-0.0.2}/unitytk.egg-info/requires.txt +0 -0
- {unitytk-0.0.1 → unitytk-0.0.2}/unitytk.egg-info/top_level.txt +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 m3trik@live.com
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 m3trik@live.com
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
unitytk-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: unitytk
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: A modular Python toolkit for Unity interaction.
|
|
5
|
+
Author-email: Ryan Simpson <m3trik@outlook.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/unitytk/unitytk
|
|
8
|
+
Project-URL: Repository, https://github.com/unitytk/unitytk
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: pythontk>=0.7.71
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
[](../test/)
|
|
19
|
+
|
|
20
|
+
# unitytk
|
|
21
|
+
|
|
22
|
+
A modular Python toolkit for Unity interaction — Python-side helpers for preparing DCC exports for Unity, plus the C# editor scripts they pair with (shipped under [`templates/`](../unitytk/templates/README.md)).
|
|
23
|
+
|
|
24
|
+
## More
|
|
25
|
+
|
|
26
|
+
- [`templates/README.md`](../unitytk/templates/README.md) — the C# template catalog (opacity, audio events, shot metadata, lightmaps), the FBX user-property import contract, and deployment (`TemplateDeployer.deploy_all`)
|
|
27
|
+
- [mayatk's `data_nodes.md`](../../mayatk/docs/data_nodes.md) — the DCC-side producer spec these templates consume (`data_export` carrier + channel registry)
|
|
28
|
+
- [`CHANGELOG.md`](../CHANGELOG.md) — notable changes
|
|
29
|
+
- [`CLAUDE.md`](../CLAUDE.md) — contributor / agent conventions
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[](../test/)
|
|
2
|
+
|
|
3
|
+
# unitytk
|
|
4
|
+
|
|
5
|
+
A modular Python toolkit for Unity interaction — Python-side helpers for preparing DCC exports for Unity, plus the C# editor scripts they pair with (shipped under [`templates/`](../unitytk/templates/README.md)).
|
|
6
|
+
|
|
7
|
+
## More
|
|
8
|
+
|
|
9
|
+
- [`templates/README.md`](../unitytk/templates/README.md) — the C# template catalog (opacity, audio events, shot metadata, lightmaps), the FBX user-property import contract, and deployment (`TemplateDeployer.deploy_all`)
|
|
10
|
+
- [mayatk's `data_nodes.md`](../../mayatk/docs/data_nodes.md) — the DCC-side producer spec these templates consume (`data_export` carrier + channel registry)
|
|
11
|
+
- [`CHANGELOG.md`](../CHANGELOG.md) — notable changes
|
|
12
|
+
- [`CLAUDE.md`](../CLAUDE.md) — contributor / agent conventions
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "unitytk"
|
|
7
|
-
dynamic = ["version", "readme"]
|
|
8
|
-
description = "A modular Python toolkit for Unity interaction."
|
|
9
|
-
authors = [{name = "Ryan Simpson", email = "m3trik@outlook.com"}]
|
|
10
|
-
license = {text = "MIT"}
|
|
11
|
-
requires-python = ">=3.9"
|
|
12
|
-
classifiers = [
|
|
13
|
-
"Programming Language :: Python :: 3",
|
|
14
|
-
"License :: OSI Approved :: MIT License",
|
|
15
|
-
"Operating System :: OS Independent",
|
|
16
|
-
]
|
|
17
|
-
dependencies = ["pythontk>=0.7.71"]
|
|
18
|
-
|
|
19
|
-
[project.urls]
|
|
20
|
-
Homepage = "https://github.com/unitytk/unitytk"
|
|
21
|
-
Repository = "https://github.com/unitytk/unitytk"
|
|
22
|
-
|
|
23
|
-
[tool.setuptools.dynamic]
|
|
24
|
-
version = {attr = "unitytk.__version__"}
|
|
25
|
-
readme = {file = "docs/README.md", content-type = "text/markdown"}
|
|
26
|
-
|
|
27
|
-
[tool.setuptools.packages.find]
|
|
28
|
-
where = ["."]
|
|
29
|
-
include = ["unitytk*"]
|
|
30
|
-
|
|
31
|
-
[tool.setuptools.package-data]
|
|
32
|
-
"*" = ["*.ui", "*.json", "*.txt", "*.md", "*.cs"]
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "unitytk"
|
|
7
|
+
dynamic = ["version", "readme"]
|
|
8
|
+
description = "A modular Python toolkit for Unity interaction."
|
|
9
|
+
authors = [{name = "Ryan Simpson", email = "m3trik@outlook.com"}]
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
]
|
|
17
|
+
dependencies = ["pythontk>=0.7.71"]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/unitytk/unitytk"
|
|
21
|
+
Repository = "https://github.com/unitytk/unitytk"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.dynamic]
|
|
24
|
+
version = {attr = "unitytk.__version__"}
|
|
25
|
+
readme = {file = "docs/README.md", content-type = "text/markdown"}
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["."]
|
|
29
|
+
include = ["unitytk*"]
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.package-data]
|
|
32
|
+
"*" = ["*.ui", "*.json", "*.txt", "*.md", "*.cs", "*.hlsl", "*.shader"]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[egg_info]
|
|
2
|
-
tag_build =
|
|
3
|
-
tag_date = 0
|
|
4
|
-
|
|
1
|
+
[egg_info]
|
|
2
|
+
tag_build =
|
|
3
|
+
tag_date = 0
|
|
4
|
+
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# !/usr/bin/python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
"""Tests for unitytk.CopyToAssetsDeliverer -- copy-to-Assets hand-off Strategy (no DCC)."""
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
from pythontk.core_utils.app_handoff import HandoffRequest, Payload
|
|
10
|
+
from unitytk.asset_delivery import (
|
|
11
|
+
CopyToAssetsDeliverer,
|
|
12
|
+
FileToUnityBridge,
|
|
13
|
+
CopyToAssetsDeliverer,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _Logger:
|
|
18
|
+
"""Minimal logger capturing messages by level."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.errors = []
|
|
22
|
+
self.warnings = []
|
|
23
|
+
self.infos = []
|
|
24
|
+
|
|
25
|
+
def error(self, m):
|
|
26
|
+
self.errors.append(m)
|
|
27
|
+
|
|
28
|
+
def warning(self, m):
|
|
29
|
+
self.warnings.append(m)
|
|
30
|
+
|
|
31
|
+
def info(self, m):
|
|
32
|
+
self.infos.append(m)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _StubBridge:
|
|
36
|
+
"""A bare host with the contract the deliverer reads off the bridge."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, project_path=None):
|
|
39
|
+
self.project_path = project_path
|
|
40
|
+
self.logger = _Logger()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _request(params=None):
|
|
44
|
+
return HandoffRequest(template="import", mode="copy_to_assets", params=params or {})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _payload(fbx, default_name="Export"):
|
|
48
|
+
return Payload(primary=fbx, extras={"default_asset_name": default_name})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SanitizeTest(unittest.TestCase):
|
|
52
|
+
def test_strips_invalid_and_paths(self):
|
|
53
|
+
self.assertEqual(CopyToAssetsDeliverer.sanitize_asset_name("my/model:v2"), "my_model_v2")
|
|
54
|
+
|
|
55
|
+
def test_collapses_and_trims(self):
|
|
56
|
+
self.assertEqual(CopyToAssetsDeliverer.sanitize_asset_name(" a b "), "a b")
|
|
57
|
+
|
|
58
|
+
def test_empty_falls_back(self):
|
|
59
|
+
self.assertEqual(CopyToAssetsDeliverer.sanitize_asset_name("///"), "Export")
|
|
60
|
+
self.assertEqual(CopyToAssetsDeliverer.sanitize_asset_name(""), "Export")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DeliveryTest(unittest.TestCase):
|
|
64
|
+
def setUp(self):
|
|
65
|
+
self.tmp = tempfile.mkdtemp()
|
|
66
|
+
self.project = os.path.join(self.tmp, "MyUnityProj")
|
|
67
|
+
os.makedirs(os.path.join(self.project, "Assets"))
|
|
68
|
+
# A fake exported FBX.
|
|
69
|
+
self.fbx = os.path.join(self.tmp, "export.fbx")
|
|
70
|
+
with open(self.fbx, "wb") as fh:
|
|
71
|
+
fh.write(b"FBX-BYTES")
|
|
72
|
+
self.deliverer = CopyToAssetsDeliverer()
|
|
73
|
+
|
|
74
|
+
def tearDown(self):
|
|
75
|
+
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
76
|
+
|
|
77
|
+
def _deliver(self, params, default_name="Export", project=None):
|
|
78
|
+
bridge = _StubBridge(project_path=project or self.project)
|
|
79
|
+
result = self.deliverer.deliver(
|
|
80
|
+
bridge, _payload(self.fbx, default_name), _request(params)
|
|
81
|
+
)
|
|
82
|
+
return bridge, result
|
|
83
|
+
|
|
84
|
+
def test_preflight_rejects_non_project(self):
|
|
85
|
+
bridge = _StubBridge(project_path=os.path.join(self.tmp, "not_a_project"))
|
|
86
|
+
self.assertFalse(self.deliverer.preflight(bridge, _request()))
|
|
87
|
+
self.assertTrue(bridge.logger.errors)
|
|
88
|
+
|
|
89
|
+
def test_preflight_accepts_project(self):
|
|
90
|
+
bridge = _StubBridge(project_path=self.project)
|
|
91
|
+
self.assertTrue(self.deliverer.preflight(bridge, _request()))
|
|
92
|
+
|
|
93
|
+
def test_deliver_copies_into_assets_subdir(self):
|
|
94
|
+
_bridge, result = self._deliver(
|
|
95
|
+
{"ASSETS_SUBDIR": "Models"}, default_name="Robot"
|
|
96
|
+
)
|
|
97
|
+
self.assertIsNotNone(result)
|
|
98
|
+
dest = result["asset"]
|
|
99
|
+
self.assertEqual(
|
|
100
|
+
dest, os.path.join(self.project, "Assets", "Models", "Robot.fbx")
|
|
101
|
+
)
|
|
102
|
+
self.assertTrue(os.path.isfile(dest))
|
|
103
|
+
self.assertEqual(open(dest, "rb").read(), b"FBX-BYTES")
|
|
104
|
+
self.assertFalse(result["launched"])
|
|
105
|
+
|
|
106
|
+
def test_deliver_uses_default_subdir_and_asset_name(self):
|
|
107
|
+
_bridge, result = self._deliver({}, default_name="Cube")
|
|
108
|
+
self.assertEqual(
|
|
109
|
+
result["asset"],
|
|
110
|
+
os.path.join(self.project, "Assets", "Imported", "Cube.fbx"),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def test_explicit_asset_name_overrides_and_sanitizes(self):
|
|
114
|
+
_bridge, result = self._deliver({"ASSET_NAME": "Hero/Mesh:01"}, default_name="Cube")
|
|
115
|
+
self.assertEqual(os.path.basename(result["asset"]), "Hero_Mesh_01.fbx")
|
|
116
|
+
|
|
117
|
+
def test_launch_mode_open_invokes_windowed_launch(self):
|
|
118
|
+
# Patch the deliverer's launch hook to avoid spawning a real editor.
|
|
119
|
+
bridge = _StubBridge(project_path=self.project)
|
|
120
|
+
called = {}
|
|
121
|
+
|
|
122
|
+
def _fake_launch(b, params=None, *, batch_mode=False):
|
|
123
|
+
called["project"] = b.project_path
|
|
124
|
+
called["batch"] = batch_mode
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
self.deliverer._launch_editor = staticmethod(_fake_launch)
|
|
128
|
+
result = self.deliverer.deliver(
|
|
129
|
+
bridge, _payload(self.fbx, "Cube"), _request({"LAUNCH_MODE": "open"})
|
|
130
|
+
)
|
|
131
|
+
self.assertTrue(result["launched"])
|
|
132
|
+
self.assertEqual(called["project"], self.project)
|
|
133
|
+
self.assertFalse(called["batch"]) # "open" -> windowed, not batch
|
|
134
|
+
|
|
135
|
+
def test_launch_mode_headless_runs_batch(self):
|
|
136
|
+
bridge = _StubBridge(project_path=self.project)
|
|
137
|
+
called = {}
|
|
138
|
+
|
|
139
|
+
def _fake_launch(b, params=None, *, batch_mode=False):
|
|
140
|
+
called["batch"] = batch_mode
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
self.deliverer._launch_editor = staticmethod(_fake_launch)
|
|
144
|
+
result = self.deliverer.deliver(
|
|
145
|
+
bridge, _payload(self.fbx, "Cube"), _request({"LAUNCH_MODE": "headless"})
|
|
146
|
+
)
|
|
147
|
+
self.assertTrue(result["launched"])
|
|
148
|
+
self.assertTrue(called["batch"]) # "headless" -> batch mode
|
|
149
|
+
|
|
150
|
+
def test_no_launch_mode_does_not_launch(self):
|
|
151
|
+
bridge = _StubBridge(project_path=self.project)
|
|
152
|
+
called = {"hit": False}
|
|
153
|
+
|
|
154
|
+
def _fake_launch(b, params=None, *, batch_mode=False):
|
|
155
|
+
called["hit"] = True
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
self.deliverer._launch_editor = staticmethod(_fake_launch)
|
|
159
|
+
result = self.deliverer.deliver(
|
|
160
|
+
bridge, _payload(self.fbx, "Cube"), _request({})
|
|
161
|
+
)
|
|
162
|
+
self.assertFalse(result["launched"])
|
|
163
|
+
self.assertFalse(called["hit"])
|
|
164
|
+
|
|
165
|
+
def test_legacy_launch_editor_bool_is_ignored(self):
|
|
166
|
+
"""The legacy boolean LAUNCH_EDITOR is no longer honored — only LAUNCH_MODE
|
|
167
|
+
drives launching, so an old key resolves to no-launch."""
|
|
168
|
+
bridge = _StubBridge(project_path=self.project)
|
|
169
|
+
called = {"hit": False}
|
|
170
|
+
|
|
171
|
+
def _fake_launch(b, params=None, *, batch_mode=False):
|
|
172
|
+
called["hit"] = True
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
self.deliverer._launch_editor = staticmethod(_fake_launch)
|
|
176
|
+
result = self.deliverer.deliver(
|
|
177
|
+
bridge, _payload(self.fbx, "Cube"), _request({"LAUNCH_EDITOR": True})
|
|
178
|
+
)
|
|
179
|
+
self.assertFalse(result["launched"])
|
|
180
|
+
self.assertFalse(called["hit"])
|
|
181
|
+
|
|
182
|
+
def test_launch_passes_unity_version_to_launcher(self):
|
|
183
|
+
"""A chosen UNITY_VERSION is forwarded to the launcher's executable_path."""
|
|
184
|
+
import unitytk.asset_delivery as ad
|
|
185
|
+
|
|
186
|
+
bridge = _StubBridge(project_path=self.project)
|
|
187
|
+
seen = {}
|
|
188
|
+
|
|
189
|
+
class _FakeLauncher:
|
|
190
|
+
def __init__(self, executable_path=None, project_path=None):
|
|
191
|
+
seen["executable_path"] = executable_path
|
|
192
|
+
seen["project_path"] = project_path
|
|
193
|
+
self.executable_path = executable_path or "auto.exe"
|
|
194
|
+
|
|
195
|
+
def launch_editor(self, batch_mode=False, extra_args=None, detached=True):
|
|
196
|
+
seen["launched"] = True
|
|
197
|
+
seen["batch_mode"] = batch_mode
|
|
198
|
+
return object()
|
|
199
|
+
|
|
200
|
+
orig = ad.CopyToAssetsDeliverer._launch_editor
|
|
201
|
+
# Patch UnityLauncher inside the real hook (deferred import target).
|
|
202
|
+
import unitytk.launcher as launcher_mod
|
|
203
|
+
real = launcher_mod.UnityLauncher
|
|
204
|
+
launcher_mod.UnityLauncher = _FakeLauncher
|
|
205
|
+
try:
|
|
206
|
+
self.deliverer._launch_editor = orig # use the real hook
|
|
207
|
+
self.deliverer.deliver(
|
|
208
|
+
bridge,
|
|
209
|
+
_payload(self.fbx, "Cube"),
|
|
210
|
+
_request({"LAUNCH_MODE": "open", "UNITY_VERSION": "2022.3.4f1"}),
|
|
211
|
+
)
|
|
212
|
+
finally:
|
|
213
|
+
launcher_mod.UnityLauncher = real
|
|
214
|
+
self.assertEqual(seen["executable_path"], "2022.3.4f1")
|
|
215
|
+
self.assertEqual(seen["project_path"], self.project)
|
|
216
|
+
self.assertTrue(seen.get("launched"))
|
|
217
|
+
|
|
218
|
+
def test_deliver_rejects_missing_assets(self):
|
|
219
|
+
_bridge, result = self._deliver({}, project=os.path.join(self.tmp, "nope"))
|
|
220
|
+
self.assertIsNone(result)
|
|
221
|
+
|
|
222
|
+
def test_subdir_supports_nested_and_either_separator(self):
|
|
223
|
+
for raw in ("Models/Hero", r"Models\Hero", "Models//Hero/", "/Models/Hero"):
|
|
224
|
+
_bridge, result = self._deliver({"ASSETS_SUBDIR": raw}, default_name="Cube")
|
|
225
|
+
self.assertEqual(
|
|
226
|
+
result["asset"],
|
|
227
|
+
os.path.join(self.project, "Assets", "Models", "Hero", "Cube.fbx"),
|
|
228
|
+
f"raw={raw!r}",
|
|
229
|
+
)
|
|
230
|
+
self.assertEqual(result["subdir"], "Models/Hero")
|
|
231
|
+
|
|
232
|
+
def test_subdir_cannot_escape_assets_via_traversal(self):
|
|
233
|
+
# '..' components are dropped -> stays inside Assets/ (no path traversal).
|
|
234
|
+
_bridge, result = self._deliver(
|
|
235
|
+
{"ASSETS_SUBDIR": "../../Plugins"}, default_name="Cube"
|
|
236
|
+
)
|
|
237
|
+
dest = result["asset"]
|
|
238
|
+
self.assertEqual(
|
|
239
|
+
dest, os.path.join(self.project, "Assets", "Plugins", "Cube.fbx")
|
|
240
|
+
)
|
|
241
|
+
# The resolved path is genuinely under Assets/ (defensive realpath check).
|
|
242
|
+
assets = os.path.realpath(os.path.join(self.project, "Assets"))
|
|
243
|
+
self.assertTrue(os.path.realpath(dest).startswith(assets))
|
|
244
|
+
|
|
245
|
+
def test_subdir_cannot_escape_assets_via_drive_letter(self):
|
|
246
|
+
# A Windows drive designator in a component ('C:/Models', or the
|
|
247
|
+
# drive-relative 'C:Models') must not make os.path.join produce a path
|
|
248
|
+
# that escapes Assets/ (which then also crashed os.path.relpath cross-drive).
|
|
249
|
+
for raw, expected_sub in (
|
|
250
|
+
("C:/Models", os.path.join("Assets", "Models")),
|
|
251
|
+
("C:Models", os.path.join("Assets", "Models")),
|
|
252
|
+
("C:", "Assets"),
|
|
253
|
+
(r"C:\Models\Hero", os.path.join("Assets", "Models", "Hero")),
|
|
254
|
+
):
|
|
255
|
+
_bridge, result = self._deliver({"ASSETS_SUBDIR": raw}, default_name="Cube")
|
|
256
|
+
self.assertIsNotNone(result, f"raw={raw!r}")
|
|
257
|
+
dest = result["asset"]
|
|
258
|
+
self.assertEqual(
|
|
259
|
+
dest,
|
|
260
|
+
os.path.join(self.project, expected_sub, "Cube.fbx"),
|
|
261
|
+
f"raw={raw!r}",
|
|
262
|
+
)
|
|
263
|
+
# Genuinely under Assets/ (no drive escape).
|
|
264
|
+
assets = os.path.realpath(os.path.join(self.project, "Assets"))
|
|
265
|
+
self.assertTrue(
|
|
266
|
+
os.path.realpath(dest).startswith(assets), f"raw={raw!r}"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def test_explicit_subdir_reducing_to_nothing_drops_into_assets_root(self):
|
|
270
|
+
for raw in (".", "/", "../.."):
|
|
271
|
+
_bridge, result = self._deliver({"ASSETS_SUBDIR": raw}, default_name="Cube")
|
|
272
|
+
self.assertEqual(
|
|
273
|
+
result["asset"], os.path.join(self.project, "Assets", "Cube.fbx"),
|
|
274
|
+
f"raw={raw!r}",
|
|
275
|
+
)
|
|
276
|
+
self.assertEqual(result["subdir"], "")
|
|
277
|
+
|
|
278
|
+
def test_blank_subdir_falls_back_to_default(self):
|
|
279
|
+
for raw in ("", " "):
|
|
280
|
+
_bridge, result = self._deliver({"ASSETS_SUBDIR": raw}, default_name="Cube")
|
|
281
|
+
self.assertEqual(
|
|
282
|
+
result["asset"],
|
|
283
|
+
os.path.join(self.project, "Assets", "Imported", "Cube.fbx"),
|
|
284
|
+
f"raw={raw!r}",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class FileToUnityBridgeTest(unittest.TestCase):
|
|
289
|
+
"""The DCC-free file-driven bridge: pick a model file -> copy into Assets/."""
|
|
290
|
+
|
|
291
|
+
def setUp(self):
|
|
292
|
+
self.tmp = tempfile.mkdtemp()
|
|
293
|
+
self.project = os.path.join(self.tmp, "MyUnityProj")
|
|
294
|
+
os.makedirs(os.path.join(self.project, "Assets"))
|
|
295
|
+
self.fbx = os.path.join(self.tmp, "robot.fbx")
|
|
296
|
+
with open(self.fbx, "wb") as fh:
|
|
297
|
+
fh.write(b"FBX-BYTES")
|
|
298
|
+
|
|
299
|
+
def tearDown(self):
|
|
300
|
+
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
301
|
+
|
|
302
|
+
def test_send_copies_file_into_assets(self):
|
|
303
|
+
bridge = FileToUnityBridge(project_path=self.project)
|
|
304
|
+
result = bridge.send(model_path=self.fbx, params={"ASSETS_SUBDIR": "Models"})
|
|
305
|
+
self.assertIsNotNone(result)
|
|
306
|
+
dest = os.path.join(self.project, "Assets", "Models", "robot.fbx")
|
|
307
|
+
self.assertEqual(result["asset"], dest)
|
|
308
|
+
self.assertTrue(os.path.isfile(dest))
|
|
309
|
+
self.assertFalse(result["launched"])
|
|
310
|
+
|
|
311
|
+
def test_send_default_asset_name_is_file_stem(self):
|
|
312
|
+
bridge = FileToUnityBridge(project_path=self.project)
|
|
313
|
+
result = bridge.send(model_path=self.fbx, params={})
|
|
314
|
+
self.assertEqual(
|
|
315
|
+
result["asset"],
|
|
316
|
+
os.path.join(self.project, "Assets", "Imported", "robot.fbx"),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def test_send_preserves_non_fbx_extension(self):
|
|
320
|
+
obj = os.path.join(self.tmp, "scan.obj")
|
|
321
|
+
with open(obj, "wb") as fh:
|
|
322
|
+
fh.write(b"OBJ")
|
|
323
|
+
bridge = FileToUnityBridge(project_path=self.project)
|
|
324
|
+
result = bridge.send(model_path=obj, params={})
|
|
325
|
+
self.assertEqual(os.path.basename(result["asset"]), "scan.obj")
|
|
326
|
+
|
|
327
|
+
def test_send_missing_file_returns_none(self):
|
|
328
|
+
bridge = FileToUnityBridge(project_path=self.project)
|
|
329
|
+
self.assertIsNone(bridge.send(model_path=os.path.join(self.tmp, "nope.fbx")))
|
|
330
|
+
|
|
331
|
+
def test_send_not_a_project_returns_none(self):
|
|
332
|
+
bridge = FileToUnityBridge(project_path=os.path.join(self.tmp, "not_a_proj"))
|
|
333
|
+
self.assertIsNone(bridge.send(model_path=self.fbx))
|
|
334
|
+
|
|
335
|
+
def test_list_template_modes_matches_deliverer(self):
|
|
336
|
+
bridge = FileToUnityBridge()
|
|
337
|
+
self.assertEqual(
|
|
338
|
+
bridge.list_template_modes(), list(CopyToAssetsDeliverer.DELIVERY_MODES)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
if __name__ == "__main__":
|
|
343
|
+
unittest.main()
|