@mflrevan/ucp 0.6.0 → 0.6.2

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.
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 7b8f17a452e84a2eb71a124ba55cf321
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -168,19 +168,41 @@ namespace UCP.Bridge
168
168
  sceneView.Repaint();
169
169
  SceneView.RepaintAll();
170
170
 
171
+ ResolveSceneViewCamera(sceneView, out var cameraPosition, out var cameraRotation);
172
+
171
173
  return new Dictionary<string, object>
172
174
  {
173
175
  ["status"] = "ok",
174
176
  ["instanceId"] = instanceId,
175
177
  ["name"] = target.name,
176
178
  ["pivot"] = VectorToList(sceneView.pivot),
177
- ["cameraPosition"] = VectorToList(sceneView.camera.transform.position),
178
- ["cameraRotationEuler"] = VectorToList(sceneView.camera.transform.rotation.eulerAngles),
179
+ ["cameraPosition"] = VectorToList(cameraPosition),
180
+ ["cameraRotationEuler"] = VectorToList(cameraRotation.eulerAngles),
179
181
  ["size"] = sceneView.size,
180
182
  ["axis"] = axis.HasValue ? VectorToList(axis.Value.normalized) : null
181
183
  };
182
184
  }
183
185
 
186
+ /// <summary>
187
+ /// Derive the Scene view camera pose from the authoritative view state
188
+ /// (<c>pivot</c>/<c>rotation</c>/<c>cameraDistance</c>) rather than reading
189
+ /// <c>sceneView.camera.transform</c>.
190
+ ///
191
+ /// <c>LookAtDirect</c> updates the view state immediately, but the camera transform is only
192
+ /// synced when the Scene view actually repaints. <c>Repaint()</c> merely queues that, so in
193
+ /// batch mode - and for any caller reading the response in the same frame as the focus -
194
+ /// the camera transform still holds the *previous* pose and the reported values were simply
195
+ /// wrong.
196
+ /// </summary>
197
+ private static void ResolveSceneViewCamera(
198
+ SceneView sceneView,
199
+ out Vector3 position,
200
+ out Quaternion rotation)
201
+ {
202
+ rotation = sceneView.rotation;
203
+ position = sceneView.pivot - (rotation * Vector3.forward) * sceneView.cameraDistance;
204
+ }
205
+
184
206
  private static Vector3? TryReadAxis(Dictionary<string, object> parameters)
185
207
  {
186
208
  if (parameters == null || !parameters.TryGetValue("axis", out var axisObj) || axisObj == null)
@@ -12,29 +12,35 @@ namespace UCP.Bridge
12
12
  router.Register("exec/run", HandleRun);
13
13
  }
14
14
 
15
- private static List<IUCPScript> DiscoverScripts()
15
+ /// <summary>
16
+ /// Implementing types, cached for the lifetime of the app domain. The scan itself is the
17
+ /// expensive part -- `GetTypes()` over every loaded assembly -- and its result cannot go
18
+ /// stale without a domain reload, which resets this static anyway.
19
+ /// </summary>
20
+ private static Type[] s_scriptTypes;
21
+
22
+ private static Type[] DiscoverScriptTypes()
16
23
  {
17
- var scripts = new List<IUCPScript>();
24
+ if (s_scriptTypes != null) return s_scriptTypes;
25
+
18
26
  var interfaceType = typeof(IUCPScript);
27
+ var bridgeAssemblyName = interfaceType.Assembly.GetName().Name;
28
+ var types = new List<Type>();
19
29
 
20
30
  foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
21
31
  {
32
+ // A type can only implement IUCPScript if its assembly references the one that
33
+ // declares it. Checking cheap reference metadata first avoids materialising the
34
+ // full type list of every framework and Unity assembly in the domain.
35
+ if (assembly != interfaceType.Assembly && !ReferencesAssembly(assembly, bridgeAssemblyName))
36
+ continue;
37
+
22
38
  try
23
39
  {
24
40
  foreach (var type in assembly.GetTypes())
25
41
  {
26
42
  if (interfaceType.IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface)
27
- {
28
- try
29
- {
30
- var instance = (IUCPScript)Activator.CreateInstance(type);
31
- scripts.Add(instance);
32
- }
33
- catch (Exception ex)
34
- {
35
- Debug.LogWarning($"[UCP] Failed to instantiate script {type.Name}: {ex.Message}");
36
- }
37
- }
43
+ types.Add(type);
38
44
  }
39
45
  }
40
46
  catch (System.Reflection.ReflectionTypeLoadException)
@@ -43,9 +49,71 @@ namespace UCP.Bridge
43
49
  }
44
50
  }
