diff --git a/.agents/skills/uloop-clear-console/SKILL.md b/.agents/skills/uloop-clear-console/SKILL.md index 5266b38938..5c633ca30b 100644 --- a/.agents/skills/uloop-clear-console/SKILL.md +++ b/.agents/skills/uloop-clear-console/SKILL.md @@ -39,11 +39,11 @@ uloop clear-console --add-confirmation-message ## Output Returns JSON with: -- `Success` (boolean): Whether the clear operation succeeded -- `ClearedLogCount` (number): Total number of log entries that were cleared -- `ClearedCounts` (object): Breakdown by log type - - `ErrorCount` (number): Errors cleared - - `WarningCount` (number): Warnings cleared - - `LogCount` (number): Info logs cleared -- `Message` (string): Description of the result; carries the failure summary when the operation fails (e.g. `"Failed to clear console: ..."`) -- `ErrorMessage` (string): Currently always empty for this tool — read `Message` for failure details +- `success` (boolean): Whether the clear operation succeeded +- `clearedLogCount` (number): Total number of log entries that were cleared +- `clearedCounts` (object): Breakdown by log type + - `errorCount` (number): Errors cleared + - `warningCount` (number): Warnings cleared + - `logCount` (number): Info logs cleared +- `message` (string): Description of the result; carries the failure summary when the operation fails (e.g. `"Failed to clear console: ..."`) +- `errorMessage` (string): Currently always empty for this tool — read `message` for failure details diff --git a/.agents/skills/uloop-compile/SKILL.md b/.agents/skills/uloop-compile/SKILL.md index 2efef968d7..9a2780a2a1 100644 --- a/.agents/skills/uloop-compile/SKILL.md +++ b/.agents/skills/uloop-compile/SKILL.md @@ -44,6 +44,7 @@ uloop compile --stop-on-external-scene-changes ## Output Returns JSON: -- `Success`: boolean -- `ErrorCount`: number -- `WarningCount`: number +- `success`: boolean or null +- `errorCount`: number or null +- `warningCount`: number or null +- `message`: string diff --git a/.agents/skills/uloop-control-play-mode/SKILL.md b/.agents/skills/uloop-control-play-mode/SKILL.md index e215f43e7f..662c027629 100644 --- a/.agents/skills/uloop-control-play-mode/SKILL.md +++ b/.agents/skills/uloop-control-play-mode/SKILL.md @@ -49,16 +49,16 @@ uloop control-play-mode --action Step ## Output Returns JSON with the current play mode state: -- `IsPlaying`: Whether Unity is currently in play mode -- `IsPaused`: Whether play mode is paused -- `Changed`: Whether the requested action changed the current play mode state -- `WasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped -- `Message`: Description of the action performed +- `isPlaying`: Whether Unity is currently in play mode +- `isPaused`: Whether play mode is paused +- `changed`: Whether the requested action changed the current play mode state +- `wasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped +- `message`: Description of the action performed ## Notes - Play action starts the game in the Unity Editor (also resumes from pause) -- Stop action exits play mode and returns to edit mode. If Play Mode was already stopped, `Changed` is `false`, `WasAlreadyStopped` is `true`, and `Message` is `Play mode was already stopped`. +- Stop action exits play mode and returns to edit mode. If Play Mode was already stopped, `changed` is `false`, `wasAlreadyStopped` is `true`, and `message` is `Play mode was already stopped`. - Pause action pauses the game while remaining in play mode - Step action advances exactly one frame and leaves play mode paused (the Editor's Next Frame button; independent of Time.timeScale). Requires PlayMode to be running; repeat to walk transitions frame by frame - Useful for automated testing workflows diff --git a/.agents/skills/uloop-execute-dynamic-code/SKILL.md b/.agents/skills/uloop-execute-dynamic-code/SKILL.md index e237a67a7c..77060d33fb 100644 --- a/.agents/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.agents/skills/uloop-execute-dynamic-code/SKILL.md @@ -44,18 +44,18 @@ return x; ## Output Returns JSON: -- `Success`: boolean — overall execution success -- `Result`: string — value of the snippet's `return` statement (empty when omitted) -- `Logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` -- `CompilationErrors`: object[] — Roslyn diagnostics with `Message`, `Line`, `Column`, `ErrorCode`, optional `Hint` and `Suggestions` -- `ErrorMessage`: string — top-level failure summary (empty on success) -- `Error`: string — alias of `ErrorMessage` -- `SecurityLevel`: string — dynamic-code security level active for the request -- `UpdatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) -- `DiagnosticsSummary`: string|null — compact summary when diagnostics are available -- `Diagnostics`: object[] — structured diagnostics; same shape as `CompilationErrors`, usually populated together with it - -On `Success: false`, inspect `CompilationErrors` first. If empty, read `ErrorMessage` (and `Logs` for extra context) — the failure may be a runtime exception, security violation, cancellation, or an "execution in progress" rejection, all of which return empty `CompilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. +- `success`: boolean — overall execution success +- `result`: string — value of the snippet's `return` statement (empty when omitted) +- `logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` +- `compilationErrors`: object[] — Roslyn diagnostics with `message`, `line`, `column`, `errorCode`, optional `hint` and `suggestions` +- `errorMessage`: string — top-level failure summary (empty on success) +- `error`: string — alias of `errorMessage` +- `securityLevel`: string — dynamic-code security level active for the request +- `updatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) +- `diagnosticsSummary`: string|null — compact summary when diagnostics are available +- `diagnostics`: object[] — structured diagnostics; same shape as `compilationErrors`, usually populated together with it + +On `success: false`, inspect `compilationErrors` first. If empty, read `errorMessage` (and `logs` for extra context) — the failure may be a runtime exception, security violation, cancellation, or an "execution in progress" rejection, all of which return empty `compilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. ## Code Examples by Category diff --git a/.agents/skills/uloop-execute-dynamic-code/references/material-operations.md b/.agents/skills/uloop-execute-dynamic-code/references/material-operations.md index 3bd98697ab..4defb5c2fe 100644 --- a/.agents/skills/uloop-execute-dynamic-code/references/material-operations.md +++ b/.agents/skills/uloop-execute-dynamic-code/references/material-operations.md @@ -41,6 +41,10 @@ using UnityEditor; string matPath = "Assets/Materials/MyMaterial.mat"; Material mat = AssetDatabase.LoadAssetAtPath(matPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} mat.SetFloat("_Metallic", 0.8f); mat.SetFloat("_Glossiness", 0.6f); @@ -83,6 +87,10 @@ using UnityEditor; string matPath = "Assets/Materials/MyMaterial.mat"; Material mat = AssetDatabase.LoadAssetAtPath(matPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} GameObject selected = Selection.activeGameObject; if (selected == null) @@ -108,6 +116,10 @@ using UnityEditor; string matPath = "Assets/Materials/MyMaterial.mat"; Material mat = AssetDatabase.LoadAssetAtPath(matPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} mat.EnableKeyword("_EMISSION"); mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive; diff --git a/.agents/skills/uloop-execute-dynamic-code/references/scene-operations.md b/.agents/skills/uloop-execute-dynamic-code/references/scene-operations.md index 7210fbb12c..d34db4c49f 100644 --- a/.agents/skills/uloop-execute-dynamic-code/references/scene-operations.md +++ b/.agents/skills/uloop-execute-dynamic-code/references/scene-operations.md @@ -89,6 +89,8 @@ return $"Set {child.name}'s parent to {parent.name}"; ## Get All Children ```csharp +using System.Collections.Generic; + GameObject parent = Selection.activeGameObject; if (parent == null) { @@ -166,6 +168,8 @@ return $"Created new scene: {newScene.name}"; ## Get All Root GameObjects in Scene ```csharp +using System.Collections.Generic; + UnityEngine.SceneManagement.Scene scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); GameObject[] roots = scene.GetRootGameObjects(); diff --git a/.agents/skills/uloop-focus-window/SKILL.md b/.agents/skills/uloop-focus-window/SKILL.md index 9379ef080b..515bfa29c3 100644 --- a/.agents/skills/uloop-focus-window/SKILL.md +++ b/.agents/skills/uloop-focus-window/SKILL.md @@ -33,8 +33,8 @@ uloop focus-window ## Output Returns JSON with: -- `Success`: Whether the focus operation succeeded -- `Message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) +- `success`: Whether the focus operation succeeded +- `message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) These are the only two fields. There is no PID, window-handle, or platform field returned to the caller. diff --git a/.agents/skills/uloop-get-logs/SKILL.md b/.agents/skills/uloop-get-logs/SKILL.md index dd10950d47..2ecb25aff7 100644 --- a/.agents/skills/uloop-get-logs/SKILL.md +++ b/.agents/skills/uloop-get-logs/SKILL.md @@ -52,13 +52,13 @@ uloop get-logs --search-text "Missing.*Component" --use-regex ## Output Returns JSON with: -- `TotalCount` (number): Total logs available before max-count clipping -- `DisplayedCount` (number): Logs returned in this response (≤ `--max-count`) -- `LogType` (string): The `--log-type` filter that was applied -- `MaxCount` (number): The `--max-count` cap that was applied -- `SearchText` (string): The `--search-text` filter that was applied (empty when omitted) -- `IncludeStackTrace` (boolean): Whether stack traces are included in `Logs[]` -- `Logs` (array): Each entry has: - - `Type` (string): `"Error"`, `"Warning"`, or `"Log"` - - `Message` (string): Log message body - - `StackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. +- `totalCount` (number): Total logs available before max-count clipping +- `displayedCount` (number): Logs returned in this response (≤ `--max-count`) +- `logType` (string): The `--log-type` filter that was applied +- `maxCount` (number): The `--max-count` cap that was applied +- `searchText` (string): The `--search-text` filter that was applied (empty when omitted) +- `includeStackTrace` (boolean): Whether stack traces are included in `logs[]` +- `logs` (array): Each entry has: + - `type` (string): `"Error"`, `"Warning"`, or `"Log"` + - `message` (string): Log message body + - `stackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. diff --git a/.agents/skills/uloop-hello-world/SKILL.md b/.agents/skills/uloop-hello-world/SKILL.md index 95a3fb2cea..2724067ddd 100644 --- a/.agents/skills/uloop-hello-world/SKILL.md +++ b/.agents/skills/uloop-hello-world/SKILL.md @@ -40,9 +40,9 @@ uloop hello-world --name "Carlos" --language spanish --no-include-timestamp ## Output Returns JSON with: -- `Message`: The greeting message -- `Language`: Language used for greeting -- `Timestamp`: Current timestamp (if enabled) +- `message`: The greeting message +- `language`: Language used for greeting +- `timestamp`: Current timestamp (if enabled) ## Notes diff --git a/.agents/skills/uloop-launch/SKILL.md b/.agents/skills/uloop-launch/SKILL.md index 7d6b06e1fe..2bf0e0929b 100644 --- a/.agents/skills/uloop-launch/SKILL.md +++ b/.agents/skills/uloop-launch/SKILL.md @@ -50,19 +50,19 @@ uloop launch --quit - Prints detected Unity version - Prints project path - If Unity is already running, focuses the existing window and verifies tool readiness -- If the process scan is blocked by the environment (e.g. sandboxed `ps`), plain launch falls back to probing the project IPC; when Unity responds it reports `AlreadyRunning: true` without focusing the window instead of failing. `--restart` and `--quit` still fail because they need the process id +- If the process scan is blocked by the environment (e.g. sandboxed `ps`), plain launch falls back to probing the project IPC; when Unity responds it reports `alreadyRunning: true` without focusing the window instead of failing. `--restart` and `--quit` still fail because they need the process id - If launching or restarting, prints when it is waiting for Unity CLI Loop server readiness - If launching or restarting, waits until Unity finishes startup and the CLI can connect to the project - Successful launch, restart, existing-process, and quit paths return JSON with: - - `Success`: whether the command completed - - `Ready`: whether Unity CLI Loop is ready for commands - - `ServerReady`: whether the Unity CLI Loop server accepted requests - - `ProjectIpcReady`: whether the project IPC path accepted tool requests - - `AlreadyRunning`: whether an existing Unity process was reused - - `Launched`: whether this command launched a Unity process - - `Restarted`: whether this command stopped an existing process and launched a new one - - `Quit`: whether this command stopped Unity without launching a new process - - `PreviousProcessId`: process ID stopped by restart or quit, when available - - `CurrentProcessId`: current Unity process ID, when available - - `ProjectRoot`: resolved project root - - `Message`: readiness summary + - `success`: whether the command completed + - `ready`: whether Unity CLI Loop is ready for commands + - `serverReady`: whether the Unity CLI Loop server accepted requests + - `projectIpcReady`: whether the project IPC path accepted tool requests + - `alreadyRunning`: whether an existing Unity process was reused + - `launched`: whether this command launched a Unity process + - `restarted`: whether this command stopped an existing process and launched a new one + - `quit`: whether this command stopped Unity without launching a new process + - `previousProcessId`: process ID stopped by restart or quit, when available + - `currentProcessId`: current Unity process ID, when available + - `projectRoot`: resolved project root + - `message`: readiness summary diff --git a/.agents/skills/uloop-record-input/SKILL.md b/.agents/skills/uloop-record-input/SKILL.md index 2e824ff6a8..6b1c530f38 100644 --- a/.agents/skills/uloop-record-input/SKILL.md +++ b/.agents/skills/uloop-record-input/SKILL.md @@ -45,11 +45,11 @@ Replay injects input frame-by-frame, so the game must also be deterministic to p ## Output The CLI prints JSON with: -- `Success`: Whether the operation succeeded -- `Message`: Status message -- `Action`: Echoes which action was executed (`Start` or `Stop`) -- `OutputPath`: Path to saved recording (nullable; populated on `Stop` only) -- `TotalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) -- `DurationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) - -The CLI output contains only these six payload fields. Internal metadata such as `Ver` is removed before printing. There is no `RecordingId`, `StartTimestamp`, `KeysCaptured`, or per-frame data in the response — frame data lives only in the JSON file at `OutputPath`. +- `success`: Whether the operation succeeded +- `message`: Status message +- `action`: Echoes which action was executed (`Start` or `Stop`) +- `outputPath`: Path to saved recording (nullable; populated on `Stop` only) +- `totalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) +- `durationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) + +The CLI output contains only these six payload fields. Internal metadata such as `ver` is removed before printing. There is no `recordingId`, `startTimestamp`, `keysCaptured`, or per-frame data in the response — frame data lives only in the JSON file at `outputPath`. diff --git a/.agents/skills/uloop-record-input/references/deterministic-replay.md b/.agents/skills/uloop-record-input/references/deterministic-replay.md index d2a8b26489..1250b33564 100644 --- a/.agents/skills/uloop-record-input/references/deterministic-replay.md +++ b/.agents/skills/uloop-record-input/references/deterministic-replay.md @@ -7,9 +7,9 @@ Replay injects input frame-by-frame, so the game must produce identical results | `Time.deltaTime` for movement | Fixed per-frame constant (e.g. `MOVE_SPEED = 0.1f`) | Delta time varies between runs even at the same target frame rate | | `Random.Range()` / `UnityEngine.Random` | Seeded random (`new System.Random(fixedSeed)`) or remove randomness | Random sequences differ between runs | | `Rigidbody` / Physics simulation | Kinematic movement via `Transform.Translate` | Physics is non-deterministic across runs | -| `WaitForSeconds(n)` in coroutines | `WaitForEndOfFrame` or frame counting | Real-time waits depend on frame timing | +| `WaitForSeconds(n)` in coroutines | Frame counting | Real-time waits depend on frame timing | | `Time.time` / `Time.realtimeSinceStartup` | Frame counter (`Time.frameCount - startFrame`) | Time values drift between runs | -| `FindObjectsOfType` without sort | `FindObjectsByType(FindObjectsSortMode.InstanceID)` | Iteration order is non-deterministic | +| `FindObjectsOfType` without sort | Serialized object lists or sorting by a stable project-defined key | Unity object iteration order is not replay-stable | | `async/await` with `Task.Delay` | Frame-based waiting | Real-time delays are non-deterministic | -Set `Application.targetFrameRate = 60` or another fixed target to reduce frame timing variance. See `InputReplayVerificationController` for a complete example of deterministic game logic. +Set `QualitySettings.vSyncCount = 0` before `Application.targetFrameRate = 60` or another fixed target to reduce frame timing variance. See `InputReplayVerificationController` for a complete example of deterministic game logic. diff --git a/.agents/skills/uloop-replay-input/SKILL.md b/.agents/skills/uloop-replay-input/SKILL.md index 3ca4c78ac2..d69e1ad07e 100644 --- a/.agents/skills/uloop-replay-input/SKILL.md +++ b/.agents/skills/uloop-replay-input/SKILL.md @@ -49,13 +49,13 @@ Replay injects the exact same input frame-by-frame, but the game must also be de ## Output Returns JSON with: -- `Success`: Whether the operation succeeded -- `Message`: Status message -- `Action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) -- `InputPath`: Path to recording file (nullable string; populated on `Start` only) -- `CurrentFrame`: Current replay frame index (nullable int) -- `TotalFrames`: Total frames in the recording (nullable int) -- `Progress`: Replay progress (nullable float in 0.0 – 1.0) -- `IsReplaying`: Whether replay is currently active (nullable bool) - -These are the only eight fields. There is no `LoopCount`, `ElapsedSeconds`, `OverlayVisible`, or per-frame inspection data in the response. +- `success`: Whether the operation succeeded +- `message`: Status message +- `action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) +- `inputPath`: Path to recording file (nullable string; populated on `Start` only) +- `currentFrame`: Current replay frame index (nullable int) +- `totalFrames`: Total frames in the recording (nullable int) +- `progress`: Replay progress (nullable float in 0.0 – 1.0) +- `isReplaying`: Whether replay is currently active (nullable bool) + +These are the only eight fields. There is no `loopCount`, `elapsedSeconds`, `overlayVisible`, or per-frame inspection data in the response. diff --git a/.agents/skills/uloop-replay-input/references/deterministic-replay.md b/.agents/skills/uloop-replay-input/references/deterministic-replay.md index d2a8b26489..1250b33564 100644 --- a/.agents/skills/uloop-replay-input/references/deterministic-replay.md +++ b/.agents/skills/uloop-replay-input/references/deterministic-replay.md @@ -7,9 +7,9 @@ Replay injects input frame-by-frame, so the game must produce identical results | `Time.deltaTime` for movement | Fixed per-frame constant (e.g. `MOVE_SPEED = 0.1f`) | Delta time varies between runs even at the same target frame rate | | `Random.Range()` / `UnityEngine.Random` | Seeded random (`new System.Random(fixedSeed)`) or remove randomness | Random sequences differ between runs | | `Rigidbody` / Physics simulation | Kinematic movement via `Transform.Translate` | Physics is non-deterministic across runs | -| `WaitForSeconds(n)` in coroutines | `WaitForEndOfFrame` or frame counting | Real-time waits depend on frame timing | +| `WaitForSeconds(n)` in coroutines | Frame counting | Real-time waits depend on frame timing | | `Time.time` / `Time.realtimeSinceStartup` | Frame counter (`Time.frameCount - startFrame`) | Time values drift between runs | -| `FindObjectsOfType` without sort | `FindObjectsByType(FindObjectsSortMode.InstanceID)` | Iteration order is non-deterministic | +| `FindObjectsOfType` without sort | Serialized object lists or sorting by a stable project-defined key | Unity object iteration order is not replay-stable | | `async/await` with `Task.Delay` | Frame-based waiting | Real-time delays are non-deterministic | -Set `Application.targetFrameRate = 60` or another fixed target to reduce frame timing variance. See `InputReplayVerificationController` for a complete example of deterministic game logic. +Set `QualitySettings.vSyncCount = 0` before `Application.targetFrameRate = 60` or another fixed target to reduce frame timing variance. See `InputReplayVerificationController` for a complete example of deterministic game logic. diff --git a/.agents/skills/uloop-run-tests/SKILL.md b/.agents/skills/uloop-run-tests/SKILL.md index 007d2efa5d..ba6909c219 100644 --- a/.agents/skills/uloop-run-tests/SKILL.md +++ b/.agents/skills/uloop-run-tests/SKILL.md @@ -6,13 +6,13 @@ description: "Run Unity Test Runner and report detailed results. Use for EditMod # uloop run-tests -Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `XmlPath` for detailed failure diagnosis. +Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `xmlPath` for detailed failure diagnosis. Before running `uloop run-tests`, run `uloop compile` for the same Unity project when the current task created, deleted, renamed, moved, or edited C# source files, test files, `.asmdef`, `.asmref`, package manifest files, or scripting define settings. This refreshes the AssetDatabase, lets Unity discover new tests, and surfaces compile errors before test execution. You may skip this compile step when rerunning tests without code or assembly-definition changes since the last successful compile. -Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `Success: false`, keeps `TestCount` at `0`, lists the unsaved items in `Message`, and does not start the Unity Test Runner. +Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `success: false`, keeps `testCount` at `0`, lists the unsaved items in `message`, and does not start the Unity Test Runner. -When a run returns `Status: "NoTestsFound"` or `NoTestsFound: true`, no matching tests were discovered. This is not the same as a failed test case: `HasFailures` remains `false`, `FailedCount` remains `0`, and `NoTestsFoundExplanation` explains the zero-discovery state for agents. When an unfiltered `--filter-type all` run also has `Message` set to the no-tests message, read the `Message` for asmdef hints. `run-tests` only appends those hints when it detects likely test assembly configuration issues; exact, regex, and assembly filter misses keep the plain no-tests message. +When a run returns `status: "NoTestsFound"` or `noTestsFound: true`, no matching tests were discovered. This is not the same as a failed test case: `hasFailures` remains `false`, `failedCount` remains `0`, and `noTestsFoundExplanation` explains the zero-discovery state for agents. When an unfiltered `--filter-type all` run also has `message` set to the no-tests message, read the `message` for asmdef hints. `run-tests` only appends those hints when it detects likely test assembly configuration issues; exact, regex, and assembly filter misses keep the plain no-tests message. ## Usage @@ -57,18 +57,18 @@ uloop run-tests --filter-type regex --filter-value ".*Integration.*" ## Output Returns JSON with: -- `Success` (boolean): Whether all tests passed -- `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` -- `HasFailures` (boolean): Whether any discovered test failed -- `Message` (string): Summary message -- `NoTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests -- `NoTestsFoundExplanation` (string): Agent-facing explanation when `NoTestsFound` is true; empty otherwise -- `CompletedAt` (string): ISO timestamp when the run finished -- `TestCount` (number): Total tests executed -- `PassedCount` (number): Passed tests -- `FailedCount` (number): Failed tests -- `SkippedCount` (number): Skipped tests -- `XmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `Success: true`); populated only when tests failed and the XML file exists on disk. +- `success` (boolean): Whether all tests passed +- `status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` +- `hasFailures` (boolean): Whether any discovered test failed +- `message` (string): Summary message +- `noTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests +- `noTestsFoundExplanation` (string): Agent-facing explanation when `noTestsFound` is true; empty otherwise +- `completedAt` (string): ISO timestamp when the run finished +- `testCount` (number): Total tests executed +- `passedCount` (number): Passed tests +- `failedCount` (number): Failed tests +- `skippedCount` (number): Skipped tests +- `xmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `success: true`); populated only when tests failed and the XML file exists on disk. ### XML Result File diff --git a/.agents/skills/uloop-screenshot/SKILL.md b/.agents/skills/uloop-screenshot/SKILL.md index a43b0ab808..d10d199ea2 100644 --- a/.agents/skills/uloop-screenshot/SKILL.md +++ b/.agents/skills/uloop-screenshot/SKILL.md @@ -75,18 +75,18 @@ uloop screenshot --window-name Scene --resolution-scale 0.5 --output-directory / ## Output Returns JSON with: -- `ScreenshotCount`: Number of windows captured -- `Screenshots`: Array of screenshot info, each containing: - - `ImagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. - - `FileSizeBytes`: Size of the saved file in bytes - - `Width`: Captured image width in pixels - - `Height`: Captured image height in pixels - - `CoordinateSystem`: `"gameView"` or `"window"` - - `ResolutionScale`: Resolution scale used for capture - - `YOffset`: Y offset used for gameView coordinate conversion - - `AnnotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` is used. - -For `AnnotatedElements` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. +- `screenshotCount`: Number of windows captured +- `screenshots`: Array of screenshot info, each containing: + - `imagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. + - `fileSizeBytes`: Size of the saved file in bytes + - `width`: Captured image width in pixels + - `height`: Captured image height in pixels + - `coordinateSystem`: `"gameView"` or `"window"` + - `resolutionScale`: Resolution scale used for capture + - `yOffset`: Y offset used for gameView coordinate conversion + - `annotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` is used. + +For `annotatedElements` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. When multiple windows match (e.g., multiple Inspector windows or when using `contains` mode), all matching windows are captured with numbered filenames (e.g., `Inspector_1_*.png`, `Inspector_2_*.png`). diff --git a/.agents/skills/uloop-screenshot/references/annotated-elements.md b/.agents/skills/uloop-screenshot/references/annotated-elements.md index 283109ccc2..50b899b096 100644 --- a/.agents/skills/uloop-screenshot/references/annotated-elements.md +++ b/.agents/skills/uloop-screenshot/references/annotated-elements.md @@ -2,34 +2,34 @@ Read this when using `uloop screenshot --capture-mode rendering --annotate-elements` to find coordinates for `simulate-mouse-ui` or `simulate-mouse-input`. -## AnnotatedElements Fields +## annotatedElements Fields -`AnnotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: +`annotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: -- `Label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. -- `Name`: Element name -- `Path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. -- `Type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) -- `Interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. -- `SimX`, `SimY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. -- `BoundsMinX`, `BoundsMinY`, `BoundsMaxX`, `BoundsMaxY`: Bounding box in simulate-mouse coordinates -- `SortingOrder`: Canvas sorting order. Higher values are in front. -- `SiblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. +- `label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. +- `name`: Element name +- `path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. +- `type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) +- `interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. +- `simX`, `simY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. +- `boundsMinX`, `boundsMinY`, `boundsMaxX`, `boundsMaxY`: Bounding box in simulate-mouse coordinates +- `sortingOrder`: Canvas sorting order. Higher values are in front. +- `siblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. ## Coordinate Conversion -When `CoordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: +When `coordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: ```text -sim_x = image_x / ResolutionScale -sim_y = image_y / ResolutionScale + YOffset +sim_x = image_x / resolutionScale +sim_y = image_y / resolutionScale + yOffset ``` -When `ResolutionScale` is `1.0`, this simplifies to: +When `resolutionScale` is `1.0`, this simplifies to: ```text sim_x = image_x -sim_y = image_y + YOffset +sim_y = image_y + yOffset ``` ## Annotation Readability diff --git a/.agents/skills/uloop-simulate-keyboard/SKILL.md b/.agents/skills/uloop-simulate-keyboard/SKILL.md index 2ca050cb47..6cb374cc93 100644 --- a/.agents/skills/uloop-simulate-keyboard/SKILL.md +++ b/.agents/skills/uloop-simulate-keyboard/SKILL.md @@ -49,8 +49,8 @@ Use `KeyDown` / `KeyUp` when the scenario intentionally needs a held key. - Use `UloopPausePoint.Pause("")` with `uloop-wait-for-pause-point` as the standard frame proof when this input drives a state transition you are verifying. Final logs, screenshots, and durable state supplement the paused-frame check but do not replace it. - Put the marker at a natural state transition after the app consumed the key, such as after a command is accepted, a state mutation is committed, an evaluation step resolves, or a dependent component is updated. Do not place it immediately after sending `simulate-keyboard`. - If the key handler has local variables, intermediate calculations, or branch reasons that `execute-dynamic-code` cannot inspect after the fact, log just those values near `UloopPausePoint.Pause("")` and read them with `uloop-get-logs` while Unity is paused. A pause point hit proves the line was reached, not the frame-local values. -- Treat `simulate-keyboard Success=true`, generic action logs, and final durable counters as useful evidence, but not as paused-frame proof. -- If the response has `InterruptedByPausePoint: true`, Unity is paused for inspection and the tool released its held input bookkeeping. If a `UloopPausePoint.Pause` marker caused the pause, `PausePointId` and `PausePointHitCount` identify it. Use `uloop get-logs`, `uloop get-hierarchy`, `uloop find-game-objects`, or `uloop execute-dynamic-code` before resuming. +- Treat `simulate-keyboard success=true`, generic action logs, and final durable counters as useful evidence, but not as paused-frame proof. +- If the response has `interruptedByPausePoint: true`, Unity is paused for inspection and the tool released its held input bookkeeping. If a `UloopPausePoint.Pause` marker caused the pause, `pausePointId` and `pausePointHitCount` identify it. Use `uloop get-logs`, `uloop get-hierarchy`, `uloop find-game-objects`, or `uloop execute-dynamic-code` before resuming. - Use distinct marker ids for strict phases, for example `input-read`, `state-updated`, and `result-committed`. ### KeyDown/KeyUp Rules @@ -90,15 +90,15 @@ uloop simulate-keyboard --action KeyUp --key LeftShift ## Output Returns JSON with: -- `Success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) -- `Message` (string): Description of what happened or why it failed -- `Action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) -- `KeyName` (string, nullable): The key that was acted on; may be `null` when the action could not resolve a key -- `InterruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `PausePointId` (string, nullable): The marker id when a `UloopPausePoint.Pause` marker caused the interruption -- `PausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker caused the interruption -- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one -- `PressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was actually visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge (e.g. the press was consumed by an editor-only input update) — retry the input or verify with a focused log instead of trusting `Success` alone. `null` only for `KeyUp` and for timed-out responses; pause-point interruptions still report the observed value +- `success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) +- `message` (string): Description of what happened or why it failed +- `action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) +- `keyName` (string, nullable): The key that was acted on; may be `null` when the action could not resolve a key +- `interruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released +- `pausePointId` (string, nullable): The marker id when a `UloopPausePoint.Pause` marker caused the interruption +- `pausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker caused the interruption +- `pausePointHits` (array, nullable): Every marker hit during this input as `{id, hitCount}` entries, in hit order. Read this when one input may trigger several markers; `pausePointId` only names the latest one +- `pressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was actually visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge (e.g. the press was consumed by an editor-only input update) — retry the input or verify with a focused log instead of trusting `success` alone. `null` only for `KeyUp` and for timed-out responses; pause-point interruptions still report the observed value ## Prerequisites diff --git a/.agents/skills/uloop-simulate-mouse-input/SKILL.md b/.agents/skills/uloop-simulate-mouse-input/SKILL.md index 55bb8b634d..12b73569e4 100644 --- a/.agents/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-input/SKILL.md @@ -53,7 +53,7 @@ uloop simulate-mouse-input --action [options] - Use `UloopPausePoint.Pause("")` with `uloop-wait-for-pause-point` as the standard frame proof when this input drives a state transition you are verifying. Final logs, screenshots, and durable state supplement the paused-frame check but do not replace it. - Place the pause point at a natural state transition after the app consumed the mouse input, such as after a command is accepted, a state mutation is committed, an evaluation step resolves, a tracked value changes, or a dependent component is updated. Do not place it immediately after sending `simulate-mouse-input`. - If the mouse handler has local variables, intermediate calculations, or branch reasons that `execute-dynamic-code` cannot inspect after the fact, log just those values near `UloopPausePoint.Pause("")` and read them with `uloop-get-logs` while Unity is paused. A pause point hit proves the line was reached, not the frame-local values. -- If the response has `InterruptedByPausePoint: true`, Unity is paused for inspection and the tool released its held input bookkeeping. `PausePointId` and `PausePointHitCount` identify the pause point that paused Unity. Use `uloop get-logs`, `uloop get-hierarchy`, `uloop find-game-objects`, or `uloop execute-dynamic-code` before resuming. +- If the response has `interruptedByPausePoint: true`, Unity is paused for inspection and the tool released its held input bookkeeping. `pausePointId` and `pausePointHitCount` identify the pause point that paused Unity. Use `uloop get-logs`, `uloop get-hierarchy`, `uloop find-game-objects`, or `uloop execute-dynamic-code` before resuming. - Use distinct ids for strict phases, for example `input-read`, `state-updated`, and `result-committed`. - Remove temporary pause-point/log instrumentation before final validation when it was added only for inspection. @@ -110,15 +110,15 @@ uloop simulate-mouse-input --action SmoothDelta --delta-x 300 --delta-y 0 --dura ## Output Returns JSON with: -- `Success`: Whether the operation succeeded -- `Message`: Status message -- `Action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) -- `Button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) -- `PositionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) -- `PositionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) -- `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `PausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption -- `PausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` -- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one - -There is no `DeltaX`, `DeltaY`, `ScrollX`, `ScrollY`, `Duration`, or hit-element field in the response — only the issued action, button, target position, and Pause Point interruption state are echoed back. Verify visual outcome with a follow-up screenshot. +- `success`: Whether the operation succeeded +- `message`: Status message +- `action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) +- `button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) +- `positionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) +- `positionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) +- `interruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released +- `pausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption +- `pausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` +- `pausePointHits` (array, nullable): Every marker hit during this input as `{id, hitCount}` entries, in hit order. Read this when one input may trigger several markers; `pausePointId` only names the latest one + +There is no `deltaX`, `deltaY`, `scrollX`, `scrollY`, `duration`, or hit-element field in the response — only the issued action, button, target position, and Pause Point interruption state are echoed back. Verify visual outcome with a follow-up screenshot. diff --git a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md index a400302376..f5e2799aec 100644 --- a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md @@ -13,7 +13,7 @@ Simulate mouse interaction on Unity PlayMode UI. 1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not) 2. Get UI element info: `uloop screenshot --capture-mode rendering --annotate-elements --elements-only` -3. Use the `AnnotatedElements` array to find the target element by `Label`, `Name`, or `Path` (A=frontmost, B=next, ...). Use `Interaction` to distinguish click targets from drag/drop/text targets, then use `SimX`/`SimY` directly as `--x`/`--y` coordinates. +3. Use the `annotatedElements` array to find the target element by `label`, `name`, or `path` (A=frontmost, B=next, ...). Use `interaction` to distinguish click targets from drag/drop/text targets, then use `simX`/`simY` directly as `--x`/`--y` coordinates. 4. Execute the needed `uloop simulate-mouse-ui` commands 5. Inspect the result with the lightest useful evidence: runtime state, logs, or a screenshot 6. When this UI input verifies a state transition, use Pause Point inspection from the section below as the standard frame proof @@ -38,7 +38,7 @@ uloop simulate-mouse-ui --action --x --y [options] | `--duration` | number | `0.5` | Hold duration in seconds for LongPress action. | | `--button` | enum | `Left` | Mouse button. `Click` and `LongPress` support `Left`, `Right`, and `Middle`. Drag actions support `Left` only; other buttons return an error. | | `--bypass-raycast` | flag | - | For `Click`, `LongPress`, `Drag`, and `DragStart`, bypass EventSystem raycast and dispatch pointer events directly to `--target-path`. Use when a raycast-blocking overlay visually covers the intended target. | -| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `AnnotatedElements[].Path` from screenshot JSON. | +| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `annotatedElements[].path` from screenshot JSON. | | `--drop-target-path` | string | `""` | Optional hierarchy path of a drop target for `Drag` or `DragEnd`, for example `Canvas/DropZone`. Use this when the drop zone is also behind a raycast blocker. | ### Actions @@ -70,9 +70,9 @@ uloop simulate-mouse-ui --action --x --y [options] - Origin is **top-left** (0, 0) - All positions are in **screen pixels** -- Get coordinates from `AnnotatedElements` JSON (`SimX`/`SimY`) — do NOT look up GameObject positions +- Get coordinates from `annotatedElements` JSON (`simX`/`simY`) — do NOT look up GameObject positions - Clicking or long-pressing on empty space (no UI element) still succeeds with a message indicating no element was hit -- Dragging on empty space (no draggable UI element) returns `Success = false` +- Dragging on empty space (no draggable UI element) returns `success = false` - `--bypass-raycast` still uses coordinates for pointer event positions, but chooses the clicked, long-pressed, or dragged GameObject by `--target-path` - If `--target-path` or `--drop-target-path` matches multiple active GameObjects, the command fails instead of choosing an arbitrary duplicate @@ -81,7 +81,7 @@ uloop simulate-mouse-ui --action --x --y [options] - Use `UloopPausePoint.Pause("")` with `uloop wait-for-pause-point` as the standard frame proof when this input drives a state transition you are verifying. Final logs, screenshots, and durable state supplement the paused-frame check but do not replace it. - Place the pause point at a natural transition after the app consumed the UI event, such as after a command is accepted, a state mutation is committed, a tracked value changes, a UI/domain state syncs, or a success/failure/end condition is entered. - If the UI handler has local variables, intermediate calculations, or branch reasons that `uloop execute-dynamic-code` cannot inspect after the fact, log just those values near `UloopPausePoint.Pause("")` and read them with `uloop get-logs` while Unity is paused. A pause point hit proves the line was reached, not the frame-local values. -- Treat `uloop simulate-mouse-ui` `Success=true`, generic action logs, and final durable counters as useful evidence, not paused-frame proof. +- Treat `uloop simulate-mouse-ui` `success=true`, generic action logs, and final durable counters as useful evidence, not paused-frame proof. - If a `UloopPausePoint.Pause` pauses Unity, inspect with `uloop get-logs`, `uloop get-hierarchy`, `uloop find-game-objects`, or `uloop execute-dynamic-code` before resuming. - Remove temporary pause-point/log instrumentation before final validation when it was added only for inspection. @@ -129,15 +129,15 @@ uloop simulate-mouse-ui --action DragEnd --x 600 --y 300 ## Output Returns JSON with: -- `Success`: Whether the operation succeeded -- `Message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") -- `Action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) -- `HitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) -- `PositionX`: Target X coordinate that was used -- `PositionY`: Target Y coordinate that was used -- `EndPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) -- `EndPositionY`: Drag end Y coordinate (nullable float; populated for drag actions only) - -These are the only eight fields. There is no `Button`, `Duration`, `DragSpeed`, raycast list, or pointer-event log in the response — verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. - -Note: Click and LongPress on empty space (no UI element) still return `Success = true` with `HitGameObjectName = null`. Drag actions on empty space return `Success = false`. +- `success`: Whether the operation succeeded +- `message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") +- `action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) +- `hitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) +- `positionX`: Target X coordinate that was used +- `positionY`: Target Y coordinate that was used +- `endPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) +- `endPositionY`: Drag end Y coordinate (nullable float; populated for drag actions only) + +These are the only eight fields. There is no `button`, `duration`, `dragSpeed`, raycast list, or pointer-event log in the response — verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. + +Note: Click and LongPress on empty space (no UI element) still return `success = true` with `hitGameObjectName = null`. Drag actions on empty space return `success = false`. diff --git a/.agents/skills/uloop-wait-for-pause-point/SKILL.md b/.agents/skills/uloop-wait-for-pause-point/SKILL.md index 6f3b57f2a9..430cc002c8 100644 --- a/.agents/skills/uloop-wait-for-pause-point/SKILL.md +++ b/.agents/skills/uloop-wait-for-pause-point/SKILL.md @@ -24,20 +24,20 @@ uloop enable-pause-point --id state-transition-applied --timeout-seconds 30 ``` 3. Trigger the action with a `simulate-*` command. -4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `InterruptedByPausePoint=true`: +4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `interruptedByPausePoint=true`: ```bash uloop wait-for-pause-point --id state-transition-applied --timeout-seconds 30 ``` -The hit response always embeds the log entries matching the marker id as `MatchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `MatchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. +The hit response always embeds the log entries matching the marker id as `matchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `matchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. -Read `EvidenceSummary` first when it is present. It groups `EditorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `EditorState.CapturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. +Read `evidenceSummary` first when it is present. It groups `editorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `editorState.capturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. -Use `Generation`, `EnabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `RemainingMilliseconds` and `Expired` are returned directly so you do not need to infer marker lifetime from elapsed time. +Use `generation`, `enabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `remainingMilliseconds` and `expired` are returned directly so you do not need to infer marker lifetime from elapsed time. 5. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. -6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `EditorState` describes Unity Editor play/pause state, not marker state. +6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `editorState` describes Unity Editor play/pause state, not marker state. ## When To Use @@ -47,12 +47,12 @@ Use `Generation`, `EnabledAtUtc`, and the hit sequence fields from the hit or st - If the value you need is a method local, an intermediate calculation, or a branch reason that `execute-dynamic-code` cannot reach, add a focused `Debug.Log` immediately before the marker and read it with `get-logs` while paused. Do not count the breakpoint check complete until the matching log has been read. - Good pause points include after input is consumed, a command is accepted, a state mutation is committed, an evaluation step resolves, a tracked value changes, a UI/domain state syncs, or a success/failure/end condition is entered. - Treat the pause like a lightweight breakpoint for one important transition: combine nearby debug logs with paused-frame inspection to confirm the variables and component state at that point. -- Do not treat `simulate-* Success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. +- Do not treat `simulate-* success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. - Skip this only for ordinary persistent-state checks when you are not validating simulated input delivery, event ordering, or transition-frame fidelity. ## Timeout Checks -If this command times out, the marker line was not reached while the command waited. Read `error.details.hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `error.details.recommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `id` and `timeoutSeconds`. Then inspect `error.details.status`, `hitCount`, `generation`, `enabledAtUtc`, `editorState`, `elapsedSinceEnabledMilliseconds`, and `remainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `error.details.MatchingLogs` and `error.details.EvidenceSummary` show whether the marker's focused log ever appeared. `elapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. +If this command times out, the marker line was not reached while the command waited. Read `error.details.hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `error.details.recommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `id` and `timeoutSeconds`. Then inspect `error.details.status`, `hitCount`, `generation`, `enabledAtUtc`, `editorState`, `elapsedSinceEnabledMilliseconds`, and `remainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `error.details.matchingLogs` and `error.details.evidenceSummary` show whether the marker's focused log ever appeared. `elapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. Use `uloop pause-point-status --id state-transition-applied` only when you need to confirm the marker is armed or inspect the current hit state. Add focused debug logs before the marker when local variables must be captured. diff --git a/.claude/skills/simulate-mouse-demo/SKILL.md b/.claude/skills/simulate-mouse-demo/SKILL.md index d16954fab7..5bb28c3070 100644 --- a/.claude/skills/simulate-mouse-demo/SKILL.md +++ b/.claude/skills/simulate-mouse-demo/SKILL.md @@ -36,7 +36,7 @@ Take an annotated screenshot to get exact coordinates for each interactive eleme uloop screenshot --capture-mode rendering --annotate-elements ``` -From the `AnnotatedElements` array in the response, extract `SimX` and `SimY` for: +From the `annotatedElements` array in the response, extract `simX` and `simY` for: - **ClickButton1** — red button - **ClickButton2** — blue button - **DropZone** — the drag target area @@ -45,7 +45,7 @@ From the `AnnotatedElements` array in the response, extract `SimX` and `SimY` fo - **BlueBox** — blue draggable box - **LongPressButton** — purple long-press button -**VirtualPadBackground** is also listed in the `AnnotatedElements` output (Type: "Draggable"). Extract its `SimX` and `SimY` directly — these are used as `` and `` in the command examples below. +**VirtualPadBackground** is also listed in the `annotatedElements` output (`type`: "Draggable"). Extract its `simX` and `simY` directly — these are used as `` and `` in the command examples below. ### Step 2: Click buttons, one-shot drag, and split drag — chain all in one Bash call @@ -81,28 +81,28 @@ Waypoint design (offsets from VirtualPadBackground center): - DragEnd: back to center (0, 0) ```bash -uloop simulate-mouse-ui --action Click --x --y && sleep 0.3 && \ -uloop simulate-mouse-ui --action Click --x --y && sleep 0.3 && \ -uloop simulate-mouse-ui --action Click --x --y && sleep 0.3 && \ -uloop simulate-mouse-ui --action Click --x --y && sleep 0.3 && \ -uloop simulate-mouse-ui --action LongPress --x --y --duration 3.0 && sleep 0.3 && \ -uloop simulate-mouse-ui --action Drag --from-x --from-y --x --y --drag-speed 700 && sleep 0.3 && \ -uloop simulate-mouse-ui --action DragStart --x --y && sleep 0.3 && \ -uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 400 && sleep 0.3 && \ -uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 400 && sleep 0.3 && \ -uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 400 && sleep 0.3 && \ -uloop simulate-mouse-ui --action DragEnd --x --y --drag-speed 400 && sleep 0.3 && \ -uloop simulate-mouse-ui --action Drag --from-x --from-y --x --y --drag-speed 700 && sleep 0.3 && \ -uloop simulate-mouse-ui --action DragStart --x --y && sleep 0.3 && \ -uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ -uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ -uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ -uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ -uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ -uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ -uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ -uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ -uloop simulate-mouse-ui --action DragEnd --x --y --drag-speed 300 +uloop simulate-mouse-ui --action Click --x --y && sleep 0.3 && \ +uloop simulate-mouse-ui --action Click --x --y && sleep 0.3 && \ +uloop simulate-mouse-ui --action Click --x --y && sleep 0.3 && \ +uloop simulate-mouse-ui --action Click --x --y && sleep 0.3 && \ +uloop simulate-mouse-ui --action LongPress --x --y --duration 3.0 && sleep 0.3 && \ +uloop simulate-mouse-ui --action Drag --from-x --from-y --x --y --drag-speed 700 && sleep 0.3 && \ +uloop simulate-mouse-ui --action DragStart --x --y && sleep 0.3 && \ +uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 400 && sleep 0.3 && \ +uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 400 && sleep 0.3 && \ +uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 400 && sleep 0.3 && \ +uloop simulate-mouse-ui --action DragEnd --x --y --drag-speed 400 && sleep 0.3 && \ +uloop simulate-mouse-ui --action Drag --from-x --from-y --x --y --drag-speed 700 && sleep 0.3 && \ +uloop simulate-mouse-ui --action DragStart --x --y && sleep 0.3 && \ +uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ +uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ +uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ +uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ +uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ +uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ +uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ +uloop simulate-mouse-ui --action DragMove --x --y --drag-speed 300 && sleep 0.4 && \ +uloop simulate-mouse-ui --action DragEnd --x --y --drag-speed 300 ``` ### Step 3: Verify results diff --git a/.claude/skills/uloop-clear-console/SKILL.md b/.claude/skills/uloop-clear-console/SKILL.md index 5266b38938..5c633ca30b 100644 --- a/.claude/skills/uloop-clear-console/SKILL.md +++ b/.claude/skills/uloop-clear-console/SKILL.md @@ -39,11 +39,11 @@ uloop clear-console --add-confirmation-message ## Output Returns JSON with: -- `Success` (boolean): Whether the clear operation succeeded -- `ClearedLogCount` (number): Total number of log entries that were cleared -- `ClearedCounts` (object): Breakdown by log type - - `ErrorCount` (number): Errors cleared - - `WarningCount` (number): Warnings cleared - - `LogCount` (number): Info logs cleared -- `Message` (string): Description of the result; carries the failure summary when the operation fails (e.g. `"Failed to clear console: ..."`) -- `ErrorMessage` (string): Currently always empty for this tool — read `Message` for failure details +- `success` (boolean): Whether the clear operation succeeded +- `clearedLogCount` (number): Total number of log entries that were cleared +- `clearedCounts` (object): Breakdown by log type + - `errorCount` (number): Errors cleared + - `warningCount` (number): Warnings cleared + - `logCount` (number): Info logs cleared +- `message` (string): Description of the result; carries the failure summary when the operation fails (e.g. `"Failed to clear console: ..."`) +- `errorMessage` (string): Currently always empty for this tool — read `message` for failure details diff --git a/.claude/skills/uloop-compile/SKILL.md b/.claude/skills/uloop-compile/SKILL.md index 2efef968d7..9a2780a2a1 100644 --- a/.claude/skills/uloop-compile/SKILL.md +++ b/.claude/skills/uloop-compile/SKILL.md @@ -44,6 +44,7 @@ uloop compile --stop-on-external-scene-changes ## Output Returns JSON: -- `Success`: boolean -- `ErrorCount`: number -- `WarningCount`: number +- `success`: boolean or null +- `errorCount`: number or null +- `warningCount`: number or null +- `message`: string diff --git a/.claude/skills/uloop-control-play-mode/SKILL.md b/.claude/skills/uloop-control-play-mode/SKILL.md index e215f43e7f..662c027629 100644 --- a/.claude/skills/uloop-control-play-mode/SKILL.md +++ b/.claude/skills/uloop-control-play-mode/SKILL.md @@ -49,16 +49,16 @@ uloop control-play-mode --action Step ## Output Returns JSON with the current play mode state: -- `IsPlaying`: Whether Unity is currently in play mode -- `IsPaused`: Whether play mode is paused -- `Changed`: Whether the requested action changed the current play mode state -- `WasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped -- `Message`: Description of the action performed +- `isPlaying`: Whether Unity is currently in play mode +- `isPaused`: Whether play mode is paused +- `changed`: Whether the requested action changed the current play mode state +- `wasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped +- `message`: Description of the action performed ## Notes - Play action starts the game in the Unity Editor (also resumes from pause) -- Stop action exits play mode and returns to edit mode. If Play Mode was already stopped, `Changed` is `false`, `WasAlreadyStopped` is `true`, and `Message` is `Play mode was already stopped`. +- Stop action exits play mode and returns to edit mode. If Play Mode was already stopped, `changed` is `false`, `wasAlreadyStopped` is `true`, and `message` is `Play mode was already stopped`. - Pause action pauses the game while remaining in play mode - Step action advances exactly one frame and leaves play mode paused (the Editor's Next Frame button; independent of Time.timeScale). Requires PlayMode to be running; repeat to walk transitions frame by frame - Useful for automated testing workflows diff --git a/.claude/skills/uloop-execute-dynamic-code/SKILL.md b/.claude/skills/uloop-execute-dynamic-code/SKILL.md index e237a67a7c..77060d33fb 100644 --- a/.claude/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.claude/skills/uloop-execute-dynamic-code/SKILL.md @@ -44,18 +44,18 @@ return x; ## Output Returns JSON: -- `Success`: boolean — overall execution success -- `Result`: string — value of the snippet's `return` statement (empty when omitted) -- `Logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` -- `CompilationErrors`: object[] — Roslyn diagnostics with `Message`, `Line`, `Column`, `ErrorCode`, optional `Hint` and `Suggestions` -- `ErrorMessage`: string — top-level failure summary (empty on success) -- `Error`: string — alias of `ErrorMessage` -- `SecurityLevel`: string — dynamic-code security level active for the request -- `UpdatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) -- `DiagnosticsSummary`: string|null — compact summary when diagnostics are available -- `Diagnostics`: object[] — structured diagnostics; same shape as `CompilationErrors`, usually populated together with it - -On `Success: false`, inspect `CompilationErrors` first. If empty, read `ErrorMessage` (and `Logs` for extra context) — the failure may be a runtime exception, security violation, cancellation, or an "execution in progress" rejection, all of which return empty `CompilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. +- `success`: boolean — overall execution success +- `result`: string — value of the snippet's `return` statement (empty when omitted) +- `logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` +- `compilationErrors`: object[] — Roslyn diagnostics with `message`, `line`, `column`, `errorCode`, optional `hint` and `suggestions` +- `errorMessage`: string — top-level failure summary (empty on success) +- `error`: string — alias of `errorMessage` +- `securityLevel`: string — dynamic-code security level active for the request +- `updatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) +- `diagnosticsSummary`: string|null — compact summary when diagnostics are available +- `diagnostics`: object[] — structured diagnostics; same shape as `compilationErrors`, usually populated together with it + +On `success: false`, inspect `compilationErrors` first. If empty, read `errorMessage` (and `logs` for extra context) — the failure may be a runtime exception, security violation, cancellation, or an "execution in progress" rejection, all of which return empty `compilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. ## Code Examples by Category diff --git a/.claude/skills/uloop-execute-dynamic-code/references/material-operations.md b/.claude/skills/uloop-execute-dynamic-code/references/material-operations.md index 3bd98697ab..4defb5c2fe 100644 --- a/.claude/skills/uloop-execute-dynamic-code/references/material-operations.md +++ b/.claude/skills/uloop-execute-dynamic-code/references/material-operations.md @@ -41,6 +41,10 @@ using UnityEditor; string matPath = "Assets/Materials/MyMaterial.mat"; Material mat = AssetDatabase.LoadAssetAtPath(matPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} mat.SetFloat("_Metallic", 0.8f); mat.SetFloat("_Glossiness", 0.6f); @@ -83,6 +87,10 @@ using UnityEditor; string matPath = "Assets/Materials/MyMaterial.mat"; Material mat = AssetDatabase.LoadAssetAtPath(matPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} GameObject selected = Selection.activeGameObject; if (selected == null) @@ -108,6 +116,10 @@ using UnityEditor; string matPath = "Assets/Materials/MyMaterial.mat"; Material mat = AssetDatabase.LoadAssetAtPath(matPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} mat.EnableKeyword("_EMISSION"); mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive; diff --git a/.claude/skills/uloop-execute-dynamic-code/references/scene-operations.md b/.claude/skills/uloop-execute-dynamic-code/references/scene-operations.md index 7210fbb12c..d34db4c49f 100644 --- a/.claude/skills/uloop-execute-dynamic-code/references/scene-operations.md +++ b/.claude/skills/uloop-execute-dynamic-code/references/scene-operations.md @@ -89,6 +89,8 @@ return $"Set {child.name}'s parent to {parent.name}"; ## Get All Children ```csharp +using System.Collections.Generic; + GameObject parent = Selection.activeGameObject; if (parent == null) { @@ -166,6 +168,8 @@ return $"Created new scene: {newScene.name}"; ## Get All Root GameObjects in Scene ```csharp +using System.Collections.Generic; + UnityEngine.SceneManagement.Scene scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); GameObject[] roots = scene.GetRootGameObjects(); diff --git a/.claude/skills/uloop-focus-window/SKILL.md b/.claude/skills/uloop-focus-window/SKILL.md index 9379ef080b..515bfa29c3 100644 --- a/.claude/skills/uloop-focus-window/SKILL.md +++ b/.claude/skills/uloop-focus-window/SKILL.md @@ -33,8 +33,8 @@ uloop focus-window ## Output Returns JSON with: -- `Success`: Whether the focus operation succeeded -- `Message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) +- `success`: Whether the focus operation succeeded +- `message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) These are the only two fields. There is no PID, window-handle, or platform field returned to the caller. diff --git a/.claude/skills/uloop-get-logs/SKILL.md b/.claude/skills/uloop-get-logs/SKILL.md index dd10950d47..2ecb25aff7 100644 --- a/.claude/skills/uloop-get-logs/SKILL.md +++ b/.claude/skills/uloop-get-logs/SKILL.md @@ -52,13 +52,13 @@ uloop get-logs --search-text "Missing.*Component" --use-regex ## Output Returns JSON with: -- `TotalCount` (number): Total logs available before max-count clipping -- `DisplayedCount` (number): Logs returned in this response (≤ `--max-count`) -- `LogType` (string): The `--log-type` filter that was applied -- `MaxCount` (number): The `--max-count` cap that was applied -- `SearchText` (string): The `--search-text` filter that was applied (empty when omitted) -- `IncludeStackTrace` (boolean): Whether stack traces are included in `Logs[]` -- `Logs` (array): Each entry has: - - `Type` (string): `"Error"`, `"Warning"`, or `"Log"` - - `Message` (string): Log message body - - `StackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. +- `totalCount` (number): Total logs available before max-count clipping +- `displayedCount` (number): Logs returned in this response (≤ `--max-count`) +- `logType` (string): The `--log-type` filter that was applied +- `maxCount` (number): The `--max-count` cap that was applied +- `searchText` (string): The `--search-text` filter that was applied (empty when omitted) +- `includeStackTrace` (boolean): Whether stack traces are included in `logs[]` +- `logs` (array): Each entry has: + - `type` (string): `"Error"`, `"Warning"`, or `"Log"` + - `message` (string): Log message body + - `stackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. diff --git a/.claude/skills/uloop-hello-world/SKILL.md b/.claude/skills/uloop-hello-world/SKILL.md index 95a3fb2cea..2724067ddd 100644 --- a/.claude/skills/uloop-hello-world/SKILL.md +++ b/.claude/skills/uloop-hello-world/SKILL.md @@ -40,9 +40,9 @@ uloop hello-world --name "Carlos" --language spanish --no-include-timestamp ## Output Returns JSON with: -- `Message`: The greeting message -- `Language`: Language used for greeting -- `Timestamp`: Current timestamp (if enabled) +- `message`: The greeting message +- `language`: Language used for greeting +- `timestamp`: Current timestamp (if enabled) ## Notes diff --git a/.claude/skills/uloop-launch/SKILL.md b/.claude/skills/uloop-launch/SKILL.md index 7d6b06e1fe..2bf0e0929b 100644 --- a/.claude/skills/uloop-launch/SKILL.md +++ b/.claude/skills/uloop-launch/SKILL.md @@ -50,19 +50,19 @@ uloop launch --quit - Prints detected Unity version - Prints project path - If Unity is already running, focuses the existing window and verifies tool readiness -- If the process scan is blocked by the environment (e.g. sandboxed `ps`), plain launch falls back to probing the project IPC; when Unity responds it reports `AlreadyRunning: true` without focusing the window instead of failing. `--restart` and `--quit` still fail because they need the process id +- If the process scan is blocked by the environment (e.g. sandboxed `ps`), plain launch falls back to probing the project IPC; when Unity responds it reports `alreadyRunning: true` without focusing the window instead of failing. `--restart` and `--quit` still fail because they need the process id - If launching or restarting, prints when it is waiting for Unity CLI Loop server readiness - If launching or restarting, waits until Unity finishes startup and the CLI can connect to the project - Successful launch, restart, existing-process, and quit paths return JSON with: - - `Success`: whether the command completed - - `Ready`: whether Unity CLI Loop is ready for commands - - `ServerReady`: whether the Unity CLI Loop server accepted requests - - `ProjectIpcReady`: whether the project IPC path accepted tool requests - - `AlreadyRunning`: whether an existing Unity process was reused - - `Launched`: whether this command launched a Unity process - - `Restarted`: whether this command stopped an existing process and launched a new one - - `Quit`: whether this command stopped Unity without launching a new process - - `PreviousProcessId`: process ID stopped by restart or quit, when available - - `CurrentProcessId`: current Unity process ID, when available - - `ProjectRoot`: resolved project root - - `Message`: readiness summary + - `success`: whether the command completed + - `ready`: whether Unity CLI Loop is ready for commands + - `serverReady`: whether the Unity CLI Loop server accepted requests + - `projectIpcReady`: whether the project IPC path accepted tool requests + - `alreadyRunning`: whether an existing Unity process was reused + - `launched`: whether this command launched a Unity process + - `restarted`: whether this command stopped an existing process and launched a new one + - `quit`: whether this command stopped Unity without launching a new process + - `previousProcessId`: process ID stopped by restart or quit, when available + - `currentProcessId`: current Unity process ID, when available + - `projectRoot`: resolved project root + - `message`: readiness summary diff --git a/.claude/skills/uloop-record-input/SKILL.md b/.claude/skills/uloop-record-input/SKILL.md index 2e824ff6a8..6b1c530f38 100644 --- a/.claude/skills/uloop-record-input/SKILL.md +++ b/.claude/skills/uloop-record-input/SKILL.md @@ -45,11 +45,11 @@ Replay injects input frame-by-frame, so the game must also be deterministic to p ## Output The CLI prints JSON with: -- `Success`: Whether the operation succeeded -- `Message`: Status message -- `Action`: Echoes which action was executed (`Start` or `Stop`) -- `OutputPath`: Path to saved recording (nullable; populated on `Stop` only) -- `TotalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) -- `DurationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) - -The CLI output contains only these six payload fields. Internal metadata such as `Ver` is removed before printing. There is no `RecordingId`, `StartTimestamp`, `KeysCaptured`, or per-frame data in the response — frame data lives only in the JSON file at `OutputPath`. +- `success`: Whether the operation succeeded +- `message`: Status message +- `action`: Echoes which action was executed (`Start` or `Stop`) +- `outputPath`: Path to saved recording (nullable; populated on `Stop` only) +- `totalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) +- `durationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) + +The CLI output contains only these six payload fields. Internal metadata such as `ver` is removed before printing. There is no `recordingId`, `startTimestamp`, `keysCaptured`, or per-frame data in the response — frame data lives only in the JSON file at `outputPath`. diff --git a/.claude/skills/uloop-record-input/references/deterministic-replay.md b/.claude/skills/uloop-record-input/references/deterministic-replay.md index d2a8b26489..1250b33564 100644 --- a/.claude/skills/uloop-record-input/references/deterministic-replay.md +++ b/.claude/skills/uloop-record-input/references/deterministic-replay.md @@ -7,9 +7,9 @@ Replay injects input frame-by-frame, so the game must produce identical results | `Time.deltaTime` for movement | Fixed per-frame constant (e.g. `MOVE_SPEED = 0.1f`) | Delta time varies between runs even at the same target frame rate | | `Random.Range()` / `UnityEngine.Random` | Seeded random (`new System.Random(fixedSeed)`) or remove randomness | Random sequences differ between runs | | `Rigidbody` / Physics simulation | Kinematic movement via `Transform.Translate` | Physics is non-deterministic across runs | -| `WaitForSeconds(n)` in coroutines | `WaitForEndOfFrame` or frame counting | Real-time waits depend on frame timing | +| `WaitForSeconds(n)` in coroutines | Frame counting | Real-time waits depend on frame timing | | `Time.time` / `Time.realtimeSinceStartup` | Frame counter (`Time.frameCount - startFrame`) | Time values drift between runs | -| `FindObjectsOfType` without sort | `FindObjectsByType(FindObjectsSortMode.InstanceID)` | Iteration order is non-deterministic | +| `FindObjectsOfType` without sort | Serialized object lists or sorting by a stable project-defined key | Unity object iteration order is not replay-stable | | `async/await` with `Task.Delay` | Frame-based waiting | Real-time delays are non-deterministic | -Set `Application.targetFrameRate = 60` or another fixed target to reduce frame timing variance. See `InputReplayVerificationController` for a complete example of deterministic game logic. +Set `QualitySettings.vSyncCount = 0` before `Application.targetFrameRate = 60` or another fixed target to reduce frame timing variance. See `InputReplayVerificationController` for a complete example of deterministic game logic. diff --git a/.claude/skills/uloop-replay-input/SKILL.md b/.claude/skills/uloop-replay-input/SKILL.md index 3ca4c78ac2..d69e1ad07e 100644 --- a/.claude/skills/uloop-replay-input/SKILL.md +++ b/.claude/skills/uloop-replay-input/SKILL.md @@ -49,13 +49,13 @@ Replay injects the exact same input frame-by-frame, but the game must also be de ## Output Returns JSON with: -- `Success`: Whether the operation succeeded -- `Message`: Status message -- `Action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) -- `InputPath`: Path to recording file (nullable string; populated on `Start` only) -- `CurrentFrame`: Current replay frame index (nullable int) -- `TotalFrames`: Total frames in the recording (nullable int) -- `Progress`: Replay progress (nullable float in 0.0 – 1.0) -- `IsReplaying`: Whether replay is currently active (nullable bool) - -These are the only eight fields. There is no `LoopCount`, `ElapsedSeconds`, `OverlayVisible`, or per-frame inspection data in the response. +- `success`: Whether the operation succeeded +- `message`: Status message +- `action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) +- `inputPath`: Path to recording file (nullable string; populated on `Start` only) +- `currentFrame`: Current replay frame index (nullable int) +- `totalFrames`: Total frames in the recording (nullable int) +- `progress`: Replay progress (nullable float in 0.0 – 1.0) +- `isReplaying`: Whether replay is currently active (nullable bool) + +These are the only eight fields. There is no `loopCount`, `elapsedSeconds`, `overlayVisible`, or per-frame inspection data in the response. diff --git a/.claude/skills/uloop-replay-input/references/deterministic-replay.md b/.claude/skills/uloop-replay-input/references/deterministic-replay.md index d2a8b26489..1250b33564 100644 --- a/.claude/skills/uloop-replay-input/references/deterministic-replay.md +++ b/.claude/skills/uloop-replay-input/references/deterministic-replay.md @@ -7,9 +7,9 @@ Replay injects input frame-by-frame, so the game must produce identical results | `Time.deltaTime` for movement | Fixed per-frame constant (e.g. `MOVE_SPEED = 0.1f`) | Delta time varies between runs even at the same target frame rate | | `Random.Range()` / `UnityEngine.Random` | Seeded random (`new System.Random(fixedSeed)`) or remove randomness | Random sequences differ between runs | | `Rigidbody` / Physics simulation | Kinematic movement via `Transform.Translate` | Physics is non-deterministic across runs | -| `WaitForSeconds(n)` in coroutines | `WaitForEndOfFrame` or frame counting | Real-time waits depend on frame timing | +| `WaitForSeconds(n)` in coroutines | Frame counting | Real-time waits depend on frame timing | | `Time.time` / `Time.realtimeSinceStartup` | Frame counter (`Time.frameCount - startFrame`) | Time values drift between runs | -| `FindObjectsOfType` without sort | `FindObjectsByType(FindObjectsSortMode.InstanceID)` | Iteration order is non-deterministic | +| `FindObjectsOfType` without sort | Serialized object lists or sorting by a stable project-defined key | Unity object iteration order is not replay-stable | | `async/await` with `Task.Delay` | Frame-based waiting | Real-time delays are non-deterministic | -Set `Application.targetFrameRate = 60` or another fixed target to reduce frame timing variance. See `InputReplayVerificationController` for a complete example of deterministic game logic. +Set `QualitySettings.vSyncCount = 0` before `Application.targetFrameRate = 60` or another fixed target to reduce frame timing variance. See `InputReplayVerificationController` for a complete example of deterministic game logic. diff --git a/.claude/skills/uloop-run-tests/SKILL.md b/.claude/skills/uloop-run-tests/SKILL.md index 007d2efa5d..ba6909c219 100644 --- a/.claude/skills/uloop-run-tests/SKILL.md +++ b/.claude/skills/uloop-run-tests/SKILL.md @@ -6,13 +6,13 @@ description: "Run Unity Test Runner and report detailed results. Use for EditMod # uloop run-tests -Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `XmlPath` for detailed failure diagnosis. +Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `xmlPath` for detailed failure diagnosis. Before running `uloop run-tests`, run `uloop compile` for the same Unity project when the current task created, deleted, renamed, moved, or edited C# source files, test files, `.asmdef`, `.asmref`, package manifest files, or scripting define settings. This refreshes the AssetDatabase, lets Unity discover new tests, and surfaces compile errors before test execution. You may skip this compile step when rerunning tests without code or assembly-definition changes since the last successful compile. -Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `Success: false`, keeps `TestCount` at `0`, lists the unsaved items in `Message`, and does not start the Unity Test Runner. +Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `success: false`, keeps `testCount` at `0`, lists the unsaved items in `message`, and does not start the Unity Test Runner. -When a run returns `Status: "NoTestsFound"` or `NoTestsFound: true`, no matching tests were discovered. This is not the same as a failed test case: `HasFailures` remains `false`, `FailedCount` remains `0`, and `NoTestsFoundExplanation` explains the zero-discovery state for agents. When an unfiltered `--filter-type all` run also has `Message` set to the no-tests message, read the `Message` for asmdef hints. `run-tests` only appends those hints when it detects likely test assembly configuration issues; exact, regex, and assembly filter misses keep the plain no-tests message. +When a run returns `status: "NoTestsFound"` or `noTestsFound: true`, no matching tests were discovered. This is not the same as a failed test case: `hasFailures` remains `false`, `failedCount` remains `0`, and `noTestsFoundExplanation` explains the zero-discovery state for agents. When an unfiltered `--filter-type all` run also has `message` set to the no-tests message, read the `message` for asmdef hints. `run-tests` only appends those hints when it detects likely test assembly configuration issues; exact, regex, and assembly filter misses keep the plain no-tests message. ## Usage @@ -57,18 +57,18 @@ uloop run-tests --filter-type regex --filter-value ".*Integration.*" ## Output Returns JSON with: -- `Success` (boolean): Whether all tests passed -- `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` -- `HasFailures` (boolean): Whether any discovered test failed -- `Message` (string): Summary message -- `NoTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests -- `NoTestsFoundExplanation` (string): Agent-facing explanation when `NoTestsFound` is true; empty otherwise -- `CompletedAt` (string): ISO timestamp when the run finished -- `TestCount` (number): Total tests executed -- `PassedCount` (number): Passed tests -- `FailedCount` (number): Failed tests -- `SkippedCount` (number): Skipped tests -- `XmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `Success: true`); populated only when tests failed and the XML file exists on disk. +- `success` (boolean): Whether all tests passed +- `status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` +- `hasFailures` (boolean): Whether any discovered test failed +- `message` (string): Summary message +- `noTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests +- `noTestsFoundExplanation` (string): Agent-facing explanation when `noTestsFound` is true; empty otherwise +- `completedAt` (string): ISO timestamp when the run finished +- `testCount` (number): Total tests executed +- `passedCount` (number): Passed tests +- `failedCount` (number): Failed tests +- `skippedCount` (number): Skipped tests +- `xmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `success: true`); populated only when tests failed and the XML file exists on disk. ### XML Result File diff --git a/.claude/skills/uloop-screenshot/SKILL.md b/.claude/skills/uloop-screenshot/SKILL.md index a43b0ab808..d10d199ea2 100644 --- a/.claude/skills/uloop-screenshot/SKILL.md +++ b/.claude/skills/uloop-screenshot/SKILL.md @@ -75,18 +75,18 @@ uloop screenshot --window-name Scene --resolution-scale 0.5 --output-directory / ## Output Returns JSON with: -- `ScreenshotCount`: Number of windows captured -- `Screenshots`: Array of screenshot info, each containing: - - `ImagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. - - `FileSizeBytes`: Size of the saved file in bytes - - `Width`: Captured image width in pixels - - `Height`: Captured image height in pixels - - `CoordinateSystem`: `"gameView"` or `"window"` - - `ResolutionScale`: Resolution scale used for capture - - `YOffset`: Y offset used for gameView coordinate conversion - - `AnnotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` is used. - -For `AnnotatedElements` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. +- `screenshotCount`: Number of windows captured +- `screenshots`: Array of screenshot info, each containing: + - `imagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. + - `fileSizeBytes`: Size of the saved file in bytes + - `width`: Captured image width in pixels + - `height`: Captured image height in pixels + - `coordinateSystem`: `"gameView"` or `"window"` + - `resolutionScale`: Resolution scale used for capture + - `yOffset`: Y offset used for gameView coordinate conversion + - `annotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` is used. + +For `annotatedElements` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. When multiple windows match (e.g., multiple Inspector windows or when using `contains` mode), all matching windows are captured with numbered filenames (e.g., `Inspector_1_*.png`, `Inspector_2_*.png`). diff --git a/.claude/skills/uloop-screenshot/references/annotated-elements.md b/.claude/skills/uloop-screenshot/references/annotated-elements.md index 283109ccc2..50b899b096 100644 --- a/.claude/skills/uloop-screenshot/references/annotated-elements.md +++ b/.claude/skills/uloop-screenshot/references/annotated-elements.md @@ -2,34 +2,34 @@ Read this when using `uloop screenshot --capture-mode rendering --annotate-elements` to find coordinates for `simulate-mouse-ui` or `simulate-mouse-input`. -## AnnotatedElements Fields +## annotatedElements Fields -`AnnotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: +`annotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: -- `Label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. -- `Name`: Element name -- `Path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. -- `Type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) -- `Interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. -- `SimX`, `SimY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. -- `BoundsMinX`, `BoundsMinY`, `BoundsMaxX`, `BoundsMaxY`: Bounding box in simulate-mouse coordinates -- `SortingOrder`: Canvas sorting order. Higher values are in front. -- `SiblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. +- `label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. +- `name`: Element name +- `path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. +- `type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) +- `interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. +- `simX`, `simY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. +- `boundsMinX`, `boundsMinY`, `boundsMaxX`, `boundsMaxY`: Bounding box in simulate-mouse coordinates +- `sortingOrder`: Canvas sorting order. Higher values are in front. +- `siblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. ## Coordinate Conversion -When `CoordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: +When `coordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: ```text -sim_x = image_x / ResolutionScale -sim_y = image_y / ResolutionScale + YOffset +sim_x = image_x / resolutionScale +sim_y = image_y / resolutionScale + yOffset ``` -When `ResolutionScale` is `1.0`, this simplifies to: +When `resolutionScale` is `1.0`, this simplifies to: ```text sim_x = image_x -sim_y = image_y + YOffset +sim_y = image_y + yOffset ``` ## Annotation Readability diff --git a/.claude/skills/uloop-simulate-keyboard/SKILL.md b/.claude/skills/uloop-simulate-keyboard/SKILL.md index 2ca050cb47..6cb374cc93 100644 --- a/.claude/skills/uloop-simulate-keyboard/SKILL.md +++ b/.claude/skills/uloop-simulate-keyboard/SKILL.md @@ -49,8 +49,8 @@ Use `KeyDown` / `KeyUp` when the scenario intentionally needs a held key. - Use `UloopPausePoint.Pause("")` with `uloop-wait-for-pause-point` as the standard frame proof when this input drives a state transition you are verifying. Final logs, screenshots, and durable state supplement the paused-frame check but do not replace it. - Put the marker at a natural state transition after the app consumed the key, such as after a command is accepted, a state mutation is committed, an evaluation step resolves, or a dependent component is updated. Do not place it immediately after sending `simulate-keyboard`. - If the key handler has local variables, intermediate calculations, or branch reasons that `execute-dynamic-code` cannot inspect after the fact, log just those values near `UloopPausePoint.Pause("")` and read them with `uloop-get-logs` while Unity is paused. A pause point hit proves the line was reached, not the frame-local values. -- Treat `simulate-keyboard Success=true`, generic action logs, and final durable counters as useful evidence, but not as paused-frame proof. -- If the response has `InterruptedByPausePoint: true`, Unity is paused for inspection and the tool released its held input bookkeeping. If a `UloopPausePoint.Pause` marker caused the pause, `PausePointId` and `PausePointHitCount` identify it. Use `uloop get-logs`, `uloop get-hierarchy`, `uloop find-game-objects`, or `uloop execute-dynamic-code` before resuming. +- Treat `simulate-keyboard success=true`, generic action logs, and final durable counters as useful evidence, but not as paused-frame proof. +- If the response has `interruptedByPausePoint: true`, Unity is paused for inspection and the tool released its held input bookkeeping. If a `UloopPausePoint.Pause` marker caused the pause, `pausePointId` and `pausePointHitCount` identify it. Use `uloop get-logs`, `uloop get-hierarchy`, `uloop find-game-objects`, or `uloop execute-dynamic-code` before resuming. - Use distinct marker ids for strict phases, for example `input-read`, `state-updated`, and `result-committed`. ### KeyDown/KeyUp Rules @@ -90,15 +90,15 @@ uloop simulate-keyboard --action KeyUp --key LeftShift ## Output Returns JSON with: -- `Success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) -- `Message` (string): Description of what happened or why it failed -- `Action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) -- `KeyName` (string, nullable): The key that was acted on; may be `null` when the action could not resolve a key -- `InterruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `PausePointId` (string, nullable): The marker id when a `UloopPausePoint.Pause` marker caused the interruption -- `PausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker caused the interruption -- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one -- `PressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was actually visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge (e.g. the press was consumed by an editor-only input update) — retry the input or verify with a focused log instead of trusting `Success` alone. `null` only for `KeyUp` and for timed-out responses; pause-point interruptions still report the observed value +- `success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) +- `message` (string): Description of what happened or why it failed +- `action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) +- `keyName` (string, nullable): The key that was acted on; may be `null` when the action could not resolve a key +- `interruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released +- `pausePointId` (string, nullable): The marker id when a `UloopPausePoint.Pause` marker caused the interruption +- `pausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker caused the interruption +- `pausePointHits` (array, nullable): Every marker hit during this input as `{id, hitCount}` entries, in hit order. Read this when one input may trigger several markers; `pausePointId` only names the latest one +- `pressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was actually visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge (e.g. the press was consumed by an editor-only input update) — retry the input or verify with a focused log instead of trusting `success` alone. `null` only for `KeyUp` and for timed-out responses; pause-point interruptions still report the observed value ## Prerequisites diff --git a/.claude/skills/uloop-simulate-mouse-input/SKILL.md b/.claude/skills/uloop-simulate-mouse-input/SKILL.md index 55bb8b634d..12b73569e4 100644 --- a/.claude/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-input/SKILL.md @@ -53,7 +53,7 @@ uloop simulate-mouse-input --action [options] - Use `UloopPausePoint.Pause("")` with `uloop-wait-for-pause-point` as the standard frame proof when this input drives a state transition you are verifying. Final logs, screenshots, and durable state supplement the paused-frame check but do not replace it. - Place the pause point at a natural state transition after the app consumed the mouse input, such as after a command is accepted, a state mutation is committed, an evaluation step resolves, a tracked value changes, or a dependent component is updated. Do not place it immediately after sending `simulate-mouse-input`. - If the mouse handler has local variables, intermediate calculations, or branch reasons that `execute-dynamic-code` cannot inspect after the fact, log just those values near `UloopPausePoint.Pause("")` and read them with `uloop-get-logs` while Unity is paused. A pause point hit proves the line was reached, not the frame-local values. -- If the response has `InterruptedByPausePoint: true`, Unity is paused for inspection and the tool released its held input bookkeeping. `PausePointId` and `PausePointHitCount` identify the pause point that paused Unity. Use `uloop get-logs`, `uloop get-hierarchy`, `uloop find-game-objects`, or `uloop execute-dynamic-code` before resuming. +- If the response has `interruptedByPausePoint: true`, Unity is paused for inspection and the tool released its held input bookkeeping. `pausePointId` and `pausePointHitCount` identify the pause point that paused Unity. Use `uloop get-logs`, `uloop get-hierarchy`, `uloop find-game-objects`, or `uloop execute-dynamic-code` before resuming. - Use distinct ids for strict phases, for example `input-read`, `state-updated`, and `result-committed`. - Remove temporary pause-point/log instrumentation before final validation when it was added only for inspection. @@ -110,15 +110,15 @@ uloop simulate-mouse-input --action SmoothDelta --delta-x 300 --delta-y 0 --dura ## Output Returns JSON with: -- `Success`: Whether the operation succeeded -- `Message`: Status message -- `Action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) -- `Button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) -- `PositionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) -- `PositionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) -- `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `PausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption -- `PausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` -- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one - -There is no `DeltaX`, `DeltaY`, `ScrollX`, `ScrollY`, `Duration`, or hit-element field in the response — only the issued action, button, target position, and Pause Point interruption state are echoed back. Verify visual outcome with a follow-up screenshot. +- `success`: Whether the operation succeeded +- `message`: Status message +- `action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) +- `button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) +- `positionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) +- `positionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) +- `interruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released +- `pausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption +- `pausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` +- `pausePointHits` (array, nullable): Every marker hit during this input as `{id, hitCount}` entries, in hit order. Read this when one input may trigger several markers; `pausePointId` only names the latest one + +There is no `deltaX`, `deltaY`, `scrollX`, `scrollY`, `duration`, or hit-element field in the response — only the issued action, button, target position, and Pause Point interruption state are echoed back. Verify visual outcome with a follow-up screenshot. diff --git a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md index a400302376..f5e2799aec 100644 --- a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md @@ -13,7 +13,7 @@ Simulate mouse interaction on Unity PlayMode UI. 1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not) 2. Get UI element info: `uloop screenshot --capture-mode rendering --annotate-elements --elements-only` -3. Use the `AnnotatedElements` array to find the target element by `Label`, `Name`, or `Path` (A=frontmost, B=next, ...). Use `Interaction` to distinguish click targets from drag/drop/text targets, then use `SimX`/`SimY` directly as `--x`/`--y` coordinates. +3. Use the `annotatedElements` array to find the target element by `label`, `name`, or `path` (A=frontmost, B=next, ...). Use `interaction` to distinguish click targets from drag/drop/text targets, then use `simX`/`simY` directly as `--x`/`--y` coordinates. 4. Execute the needed `uloop simulate-mouse-ui` commands 5. Inspect the result with the lightest useful evidence: runtime state, logs, or a screenshot 6. When this UI input verifies a state transition, use Pause Point inspection from the section below as the standard frame proof @@ -38,7 +38,7 @@ uloop simulate-mouse-ui --action --x --y [options] | `--duration` | number | `0.5` | Hold duration in seconds for LongPress action. | | `--button` | enum | `Left` | Mouse button. `Click` and `LongPress` support `Left`, `Right`, and `Middle`. Drag actions support `Left` only; other buttons return an error. | | `--bypass-raycast` | flag | - | For `Click`, `LongPress`, `Drag`, and `DragStart`, bypass EventSystem raycast and dispatch pointer events directly to `--target-path`. Use when a raycast-blocking overlay visually covers the intended target. | -| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `AnnotatedElements[].Path` from screenshot JSON. | +| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `annotatedElements[].path` from screenshot JSON. | | `--drop-target-path` | string | `""` | Optional hierarchy path of a drop target for `Drag` or `DragEnd`, for example `Canvas/DropZone`. Use this when the drop zone is also behind a raycast blocker. | ### Actions @@ -70,9 +70,9 @@ uloop simulate-mouse-ui --action --x --y [options] - Origin is **top-left** (0, 0) - All positions are in **screen pixels** -- Get coordinates from `AnnotatedElements` JSON (`SimX`/`SimY`) — do NOT look up GameObject positions +- Get coordinates from `annotatedElements` JSON (`simX`/`simY`) — do NOT look up GameObject positions - Clicking or long-pressing on empty space (no UI element) still succeeds with a message indicating no element was hit -- Dragging on empty space (no draggable UI element) returns `Success = false` +- Dragging on empty space (no draggable UI element) returns `success = false` - `--bypass-raycast` still uses coordinates for pointer event positions, but chooses the clicked, long-pressed, or dragged GameObject by `--target-path` - If `--target-path` or `--drop-target-path` matches multiple active GameObjects, the command fails instead of choosing an arbitrary duplicate @@ -81,7 +81,7 @@ uloop simulate-mouse-ui --action --x --y [options] - Use `UloopPausePoint.Pause("")` with `uloop wait-for-pause-point` as the standard frame proof when this input drives a state transition you are verifying. Final logs, screenshots, and durable state supplement the paused-frame check but do not replace it. - Place the pause point at a natural transition after the app consumed the UI event, such as after a command is accepted, a state mutation is committed, a tracked value changes, a UI/domain state syncs, or a success/failure/end condition is entered. - If the UI handler has local variables, intermediate calculations, or branch reasons that `uloop execute-dynamic-code` cannot inspect after the fact, log just those values near `UloopPausePoint.Pause("")` and read them with `uloop get-logs` while Unity is paused. A pause point hit proves the line was reached, not the frame-local values. -- Treat `uloop simulate-mouse-ui` `Success=true`, generic action logs, and final durable counters as useful evidence, not paused-frame proof. +- Treat `uloop simulate-mouse-ui` `success=true`, generic action logs, and final durable counters as useful evidence, not paused-frame proof. - If a `UloopPausePoint.Pause` pauses Unity, inspect with `uloop get-logs`, `uloop get-hierarchy`, `uloop find-game-objects`, or `uloop execute-dynamic-code` before resuming. - Remove temporary pause-point/log instrumentation before final validation when it was added only for inspection. @@ -129,15 +129,15 @@ uloop simulate-mouse-ui --action DragEnd --x 600 --y 300 ## Output Returns JSON with: -- `Success`: Whether the operation succeeded -- `Message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") -- `Action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) -- `HitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) -- `PositionX`: Target X coordinate that was used -- `PositionY`: Target Y coordinate that was used -- `EndPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) -- `EndPositionY`: Drag end Y coordinate (nullable float; populated for drag actions only) - -These are the only eight fields. There is no `Button`, `Duration`, `DragSpeed`, raycast list, or pointer-event log in the response — verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. - -Note: Click and LongPress on empty space (no UI element) still return `Success = true` with `HitGameObjectName = null`. Drag actions on empty space return `Success = false`. +- `success`: Whether the operation succeeded +- `message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") +- `action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) +- `hitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) +- `positionX`: Target X coordinate that was used +- `positionY`: Target Y coordinate that was used +- `endPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) +- `endPositionY`: Drag end Y coordinate (nullable float; populated for drag actions only) + +These are the only eight fields. There is no `button`, `duration`, `dragSpeed`, raycast list, or pointer-event log in the response — verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. + +Note: Click and LongPress on empty space (no UI element) still return `success = true` with `hitGameObjectName = null`. Drag actions on empty space return `success = false`. diff --git a/.claude/skills/uloop-wait-for-pause-point/SKILL.md b/.claude/skills/uloop-wait-for-pause-point/SKILL.md index 6f3b57f2a9..430cc002c8 100644 --- a/.claude/skills/uloop-wait-for-pause-point/SKILL.md +++ b/.claude/skills/uloop-wait-for-pause-point/SKILL.md @@ -24,20 +24,20 @@ uloop enable-pause-point --id state-transition-applied --timeout-seconds 30 ``` 3. Trigger the action with a `simulate-*` command. -4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `InterruptedByPausePoint=true`: +4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `interruptedByPausePoint=true`: ```bash uloop wait-for-pause-point --id state-transition-applied --timeout-seconds 30 ``` -The hit response always embeds the log entries matching the marker id as `MatchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `MatchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. +The hit response always embeds the log entries matching the marker id as `matchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `matchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. -Read `EvidenceSummary` first when it is present. It groups `EditorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `EditorState.CapturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. +Read `evidenceSummary` first when it is present. It groups `editorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `editorState.capturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. -Use `Generation`, `EnabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `RemainingMilliseconds` and `Expired` are returned directly so you do not need to infer marker lifetime from elapsed time. +Use `generation`, `enabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `remainingMilliseconds` and `expired` are returned directly so you do not need to infer marker lifetime from elapsed time. 5. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. -6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `EditorState` describes Unity Editor play/pause state, not marker state. +6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `editorState` describes Unity Editor play/pause state, not marker state. ## When To Use @@ -47,12 +47,12 @@ Use `Generation`, `EnabledAtUtc`, and the hit sequence fields from the hit or st - If the value you need is a method local, an intermediate calculation, or a branch reason that `execute-dynamic-code` cannot reach, add a focused `Debug.Log` immediately before the marker and read it with `get-logs` while paused. Do not count the breakpoint check complete until the matching log has been read. - Good pause points include after input is consumed, a command is accepted, a state mutation is committed, an evaluation step resolves, a tracked value changes, a UI/domain state syncs, or a success/failure/end condition is entered. - Treat the pause like a lightweight breakpoint for one important transition: combine nearby debug logs with paused-frame inspection to confirm the variables and component state at that point. -- Do not treat `simulate-* Success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. +- Do not treat `simulate-* success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. - Skip this only for ordinary persistent-state checks when you are not validating simulated input delivery, event ordering, or transition-frame fidelity. ## Timeout Checks -If this command times out, the marker line was not reached while the command waited. Read `error.details.hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `error.details.recommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `id` and `timeoutSeconds`. Then inspect `error.details.status`, `hitCount`, `generation`, `enabledAtUtc`, `editorState`, `elapsedSinceEnabledMilliseconds`, and `remainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `error.details.MatchingLogs` and `error.details.EvidenceSummary` show whether the marker's focused log ever appeared. `elapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. +If this command times out, the marker line was not reached while the command waited. Read `error.details.hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `error.details.recommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `id` and `timeoutSeconds`. Then inspect `error.details.status`, `hitCount`, `generation`, `enabledAtUtc`, `editorState`, `elapsedSinceEnabledMilliseconds`, and `remainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `error.details.matchingLogs` and `error.details.evidenceSummary` show whether the marker's focused log ever appeared. `elapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. Use `uloop pause-point-status --id state-transition-applied` only when you need to confirm the marker is armed or inspect the current hit state. Add focused debug logs before the marker when local variables must be captured. diff --git a/.codex/skills/uloop-clear-console/SKILL.md b/.codex/skills/uloop-clear-console/SKILL.md new file mode 100644 index 0000000000..5c633ca30b --- /dev/null +++ b/.codex/skills/uloop-clear-console/SKILL.md @@ -0,0 +1,49 @@ +--- +name: uloop-clear-console +toolName: clear-console +description: "Clear Unity Console entries. Use before compile, tests, or debugging when stale logs would hide the current result." +--- + +# uloop clear-console + +Clear Unity console logs. + +## Usage + +```bash +uloop clear-console [--add-confirmation-message] +``` + +## Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--add-confirmation-message` | flag | - | Add confirmation message after clearing | + +## Global Options + +| Option | Description | +|--------|-------------| +| `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | + +## Examples + +```bash +# Clear console +uloop clear-console + +# Clear with confirmation +uloop clear-console --add-confirmation-message +``` + +## Output + +Returns JSON with: +- `success` (boolean): Whether the clear operation succeeded +- `clearedLogCount` (number): Total number of log entries that were cleared +- `clearedCounts` (object): Breakdown by log type + - `errorCount` (number): Errors cleared + - `warningCount` (number): Warnings cleared + - `logCount` (number): Info logs cleared +- `message` (string): Description of the result; carries the failure summary when the operation fails (e.g. `"Failed to clear console: ..."`) +- `errorMessage` (string): Currently always empty for this tool — read `message` for failure details diff --git a/.codex/skills/uloop-compile/SKILL.md b/.codex/skills/uloop-compile/SKILL.md new file mode 100644 index 0000000000..9a2780a2a1 --- /dev/null +++ b/.codex/skills/uloop-compile/SKILL.md @@ -0,0 +1,50 @@ +--- +name: uloop-compile +toolName: compile +description: "Compile the Unity project and report errors/warnings. Use after C# edits." +--- + +# uloop compile + +Execute Unity project compilation. + +## Usage + +```bash +uloop compile [--force-recompile] [--no-wait-for-domain-reload] [--stop-on-external-scene-changes] +``` + +## Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--force-recompile` | flag | - | Use for broader validation, including warnings hidden by other asmdefs; much slower than normal compile | +| `--no-wait-for-domain-reload` | flag | - | Return before Domain Reload completion | +| `--stop-on-external-scene-changes` | flag | - | Stop before compilation if open Scene files changed externally instead of auto-reloading them | + +## Global Options + +| Option | Description | +|--------|-------------| +| `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | + +## Examples + +```bash +# Check compilation +uloop compile + +# Start compilation without waiting for Domain Reload completion +uloop compile --no-wait-for-domain-reload + +# Stop instead of auto-reloading externally changed open Scene files +uloop compile --stop-on-external-scene-changes +``` + +## Output + +Returns JSON: +- `success`: boolean or null +- `errorCount`: number or null +- `warningCount`: number or null +- `message`: string diff --git a/.codex/skills/uloop-control-play-mode/SKILL.md b/.codex/skills/uloop-control-play-mode/SKILL.md new file mode 100644 index 0000000000..662c027629 --- /dev/null +++ b/.codex/skills/uloop-control-play-mode/SKILL.md @@ -0,0 +1,65 @@ +--- +name: uloop-control-play-mode +toolName: control-play-mode +description: "Control Unity Editor Play Mode. Use to start, stop, or pause Play Mode for runtime behavior checks and frame inspection." +--- + +# uloop control-play-mode + +Control Unity Editor play mode (play/stop/pause). + +## Usage + +```bash +uloop control-play-mode [options] +``` + +## Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--action` | string | `Play` | Action to perform: `Play`, `Stop`, `Pause`, `Step` | +| `--timeout-seconds` | integer | `180` | Maximum seconds to wait for the requested play mode state | + +## Global Options + +| Option | Description | +|--------|-------------| +| `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | + +## Examples + +```bash +# Start play mode +uloop control-play-mode --action Play + +# Start play mode with a longer wait budget +uloop control-play-mode --action Play --timeout-seconds 600 + +# Stop play mode +uloop control-play-mode --action Stop + +# Pause play mode +uloop control-play-mode --action Pause + +# Advance exactly one frame while paused (Next Frame button) +uloop control-play-mode --action Step +``` + +## Output + +Returns JSON with the current play mode state: +- `isPlaying`: Whether Unity is currently in play mode +- `isPaused`: Whether play mode is paused +- `changed`: Whether the requested action changed the current play mode state +- `wasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped +- `message`: Description of the action performed + +## Notes + +- Play action starts the game in the Unity Editor (also resumes from pause) +- Stop action exits play mode and returns to edit mode. If Play Mode was already stopped, `changed` is `false`, `wasAlreadyStopped` is `true`, and `message` is `Play mode was already stopped`. +- Pause action pauses the game while remaining in play mode +- Step action advances exactly one frame and leaves play mode paused (the Editor's Next Frame button; independent of Time.timeScale). Requires PlayMode to be running; repeat to walk transitions frame by frame +- Useful for automated testing workflows +- The command waits for the requested state before returning. Increase `--timeout-seconds` for projects with slow PlayMode entry. diff --git a/.codex/skills/uloop-execute-dynamic-code/SKILL.md b/.codex/skills/uloop-execute-dynamic-code/SKILL.md new file mode 100644 index 0000000000..77060d33fb --- /dev/null +++ b/.codex/skills/uloop-execute-dynamic-code/SKILL.md @@ -0,0 +1,89 @@ +--- +name: uloop-execute-dynamic-code +toolName: execute-dynamic-code +description: "Execute C# with Unity APIs when existing uloop tools cannot inspect or edit enough. Use for reachable scene/component state, scene/prefab/menu automation, and PlayMode checks" +context: fork +--- + +# Task + +Execute the following request using `uloop execute-dynamic-code`: $ARGUMENTS + +For basic selected GameObject discovery or property inspection, use `find-game-objects --search-mode Selected` before this tool. Use this tool after the built-in inspection tools are not enough or when you need to modify Unity state. + +This tool can inspect reachable Unity state, such as GameObjects, components, public properties, static values, and method results. It cannot directly read local variables or intermediate calculations inside an already-running method. When those values matter, add a focused `Debug.Log` immediately before `UloopPausePoint.Pause("")`, then run `get-logs --search-text ` while Unity is paused. Do not replace that log read with execute-dynamic-code. + +## Workflow + +1. Read the relevant reference file(s) from the Code Examples section below +2. Construct C# code based on the reference examples +3. Execute inline code: `uloop execute-dynamic-code --code ''` +4. If execution fails, adjust code and retry +5. Report the execution result + +## Parameters + +- `--code ''`: Inline C# statements to execute. Use direct statements only; `return` is optional, and `using` directives may appear at the top of the snippet. +- `--code-file `: Read the C# statements from a file instead of `--code`. Use this when the active shell or launcher cannot preserve inline code exactly. Exactly one of `--code` or `--code-file` is required; combining them is an error. +- **Shell quoting**: bash/zsh uses single quotes, for example `uloop execute-dynamic-code --code 'using UnityEngine; return Mathf.PI;'`. PowerShell 7 (`pwsh`) can pass multiline snippets without `--code-file` by using a single-quoted here-string, for example `$code = @' ... '@; uloop execute-dynamic-code --code $code`. Windows PowerShell 5.1 removes unescaped double quotes when invoking native commands; escape C# double quotes as `\"` for short inline snippets, or use `--code-file` when the snippet has many strings. +- `--parameters {}` (advanced, optional): Pass a shell-quoted JSON object literal when reusing a snippet with varying data or when keeping values outside the code. Values are exposed as `parameters["param0"]`, `parameters["param1"]`, and so on. Omit this flag for most snippets. Do not pass a JSON string value such as `"{\"param0\":\"value\"}"`. +- `--wait-for-domain-reload` (optional): Wait for Domain Reload recovery after snippets that intentionally trigger Unity script reload or import work. Omit this for normal inspection and editor-state workflows. + +## Code Rules + +Write direct statements only — no class/namespace/method wrappers. Return is optional. + +```csharp +using UnityEngine; +float x = Mathf.PI; +return x; +``` + +**Forbidden** — these will be rejected at compile time: `System.IO.*`, `AssetDatabase.CreateFolder`, creating/editing `.cs`/`.asmdef` files. Use terminal commands for file operations instead. + +## Output + +Returns JSON: +- `success`: boolean — overall execution success +- `result`: string — value of the snippet's `return` statement (empty when omitted) +- `logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` +- `compilationErrors`: object[] — Roslyn diagnostics with `message`, `line`, `column`, `errorCode`, optional `hint` and `suggestions` +- `errorMessage`: string — top-level failure summary (empty on success) +- `error`: string — alias of `errorMessage` +- `securityLevel`: string — dynamic-code security level active for the request +- `updatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) +- `diagnosticsSummary`: string|null — compact summary when diagnostics are available +- `diagnostics`: object[] — structured diagnostics; same shape as `compilationErrors`, usually populated together with it + +On `success: false`, inspect `compilationErrors` first. If empty, read `errorMessage` (and `logs` for extra context) — the failure may be a runtime exception, security violation, cancellation, or an "execution in progress" rejection, all of which return empty `compilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. + +## Code Examples by Category + +For detailed code examples, refer to these files: + +- **Prefab operations**: See [references/prefab-operations.md](references/prefab-operations.md) + - Create prefabs, instantiate, add components, modify properties +- **Material operations**: See [references/material-operations.md](references/material-operations.md) + - Create materials, set shaders/textures, modify properties +- **Asset operations**: See [references/asset-operations.md](references/asset-operations.md) + - Find/search assets, duplicate, move, rename, load +- **ScriptableObject**: See [references/scriptableobject.md](references/scriptableobject.md) + - Create ScriptableObjects, modify with SerializedObject +- **Scene operations**: See [references/scene-operations.md](references/scene-operations.md) + - Create/modify GameObjects, set parents, wire references, load scenes +- **Batch operations**: See [references/batch-operations.md](references/batch-operations.md) + - Bulk modify objects, batch add/remove components, rename, layer/tag/material replacement +- **Cleanup operations**: See [references/cleanup-operations.md](references/cleanup-operations.md) + - Detect broken scripts, missing references, unused materials, empty GameObjects +- **Undo operations**: See [references/undo-operations.md](references/undo-operations.md) + - Undo-aware operations: RecordObject, AddComponent, SetParent, grouping +- **Selection operations**: See [references/selection-operations.md](references/selection-operations.md) + - Get/set selection, multi-select, filter by type/editability +- **PlayMode automation (zsh)**: See [references/playmode-automation-zsh.md](references/playmode-automation-zsh.md) + - Click UI buttons, invoke methods, set fields, tool combination workflows for zsh users +- **PlayMode automation (PowerShell)**: See [references/playmode-automation-powershell.md](references/playmode-automation-powershell.md) + - Click UI buttons, invoke methods, set fields, tool combination workflows for PowerShell users +- **PlayMode UI controls**: See [references/playmode-ui-controls.md](references/playmode-ui-controls.md) + - InputField, Slider, Toggle, Dropdown, drag & drop simulation, list all UI controls +- **PlayMode inspection**: See [references/playmode-inspection.md](references/playmode-inspection.md) + - Scene info, game state via reflection, physics state, raycast checks, GameObject search, position/rotation diff --git a/.codex/skills/uloop-execute-dynamic-code/references/asset-operations.md b/.codex/skills/uloop-execute-dynamic-code/references/asset-operations.md new file mode 100644 index 0000000000..31ddf7adc1 --- /dev/null +++ b/.codex/skills/uloop-execute-dynamic-code/references/asset-operations.md @@ -0,0 +1,194 @@ +# Asset Operations + +Code examples for AssetDatabase operations using `execute-dynamic-code`. + +## Find Assets by Type + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab"); +List paths = new List(); + +foreach (string guid in prefabGuids) +{ + paths.Add(AssetDatabase.GUIDToAssetPath(guid)); +} +return $"Found {paths.Count} prefabs"; +``` + +## Find Assets by Name + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string searchName = "Player"; +string[] guids = AssetDatabase.FindAssets(searchName); +List paths = new List(); + +foreach (string guid in guids) +{ + paths.Add(AssetDatabase.GUIDToAssetPath(guid)); +} +return $"Found {paths.Count} assets matching '{searchName}'"; +``` + +## Find Assets in Folder + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string folder = "Assets/Prefabs"; +string[] guids = AssetDatabase.FindAssets("t:Prefab", new[] { folder }); +List paths = new List(); + +foreach (string guid in guids) +{ + paths.Add(AssetDatabase.GUIDToAssetPath(guid)); +} +return $"Found {paths.Count} prefabs in {folder}"; +``` + +## Duplicate Asset + +```csharp +using UnityEditor; + +string sourcePath = "Assets/Materials/MyMaterial.mat"; +string destPath = "Assets/Materials/MyMaterial_Backup.mat"; + +bool success = AssetDatabase.CopyAsset(sourcePath, destPath); +return success ? $"Copied to {destPath}" : "Copy failed"; +``` + +## Move Asset + +```csharp +using UnityEditor; + +string sourcePath = "Assets/OldFolder/MyAsset.asset"; +string destPath = "Assets/NewFolder/MyAsset.asset"; + +string error = AssetDatabase.MoveAsset(sourcePath, destPath); +return string.IsNullOrEmpty(error) ? $"Moved to {destPath}" : $"Error: {error}"; +``` + +## Rename Asset + +```csharp +using UnityEditor; + +string assetPath = "Assets/Materials/OldName.mat"; +string newName = "NewName"; + +string error = AssetDatabase.RenameAsset(assetPath, newName); +return string.IsNullOrEmpty(error) ? $"Renamed to {newName}" : $"Error: {error}"; +``` + +## Rename Asset (Undo-supported) + +```csharp +using UnityEditor; + +// ObjectNames.SetNameSmart() supports Undo (AssetDatabase.RenameAsset() does NOT) +Object selected = Selection.activeObject; +if (selected == null) +{ + return "No asset selected"; +} + +string oldName = selected.name; +ObjectNames.SetNameSmart(selected, "NewName"); +AssetDatabase.SaveAssets(); +return $"Renamed {oldName} to {selected.name}"; +``` + +## Get Asset Path from Object + +```csharp +using UnityEditor; + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No object selected"; +} + +string path = AssetDatabase.GetAssetPath(selected); +if (string.IsNullOrEmpty(path)) +{ + return "Selected object is not an asset (scene object)"; +} +return $"Asset path: {path}"; +``` + +## Load Asset at Path + +```csharp +using UnityEditor; + +string path = "Assets/Prefabs/Player.prefab"; +GameObject asset = AssetDatabase.LoadAssetAtPath(path); + +if (asset == null) +{ + return $"Asset not found at {path}"; +} +return $"Loaded: {asset.name}"; +``` + +## Get All Assets of Type + +```csharp +using UnityEditor; + +string[] scriptGuids = AssetDatabase.FindAssets("t:MonoScript"); +int count = 0; + +foreach (string guid in scriptGuids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + if (path.StartsWith("Assets/")) + { + count++; + } +} +return $"Found {count} scripts in Assets folder"; +``` + +## Check if Asset Exists + +```csharp +using UnityEditor; + +string path = "Assets/Prefabs/Player.prefab"; +string guid = AssetDatabase.AssetPathToGUID(path); + +bool exists = !string.IsNullOrEmpty(guid); +return exists ? $"Asset exists: {path}" : $"Asset not found: {path}"; +``` + +## Get Asset Dependencies + +```csharp +using UnityEditor; + +string assetPath = "Assets/Prefabs/Player.prefab"; +string[] dependencies = AssetDatabase.GetDependencies(assetPath, true); + +return $"Asset has {dependencies.Length} dependencies"; +``` + +## Refresh AssetDatabase + +Use this after terminal-based asset file changes when Unity needs to import them and generate `.meta` files. + +```csharp +using UnityEditor; + +AssetDatabase.Refresh(); +return "AssetDatabase refreshed"; +``` diff --git a/.codex/skills/uloop-execute-dynamic-code/references/batch-operations.md b/.codex/skills/uloop-execute-dynamic-code/references/batch-operations.md new file mode 100644 index 0000000000..875c3fc65d --- /dev/null +++ b/.codex/skills/uloop-execute-dynamic-code/references/batch-operations.md @@ -0,0 +1,399 @@ +# Batch Operations + +Code examples for batch processing using `execute-dynamic-code`. + +## Batch Modify Selected Objects + +```csharp +using UnityEditor; +using System.Collections.Generic; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Modify"); + +foreach (GameObject obj in selected) +{ + Undo.RecordObject(obj.transform, ""); + obj.transform.localScale = Vector3.one * 2; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Scaled {selected.Length} objects (Single undo step)"; +``` + +## Edit Multiple Objects with SerializedObject + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +List transforms = new List(); +foreach (GameObject obj in selected) +{ + transforms.Add(obj.transform); +} + +SerializedObject serializedObj = new SerializedObject(transforms.ToArray()); +SerializedProperty positionProp = serializedObj.FindProperty("m_LocalPosition"); +positionProp.vector3Value = Vector3.zero; +serializedObj.ApplyModifiedProperties(); + +return $"Reset position of {selected.Length} objects"; +``` + +## Batch Add Component + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Add Rigidbody"); + +int addedCount = 0; +foreach (GameObject obj in selected) +{ + if (obj.GetComponent() == null) + { + Undo.AddComponent(obj); + addedCount++; + } +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Added Rigidbody to {addedCount} objects"; +``` + +## Batch Process Assets with StartAssetEditing + +```csharp +using UnityEditor; + +string[] guids = AssetDatabase.FindAssets("t:Material", new[] { "Assets/Materials" }); +if (guids.Length == 0) +{ + return "No materials found"; +} + +AssetDatabase.StartAssetEditing(); +try +{ + int modified = 0; + foreach (string guid in guids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + Material mat = AssetDatabase.LoadAssetAtPath(path); + if (mat != null) + { + mat.color = Color.white; + EditorUtility.SetDirty(mat); + modified++; + } + } + + AssetDatabase.SaveAssets(); + return $"Reset color of {modified} materials"; +} +finally +{ + AssetDatabase.StopAssetEditing(); +} + +``` + +## Batch Rename GameObjects + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Rename"); + +for (int i = 0; i < selected.Length; i++) +{ + Undo.RecordObject(selected[i], ""); + selected[i].name = $"Item_{i:D3}"; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Renamed {selected.Length} objects"; +``` + +## Batch Set Layer + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int layer = LayerMask.NameToLayer("Default"); + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Set Layer"); + +foreach (GameObject obj in selected) +{ + Undo.RecordObject(obj, ""); + obj.layer = layer; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Set layer of {selected.Length} objects to Default"; +``` + +## Batch Set Tag + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Set Tag"); + +foreach (GameObject obj in selected) +{ + Undo.RecordObject(obj, ""); + obj.tag = "Enemy"; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Tagged {selected.Length} objects as Enemy"; +``` + +## Batch Modify ScriptableObjects + +```csharp +using UnityEditor; + +string[] guids = AssetDatabase.FindAssets("t:ScriptableObject", new[] { "Assets/Data" }); +if (guids.Length == 0) +{ + return "No ScriptableObjects found"; +} + +int modified = 0; +foreach (string guid in guids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + ScriptableObject so = AssetDatabase.LoadAssetAtPath(path); + if (so == null) continue; + + SerializedObject serializedObj = new SerializedObject(so); + SerializedProperty prop = serializedObj.FindProperty("isEnabled"); + if (prop != null) + { + prop.boolValue = true; + serializedObj.ApplyModifiedProperties(); + EditorUtility.SetDirty(so); + modified++; + } +} + +AssetDatabase.SaveAssets(); +return $"Enabled {modified} ScriptableObjects"; +``` + +## Batch Remove Component + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Remove Rigidbody"); + +int removedCount = 0; +foreach (GameObject obj in selected) +{ + Rigidbody rb = obj.GetComponent(); + if (rb != null) + { + Undo.DestroyObjectImmediate(rb); + removedCount++; + } +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Removed Rigidbody from {removedCount} objects"; +``` + +## Batch Set Static Flags + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Set Static"); + +foreach (GameObject obj in selected) +{ + Undo.RecordObject(obj, ""); + GameObjectUtility.SetStaticEditorFlags(obj, StaticEditorFlags.BatchingStatic | StaticEditorFlags.OccludeeStatic); +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Set static flags on {selected.Length} objects"; +``` + +## Batch Process with Progress Bar + +```csharp +using UnityEditor; + +string[] guids = AssetDatabase.FindAssets("t:Texture2D"); +if (guids.Length == 0) +{ + return "No textures found"; +} + +int processed = 0; +foreach (string guid in guids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter; + if (importer != null && importer.maxTextureSize > 1024) + { + importer.maxTextureSize = 1024; + importer.SaveAndReimport(); + processed++; + } + + if (processed % 10 == 0) + { + EditorUtility.DisplayProgressBar("Processing Textures", path, (float)processed / guids.Length); + } +} + +EditorUtility.ClearProgressBar(); +return $"Resized {processed} textures to max 1024"; +``` + +## Batch Align Objects + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length < 2) +{ + return "Select at least 2 objects"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Align Objects"); + +float startX = selected[0].transform.position.x; +float spacing = 2f; + +for (int i = 0; i < selected.Length; i++) +{ + Undo.RecordObject(selected[i].transform, ""); + Vector3 pos = selected[i].transform.position; + pos.x = startX + (i * spacing); + selected[i].transform.position = pos; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Aligned {selected.Length} objects with {spacing}m spacing"; +``` + +## Batch Rename Assets (Undo-supported) + +```csharp +using UnityEditor; + +// ObjectNames.SetNameSmart() supports Undo (AssetDatabase.RenameAsset() does NOT) +Object[] selected = Selection.objects; +if (selected.Length == 0) +{ + return "No assets selected"; +} + +for (int i = 0; i < selected.Length; i++) +{ + string newName = $"{i:D3}_{selected[i].name}"; + ObjectNames.SetNameSmart(selected[i], newName); +} + +AssetDatabase.SaveAssets(); +return $"Renamed {selected.Length} assets"; +``` + +## Batch Replace Material + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +string materialPath = "Assets/Materials/NewMaterial.mat"; +Material newMat = AssetDatabase.LoadAssetAtPath(materialPath); +if (newMat == null) +{ + return $"Material not found at {materialPath}"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Replace Material"); + +int replaced = 0; +foreach (GameObject obj in selected) +{ + MeshRenderer renderer = obj.GetComponent(); + if (renderer != null) + { + Undo.RecordObject(renderer, ""); + renderer.sharedMaterial = newMat; + replaced++; + } +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Replaced material on {replaced} objects"; +``` diff --git a/.codex/skills/uloop-execute-dynamic-code/references/cleanup-operations.md b/.codex/skills/uloop-execute-dynamic-code/references/cleanup-operations.md new file mode 100644 index 0000000000..40eec9a52f --- /dev/null +++ b/.codex/skills/uloop-execute-dynamic-code/references/cleanup-operations.md @@ -0,0 +1,403 @@ +# Cleanup Operations + +Code examples for project cleanup operations using `execute-dynamic-code`. + +## Detect Missing Scripts on GameObject + +```csharp +using UnityEditor; + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No GameObject selected"; +} + +int missingCount = GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(selected); +return $"{selected.name} has {missingCount} missing script(s)"; +``` + +## Remove Missing Scripts from GameObject + +```csharp +using UnityEditor; + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No GameObject selected"; +} + +int removedCount = GameObjectUtility.RemoveMonoBehavioursWithMissingScript(selected); +return $"Removed {removedCount} missing script(s) from {selected.name}"; +``` + +## Scan Scene for Missing Scripts + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List objectsWithMissing = new List(); + +foreach (GameObject obj in allObjects) +{ + int count = GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(obj); + if (count > 0) + { + objectsWithMissing.Add($"{obj.name} ({count})"); + } +} + +if (objectsWithMissing.Count == 0) +{ + return "No missing scripts found in scene"; +} + +return $"Objects with missing scripts: {string.Join(", ", objectsWithMissing)}"; +``` + +## Remove All Missing Scripts from Scene + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +int totalRemoved = 0; + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Remove All Missing Scripts"); + +foreach (GameObject obj in allObjects) +{ + int removed = GameObjectUtility.RemoveMonoBehavioursWithMissingScript(obj); + totalRemoved += removed; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Removed {totalRemoved} missing scripts from scene"; +``` + +## Detect Missing References in Component + +```csharp +using UnityEditor; + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No GameObject selected"; +} + +List missingRefs = new List(); + +Component[] components = selected.GetComponents(); +foreach (Component comp in components) +{ + if (comp == null) continue; + + SerializedObject so = new SerializedObject(comp); + SerializedProperty prop = so.GetIterator(); + + while (prop.NextVisible(true)) + { + if (prop.propertyType == SerializedPropertyType.ObjectReference) + { + if (prop.objectReferenceValue == null && prop.objectReferenceInstanceIDValue != 0) + { + missingRefs.Add($"{comp.GetType().Name}.{prop.name}"); + } + } + } +} + +if (missingRefs.Count == 0) +{ + return "No missing references found"; +} + +return $"Missing references: {string.Join(", ", missingRefs)}"; +``` + +## Scan Scene for Missing References + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List results = new List(); + +foreach (GameObject obj in allObjects) +{ + Component[] components = obj.GetComponents(); + foreach (Component comp in components) + { + if (comp == null) continue; + + SerializedObject so = new SerializedObject(comp); + SerializedProperty prop = so.GetIterator(); + + while (prop.NextVisible(true)) + { + if (prop.propertyType == SerializedPropertyType.ObjectReference) + { + if (prop.objectReferenceValue == null && prop.objectReferenceInstanceIDValue != 0) + { + results.Add($"{obj.name}/{comp.GetType().Name}.{prop.name}"); + } + } + } + } +} + +if (results.Count == 0) +{ + return "No missing references found in scene"; +} + +return $"Missing references ({results.Count}): {string.Join(", ", results.Take(10))}..."; +``` + +## Find Unused Materials in Project + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string[] materialGuids = AssetDatabase.FindAssets("t:Material"); +HashSet usedMaterials = new HashSet(); + +string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab"); +foreach (string guid in prefabGuids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + string[] deps = AssetDatabase.GetDependencies(path, true); + foreach (string dep in deps) + { + if (dep.EndsWith(".mat")) + { + usedMaterials.Add(dep); + } + } +} + +// This scan only checks prefab dependencies. Verify scene and other asset +// references manually before deleting any reported materials. +List unusedMaterials = new List(); +foreach (string guid in materialGuids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + if (!usedMaterials.Contains(path)) + { + unusedMaterials.Add(path); + } +} + +return $"Found {unusedMaterials.Count} materials not referenced by prefabs. Verify scene and other asset references manually before deleting."; +``` + +## Find Empty GameObjects + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List emptyObjects = new List(); + +foreach (GameObject obj in allObjects) +{ + Component[] components = obj.GetComponents(); + if (components.Length == 1 && obj.transform.childCount == 0) + { + emptyObjects.Add(obj.name); + } +} + +if (emptyObjects.Count == 0) +{ + return "No empty GameObjects found"; +} + +return $"Empty objects ({emptyObjects.Count}): {string.Join(", ", emptyObjects.Take(20))}"; +``` + +## Find Duplicate Names in Hierarchy + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +Dictionary nameCounts = new Dictionary(); + +foreach (GameObject obj in allObjects) +{ + if (nameCounts.ContainsKey(obj.name)) + { + nameCounts[obj.name]++; + } + else + { + nameCounts[obj.name] = 1; + } +} + +List duplicates = new List(); +foreach (KeyValuePair kvp in nameCounts) +{ + if (kvp.Value > 1) + { + duplicates.Add($"{kvp.Key} ({kvp.Value})"); + } +} + +if (duplicates.Count == 0) +{ + return "No duplicate names found"; +} + +return $"Duplicate names: {string.Join(", ", duplicates.Take(15))}"; +``` + +## Check for Broken Prefab Instances + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List brokenPrefabs = new List(); + +foreach (GameObject obj in allObjects) +{ + if (PrefabUtility.IsPartOfPrefabInstance(obj)) + { + GameObject prefabAsset = PrefabUtility.GetCorrespondingObjectFromSource(obj); + if (prefabAsset == null) + { + brokenPrefabs.Add(obj.name); + } + } +} + +if (brokenPrefabs.Count == 0) +{ + return "No broken prefab instances found"; +} + +return $"Broken prefab instances: {string.Join(", ", brokenPrefabs)}"; +``` + +## Find Objects with Negative Scale + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List negativeScale = new List(); + +foreach (GameObject obj in allObjects) +{ + Vector3 scale = obj.transform.localScale; + if (scale.x < 0 || scale.y < 0 || scale.z < 0) + { + negativeScale.Add($"{obj.name} ({scale})"); + } +} + +if (negativeScale.Count == 0) +{ + return "No objects with negative scale found"; +} + +return $"Negative scale objects: {string.Join(", ", negativeScale.Take(10))}"; +``` + +## Remove Empty Leaf GameObjects + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Remove Empty Leaves"); + +int removedCount = 0; +foreach (GameObject obj in allObjects) +{ + if (obj == null) continue; + + Component[] components = obj.GetComponents(); + if (components.Length == 1 && obj.transform.childCount == 0) + { + Undo.DestroyObjectImmediate(obj); + removedCount++; + } +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Removed {removedCount} empty leaf GameObjects"; +``` + +## Find Large Meshes + +```csharp +using UnityEditor; + +string[] meshGuids = AssetDatabase.FindAssets("t:Mesh"); +List largeMeshes = new List(); +int threshold = 10000; + +foreach (string guid in meshGuids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + Mesh mesh = AssetDatabase.LoadAssetAtPath(path); + if (mesh != null && mesh.vertexCount > threshold) + { + largeMeshes.Add($"{path} ({mesh.vertexCount} verts)"); + } +} + +if (largeMeshes.Count == 0) +{ + return $"No meshes with more than {threshold} vertices found"; +} + +return $"Large meshes: {string.Join(", ", largeMeshes.Take(10))}"; +``` + +## Validate Asset References + +```csharp +using UnityEditor; + +string[] guids = AssetDatabase.FindAssets("t:ScriptableObject", new[] { "Assets/Data" }); +List invalidRefs = new List(); + +foreach (string guid in guids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + ScriptableObject so = AssetDatabase.LoadAssetAtPath(path); + if (so == null) continue; + + SerializedObject serializedObj = new SerializedObject(so); + SerializedProperty prop = serializedObj.GetIterator(); + + while (prop.NextVisible(true)) + { + if (prop.propertyType == SerializedPropertyType.ObjectReference) + { + if (prop.objectReferenceValue == null && prop.objectReferenceInstanceIDValue != 0) + { + invalidRefs.Add($"{path}: {prop.name}"); + } + } + } +} + +if (invalidRefs.Count == 0) +{ + return "All asset references are valid"; +} + +return $"Invalid references ({invalidRefs.Count}): {string.Join(", ", invalidRefs.Take(10))}"; +``` diff --git a/.codex/skills/uloop-execute-dynamic-code/references/material-operations.md b/.codex/skills/uloop-execute-dynamic-code/references/material-operations.md new file mode 100644 index 0000000000..4defb5c2fe --- /dev/null +++ b/.codex/skills/uloop-execute-dynamic-code/references/material-operations.md @@ -0,0 +1,172 @@ +# Material Operations + +Code examples for Material operations using `execute-dynamic-code`. + +## Create a New Material + +```csharp +using UnityEditor; + +Shader shader = Shader.Find("Standard"); +Material mat = new Material(shader); +mat.name = "MyMaterial"; +string path = "Assets/Materials/MyMaterial.mat"; +AssetDatabase.CreateAsset(mat, path); +AssetDatabase.SaveAssets(); +return $"Material created at {path}"; +``` + +## Set Material Color + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +Material mat = AssetDatabase.LoadAssetAtPath(matPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} + +mat.SetColor("_Color", new Color(1f, 0.5f, 0f, 1f)); +EditorUtility.SetDirty(mat); +AssetDatabase.SaveAssets(); +return "Material color set to orange"; +``` + +## Set Material Properties (Float, Vector) + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +Material mat = AssetDatabase.LoadAssetAtPath(matPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} + +mat.SetFloat("_Metallic", 0.8f); +mat.SetFloat("_Glossiness", 0.6f); +mat.SetVector("_EmissionColor", new Vector4(1, 1, 0, 1)); + +EditorUtility.SetDirty(mat); +AssetDatabase.SaveAssets(); +return "Material properties updated"; +``` + +## Assign Texture to Material + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +string texPath = "Assets/Textures/MyTexture.png"; + +Material mat = AssetDatabase.LoadAssetAtPath(matPath); +Texture2D tex = AssetDatabase.LoadAssetAtPath(texPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} +if (tex == null) +{ + return $"Texture not found at {texPath}"; +} + +mat.SetTexture("_MainTex", tex); +EditorUtility.SetDirty(mat); +AssetDatabase.SaveAssets(); +return $"Assigned {tex.name} to material"; +``` + +## Assign Material to GameObject + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +Material mat = AssetDatabase.LoadAssetAtPath(matPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No GameObject selected"; +} + +Renderer renderer = selected.GetComponent(); +if (renderer == null) +{ + return "Selected object has no Renderer"; +} + +renderer.sharedMaterial = mat; +EditorUtility.SetDirty(selected); +return $"Assigned {mat.name} to {selected.name}"; +``` + +## Enable/Disable Material Keywords + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +Material mat = AssetDatabase.LoadAssetAtPath(matPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} + +mat.EnableKeyword("_EMISSION"); +mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive; + +EditorUtility.SetDirty(mat); +AssetDatabase.SaveAssets(); +return "Emission enabled on material"; +``` + +## Find All Materials Using a Shader + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string shaderName = "Standard"; +string[] guids = AssetDatabase.FindAssets("t:Material"); +List matchingMaterials = new List(); + +foreach (string guid in guids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + Material mat = AssetDatabase.LoadAssetAtPath(path); + if (mat != null && mat.shader != null && mat.shader.name == shaderName) + { + matchingMaterials.Add(path); + } +} +return $"Found {matchingMaterials.Count} materials using {shaderName}"; +``` + +## Duplicate Material + +```csharp +using UnityEditor; + +string sourcePath = "Assets/Materials/MyMaterial.mat"; +string destPath = "Assets/Materials/MyMaterial_Copy.mat"; + +Material source = AssetDatabase.LoadAssetAtPath(sourcePath); +if (source == null) +{ + return $"Material not found at {sourcePath}"; +} + +Material copy = new Material(source); +AssetDatabase.CreateAsset(copy, destPath); +AssetDatabase.SaveAssets(); +return $"Material duplicated to {destPath}"; +``` diff --git a/.codex/skills/uloop-execute-dynamic-code/references/playmode-automation-powershell.md b/.codex/skills/uloop-execute-dynamic-code/references/playmode-automation-powershell.md new file mode 100644 index 0000000000..fe39803099 --- /dev/null +++ b/.codex/skills/uloop-execute-dynamic-code/references/playmode-automation-powershell.md @@ -0,0 +1,376 @@ +# PlayMode Automation (PowerShell) + +Code examples for runtime automation during Play mode using `execute-dynamic-code`. +These examples manipulate live scene objects while the game is running. +Shell command examples in this file target `PowerShell`. + +## When to use dedicated mouse simulation tools instead + +The examples in this file call UI handlers or runtime methods from C#. +That remains the better choice when you want targeted automation, direct state control, or a quick diagnostic path. +Use dedicated mouse tools only when the input route itself is part of what you need to verify. + +Choose the tool based on what you are trying to validate: + +| Scenario | Recommended tool | Why | +|----------|------------------|-----| +| Verify that a uGUI element responds through the real EventSystem pointer path | `simulate-mouse-ui` | Fires `PointerDown` / `PointerUp` / `PointerClick` / drag events through EventSystem raycasts instead of bypassing the UI input route. | +| Test gameplay that reads `Mouse.current`, button state, delta, or scroll | `simulate-mouse-input` | Injects Input System mouse state into `Mouse.current`, so game code can observe `wasPressedThisFrame`, movement delta, and scroll like player input. This assumes the project uses the New Input System (`Input System Package (New)` or `Both`). If that is not available in the target project, prefer `execute-dynamic-code` for a project-specific workaround instead of changing project settings just to use this tool. | +| Jump straight to a known button callback, invoke a method, inspect state, or set up a test precondition | `execute-dynamic-code` | Best when you intentionally want direct automation without reproducing the full input pipeline. | +| Drive custom runtime behavior that does not map cleanly to the built-in mouse tools | `execute-dynamic-code` | Lets you call project-specific methods, inspect scene objects, and prototype one-off flows immediately. | + +In short: do not default everything to mouse simulation. +Use `execute-dynamic-code` for direct automation and diagnostics, and switch to `simulate-mouse-ui` or `simulate-mouse-input` when reproducing the real input path is the thing you need to test. + +## PowerShell Quoting Notes + +Use these patterns when you need shell-safe inline code. +PowerShell's equivalent to a bash heredoc is a single-quoted here-string. +In PowerShell 7 (`pwsh`), it can be passed to `--code` directly without creating a temporary file: + +```powershell +uloop execute-dynamic-code --code @' +using UnityEngine; +using UnityEngine.SceneManagement; + +Scene scene = SceneManager.GetActiveScene(); +GameObject[] objects = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); +return $"{scene.name}: {objects.Length} objects"; +'@ +``` + +Use `--code-file` when the active shell or launcher cannot preserve inline code exactly, especially for Windows PowerShell 5.1 snippets with many C# string literals. + +### Native launcher check + +Multi-line `--code` expects PowerShell to call the native Go `uloop.exe` launcher directly. +If `Get-Command uloop` resolves to an old `.cmd` shim from a previous installation, run `uloop install` and open a new terminal before relying on multi-line inline snippets. + +```powershell +(Get-Command uloop).Source +``` + +### Double quotes inside C# strings + +PowerShell 7 (`pwsh`) preserves C# double quotes inside single-quoted native command arguments. + +```powershell +uloop execute-dynamic-code --code 'return "Hello from PowerShell";' +``` + +Windows PowerShell 5.1 removes unescaped double quotes when invoking native commands. +Escape C# double quotes as `\"` when using inline `--code` from Windows PowerShell 5.1. + +```powershell +uloop execute-dynamic-code --code 'return \"Hello from Windows PowerShell\";' +``` + +### Single quotes inside inline C# code + +If the C# snippet itself contains a single quote, double it inside the PowerShell single-quoted string. + +```powershell +uloop execute-dynamic-code --code 'char initial = ''A''; return initial.ToString();' +``` + +### JSON object values passed via `--parameters` + +Pass a JSON object literal, not a JSON string value. Wrap the object literal in single quotes so PowerShell passes the inner double quotes through unchanged. + +```powershell +uloop execute-dynamic-code --code 'return parameters["param0"];' --parameters '{"param0":"Hello from PowerShell"}' +``` + +### Multi-line C# snippets + +Use a here-string variable when you want to prepare or reuse a snippet before executing it in PowerShell 7 (`pwsh`). + +```powershell +$code = @' +using UnityEngine; + +GameObject obj = GameObject.Find("Player"); +if (obj == null) return "Player not found"; + +return obj.name; +'@ + +uloop execute-dynamic-code --code $code +``` + +When using Windows PowerShell 5.1, escape C# double quotes inside the here-string before passing it to native `uloop.exe`. + +```powershell +$code = @' +using UnityEngine; + +GameObject obj = GameObject.Find(\"Player\"); +if (obj == null) return \"Player not found\"; + +return obj.name; +'@ + +uloop execute-dynamic-code --code $code +``` + +You can combine multi-line code with a multi-line JSON object literal the same way. + +```powershell +$code = @' +return parameters["param0"]; +'@ + +$parameters = @' +{"param0":"Hello from PowerShell"} +'@ + +uloop execute-dynamic-code --code $code --parameters $parameters +``` + +## Click UI Button by Path + +```csharp +using UnityEngine.UI; + +Button btn = GameObject.Find("Canvas/StartButton")?.GetComponent