45
51
 
52
+ s_scriptTypes = types.ToArray();
53
+ return s_scriptTypes;
54
+ }
55
+
56
+ private static bool ReferencesAssembly(System.Reflection.Assembly assembly, string name)
57
+ {
58
+ try
59
+ {
60
+ foreach (var reference in assembly.GetReferencedAssemblies())
61
+ {
62
+ if (string.Equals(reference.Name, name, StringComparison.Ordinal)) return true;
63
+ }
64
+ }
65
+ catch
66
+ {
67
+ // Dynamic assemblies can refuse to report references; treat them as non-matching.
68
+ }
69
+
70
+ return false;
71
+ }
72
+
73
+ private static IUCPScript Instantiate(Type type)
74
+ {
75
+ try
76
+ {
77
+ return (IUCPScript)Activator.CreateInstance(type);
78
+ }
79
+ catch (Exception ex)
80
+ {
81
+ Debug.LogWarning($"[UCP] Failed to instantiate script {type.Name}: {ex.Message}");
82
+ return null;
83
+ }
84
+ }
85
+
86
+ private static List<IUCPScript> DiscoverScripts()
87
+ {
88
+ var scripts = new List<IUCPScript>();
89
+ foreach (var type in DiscoverScriptTypes())
90
+ {
91
+ var instance = Instantiate(type);
92
+ if (instance != null) scripts.Add(instance);
93
+ }
94
+
46
95
  return scripts;
47
96
  }
48
97
 
98
+ /// <summary>
99
+ /// Resolve one script by name, stopping at the first match.
100
+ /// `Name` is an instance member, so candidates must be constructed to be identified --
101
+ /// but running *every* script's constructor to invoke one of them is a side effect nobody
102
+ /// asked for, so stop as soon as the target is found.
103
+ /// </summary>
104
+ private static IUCPScript FindScript(string name)
105
+ {
106
+ foreach (var type in DiscoverScriptTypes())
107
+ {
108
+ var instance = Instantiate(type);
109
+ if (instance == null) continue;
110
+ if (string.Equals(instance.Name, name, StringComparison.OrdinalIgnoreCase))
111
+ return instance;
112
+ }
113
+
114
+ return null;
115
+ }
116
+
49
117
  private static object HandleList(string paramsJson)
50
118
  {
51
119
  var scripts = DiscoverScripts();
@@ -78,16 +146,7 @@ namespace UCP.Bridge
78
146
  if (p.TryGetValue("params", out var paramsObj) && paramsObj != null)
79
147
  scriptParams = MiniJson.Serialize(paramsObj);
80
148
 
81
- var scripts = DiscoverScripts();
82
- IUCPScript target = null;
83
- foreach (var s in scripts)
84
- {
85
- if (string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase))
86
- {
87
- target = s;
88
- break;
89
- }
90
- }
149
+ var target = FindScript(name);
91
150
 
92
151
  if (target == null)
93
152
  throw new ArgumentException($"Script not found: {name}. Use exec/list to see available scripts.");
@@ -54,7 +54,10 @@ namespace UCP.Bridge
54
54
 
55
55
  if (!string.IsNullOrEmpty(filter))
56
56
  {
57
- executionSettings.filters[0].testNames = new[] { filter };
57
+ // groupNames is regex-matched against each test's full name, so a bare class or
58
+ // method name selects what a caller expects. testNames requires an exact
59
+ // fully-qualified match, which silently selected nothing for "ControllerSmokeTests".
60
+ executionSettings.filters[0].groupNames = new[] { filter };
58
61
  }
59
62
 
60
63
  var shouldWaitForPlayModeExit =
@@ -208,6 +211,12 @@ namespace UCP.Bridge
208
211
  return;
209
212
  }
210
213
 
214
+ // A filter that matches nothing still yields a root suite -- childless, and named
215
+ // after the project. Counting it as a leaf reported "1 passed" for a run that
216
+ // executed no tests, so a typo'd filter looked like a green suite.
217
+ if (result.Test != null && result.Test.IsSuite)
218
+ return;
219
+
211
220
  string status;
212
221
  switch (result.TestStatus)
213
222
  {