From 20f5c9dd2debc432430198794689f936887a8a44 Mon Sep 17 00:00:00 2001 From: hatayama <842587+hatayama@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:34:50 +0900 Subject: [PATCH 1/4] Show CLI option names in uloop list Format the live tool catalog before printing so users see the flags the native CLI actually accepts instead of C# schema property names. Keep sync/cache payloads raw and cover command-specific aliases plus the CLI-only code-file option. --- cli/internal/cli/list_output.go | 105 ++++++++++++++++++++ cli/internal/cli/list_output_test.go | 139 +++++++++++++++++++++++++++ cli/internal/cli/run.go | 2 +- 3 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 cli/internal/cli/list_output.go create mode 100644 cli/internal/cli/list_output_test.go diff --git a/cli/internal/cli/list_output.go b/cli/internal/cli/list_output.go new file mode 100644 index 0000000000..25cbf444d4 --- /dev/null +++ b/cli/internal/cli/list_output.go @@ -0,0 +1,105 @@ +package cli + +import ( + "encoding/json" + "sort" +) + +type listCatalog struct { + Version string `json:"version,omitempty"` + ServerVersion string `json:"serverVersion,omitempty"` + UpdatedAt string `json:"updatedAt,omitempty"` + Tools []listTool `json:"tools"` +} + +type listTool struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Options []listOption `json:"options"` +} + +type listOption struct { + Name string `json:"name"` + Type string `json:"type,omitempty"` + Description string `json:"description,omitempty"` + Default any `json:"default,omitempty"` + Values []string `json:"values,omitempty"` +} + +func formatToolListResult(result json.RawMessage) json.RawMessage { + var cache toolsCache + if err := json.Unmarshal(result, &cache); err != nil { + return result + } + + content, err := json.Marshal(newListCatalog(cache)) + if err != nil { + panic(err) + } + return content +} + +func newListCatalog(cache toolsCache) listCatalog { + tools := make([]listTool, 0, len(cache.Tools)) + for _, tool := range cache.Tools { + tools = append(tools, newListTool(tool)) + } + + return listCatalog{ + Version: cache.Version, + ServerVersion: cache.ServerVersion, + UpdatedAt: cache.UpdatedAt, + Tools: tools, + } +} + +func newListTool(tool toolDefinition) listTool { + return listTool{ + Name: tool.Name, + Description: tool.Description, + Options: listOptionsForTool(tool), + } +} + +func listOptionsForTool(tool toolDefinition) []listOption { + schema := tool.EffectiveInputSchema() + options := make([]listOption, 0, len(schema.Properties)) + for propertyName, property := range schema.Properties { + if property.Hidden { + continue + } + options = append(options, newListOption(tool, propertyName, property)) + } + + options = appendDynamicCodeFileListOption(tool, options) + sort.Slice(options, func(i int, j int) bool { + return options[i].Name < options[j].Name + }) + return options +} + +func newListOption(tool toolDefinition, propertyName string, property toolProperty) listOption { + return listOption{ + Name: "--" + optionNameForProperty(tool.Name, propertyName, property), + Type: property.Type, + Description: optionSummary(tool.Name, propertyName, property), + Default: property.EffectiveDefault(), + Values: property.Enum, + } +} + +func appendDynamicCodeFileListOption(tool toolDefinition, options []listOption) []listOption { + if tool.Name != executeDynamicCodeCommandName { + return options + } + for _, option := range options { + if option.Name == dynamicCodeFileOptionName { + return options + } + } + return append(options, listOption{ + Name: dynamicCodeFileOptionName, + Type: "string", + Description: dynamicCodeFileOptionDescription, + }) +} diff --git a/cli/internal/cli/list_output_test.go b/cli/internal/cli/list_output_test.go new file mode 100644 index 0000000000..99d2ddcb4c --- /dev/null +++ b/cli/internal/cli/list_output_test.go @@ -0,0 +1,139 @@ +package cli + +import ( + "encoding/json" + "testing" +) + +// Tests that list output exposes the actual CLI option names instead of schema property names. +func TestFormatToolListResultUsesCliOptionNames(t *testing.T) { + result := formatToolListResult([]byte(`{ + "Tools": [ + { + "name": "screenshot", + "parameterSchema": { + "Properties": { + "CaptureMode": { + "Type": "string", + "Description": "Capture mode", + "DefaultValue": "window", + "Enum": ["window", "rendering"] + }, + "AnnotateElements": { + "Type": "boolean", + "Description": "Annotate elements", + "DefaultValue": false + } + } + } + } + ] +}`)) + + catalog := decodeListCatalog(t, result) + tool := findListTool(t, catalog, "screenshot") + + captureMode := findListOption(t, tool, "--capture-mode") + if captureMode.Type != "string" { + t.Fatalf("capture mode type mismatch: %#v", captureMode) + } + if captureMode.Default != "window" { + t.Fatalf("capture mode default mismatch: %#v", captureMode) + } + if len(captureMode.Values) != 2 || captureMode.Values[0] != "window" || captureMode.Values[1] != "rendering" { + t.Fatalf("capture mode values mismatch: %#v", captureMode) + } + findListOption(t, tool, "--annotate-elements") + assertListOptionMissing(t, tool, "CaptureMode") +} + +// Tests that list output uses command-specific option aliases. +func TestNewListCatalogUsesSpecialOptionAliases(t *testing.T) { + cache := toolsCache{ + Tools: []toolDefinition{ + { + Name: runTestsCommandName, + InputSchema: inputSchema{ + Properties: map[string]toolProperty{ + "SaveBeforeRun": {Type: "boolean", Default: true}, + }, + }, + }, + { + Name: compileCommandName, + InputSchema: inputSchema{ + Properties: map[string]toolProperty{ + reloadExternalSceneChangesPropertyName: {Type: "boolean", Default: true}, + }, + }, + }, + }, + } + + catalog := newListCatalog(cache) + + runTestsTool := findListTool(t, catalog, runTestsCommandName) + findListOption(t, runTestsTool, "--fail-on-unsaved-changes") + assertListOptionMissing(t, runTestsTool, "--no-save-before-run") + + compileTool := findListTool(t, catalog, compileCommandName) + findListOption(t, compileTool, "--stop-on-external-scene-changes") + assertListOptionMissing(t, compileTool, "--no-reload-external-scene-changes") +} + +// Tests that list output includes CLI-side options that are not Unity schema properties. +func TestNewListCatalogIncludesExecuteDynamicCodeCodeFile(t *testing.T) { + tool, ok := findTool(loadDefaultTools(), executeDynamicCodeCommandName) + if !ok { + t.Fatal("execute-dynamic-code was not found in default tools") + } + + catalog := newListCatalog(toolsCache{Tools: []toolDefinition{tool}}) + executeDynamicCode := findListTool(t, catalog, executeDynamicCodeCommandName) + + findListOption(t, executeDynamicCode, dynamicCodeFileOptionName) +} + +func decodeListCatalog(t *testing.T, content []byte) listCatalog { + t.Helper() + + var catalog listCatalog + if err := json.Unmarshal(content, &catalog); err != nil { + t.Fatalf("failed to decode list catalog: %v\n%s", err, content) + } + return catalog +} + +func findListTool(t *testing.T, catalog listCatalog, name string) listTool { + t.Helper() + + for _, tool := range catalog.Tools { + if tool.Name == name { + return tool + } + } + t.Fatalf("tool %q was not listed: %#v", name, catalog.Tools) + return listTool{} +} + +func findListOption(t *testing.T, tool listTool, name string) listOption { + t.Helper() + + for _, option := range tool.Options { + if option.Name == name { + return option + } + } + t.Fatalf("option %q was not listed: %#v", name, tool.Options) + return listOption{} +} + +func assertListOptionMissing(t *testing.T, tool listTool, name string) { + t.Helper() + + for _, option := range tool.Options { + if option.Name == name { + t.Fatalf("option %q should not be listed: %#v", name, tool.Options) + } + } +} diff --git a/cli/internal/cli/run.go b/cli/internal/cli/run.go index bf4fa3b8e6..6f1d8c09a8 100644 --- a/cli/internal/cli/run.go +++ b/cli/internal/cli/run.go @@ -360,7 +360,7 @@ func runList(ctx context.Context, connection unityipc.Connection, stdout io.Writ }) return 1 } - writeJSON(stdout, outcome.Result) + writeJSON(stdout, formatToolListResult(outcome.Result)) return 0 } From 3c8f4950af9d65e2f187d85fcce82afe2ffc1d33 Mon Sep 17 00:00:00 2001 From: hatayama <842587+hatayama@users.noreply.github.com> Date: Tue, 16 Jun 2026 20:14:05 +0900 Subject: [PATCH 2/4] Use camelCase for CLI JSON responses Standardize Unity JSON-RPC response serialization and Go CLI output structs on camelCase so PowerShell and JSON consumers can aggregate command results consistently. Keep the IPC protocol generation unchanged because the CLI/package wire exchange remains interoperable while the user-facing response keys change. --- .../Demo/scripts/verify-replay-via-cli.ps1 | 24 +++--- .../Demo/scripts/verify-replay-via-cli.sh | 10 +-- .../CompileSessionResultServiceTests.cs | 44 ++++++++++ .../Editor/CompileStatusBridgeCommandTests.cs | 22 ++--- .../ExecuteDynamicCodeUseCaseTests.cs | 12 +-- .../JsonRpcProcessorCliVersionGateTests.cs | 4 +- .../Compile/CompileSessionResultService.cs | 5 +- .../Api/CompileStatusBridgeCommand.cs | 14 +-- .../Infrastructure/Api/JsonRpcProcessor.cs | 9 +- .../Api/JsonRpcResponseSerializer.cs | 28 ++++++ .../Api/JsonRpcResponseSerializer.cs.meta | 11 +++ cli/internal/cli/busy_status.go | 12 +-- cli/internal/cli/compile_wait.go | 20 ++--- cli/internal/cli/compile_wait_test.go | 49 ++++++----- cli/internal/cli/control_play_mode_wait.go | 22 ++--- .../cli/control_play_mode_wait_test.go | 16 ++-- cli/internal/cli/debug_timing.go | 3 +- cli/internal/cli/debug_timing_test.go | 25 +++++- cli/internal/cli/dynamic_code_wait.go | 10 ++- cli/internal/cli/focus.go | 4 +- cli/internal/cli/launch_ready.go | 24 +++--- cli/internal/cli/launch_test.go | 10 +-- cli/internal/cli/list_output.go | 9 +- cli/internal/cli/list_output_test.go | 28 +++--- cli/internal/cli/pause_point_logs.go | 66 +++++++------- cli/internal/cli/pause_point_wait.go | 46 +++++----- cli/internal/cli/pause_point_wait_test.go | 18 ++-- cli/internal/cli/run.go | 2 +- cli/internal/cli/tool_readiness.go | 4 +- cli/internal/cli/tools_test.go | 24 +++--- cli/internal/tools/types.go | 2 +- scripts/refresh-neighbor-game-skills-macos.sh | 2 +- .../refresh-neighbor-game-skills-windows.ps1 | 2 +- .../refresh-neighbor-game-skills-windows.sh | 4 +- scripts/run-windows-e2e.ps1 | 20 ++--- scripts/smoke-cli-recovery-readiness.go | 4 +- scripts/test-simulate-mouse-demo.ps1 | 86 +++++++++---------- scripts/test-simulate-mouse-demo.sh | 84 +++++++++--------- scripts/test-smoke-cli-recovery-readiness.sh | 10 +-- 39 files changed, 459 insertions(+), 330 deletions(-) create mode 100644 Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs create mode 100644 Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs.meta diff --git a/Assets/Tests/Demo/scripts/verify-replay-via-cli.ps1 b/Assets/Tests/Demo/scripts/verify-replay-via-cli.ps1 index 750194e949..81f41b8137 100644 --- a/Assets/Tests/Demo/scripts/verify-replay-via-cli.ps1 +++ b/Assets/Tests/Demo/scripts/verify-replay-via-cli.ps1 @@ -118,8 +118,8 @@ function Invoke-UloopJsonChecked { [string]$text = Invoke-Uloop -CommandArguments $CommandArguments [pscustomobject]$json = $text | ConvertFrom-Json - if ($json.PSObject.Properties.Name -contains "Success" -and $json.Success -ne $true) { - throw "uloop $($CommandArguments -join " ") returned Success=false: $text" + if ($json.PSObject.Properties.Name -contains "success" -and $json.success -ne $true) { + throw "uloop $($CommandArguments -join " ") returned success=false: $text" } return $json @@ -133,8 +133,8 @@ function Assert-DynamicCodeResult { ) [string]$actualResult = "" - if ($null -ne $Result.Result) { - $actualResult = $Result.Result.ToString() + if ($null -ne $Result.result) { + $actualResult = $Result.result.ToString() } if ($actualResult -eq $ExpectedResult) { @@ -185,7 +185,7 @@ return SceneManager.GetActiveScene().path; "@ [pscustomobject]$result = Invoke-UloopJsonChecked -CommandArguments @("execute-dynamic-code", "--code", $code) - if ($result.Result -ne $ScenePath) { + if ($result.result -ne $ScenePath) { throw "Failed to load ${ScenePath}: $($result | ConvertTo-Json -Depth 10)" } } @@ -305,7 +305,7 @@ function Start-ReplayInput { } [pscustomobject]$replayStartJson = $replayResult.Text | ConvertFrom-Json - return $replayStartJson.Success -eq $true + return $replayStartJson.success -eq $true } function Get-ReplayStatus { @@ -319,7 +319,7 @@ function Get-ReplayStatus { } [pscustomobject]$status = $result.Text | ConvertFrom-Json - if ($status.PSObject.Properties.Name -contains "Success" -and $status.Success -ne $true) { + if ($status.PSObject.Properties.Name -contains "success" -and $status.success -ne $true) { return [pscustomobject]@{ IsReplaying = $null Progress = $null @@ -328,8 +328,8 @@ function Get-ReplayStatus { } return [pscustomobject]@{ - IsReplaying = $status.IsReplaying - Progress = $status.Progress + IsReplaying = $status.isReplaying + Progress = $status.progress RawText = $result.Text } } @@ -500,13 +500,13 @@ if ($recordStopResult.ExitCode -ne 0) { } [pscustomobject]$recordStopJson = $recordStopResult.Text | ConvertFrom-Json -if ($recordStopJson.Success -ne $true) { +if ($recordStopJson.success -ne $true) { throw "record-input Stop reported failure: $($recordStopResult.Text)" } [string]$recordingInputPath = "" -if ($null -ne $recordStopJson.OutputPath) { - $recordingInputPath = $recordStopJson.OutputPath.ToString() +if ($null -ne $recordStopJson.outputPath) { + $recordingInputPath = $recordStopJson.outputPath.ToString() } if ($AutomatedInput) { diff --git a/Assets/Tests/Demo/scripts/verify-replay-via-cli.sh b/Assets/Tests/Demo/scripts/verify-replay-via-cli.sh index 1e8329e4a4..e961a5732c 100755 --- a/Assets/Tests/Demo/scripts/verify-replay-via-cli.sh +++ b/Assets/Tests/Demo/scripts/verify-replay-via-cli.sh @@ -66,9 +66,9 @@ run_uloop_json() { printf '%s\n' "$output" >&2 fail "uloop $* failed" } - if printf '%s\n' "$output" | grep -Eq '"Success"[[:space:]]*:[[:space:]]*false'; then + if printf '%s\n' "$output" | grep -Eq '"success"[[:space:]]*:[[:space:]]*false'; then printf '%s\n' "$output" >&2 - fail "uloop $* returned Success=false" + fail "uloop $* returned success=false" fi printf '%s\n' "$output" @@ -79,7 +79,7 @@ assert_json_result() { expected=$2 context=$3 - actual=$(printf '%s\n' "$json" | sed -n 's/.*"Result"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + actual=$(printf '%s\n' "$json" | sed -n 's/.*"result"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') [ "$actual" = "$expected" ] || fail "$context: expected '$expected', got '$actual'" } @@ -208,7 +208,7 @@ fi echo "[5/9] Stopping recording via CLI..." RECORD_STOP_RESULT=$(run_uloop_json record-input --action Stop) echo " $RECORD_STOP_RESULT" -RECORDING_INPUT_PATH=$(printf '%s\n' "$RECORD_STOP_RESULT" | sed -n 's/.*"OutputPath"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') +RECORDING_INPUT_PATH=$(printf '%s\n' "$RECORD_STOP_RESULT" | sed -n 's/.*"outputPath"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') echo "[6/9] Saving recording event log..." save_log "$RECORDING_LOG" @@ -239,7 +239,7 @@ sleep 2 waited=0 while [ $waited -lt 60 ]; do STATUS_RESULT=$(run_uloop replay-input --action Status 2>&1) || true - playing=$(echo "$STATUS_RESULT" | grep -o '"IsReplaying": *[a-z]*' | sed 's/.*: *//') + playing=$(echo "$STATUS_RESULT" | grep -o '"isReplaying": *[a-z]*' | sed 's/.*: *//') if [ "$playing" = "false" ]; then echo " Replay completed." break diff --git a/Assets/Tests/Editor/CompileSessionResultServiceTests.cs b/Assets/Tests/Editor/CompileSessionResultServiceTests.cs index 7d16a327ea..1dcf606034 100644 --- a/Assets/Tests/Editor/CompileSessionResultServiceTests.cs +++ b/Assets/Tests/Editor/CompileSessionResultServiceTests.cs @@ -2,7 +2,9 @@ using NUnit.Framework; using UnityEditor.Compilation; +using io.github.hatayama.UnityCliLoop.Domain; using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.Infrastructure; using io.github.hatayama.UnityCliLoop.ToolContracts; namespace io.github.hatayama.UnityCliLoop.Tests.Editor @@ -167,5 +169,47 @@ public void CreateCompileResult_WhenUnityTestFrameworkSymbolIsMissing_AddsTestAs Assert.That(response.Message, Does.Contain("TestAssemblies")); Assert.That(response.Message, Does.Contain("com.unity.test-framework")); } + + [Test] + public void StoreCompileResult_WhenResultIsPersisted_UsesCamelCaseJson() + { + // Verifies delayed compile polling reads the same camelCase response contract as immediate tool responses. + UnityCliLoopEditorSessionStateService sessionStateService = + UnityCliLoopEditorSessionStateTestFactory.CreateService(); + UnityCliLoopEditorSessionStateSnapshot originalSnapshot = + UnityCliLoopEditorSessionStateTestFactory.CaptureSnapshot(sessionStateService); + sessionStateService.ClearAll(); + + try + { + UnityCliLoopCompileResult response = new UnityCliLoopCompileResult + { + Success = true, + ErrorCount = 0, + WarningCount = 0, + Errors = Array.Empty(), + Warnings = Array.Empty(), + Message = "Compilation completed." + }; + + CompileSessionResultService.StoreCompileResult( + sessionStateService, + "compile_test_request", + forceRecompile: false, + response, + "compile_test_request"); + + UnityCliLoopStoredCompileResult storedResult = + sessionStateService.GetCompileResult("compile_test_request"); + + Assert.That(storedResult.ResultJson, Does.Contain("\"success\":true")); + Assert.That(storedResult.ResultJson, Does.Contain("\"errorCount\":0")); + Assert.That(storedResult.ResultJson, Does.Not.Contain("\"Success\"")); + } + finally + { + originalSnapshot.Restore(sessionStateService); + } + } } } diff --git a/Assets/Tests/Editor/CompileStatusBridgeCommandTests.cs b/Assets/Tests/Editor/CompileStatusBridgeCommandTests.cs index 5ad5886515..ba15d36ea9 100644 --- a/Assets/Tests/Editor/CompileStatusBridgeCommandTests.cs +++ b/Assets/Tests/Editor/CompileStatusBridgeCommandTests.cs @@ -37,7 +37,7 @@ public void BuildResponse_WhenUnityIsIdleAndResultMatches_ReturnsReadyResult() _sessionStateService.StoreCompileResult( "compile_test_request", forceRecompile: false, - resultJson: "{\"Success\":true}", + resultJson: "{\"success\":true}", completedAtUtc: System.DateTime.UtcNow); GetCompileStatusResponse response = CompileStatusBridgeCommand.BuildResponse( @@ -50,7 +50,7 @@ public void BuildResponse_WhenUnityIsIdleAndResultMatches_ReturnsReadyResult() Assert.That(response.Ready, Is.True); Assert.That(response.HasResult, Is.True); Assert.That(response.Result, Is.Not.Null); - Assert.That(response.Result["Success"]?.Value(), Is.True); + Assert.That(response.Result["success"]?.Value(), Is.True); } [Test] @@ -60,7 +60,7 @@ public void BuildResponse_WhenUnityIsStillCompiling_ReturnsNotReadyWithStoredRes _sessionStateService.StoreCompileResult( "compile_test_request", forceRecompile: false, - resultJson: "{\"Success\":true}", + resultJson: "{\"success\":true}", completedAtUtc: System.DateTime.UtcNow); GetCompileStatusResponse response = CompileStatusBridgeCommand.BuildResponse( @@ -82,7 +82,7 @@ public void BuildResponse_WhenRequestIdDiffers_DoesNotReturnStaleResult() _sessionStateService.StoreCompileResult( "compile_old_request", forceRecompile: false, - resultJson: "{\"Success\":true}", + resultJson: "{\"success\":true}", completedAtUtc: System.DateTime.UtcNow); GetCompileStatusResponse response = CompileStatusBridgeCommand.BuildResponse( @@ -138,10 +138,10 @@ public void BuildResponse_WhenUnityIsIdleAndPendingRequestObservedReload_Returns Assert.That(response.Ready, Is.True); Assert.That(response.HasResult, Is.True); - Assert.That(response.Result["Success"]?.Type, Is.EqualTo(JTokenType.Null)); - Assert.That(response.Result["ErrorCount"]?.Type, Is.EqualTo(JTokenType.Null)); - Assert.That(response.Result["Warnings"]?.Type, Is.EqualTo(JTokenType.Null)); - Assert.That(response.Result["Message"]?.ToString(), Does.Contain("reloaded scripts")); + Assert.That(response.Result["success"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["errorCount"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["warnings"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["message"]?.ToString(), Does.Contain("reloaded scripts")); Assert.That(_sessionStateService.GetPendingCompileRequest().HasRequest, Is.False); } @@ -186,10 +186,10 @@ public void BuildResponse_WhenForceCompilePendingRequestHasNoResult_ReturnsExpla Assert.That(response.Ready, Is.True); Assert.That(response.HasResult, Is.True); - Assert.That(response.Result["Success"]?.Type, Is.EqualTo(JTokenType.Null)); - Assert.That(response.Result["ErrorCount"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["success"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["errorCount"]?.Type, Is.EqualTo(JTokenType.Null)); Assert.That( - response.Result["Message"]?.ToString(), + response.Result["message"]?.ToString(), Is.EqualTo(ForceCompileUnknownResult.MessageText)); } } diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs index 7a0c22ef97..8122deba2b 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs @@ -10,6 +10,7 @@ using io.github.hatayama.UnityCliLoop.Application; using io.github.hatayama.UnityCliLoop.Domain; using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.Infrastructure; using io.github.hatayama.UnityCliLoop.ToolContracts; namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests @@ -37,12 +38,13 @@ public void ExecuteDynamicCodeResponse_WhenSerializedWithoutTimings_DoesNotExpos EmitTimingsInJsonResponse = false }; - JObject serializedResponse = JObject.Parse(JsonConvert.SerializeObject(response)); + JObject serializedResponse = JObject.Parse( + JsonConvert.SerializeObject(response, JsonRpcResponseSerializer.Settings)); - Assert.That(serializedResponse["Timings"], Is.Null); - Assert.That(serializedResponse["EmitTimingsInJsonResponse"], Is.Null); - Assert.That(serializedResponse["EmitsTimingsInJsonResponse"], Is.Null); - Assert.That(serializedResponse["DomainReloadWaitRequired"], Is.Null); + Assert.That(serializedResponse["timings"], Is.Null); + Assert.That(serializedResponse["emitTimingsInJsonResponse"], Is.Null); + Assert.That(serializedResponse["emitsTimingsInJsonResponse"], Is.Null); + Assert.That(serializedResponse["domainReloadWaitRequired"], Is.Null); } [Test] diff --git a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs index aa1f8c99aa..9b15e98e2b 100644 --- a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs +++ b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs @@ -32,6 +32,8 @@ public async Task ProcessRequest_WhenProtocolVersionMatches_AllowsRequest() Assert.That(parsed["error"], Is.Null); Assert.That(parsed["result"], Is.Not.Null); + Assert.That(parsed["result"]?["unityVersion"]?.ToString(), Is.Not.Null.And.Not.Empty); + Assert.That(parsed["result"]?["UnityVersion"], Is.Null); } [Test] @@ -492,7 +494,7 @@ public async Task ProcessRequest_AfterGetHierarchyReturns_AllowsImmediateGetLogs JObject parsedLogs = JObject.Parse(logsResponse); Assert.That(parsedLogs["error"], Is.Null); - Assert.That(parsedLogs["result"]?["DisplayedCount"]?.ToObject(), Is.EqualTo(0)); + Assert.That(parsedLogs["result"]?["displayedCount"]?.ToObject(), Is.EqualTo(0)); } finally { diff --git a/Packages/src/Editor/FirstPartyTools/Compile/CompileSessionResultService.cs b/Packages/src/Editor/FirstPartyTools/Compile/CompileSessionResultService.cs index b9c393d141..492f4ff61a 100644 --- a/Packages/src/Editor/FirstPartyTools/Compile/CompileSessionResultService.cs +++ b/Packages/src/Editor/FirstPartyTools/Compile/CompileSessionResultService.cs @@ -64,7 +64,10 @@ internal static void StoreCompileResult( Debug.Assert(result != null, "result must not be null"); result.ProjectRoot = UnityCliLoopPathResolver.GetProjectRoot(); - string resultJson = JsonConvert.SerializeObject(result, Formatting.None); + string resultJson = JsonConvert.SerializeObject( + result, + Formatting.None, + JsonRpcResponseSerializer.Settings); UnityCliLoopStoredCompileResult previousResult = sessionStateService.GetCompileResult(requestId); UnityCliLoopPendingCompileRequest pendingRequest = diff --git a/Packages/src/Editor/Infrastructure/Api/CompileStatusBridgeCommand.cs b/Packages/src/Editor/Infrastructure/Api/CompileStatusBridgeCommand.cs index 4cc577ec33..5fc5fbdcb7 100644 --- a/Packages/src/Editor/Infrastructure/Api/CompileStatusBridgeCommand.cs +++ b/Packages/src/Editor/Infrastructure/Api/CompileStatusBridgeCommand.cs @@ -153,13 +153,13 @@ private static JObject CreateRecoveredCompileResult( return new JObject { - ["Success"] = JValue.CreateNull(), - ["ErrorCount"] = JValue.CreateNull(), - ["WarningCount"] = JValue.CreateNull(), - ["Errors"] = JValue.CreateNull(), - ["Warnings"] = JValue.CreateNull(), - ["Message"] = message, - ["ProjectRoot"] = UnityCliLoopPathResolver.GetProjectRoot() + ["success"] = JValue.CreateNull(), + ["errorCount"] = JValue.CreateNull(), + ["warningCount"] = JValue.CreateNull(), + ["errors"] = JValue.CreateNull(), + ["warnings"] = JValue.CreateNull(), + ["message"] = message, + ["projectRoot"] = UnityCliLoopPathResolver.GetProjectRoot() }; } diff --git a/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs b/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs index 7999aa7aae..4dac6a20e7 100644 --- a/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs +++ b/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs @@ -35,11 +35,8 @@ public static class JsonRpcProcessor // Shared by every response path; JsonConvert only reads the settings, so a single // instance avoids allocating identical settings per response. - private static readonly JsonSerializerSettings ResponseSerializerSettings = new() - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - MaxDepth = UnityCliLoopServerConfig.DEFAULT_JSON_MAX_DEPTH - }; + private static readonly JsonSerializerSettings ResponseSerializerSettings = + JsonRpcResponseSerializer.Settings; internal delegate Task JsonRpcEarlyResponseWriter( string responseJson, @@ -467,7 +464,7 @@ private static string CreateSuccessResponse(object id, UnityCliLoopToolResponse id, fallbackResult ); - return JsonConvert.SerializeObject(fallbackResponse, Formatting.None); + return JsonConvert.SerializeObject(fallbackResponse, Formatting.None, ResponseSerializerSettings); } } diff --git a/Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs b/Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs new file mode 100644 index 0000000000..a3e4f0598c --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Provides the shared JSON-RPC response serializer settings used by Unity-facing command results. + /// + public static class JsonRpcResponseSerializer + { + public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy + { + // Why: response DTO property names are the public contract, but user-supplied + // dictionary keys can be data and must not be rewritten by the transport layer. + ProcessDictionaryKeys = false, + OverrideSpecifiedNames = false + } + }, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + MaxDepth = UnityCliLoopServerConfig.DEFAULT_JSON_MAX_DEPTH + }; + } +} diff --git a/Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs.meta b/Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs.meta new file mode 100644 index 0000000000..289ef9ba81 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 42374f86d55ed3f4c85fb3d01be78368 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/cli/internal/cli/busy_status.go b/cli/internal/cli/busy_status.go index 55fe85db19..d9e7a18124 100644 --- a/cli/internal/cli/busy_status.go +++ b/cli/internal/cli/busy_status.go @@ -9,12 +9,12 @@ import ( const cliStatusBusy = "Busy" type cliStatusEnvelope struct { - Status string `json:"Status"` - Message string `json:"Message"` - RunningToolName string `json:"RunningToolName,omitempty"` - RequestedToolName string `json:"RequestedToolName,omitempty"` - IsPlaying *bool `json:"IsPlaying,omitempty"` - IsPaused *bool `json:"IsPaused,omitempty"` + Status string `json:"status"` + Message string `json:"message"` + RunningToolName string `json:"runningToolName,omitempty"` + RequestedToolName string `json:"requestedToolName,omitempty"` + IsPlaying *bool `json:"isPlaying,omitempty"` + IsPaused *bool `json:"isPaused,omitempty"` } type serverBusyStatusDetails struct { diff --git a/cli/internal/cli/compile_wait.go b/cli/internal/cli/compile_wait.go index 3c166dd6cb..41a4a0c089 100644 --- a/cli/internal/cli/compile_wait.go +++ b/cli/internal/cli/compile_wait.go @@ -32,13 +32,13 @@ type compileCompletionOptions struct { } type compileStatusResponse struct { - Ready bool `json:"Ready"` - HasResult bool `json:"HasResult"` - IsCompiling bool `json:"IsCompiling"` - IsUpdating bool `json:"IsUpdating"` - IsDomainReloadInProgress bool `json:"IsDomainReloadInProgress"` - Result json.RawMessage `json:"Result"` - Message string `json:"Message"` + Ready bool `json:"ready"` + HasResult bool `json:"hasResult"` + IsCompiling bool `json:"isCompiling"` + IsUpdating bool `json:"isUpdating"` + IsDomainReloadInProgress bool `json:"isDomainReloadInProgress"` + Result json.RawMessage `json:"result"` + Message string `json:"message"` } var queryCompileStatus = queryCompileStatusFromUnity @@ -469,9 +469,9 @@ func compileResultLogSummary(result json.RawMessage) compileResultSummary { return compileResultSummary{} } return compileResultSummary{ - success: payload["Success"], - errorCount: payload["ErrorCount"], - warningCount: payload["WarningCount"], + success: payload["success"], + errorCount: payload["errorCount"], + warningCount: payload["warningCount"], } } diff --git a/cli/internal/cli/compile_wait_test.go b/cli/internal/cli/compile_wait_test.go index 08438db19c..f24cfa7e7e 100644 --- a/cli/internal/cli/compile_wait_test.go +++ b/cli/internal/cli/compile_wait_test.go @@ -98,13 +98,13 @@ func TestShouldWaitForExecuteDynamicCodeDomainReloadSkipsCompileOnly(t *testing. // Verifies that execute-dynamic-code waits only when Unity explicitly reports a reload signal. func TestExecuteDynamicCodeDomainReloadWaitRequiredReadsResponseSignal(t *testing.T) { - result := []byte(`{"Success":true,"DomainReloadWaitRequired":true}`) + result := []byte(`{"success":true,"domainReloadWaitRequired":true}`) if !executeDynamicCodeDomainReloadWaitRequired(result) { t.Fatal("dynamic-code response should request a reload wait") } - if executeDynamicCodeDomainReloadWaitRequired([]byte(`{"Success":true}`)) { + if executeDynamicCodeDomainReloadWaitRequired([]byte(`{"success":true}`)) { t.Fatal("dynamic-code response without a reload signal should not request a wait") } } @@ -124,13 +124,22 @@ func TestShouldWaitForExecuteDynamicCodeDisconnectWaitsAfterDispatchedTransportL // Verifies that the CLI does not expose its internal reload-wait response field to users. func TestStripExecuteDynamicCodeControlResultRemovesReloadSignal(t *testing.T) { - result := stripExecuteDynamicCodeControlResult([]byte(`{"Success":true,"DomainReloadWaitRequired":true}`)) + result := stripExecuteDynamicCodeControlResult([]byte(`{"success":true,"domainReloadWaitRequired":true}`)) - if strings.Contains(string(result), "DomainReloadWaitRequired") { + if strings.Contains(string(result), "domainReloadWaitRequired") { t.Fatalf("control field leaked into user output: %s", result) } } +// Verifies that the CLI hides legacy-cased reload-wait response fields from users. +func TestStripExecuteDynamicCodeControlResultRemovesLegacyReloadSignal(t *testing.T) { + result := stripExecuteDynamicCodeControlResult([]byte(`{"success":true,"DomainReloadWaitRequired":true}`)) + + if strings.Contains(string(result), "DomainReloadWaitRequired") { + t.Fatalf("legacy control field leaked into user output: %s", result) + } +} + // Verifies that compile wait preparation creates a request id and enables reload waiting. func TestPrepareCompileWaitParamsForcesDomainReloadWait(t *testing.T) { params := map[string]any{} @@ -156,9 +165,9 @@ func TestWaitForCompileCompletionReturnsReadyStatusResult(t *testing.T) { replaceQueryCompileStatus(t, func(context.Context, unityipc.Connection, string) (compileStatusResponse, error) { callCount++ if callCount == 1 { - return compileStatusResponse{Ready: false, HasResult: true, Result: json.RawMessage(`{"Success":true}`)}, nil + return compileStatusResponse{Ready: false, HasResult: true, Result: json.RawMessage(`{"success":true}`)}, nil } - return compileStatusResponse{Ready: true, HasResult: true, Result: json.RawMessage(`{"Success":true}`)}, nil + return compileStatusResponse{Ready: true, HasResult: true, Result: json.RawMessage(`{"success":true}`)}, nil }) result, completed, err := waitForCompileCompletion(context.Background(), compileCompletionOptions{ @@ -173,7 +182,7 @@ func TestWaitForCompileCompletionReturnsReadyStatusResult(t *testing.T) { if !completed { t.Fatal("compile wait did not complete") } - if string(result) != "{\"Success\":true}" { + if string(result) != "{\"success\":true}" { t.Fatalf("result mismatch: %s", result) } } @@ -197,7 +206,7 @@ func TestWaitForCompileCompletionWritesPollLifecycleVibeLogs(t *testing.T) { return compileStatusResponse{ Ready: true, HasResult: true, - Result: json.RawMessage(`{"Success":false,"ErrorCount":2,"WarningCount":1}`), + Result: json.RawMessage(`{"success":false,"errorCount":2,"warningCount":1}`), Message: "Compile result is available.", }, nil }) @@ -214,7 +223,7 @@ func TestWaitForCompileCompletionWritesPollLifecycleVibeLogs(t *testing.T) { if !completed { t.Fatal("compile wait did not complete") } - if string(result) != `{"Success":false,"ErrorCount":2,"WarningCount":1}` { + if string(result) != `{"success":false,"errorCount":2,"warningCount":1}` { t.Fatalf("result mismatch: %s", result) } @@ -248,7 +257,7 @@ func TestWaitForCompileCompletionForceCompileWaitsForStoredResult(t *testing.T) return compileStatusResponse{ Ready: true, HasResult: true, - Result: json.RawMessage(`{"Success":null,"ErrorCount":null,"Message":"Force compilation completed"}`), + Result: json.RawMessage(`{"success":null,"errorCount":null,"message":"Force compilation completed"}`), }, nil }) @@ -273,15 +282,15 @@ func TestWaitForCompileCompletionForceCompileWaitsForStoredResult(t *testing.T) if err := json.Unmarshal(result, &payload); err != nil { t.Fatalf("force compile result is not JSON: %v", err) } - if payload["Success"] != nil { - t.Fatalf("force compile success should be unknown: %#v", payload["Success"]) + if payload["success"] != nil { + t.Fatalf("force compile success should be unknown: %#v", payload["success"]) } - if payload["ErrorCount"] != nil || payload["WarningCount"] != nil { + if payload["errorCount"] != nil || payload["warningCount"] != nil { t.Fatalf("force compile counts should be unknown: %#v", payload) } - message, ok := payload["Message"].(string) + message, ok := payload["message"].(string) if !ok || !strings.Contains(message, "Force compilation completed") { - t.Fatalf("force compile message mismatch: %#v", payload["Message"]) + t.Fatalf("force compile message mismatch: %#v", payload["message"]) } } @@ -430,7 +439,7 @@ func TestRunCompileWithDomainReloadWaitWritesRequestLifecycleVibeLogs(t *testing return compileStatusResponse{ Ready: true, HasResult: true, - Result: json.RawMessage(`{"Success":false,"ErrorCount":1,"WarningCount":0}`), + Result: json.RawMessage(`{"success":false,"errorCount":1,"warningCount":0}`), }, nil }) @@ -505,10 +514,10 @@ func TestShouldWaitForCompileStatusAllowsAcceptedFinalResponseTimeout(t *testing // Verifies compile readiness warmup only runs after confirmed successful results. func TestCompileResultReadinessWaitMode(t *testing.T) { cases := map[string]compileReadinessWaitMode{ - `{"Success":true}`: compileReadinessWaitWarmup, - `{"Success":false,"Errors":[{"Message":"boom"}]}`: compileReadinessWaitNone, - `{"Success":null,"Message":"indeterminate"}`: compileReadinessWaitNone, - `{"Message":"indeterminate"}`: compileReadinessWaitNone, + `{"success":true}`: compileReadinessWaitWarmup, + `{"success":false,"errors":[{"message":"boom"}]}`: compileReadinessWaitNone, + `{"success":null,"message":"indeterminate"}`: compileReadinessWaitNone, + `{"message":"indeterminate"}`: compileReadinessWaitNone, } for result, expected := range cases { diff --git a/cli/internal/cli/control_play_mode_wait.go b/cli/internal/cli/control_play_mode_wait.go index b71ab906d1..f62d0ad2b3 100644 --- a/cli/internal/cli/control_play_mode_wait.go +++ b/cli/internal/cli/control_play_mode_wait.go @@ -23,20 +23,20 @@ const ( var controlPlayModeStatePoll = 50 * time.Millisecond type controlPlayModeResponse struct { - IsPlaying bool `json:"IsPlaying"` - IsPaused bool `json:"IsPaused"` - Changed bool `json:"Changed"` - WasAlreadyStopped bool `json:"WasAlreadyStopped"` - BlockedByCompileErrors bool `json:"BlockedByCompileErrors"` - CompileErrorCount int `json:"CompileErrorCount"` - CompileErrors []controlPlayModeCompileError `json:"CompileErrors"` - Message string `json:"Message"` + IsPlaying bool `json:"isPlaying"` + IsPaused bool `json:"isPaused"` + Changed bool `json:"changed"` + WasAlreadyStopped bool `json:"wasAlreadyStopped"` + BlockedByCompileErrors bool `json:"blockedByCompileErrors"` + CompileErrorCount int `json:"compileErrorCount"` + CompileErrors []controlPlayModeCompileError `json:"compileErrors"` + Message string `json:"message"` } type controlPlayModeCompileError struct { - Message string `json:"Message"` - File string `json:"File"` - Line int `json:"Line"` + Message string `json:"message"` + File string `json:"file"` + Line int `json:"line"` } func shouldWaitForControlPlayModeState(command string, params map[string]any) bool { diff --git a/cli/internal/cli/control_play_mode_wait_test.go b/cli/internal/cli/control_play_mode_wait_test.go index b3409c7c34..30b5ad521b 100644 --- a/cli/internal/cli/control_play_mode_wait_test.go +++ b/cli/internal/cli/control_play_mode_wait_test.go @@ -36,8 +36,8 @@ func TestRunControlPlayModeWithStateWaitPollsStatusAfterStaleInitialResponse(t * requests, serverErr, []string{ - `{"IsPlaying":false,"IsPaused":false,"Message":"Play mode started"}`, - `{"IsPlaying":true,"IsPaused":false,"Message":"Play mode status"}`, + `{"isPlaying":false,"isPaused":false,"message":"Play mode started"}`, + `{"isPlaying":true,"isPaused":false,"message":"Play mode status"}`, }) connection := unityipc.Connection{ @@ -113,8 +113,8 @@ func TestRunControlPlayModeWithStateWaitPreservesStopChangeFields(t *testing.T) make(chan map[string]any, 2), serverErr, []string{ - `{"IsPlaying":true,"IsPaused":false,"Changed":true,"WasAlreadyStopped":false,"Message":"Play mode stopped"}`, - `{"IsPlaying":false,"IsPaused":false,"Message":"Play mode status"}`, + `{"isPlaying":true,"isPaused":false,"changed":true,"wasAlreadyStopped":false,"message":"Play mode stopped"}`, + `{"isPlaying":false,"isPaused":false,"message":"Play mode status"}`, }) connection := unityipc.Connection{ @@ -194,7 +194,7 @@ func TestRunControlPlayModeWithStateWaitFailsWhenStateNeverMatches(t *testing.T) go serveRepeatedControlPlayModeResponse( listener, serverErr, - `{"IsPlaying":false,"IsPaused":false,"Message":"Play mode status"}`) + `{"isPlaying":false,"isPaused":false,"message":"Play mode status"}`) connection := unityipc.Connection{ Endpoint: unityipc.Endpoint{ @@ -253,7 +253,7 @@ func TestRunControlPlayModeWithStateWaitFailsImmediatelyWhenCompileErrorsBlockPl requests, serverErr, []string{ - `{"IsPlaying":false,"IsPaused":false,"BlockedByCompileErrors":true,"CompileErrorCount":1,"CompileErrors":[{"Message":"CS1002: ; expected","File":"Assets/Scripts/Sample.cs","Line":12}],"Message":"Play mode could not start because Unity has compiler errors."}`, + `{"isPlaying":false,"isPaused":false,"blockedByCompileErrors":true,"compileErrorCount":1,"compileErrors":[{"message":"CS1002: ; expected","file":"Assets/Scripts/Sample.cs","line":12}],"message":"Play mode could not start because Unity has compiler errors."}`, }) connection := unityipc.Connection{ @@ -334,8 +334,8 @@ func TestRunControlPlayModeWithStateWaitFailsWhenCompileErrorsAppearDuringPollin requests, serverErr, []string{ - `{"IsPlaying":false,"IsPaused":false,"Message":"Play mode started"}`, - `{"IsPlaying":false,"IsPaused":false,"BlockedByCompileErrors":true,"CompileErrorCount":1,"CompileErrors":[{"Message":"CS1525: invalid expression","File":"Assets/Scripts/Sample.cs","Line":3}],"Message":"Play mode could not start because Unity has compiler errors."}`, + `{"isPlaying":false,"isPaused":false,"message":"Play mode started"}`, + `{"isPlaying":false,"isPaused":false,"blockedByCompileErrors":true,"compileErrorCount":1,"compileErrors":[{"message":"CS1525: invalid expression","file":"Assets/Scripts/Sample.cs","line":3}],"message":"Play mode could not start because Unity has compiler errors."}`, }) connection := unityipc.Connection{ diff --git a/cli/internal/cli/debug_timing.go b/cli/internal/cli/debug_timing.go index 65cae731b1..7193f8fce7 100644 --- a/cli/internal/cli/debug_timing.go +++ b/cli/internal/cli/debug_timing.go @@ -56,6 +56,7 @@ func stripDebugTimingResult(command string, result json.RawMessage) json.RawMess return result } + delete(payload, "timings") delete(payload, "Timings") sanitized, err := json.Marshal(payload) if err != nil { @@ -70,7 +71,7 @@ func extractUnityDebugTimings(command string, result json.RawMessage) []string { } var payload struct { - Timings []string `json:"Timings"` + Timings []string `json:"timings"` } if err := json.Unmarshal(result, &payload); err != nil { return nil diff --git a/cli/internal/cli/debug_timing_test.go b/cli/internal/cli/debug_timing_test.go index 6216344406..92eb629447 100644 --- a/cli/internal/cli/debug_timing_test.go +++ b/cli/internal/cli/debug_timing_test.go @@ -87,7 +87,7 @@ func TestWriteDebugTiming_WhenUnityTimingsExist_ShouldWriteUnityTimingLines(t *t t.Setenv(debugTimingEnvName, "1") var stderr bytes.Buffer outcome := unityipc.UnitySendOutcome{ - Result: []byte(`{"Success":true,"Timings":["[Perf] Build: 12.3ms","[Perf] Execution: 4.5ms"]}`), + Result: []byte(`{"success":true,"timings":["[Perf] Build: 12.3ms","[Perf] Execution: 4.5ms"]}`), } writeDebugTiming(&stderr, executeDynamicCodeCommandName, time.Millisecond, outcome) @@ -108,14 +108,31 @@ func TestStripDebugTimingResult_WhenUnityTimingsExist_ShouldRemoveTimings(t *tes t.Setenv(debugTimingEnvName, "1") result := stripDebugTimingResult( executeDynamicCodeCommandName, - []byte(`{"Success":true,"Result":"ok","Timings":["[Perf] Build: 12.3ms"]}`), + []byte(`{"success":true,"result":"ok","timings":["[Perf] Build: 12.3ms"]}`), ) output := string(result) - if strings.Contains(output, "Timings") { + if strings.Contains(output, "timings") { t.Fatalf("Timings remained in sanitized result: %s", output) } - if !strings.Contains(output, `"Result":"ok"`) { + if !strings.Contains(output, `"result":"ok"`) { + t.Fatalf("sanitized result lost normal fields: %s", output) + } +} + +// Verifies that legacy-cased debug-only Unity timings are also removed from JSON stdout. +func TestStripDebugTimingResult_WhenLegacyUnityTimingsExist_ShouldRemoveTimings(t *testing.T) { + t.Setenv(debugTimingEnvName, "1") + result := stripDebugTimingResult( + executeDynamicCodeCommandName, + []byte(`{"success":true,"result":"ok","Timings":["[Perf] Build: 12.3ms"]}`), + ) + + output := string(result) + if strings.Contains(output, "Timings") { + t.Fatalf("legacy Timings remained in sanitized result: %s", output) + } + if !strings.Contains(output, `"result":"ok"`) { t.Fatalf("sanitized result lost normal fields: %s", output) } } diff --git a/cli/internal/cli/dynamic_code_wait.go b/cli/internal/cli/dynamic_code_wait.go index cc6ae83ff0..f31d0ce960 100644 --- a/cli/internal/cli/dynamic_code_wait.go +++ b/cli/internal/cli/dynamic_code_wait.go @@ -7,9 +7,10 @@ import ( ) const ( - domainReloadWaitParam = "WaitForDomainReload" - dynamicCodeCompileOnlyParam = "CompileOnly" - dynamicCodeDomainReloadWaitRequiredField = "DomainReloadWaitRequired" + domainReloadWaitParam = "WaitForDomainReload" + dynamicCodeCompileOnlyParam = "CompileOnly" + dynamicCodeDomainReloadWaitRequiredField = "domainReloadWaitRequired" + legacyDynamicCodeDomainReloadWaitRequiredField = "DomainReloadWaitRequired" ) func shouldWaitForExecuteDynamicCodeDomainReload(command string, params map[string]any) bool { @@ -33,7 +34,7 @@ func domainReloadWaitEnabled(params map[string]any, defaultValue bool) bool { func executeDynamicCodeDomainReloadWaitRequired(result []byte) bool { var payload struct { - DomainReloadWaitRequired bool `json:"DomainReloadWaitRequired"` + DomainReloadWaitRequired bool `json:"domainReloadWaitRequired"` } if err := json.Unmarshal(result, &payload); err != nil { return false @@ -58,6 +59,7 @@ func stripExecuteDynamicCodeControlResult(result []byte) []byte { } delete(payload, dynamicCodeDomainReloadWaitRequiredField) + delete(payload, legacyDynamicCodeDomainReloadWaitRequiredField) sanitized, err := json.Marshal(payload) if err != nil { return result diff --git a/cli/internal/cli/focus.go b/cli/internal/cli/focus.go index 96a02fafbb..089ffe0fb9 100644 --- a/cli/internal/cli/focus.go +++ b/cli/internal/cli/focus.go @@ -31,8 +31,8 @@ type unityProcess struct { } type focusResponse struct { - Success bool `json:"Success"` - Message string `json:"Message"` + Success bool `json:"success"` + Message string `json:"message"` } type restoreFocusFunc func(context.Context) error diff --git a/cli/internal/cli/launch_ready.go b/cli/internal/cli/launch_ready.go index cb1ecc0d88..0f0cfd1046 100644 --- a/cli/internal/cli/launch_ready.go +++ b/cli/internal/cli/launch_ready.go @@ -15,18 +15,18 @@ const ( ) type launchReadyResponse struct { - Success bool `json:"Success"` - Ready bool `json:"Ready"` - ServerReady bool `json:"ServerReady"` - ProjectIpcReady bool `json:"ProjectIpcReady"` - AlreadyRunning bool `json:"AlreadyRunning"` - Launched bool `json:"Launched"` - Restarted bool `json:"Restarted"` - Quit bool `json:"Quit"` - PreviousProcessId *int `json:"PreviousProcessId,omitempty"` - CurrentProcessId *int `json:"CurrentProcessId,omitempty"` - ProjectRoot string `json:"ProjectRoot"` - Message string `json:"Message"` + Success bool `json:"success"` + Ready bool `json:"ready"` + ServerReady bool `json:"serverReady"` + ProjectIpcReady bool `json:"projectIpcReady"` + AlreadyRunning bool `json:"alreadyRunning"` + Launched bool `json:"launched"` + Restarted bool `json:"restarted"` + Quit bool `json:"quit"` + PreviousProcessId *int `json:"previousProcessId,omitempty"` + CurrentProcessId *int `json:"currentProcessId,omitempty"` + ProjectRoot string `json:"projectRoot"` + Message string `json:"message"` } func writeLaunchReadinessWait(stdout io.Writer, spinner *terminalSpinner) { diff --git a/cli/internal/cli/launch_test.go b/cli/internal/cli/launch_test.go index 1593903eef..c33f9b4d52 100644 --- a/cli/internal/cli/launch_test.go +++ b/cli/internal/cli/launch_test.go @@ -159,11 +159,11 @@ func TestRunLaunchWritesReadyResponseAfterToolReadiness(t *testing.T) { output := stdout.String() for _, expected := range []string{ "Waiting for Unity CLI Loop server readiness...", - `"Success": true`, - `"Ready": true`, - `"ServerReady": true`, - `"ProjectIpcReady": true`, - `"Message": "Unity CLI Loop is ready."`, + `"success": true`, + `"ready": true`, + `"serverReady": true`, + `"projectIpcReady": true`, + `"message": "Unity CLI Loop is ready."`, } { if !strings.Contains(output, expected) { t.Fatalf("launch output missing %q:\n%s", expected, output) diff --git a/cli/internal/cli/list_output.go b/cli/internal/cli/list_output.go index 25cbf444d4..fadd3052a7 100644 --- a/cli/internal/cli/list_output.go +++ b/cli/internal/cli/list_output.go @@ -83,11 +83,18 @@ func newListOption(tool toolDefinition, propertyName string, property toolProper Name: "--" + optionNameForProperty(tool.Name, propertyName, property), Type: property.Type, Description: optionSummary(tool.Name, propertyName, property), - Default: property.EffectiveDefault(), + Default: listOptionDefault(property), Values: property.Enum, } } +func listOptionDefault(property toolProperty) any { + if isNegatedBooleanProperty(property) { + return false + } + return property.EffectiveDefault() +} + func appendDynamicCodeFileListOption(tool toolDefinition, options []listOption) []listOption { if tool.Name != executeDynamicCodeCommandName { return options diff --git a/cli/internal/cli/list_output_test.go b/cli/internal/cli/list_output_test.go index 99d2ddcb4c..e975affe3d 100644 --- a/cli/internal/cli/list_output_test.go +++ b/cli/internal/cli/list_output_test.go @@ -8,21 +8,21 @@ import ( // Tests that list output exposes the actual CLI option names instead of schema property names. func TestFormatToolListResultUsesCliOptionNames(t *testing.T) { result := formatToolListResult([]byte(`{ - "Tools": [ + "tools": [ { "name": "screenshot", "parameterSchema": { - "Properties": { + "properties": { "CaptureMode": { - "Type": "string", - "Description": "Capture mode", - "DefaultValue": "window", - "Enum": ["window", "rendering"] + "type": "string", + "description": "Capture mode", + "defaultValue": "window", + "enum": ["window", "rendering"] }, "AnnotateElements": { - "Type": "boolean", - "Description": "Annotate elements", - "DefaultValue": false + "type": "boolean", + "description": "Annotate elements", + "defaultValue": false } } } @@ -73,11 +73,17 @@ func TestNewListCatalogUsesSpecialOptionAliases(t *testing.T) { catalog := newListCatalog(cache) runTestsTool := findListTool(t, catalog, runTestsCommandName) - findListOption(t, runTestsTool, "--fail-on-unsaved-changes") + failOnUnsavedChanges := findListOption(t, runTestsTool, "--fail-on-unsaved-changes") + if failOnUnsavedChanges.Default != false { + t.Fatalf("fail-on-unsaved-changes default mismatch: %#v", failOnUnsavedChanges) + } assertListOptionMissing(t, runTestsTool, "--no-save-before-run") compileTool := findListTool(t, catalog, compileCommandName) - findListOption(t, compileTool, "--stop-on-external-scene-changes") + stopOnExternalSceneChanges := findListOption(t, compileTool, "--stop-on-external-scene-changes") + if stopOnExternalSceneChanges.Default != false { + t.Fatalf("stop-on-external-scene-changes default mismatch: %#v", stopOnExternalSceneChanges) + } assertListOptionMissing(t, compileTool, "--no-reload-external-scene-changes") } diff --git a/cli/internal/cli/pause_point_logs.go b/cli/internal/cli/pause_point_logs.go index 8bb57ebc59..29cbb04c2e 100644 --- a/cli/internal/cli/pause_point_logs.go +++ b/cli/internal/cli/pause_point_logs.go @@ -18,60 +18,60 @@ const ( var fetchMatchingLogs = fetchMatchingLogsFromUnity type pausePointMatchingLog struct { - Type string `json:"Type"` - Message string `json:"Message"` + Type string `json:"type"` + Message string `json:"message"` } type pausePointMatchingLogsResult struct { - SearchText string `json:"SearchText"` - TotalCount int `json:"TotalCount"` - DisplayedCount int `json:"DisplayedCount"` - MaxCount int `json:"MaxCount"` - Logs []pausePointMatchingLog `json:"Logs"` + SearchText string `json:"searchText"` + TotalCount int `json:"totalCount"` + DisplayedCount int `json:"displayedCount"` + MaxCount int `json:"maxCount"` + Logs []pausePointMatchingLog `json:"logs"` } type pausePointEvidenceSummary struct { - EditorState pausePointEditorState `json:"EditorState"` - PausePoint pausePointEvidencePausePoint `json:"PausePoint"` - MatchingLogs pausePointEvidenceLogs `json:"MatchingLogs"` - Warning string `json:"Warning"` + EditorState pausePointEditorState `json:"editorState"` + PausePoint pausePointEvidencePausePoint `json:"pausePoint"` + MatchingLogs pausePointEvidenceLogs `json:"matchingLogs"` + Warning string `json:"warning"` } type pausePointEvidencePausePoint struct { - Id string `json:"Id"` - Status string `json:"Status"` - Generation int `json:"Generation"` - HitCount int `json:"HitCount"` - MultipleHitsObserved bool `json:"MultipleHitsObserved"` - FirstHitAtUtc string `json:"FirstHitAtUtc"` - LastHitAtUtc string `json:"LastHitAtUtc"` - FirstHitSequence int `json:"FirstHitSequence"` - LastHitSequence int `json:"LastHitSequence"` + Id string `json:"id"` + Status string `json:"status"` + Generation int `json:"generation"` + HitCount int `json:"hitCount"` + MultipleHitsObserved bool `json:"multipleHitsObserved"` + FirstHitAtUtc string `json:"firstHitAtUtc"` + LastHitAtUtc string `json:"lastHitAtUtc"` + FirstHitSequence int `json:"firstHitSequence"` + LastHitSequence int `json:"lastHitSequence"` } type pausePointEvidenceLogs struct { - SearchText string `json:"SearchText"` - MatchingLogCount int `json:"MatchingLogCount"` - ReturnedLogCount int `json:"ReturnedLogCount"` - MaxCount int `json:"MaxCount"` - MayBeTruncated bool `json:"MayBeTruncated"` - MultipleMatchingLogsObserved bool `json:"MultipleMatchingLogsObserved"` + SearchText string `json:"searchText"` + MatchingLogCount int `json:"matchingLogCount"` + ReturnedLogCount int `json:"returnedLogCount"` + MaxCount int `json:"maxCount"` + MayBeTruncated bool `json:"mayBeTruncated"` + MultipleMatchingLogsObserved bool `json:"multipleMatchingLogsObserved"` } // pausePointWaitResult extends the hit response with marker-matching logs and // evidence summary so agents do not need a separate get-logs call while Unity is paused. type pausePointWaitResult struct { pausePointStatusResponse - MatchingLogs []pausePointMatchingLog `json:"MatchingLogs"` - EvidenceSummary pausePointEvidenceSummary `json:"EvidenceSummary"` + MatchingLogs []pausePointMatchingLog `json:"matchingLogs"` + EvidenceSummary pausePointEvidenceSummary `json:"evidenceSummary"` } type pausePointGetLogsResponse struct { - TotalCount int `json:"TotalCount"` - DisplayedCount int `json:"DisplayedCount"` - MaxCount int `json:"MaxCount"` - SearchText string `json:"SearchText"` - Logs []pausePointMatchingLog `json:"Logs"` + TotalCount int `json:"totalCount"` + DisplayedCount int `json:"displayedCount"` + MaxCount int `json:"maxCount"` + SearchText string `json:"searchText"` + Logs []pausePointMatchingLog `json:"logs"` } func fetchMatchingLogsFromUnity( diff --git a/cli/internal/cli/pause_point_wait.go b/cli/internal/cli/pause_point_wait.go index 487cffad5a..c18881b374 100644 --- a/cli/internal/cli/pause_point_wait.go +++ b/cli/internal/cli/pause_point_wait.go @@ -46,30 +46,30 @@ type pausePointStatusOptions struct { } type pausePointStatusResponse struct { - Id string `json:"Id"` - Status string `json:"Status"` - IsEnabled bool `json:"IsEnabled"` - IsHit bool `json:"IsHit"` - HitCount int `json:"HitCount"` - TimeoutSeconds int `json:"TimeoutSeconds"` - Expired bool `json:"Expired"` - EnabledAtUtc string `json:"EnabledAtUtc"` - ElapsedSinceEnabledMilliseconds int64 `json:"ElapsedSinceEnabledMilliseconds"` - RemainingMilliseconds int64 `json:"RemainingMilliseconds"` - Generation int `json:"Generation"` - EditorState pausePointEditorState `json:"EditorState"` - FirstHitAtUtc string `json:"FirstHitAtUtc"` - LastHitAtUtc string `json:"LastHitAtUtc"` - FirstHitSequence int `json:"FirstHitSequence"` - LastHitSequence int `json:"LastHitSequence"` - Message string `json:"Message"` - RecommendedNextAction string `json:"RecommendedNextAction"` + Id string `json:"id"` + Status string `json:"status"` + IsEnabled bool `json:"isEnabled"` + IsHit bool `json:"isHit"` + HitCount int `json:"hitCount"` + TimeoutSeconds int `json:"timeoutSeconds"` + Expired bool `json:"expired"` + EnabledAtUtc string `json:"enabledAtUtc"` + ElapsedSinceEnabledMilliseconds int64 `json:"elapsedSinceEnabledMilliseconds"` + RemainingMilliseconds int64 `json:"remainingMilliseconds"` + Generation int `json:"generation"` + EditorState pausePointEditorState `json:"editorState"` + FirstHitAtUtc string `json:"firstHitAtUtc"` + LastHitAtUtc string `json:"lastHitAtUtc"` + FirstHitSequence int `json:"firstHitSequence"` + LastHitSequence int `json:"lastHitSequence"` + Message string `json:"message"` + RecommendedNextAction string `json:"recommendedNextAction"` } type pausePointEditorState struct { - IsPlaying bool `json:"IsPlaying"` - IsPaused bool `json:"IsPaused"` - CapturedAt string `json:"CapturedAt"` + IsPlaying bool `json:"isPlaying"` + IsPaused bool `json:"isPaused"` + CapturedAt string `json:"capturedAt"` } type pausePointWaitState string @@ -219,8 +219,8 @@ func runWaitForPausePoint( // Best-effort: the timeout diagnosis must not depend on a second Unity round trip succeeding. logs, logsErr := fetchMatchingLogs(ctx, connection, options.id, options.matchingLogsMaxCount) if logsErr == nil { - waitErr.Details["MatchingLogs"] = logs.Logs - waitErr.Details["EvidenceSummary"] = buildPausePointEvidenceSummary(response, logs) + waitErr.Details["matchingLogs"] = logs.Logs + waitErr.Details["evidenceSummary"] = buildPausePointEvidenceSummary(response, logs) } } writeErrorEnvelope(stderr, waitErr) diff --git a/cli/internal/cli/pause_point_wait_test.go b/cli/internal/cli/pause_point_wait_test.go index c520d4c74e..2929dff0bc 100644 --- a/cli/internal/cli/pause_point_wait_test.go +++ b/cli/internal/cli/pause_point_wait_test.go @@ -122,7 +122,7 @@ func TestRunWaitForPausePointClearsEnabledMarkerAfterTimeout(t *testing.T) { } envelope := parsePausePointErrorEnvelope(t, stderr.Bytes()) editorState, ok := envelope.Error.Details["editorState"].(map[string]any) - if !ok || editorState["IsPlaying"] != true || editorState["IsPaused"] != false || editorState["CapturedAt"] != "Current" { + if !ok || editorState["isPlaying"] != true || editorState["isPaused"] != false || editorState["capturedAt"] != "Current" { t.Fatalf("editorState detail mismatch: %#v", envelope.Error.Details) } if envelope.Error.Details["markerMessage"] != "Pause point enabled." { @@ -352,7 +352,7 @@ func TestRunWaitForPausePointReportsNotEnabledError(t *testing.T) { t.Fatalf("status detail mismatch: %#v", envelope.Error.Details) } editorState, ok := envelope.Error.Details["editorState"].(map[string]any) - if !ok || editorState["IsPlaying"] != true || editorState["IsPaused"] != false || editorState["CapturedAt"] != "Current" { + if !ok || editorState["isPlaying"] != true || editorState["isPaused"] != false || editorState["capturedAt"] != "Current" { t.Fatalf("editorState details mismatch: %#v", envelope.Error.Details) } } @@ -524,7 +524,7 @@ func TestRunWaitForPausePointEmbedsEmptyMatchingLogsWhenNoneMatch(t *testing.T) if code != 0 { t.Fatalf("expected success, got %d with stderr %s", code, stderr.String()) } - if !strings.Contains(stdout.String(), "\"MatchingLogs\": []") { + if !strings.Contains(stdout.String(), "\"matchingLogs\": []") { t.Fatalf("MatchingLogs must be an explicit empty array: %s", stdout.String()) } var result pausePointWaitResult @@ -579,10 +579,10 @@ func TestRunWaitForPausePointIgnoresLogFetchFailure(t *testing.T) { if code != 0 { t.Fatalf("expected success despite log fetch failure, got %d with stderr %s", code, stderr.String()) } - if strings.Contains(stdout.String(), "MatchingLogs") { + if strings.Contains(stdout.String(), "matchingLogs") { t.Fatalf("MatchingLogs must be omitted when the fetch fails: %s", stdout.String()) } - if strings.Contains(stdout.String(), "EvidenceSummary") { + if strings.Contains(stdout.String(), "evidenceSummary") { t.Fatalf("EvidenceSummary must be omitted when the fetch fails: %s", stdout.String()) } } @@ -649,16 +649,16 @@ func TestRunWaitForPausePointEmbedsMatchingLogsOnTimeout(t *testing.T) { } envelope := parsePausePointErrorEnvelope(t, stderr.Bytes()) // The detail key mirrors the hit-response field name, so one spelling covers both surfaces. - matchingLogs, ok := envelope.Error.Details["MatchingLogs"].([]any) + matchingLogs, ok := envelope.Error.Details["matchingLogs"].([]any) if !ok || len(matchingLogs) != 1 { t.Fatalf("MatchingLogs detail mismatch: %#v", envelope.Error.Details) } - evidenceSummary, ok := envelope.Error.Details["EvidenceSummary"].(map[string]any) + evidenceSummary, ok := envelope.Error.Details["evidenceSummary"].(map[string]any) if !ok { t.Fatalf("EvidenceSummary detail missing: %#v", envelope.Error.Details) } - evidenceLogs, ok := evidenceSummary["MatchingLogs"].(map[string]any) - if !ok || evidenceLogs["MayBeTruncated"] != true || evidenceLogs["MatchingLogCount"] != float64(3) { + evidenceLogs, ok := evidenceSummary["matchingLogs"].(map[string]any) + if !ok || evidenceLogs["mayBeTruncated"] != true || evidenceLogs["matchingLogCount"] != float64(3) { t.Fatalf("EvidenceSummary matching logs mismatch: %#v", evidenceSummary) } } diff --git a/cli/internal/cli/run.go b/cli/internal/cli/run.go index 6f1d8c09a8..daf1f6228f 100644 --- a/cli/internal/cli/run.go +++ b/cli/internal/cli/run.go @@ -407,7 +407,7 @@ func writeJSON(stdout io.Writer, result json.RawMessage) { } type compileResultStatus struct { - Success *bool `json:"Success"` + Success *bool `json:"success"` } type compileReadinessWaitMode int diff --git a/cli/internal/cli/tool_readiness.go b/cli/internal/cli/tool_readiness.go index 7122139011..b12493eb8f 100644 --- a/cli/internal/cli/tool_readiness.go +++ b/cli/internal/cli/tool_readiness.go @@ -147,8 +147,8 @@ func executeDynamicCodeReadinessProbeParams() map[string]any { } type executeDynamicCodeReadinessResponse struct { - Success bool `json:"Success"` - ErrorMessage string `json:"ErrorMessage"` + Success bool `json:"success"` + ErrorMessage string `json:"errorMessage"` } func isExecuteDynamicCodeAvailable(projectRoot string) bool { diff --git a/cli/internal/cli/tools_test.go b/cli/internal/cli/tools_test.go index bf560e5106..a0e5c64e83 100644 --- a/cli/internal/cli/tools_test.go +++ b/cli/internal/cli/tools_test.go @@ -504,29 +504,29 @@ internal: true func TestLoadToolsAcceptsEditorParameterSchemaCache(t *testing.T) { projectRoot := t.TempDir() writeToolCache(t, projectRoot, `{ - "Tools": [ + "tools": [ { "name": "get-logs", "description": "Retrieve logs from Unity Console", "parameterSchema": { - "Properties": { + "properties": { "LogType": { - "Type": "string", - "Description": "Log type to filter", - "DefaultValue": "All" + "type": "string", + "description": "Log type to filter", + "defaultValue": "All" }, "IncludeStackTrace": { - "Type": "boolean", - "Description": "Whether to display stack trace", - "DefaultValue": false + "type": "boolean", + "description": "Whether to display stack trace", + "defaultValue": false }, "IncludeInactive": { - "Type": "boolean", - "Description": "Whether to include inactive objects", - "DefaultValue": true + "type": "boolean", + "description": "Whether to include inactive objects", + "defaultValue": true } }, - "Required": [] + "required": [] } } ] diff --git a/cli/internal/tools/types.go b/cli/internal/tools/types.go index 2a6b83f2bc..791a036ebc 100644 --- a/cli/internal/tools/types.go +++ b/cli/internal/tools/types.go @@ -24,7 +24,7 @@ type ToolProperty struct { Type string `json:"type"` Description string `json:"description,omitempty"` Default any `json:"default,omitempty"` - DefaultValue any `json:"DefaultValue,omitempty"` + DefaultValue any `json:"defaultValue,omitempty"` Hidden bool `json:"hidden,omitempty"` Enum []string `json:"enum,omitempty"` Items *struct { diff --git a/scripts/refresh-neighbor-game-skills-macos.sh b/scripts/refresh-neighbor-game-skills-macos.sh index e84b8e0f54..daa8574e5e 100644 --- a/scripts/refresh-neighbor-game-skills-macos.sh +++ b/scripts/refresh-neighbor-game-skills-macos.sh @@ -393,7 +393,7 @@ return SceneManager.GetActiveScene().path; fail "failed to execute sample scene open command: $project" fi - if ! printf '%s\n' "$response" | jq -e --arg scene_path "$sample_scene_path" '.Success == true and .Result == $scene_path' >/dev/null; then + if ! printf '%s\n' "$response" | jq -e --arg scene_path "$sample_scene_path" '.success == true and .result == $scene_path' >/dev/null; then printf '%s\n' "$response" >&2 fail "failed to open sample scene: $project" fi diff --git a/scripts/refresh-neighbor-game-skills-windows.ps1 b/scripts/refresh-neighbor-game-skills-windows.ps1 index e341d36dd0..2139eaf610 100644 --- a/scripts/refresh-neighbor-game-skills-windows.ps1 +++ b/scripts/refresh-neighbor-game-skills-windows.ps1 @@ -758,7 +758,7 @@ return SceneManager.GetActiveScene().path; } [pscustomobject]$json = $responseText | ConvertFrom-Json - if ($json.Success -eq $true -and $json.Result -eq $SampleScenePath) { + if ($json.success -eq $true -and $json.result -eq $SampleScenePath) { return } diff --git a/scripts/refresh-neighbor-game-skills-windows.sh b/scripts/refresh-neighbor-game-skills-windows.sh index 7725bc7a91..b28940df24 100644 --- a/scripts/refresh-neighbor-game-skills-windows.sh +++ b/scripts/refresh-neighbor-game-skills-windows.sh @@ -592,7 +592,7 @@ return SceneManager.GetActiveScene().path; response_matches_scene() { response=$1 if command -v jq >/dev/null 2>&1; then - printf '%s\n' "$response" | jq -e --arg scene_path "$sample_scene_path" '.Success == true and .Result == $scene_path' >/dev/null + printf '%s\n' "$response" | jq -e --arg scene_path "$sample_scene_path" '.success == true and .result == $scene_path' >/dev/null return $? fi @@ -606,7 +606,7 @@ try { } catch { exit 1 } -if (\$Value.Success -eq \$true -and \$Value.Result -eq \$ScenePath) { +if (\$Value.success -eq \$true -and \$Value.result -eq \$ScenePath) { exit 0 } exit 1 diff --git a/scripts/run-windows-e2e.ps1 b/scripts/run-windows-e2e.ps1 index ad1f9aa1ed..557970296c 100644 --- a/scripts/run-windows-e2e.ps1 +++ b/scripts/run-windows-e2e.ps1 @@ -108,8 +108,8 @@ function Invoke-UloopJsonChecked { [string]$text = Invoke-UloopChecked -CommandArguments $CommandArguments [pscustomobject]$json = $text | ConvertFrom-Json - if ($json.PSObject.Properties.Name -contains "Success" -and $json.Success -ne $true) { - throw "uloop $($CommandArguments -join " ") returned Success=false: $text" + if ($json.PSObject.Properties.Name -contains "success" -and $json.success -ne $true) { + throw "uloop $($CommandArguments -join " ") returned success=false: $text" } return $json @@ -172,11 +172,11 @@ function Wait-PlayMode { [pscustomobject]$result = $probe.Text | ConvertFrom-Json [string]$isPlaying = "" - if ($null -ne $result.Result) { - $isPlaying = $result.Result.ToString() + if ($null -ne $result.result) { + $isPlaying = $result.result.ToString() } - if ($result.Success -eq $true -and $isPlaying -eq "True") { + if ($result.success -eq $true -and $isPlaying -eq "True") { return } @@ -221,7 +221,7 @@ return SceneManager.GetActiveScene().path; "@ [pscustomobject]$result = Invoke-DynamicCode -Code $code - if ($result.Result -ne $ScenePath) { + if ($result.result -ne $ScenePath) { throw "Failed to open scene: $ScenePath" } } @@ -309,7 +309,7 @@ function Invoke-LaunchSmoke { Invoke-UloopChecked -CommandArguments @("launch") | Out-Host Wait-UnityReady [pscustomobject]$result = Invoke-DynamicCode -Code 'return "windows-launch-smoke";' - if ($result.Result -ne "windows-launch-smoke") { + if ($result.result -ne "windows-launch-smoke") { throw "launch smoke dynamic-code readiness check failed" } } @@ -376,7 +376,7 @@ if (cube == null) return -9999f; return cube.transform.position.z; '@ - return [double]$result.Result + return [double]$result.result } try { @@ -423,8 +423,8 @@ try { Invoke-Step -Name "Final Console Check" -Body { [pscustomobject]$logs = Invoke-UloopJsonChecked -CommandArguments @("get-logs", "--log-type", "All", "--max-count", "200") [int]$badCount = 0 - foreach ($log in $logs.Logs) { - if ($log.Type -eq "Error" -or $log.Type -eq "Warning") { + foreach ($log in $logs.logs) { + if ($log.type -eq "Error" -or $log.type -eq "Warning") { $badCount++ } } diff --git a/scripts/smoke-cli-recovery-readiness.go b/scripts/smoke-cli-recovery-readiness.go index 47efd51901..ee45235f94 100644 --- a/scripts/smoke-cli-recovery-readiness.go +++ b/scripts/smoke-cli-recovery-readiness.go @@ -327,7 +327,7 @@ func assertJSONSuccess(result commandResult, label string) (map[string]any, erro return nil, err } - if success, ok := payload["Success"].(bool); !ok || !success { + if success, ok := payload["success"].(bool); !ok || !success { printCommandContext(label, result) return nil, fmt.Errorf("%s returned invalid success payload: %v", label, payload) } @@ -336,7 +336,7 @@ func assertJSONSuccess(result commandResult, label string) (map[string]any, erro } func assertDynamicCodeResult(payload map[string]any) error { - result, ok := payload["Result"].(string) + result, ok := payload["result"].(string) if ok && result == expectedDynamicCodeResult { return nil } diff --git a/scripts/test-simulate-mouse-demo.ps1 b/scripts/test-simulate-mouse-demo.ps1 index c57b928b3e..718046e68f 100644 --- a/scripts/test-simulate-mouse-demo.ps1 +++ b/scripts/test-simulate-mouse-demo.ps1 @@ -109,7 +109,7 @@ function Wait-PlayMode { } [pscustomobject]$result = $probe.Text | ConvertFrom-Json - if ($result.Success -eq $true -and $result.Result -eq "True") { + if ($result.success -eq $true -and $result.result -eq "True") { return } @@ -140,14 +140,14 @@ function Assert-MouseResponse { [string]$ExpectedHit ) - if ($Response.Success -ne $true) { - throw "$ExpectedAction failed: $($Response.Message)" + if ($Response.success -ne $true) { + throw "$ExpectedAction failed: $($Response.message)" } - Assert-EqualText -Actual $Response.Action -Expected $ExpectedAction -Context "Action" + Assert-EqualText -Actual $Response.action -Expected $ExpectedAction -Context "Action" if (-not [string]::IsNullOrWhiteSpace($ExpectedHit)) { - Assert-EqualText -Actual $Response.HitGameObjectName -Expected $ExpectedHit -Context "HitGameObjectName" + Assert-EqualText -Actual $Response.hitGameObjectName -Expected $ExpectedHit -Context "hitGameObjectName" } } @@ -157,7 +157,7 @@ function Get-UiElement { [string]$Name ) - [object[]]$matches = @($Elements | Where-Object { $_.Name -eq $Name }) + [object[]]$matches = @($Elements | Where-Object { $_.name -eq $Name }) if ($matches.Count -eq 1) { return $matches[0] } @@ -240,11 +240,11 @@ return text.text; "@ [pscustomobject]$result = Invoke-UloopJson -CommandArguments @("execute-dynamic-code", "--code", $code) - if ($result.Success -ne $true) { - throw "Failed to read text from ${ObjectName}: $($result.ErrorMessage)" + if ($result.success -ne $true) { + throw "Failed to read text from ${ObjectName}: $($result.errorMessage)" } - return $result.Result + return $result.result } function Get-DropZoneStatus { @@ -261,11 +261,11 @@ return dropZone.StatusMessage; '@ ) - if ($result.Success -ne $true) { - throw "Failed to read DropZone status: $($result.ErrorMessage)" + if ($result.success -ne $true) { + throw "Failed to read DropZone status: $($result.errorMessage)" } - return $result.Result + return $result.result } function Get-LongPressButtonText { @@ -283,11 +283,11 @@ return text.text; '@ ) - if ($result.Success -ne $true) { - throw "Failed to read LongPressButton text: $($result.ErrorMessage)" + if ($result.success -ne $true) { + throw "Failed to read LongPressButton text: $($result.errorMessage)" } - return $result.Result + return $result.result } function Get-VirtualPadState { @@ -305,11 +305,11 @@ return pad.Direction.ToString("F3"); '@ ) - if ($result.Success -ne $true) { - throw "Failed to read VirtualPad state: $($result.ErrorMessage)" + if ($result.success -ne $true) { + throw "Failed to read VirtualPad state: $($result.errorMessage)" } - return $result.Result + return $result.result } function Initialize-DemoScene { @@ -330,11 +330,11 @@ return SceneManager.GetActiveScene().path; "@ [pscustomobject]$result = Invoke-UloopJson -CommandArguments @("execute-dynamic-code", "--code", $code) - if ($result.Success -ne $true) { - throw "Failed to load ${ScenePath}: $($result.ErrorMessage)" + if ($result.success -ne $true) { + throw "Failed to load ${ScenePath}: $($result.errorMessage)" } - Assert-EqualText -Actual $result.Result -Expected $ScenePath -Context "Active scene" + Assert-EqualText -Actual $result.result -Expected $ScenePath -Context "Active scene" } function Get-AnnotatedElements { @@ -346,11 +346,11 @@ function Get-AnnotatedElements { "--elements-only" ) - if ($response.ScreenshotCount -ne 1) { - throw "Expected one screenshot response, got $($response.ScreenshotCount)" + if ($response.screenshotCount -ne 1) { + throw "Expected one screenshot response, got $($response.screenshotCount)" } - [object[]]$elements = @($response.Screenshots[0].AnnotatedElements) + [object[]]$elements = @($response.screenshots[0].annotatedElements) if ($elements.Count -eq 0) { throw "No annotated UI elements were returned" } @@ -386,41 +386,41 @@ try { [pscustomobject]$virtualPad = Get-UiElement -Elements $elements -Name "VirtualPadBackground" Write-Host "[4/7] Clicking counter buttons..." - Invoke-MouseUi -Action "Click" -X $clickButton1.SimX -Y $clickButton1.SimY -TargetPath $clickButton1.Path -ExpectedHit "ClickButton1" | Out-Null - Invoke-MouseUi -Action "Click" -X $clickButton2.SimX -Y $clickButton2.SimY -TargetPath $clickButton2.Path -ExpectedHit "ClickButton2" | Out-Null - Invoke-MouseUi -Action "Click" -X $clickButton1.SimX -Y $clickButton1.SimY -TargetPath $clickButton1.Path -ExpectedHit "ClickButton1" | Out-Null - Invoke-MouseUi -Action "Click" -X $clickButton2.SimX -Y $clickButton2.SimY -TargetPath $clickButton2.Path -ExpectedHit "ClickButton2" | Out-Null + Invoke-MouseUi -Action "Click" -X $clickButton1.simX -Y $clickButton1.simY -TargetPath $clickButton1.path -ExpectedHit "ClickButton1" | Out-Null + Invoke-MouseUi -Action "Click" -X $clickButton2.simX -Y $clickButton2.simY -TargetPath $clickButton2.path -ExpectedHit "ClickButton2" | Out-Null + Invoke-MouseUi -Action "Click" -X $clickButton1.simX -Y $clickButton1.simY -TargetPath $clickButton1.path -ExpectedHit "ClickButton1" | Out-Null + Invoke-MouseUi -Action "Click" -X $clickButton2.simX -Y $clickButton2.simY -TargetPath $clickButton2.path -ExpectedHit "ClickButton2" | Out-Null [string]$counterText = Get-TextFromScene -ObjectName "CounterText" Assert-EqualText -Actual $counterText -Expected "Total Clicks: 4" -Context "CounterText" Write-Host "[5/7] Long-pressing the hold button..." - Invoke-MouseUi -Action "LongPress" -X $longPressButton.SimX -Y $longPressButton.SimY -Duration 3.2 -TargetPath $longPressButton.Path -ExpectedHit "LongPressButton" | Out-Null + Invoke-MouseUi -Action "LongPress" -X $longPressButton.simX -Y $longPressButton.simY -Duration 3.2 -TargetPath $longPressButton.path -ExpectedHit "LongPressButton" | Out-Null [string]$longPressText = Get-LongPressButtonText Assert-EqualText -Actual $longPressText -Expected "Activated!" -Context "LongPressButton label" Write-Host "[6/7] Dragging boxes into the DropZone..." - Invoke-MouseUi -Action "Drag" -FromX $redBox.SimX -FromY $redBox.SimY -X $dropZone.SimX -Y $dropZone.SimY -DragSpeed 900 -TargetPath $redBox.Path -DropTargetPath $dropZone.Path -ExpectedHit "RedBox" | Out-Null + Invoke-MouseUi -Action "Drag" -FromX $redBox.simX -FromY $redBox.simY -X $dropZone.simX -Y $dropZone.simY -DragSpeed 900 -TargetPath $redBox.path -DropTargetPath $dropZone.path -ExpectedHit "RedBox" | Out-Null Assert-EqualText -Actual (Get-DropZoneStatus) -Expected "Dropped: RedBox" -Context "DropZone after RedBox" - Invoke-MouseUi -Action "DragStart" -X $greenBox.SimX -Y $greenBox.SimY -DragSpeed 700 -TargetPath $greenBox.Path -ExpectedHit "GreenBox" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($dropZone.SimX + 150) -Y ($greenBox.SimY - 50) -DragSpeed 500 -ExpectedHit "GreenBox" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($dropZone.SimX - 150) -Y ($dropZone.SimY + 50) -DragSpeed 500 -ExpectedHit "GreenBox" | Out-Null - Invoke-MouseUi -Action "DragEnd" -X $dropZone.SimX -Y $dropZone.SimY -DragSpeed 500 -DropTargetPath $dropZone.Path -ExpectedHit "GreenBox" | Out-Null + Invoke-MouseUi -Action "DragStart" -X $greenBox.simX -Y $greenBox.simY -DragSpeed 700 -TargetPath $greenBox.path -ExpectedHit "GreenBox" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($dropZone.simX + 150) -Y ($greenBox.simY - 50) -DragSpeed 500 -ExpectedHit "GreenBox" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($dropZone.simX - 150) -Y ($dropZone.simY + 50) -DragSpeed 500 -ExpectedHit "GreenBox" | Out-Null + Invoke-MouseUi -Action "DragEnd" -X $dropZone.simX -Y $dropZone.simY -DragSpeed 500 -DropTargetPath $dropZone.path -ExpectedHit "GreenBox" | Out-Null Assert-EqualText -Actual (Get-DropZoneStatus) -Expected "Dropped: GreenBox" -Context "DropZone after GreenBox" - Invoke-MouseUi -Action "Drag" -FromX $blueBox.SimX -FromY $blueBox.SimY -X $dropZone.SimX -Y $dropZone.SimY -DragSpeed 900 -TargetPath $blueBox.Path -DropTargetPath $dropZone.Path -ExpectedHit "BlueBox" | Out-Null + Invoke-MouseUi -Action "Drag" -FromX $blueBox.simX -FromY $blueBox.simY -X $dropZone.simX -Y $dropZone.simY -DragSpeed 900 -TargetPath $blueBox.path -DropTargetPath $dropZone.path -ExpectedHit "BlueBox" | Out-Null Assert-EqualText -Actual (Get-DropZoneStatus) -Expected "Dropped: BlueBox" -Context "DropZone after BlueBox" Write-Host "[7/7] Exercising the virtual pad..." - [double]$padWidth = $virtualPad.BoundsMaxX - $virtualPad.BoundsMinX - [double]$padHeight = $virtualPad.BoundsMaxY - $virtualPad.BoundsMinY + [double]$padWidth = $virtualPad.boundsMaxX - $virtualPad.boundsMinX + [double]$padHeight = $virtualPad.boundsMaxY - $virtualPad.boundsMinY [double]$padOffset = [Math]::Min($padWidth, $padHeight) * 0.28 - Invoke-MouseUi -Action "DragStart" -X $virtualPad.SimX -Y $virtualPad.SimY -TargetPath $virtualPad.Path -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($virtualPad.SimX + $padOffset) -Y ($virtualPad.SimY - $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($virtualPad.SimX - $padOffset) -Y ($virtualPad.SimY + $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragMove" -X $virtualPad.SimX -Y ($virtualPad.SimY - $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($virtualPad.SimX + $padOffset) -Y $virtualPad.SimY -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragEnd" -X $virtualPad.SimX -Y $virtualPad.SimY -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragStart" -X $virtualPad.simX -Y $virtualPad.simY -TargetPath $virtualPad.path -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($virtualPad.simX + $padOffset) -Y ($virtualPad.simY - $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($virtualPad.simX - $padOffset) -Y ($virtualPad.simY + $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragMove" -X $virtualPad.simX -Y ($virtualPad.simY - $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($virtualPad.simX + $padOffset) -Y $virtualPad.simY -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragEnd" -X $virtualPad.simX -Y $virtualPad.simY -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null Assert-EqualText -Actual (Get-VirtualPadState) -Expected "Zero" -Context "VirtualPad state" Write-Host "" diff --git a/scripts/test-simulate-mouse-demo.sh b/scripts/test-simulate-mouse-demo.sh index de403ca019..2fea736fb5 100755 --- a/scripts/test-simulate-mouse-demo.sh +++ b/scripts/test-simulate-mouse-demo.sh @@ -71,8 +71,8 @@ assert_json_success() { json=$1 context=$2 - printf '%s\n' "$json" | jq -e '.Success == true' >/dev/null \ - || fail "$context failed: $(printf '%s\n' "$json" | jq -r '.ErrorMessage // .Message // .Error // "unknown error"')" + printf '%s\n' "$json" | jq -e '.success == true' >/dev/null \ + || fail "$context failed: $(printf '%s\n' "$json" | jq -r '.errorMessage // .message // .error // "unknown error"')" } assert_text_equals() { @@ -101,7 +101,7 @@ wait_play_mode() { attempt=0 while [ "$attempt" -lt 20 ]; do json=$(run_uloop_json execute-dynamic-code --code 'using UnityEngine; return Application.isPlaying;') - if printf '%s\n' "$json" | jq -e '.Success == true and .Result == "True"' >/dev/null; then + if printf '%s\n' "$json" | jq -e '.success == true and .result == "True"' >/dev/null; then return fi @@ -128,7 +128,7 @@ return SceneManager.GetActiveScene().path; json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Load demo scene" - scene=$(printf '%s\n' "$json" | jq -r '.Result') + scene=$(printf '%s\n' "$json" | jq -r '.result') assert_text_equals "$scene" "$SCENE_PATH" "Active scene" } @@ -214,7 +214,7 @@ return displayTexts[selectedIndex] + " / " + currentGameViewSize.x + "x" + curre json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Select Full HD Game View" - printf ' %s\n' "$(printf '%s\n' "$json" | jq -r '.Result')" + printf ' %s\n' "$(printf '%s\n' "$json" | jq -r '.result')" sleep 1 } @@ -251,7 +251,7 @@ return selectedSizeIndexProperty.GetValue(gameView, null).ToString(); json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Capture Game View size index" - printf '%s\n' "$json" | jq -r '.Result' + printf '%s\n' "$json" | jq -r '.result' } restore_game_view_size_index() { @@ -299,16 +299,16 @@ capture_annotated_elements() { --annotate-elements \ --elements-only >"$ELEMENTS_JSON" - jq -e '.ScreenshotCount == 1 and (.Screenshots[0].AnnotatedElements | length > 0)' "$ELEMENTS_JSON" >/dev/null \ + jq -e '.screenshotCount == 1 and (.screenshots[0].annotatedElements | length > 0)' "$ELEMENTS_JSON" >/dev/null \ || fail "No annotated UI elements were returned" } get_element_json() { name=$1 - count=$(jq --arg name "$name" '[.Screenshots[0].AnnotatedElements[] | select(.Name == $name)] | length' "$ELEMENTS_JSON") + count=$(jq --arg name "$name" '[.screenshots[0].annotatedElements[] | select(.name == $name)] | length' "$ELEMENTS_JSON") [ "$count" = "1" ] || fail "Expected exactly one annotated element named '$name', found $count" - jq -c --arg name "$name" '.Screenshots[0].AnnotatedElements[] | select(.Name == $name)' "$ELEMENTS_JSON" + jq -c --arg name "$name" '.screenshots[0].annotatedElements[] | select(.name == $name)' "$ELEMENTS_JSON" } element_field() { @@ -323,21 +323,21 @@ assert_mouse_response() { expected_hit=$3 printf '%s\n' "$json" | jq -e --arg action "$expected_action" \ - '.Success == true and .Action == $action' >/dev/null \ - || fail "$expected_action failed: $(printf '%s\n' "$json" | jq -r '.Message // "unknown error"')" + '.success == true and .action == $action' >/dev/null \ + || fail "$expected_action failed: $(printf '%s\n' "$json" | jq -r '.message // "unknown error"')" if [ -n "$expected_hit" ]; then - actual_hit=$(printf '%s\n' "$json" | jq -r '.HitGameObjectName // ""') - assert_text_equals "$actual_hit" "$expected_hit" "HitGameObjectName" + actual_hit=$(printf '%s\n' "$json" | jq -r '.hitGameObjectName // ""') + assert_text_equals "$actual_hit" "$expected_hit" "hitGameObjectName" fi } invoke_click() { name=$1 element=$(get_element_json "$name") - x=$(element_field "$element" "SimX") - y=$(element_field "$element" "SimY") - path=$(element_field "$element" "Path") + x=$(element_field "$element" "simX") + y=$(element_field "$element" "simY") + path=$(element_field "$element" "path") json=$(run_uloop_json simulate-mouse-ui \ --action Click \ @@ -352,9 +352,9 @@ invoke_long_press() { name=$1 duration=$2 element=$(get_element_json "$name") - x=$(element_field "$element" "SimX") - y=$(element_field "$element" "SimY") - path=$(element_field "$element" "Path") + x=$(element_field "$element" "simX") + y=$(element_field "$element" "simY") + path=$(element_field "$element" "path") json=$(run_uloop_json simulate-mouse-ui \ --action LongPress \ @@ -371,12 +371,12 @@ invoke_drag_to_drop_zone() { speed=$2 element=$(get_element_json "$name") drop_zone=$(get_element_json "DropZone") - from_x=$(element_field "$element" "SimX") - from_y=$(element_field "$element" "SimY") - to_x=$(element_field "$drop_zone" "SimX") - to_y=$(element_field "$drop_zone" "SimY") - target_path=$(element_field "$element" "Path") - drop_path=$(element_field "$drop_zone" "Path") + from_x=$(element_field "$element" "simX") + from_y=$(element_field "$element" "simY") + to_x=$(element_field "$drop_zone" "simX") + to_y=$(element_field "$drop_zone" "simY") + target_path=$(element_field "$element" "path") + drop_path=$(element_field "$drop_zone" "path") json=$(run_uloop_json simulate-mouse-ui \ --action Drag \ @@ -394,9 +394,9 @@ invoke_drag_to_drop_zone() { invoke_drag_start() { name=$1 element=$(get_element_json "$name") - x=$(element_field "$element" "SimX") - y=$(element_field "$element" "SimY") - path=$(element_field "$element" "Path") + x=$(element_field "$element" "simX") + y=$(element_field "$element" "simY") + path=$(element_field "$element" "path") json=$(run_uloop_json simulate-mouse-ui \ --action DragStart \ @@ -452,7 +452,7 @@ return text.text; json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Read text from $object_name" - printf '%s\n' "$json" | jq -r '.Result' + printf '%s\n' "$json" | jq -r '.result' } get_long_press_button_text() { @@ -468,7 +468,7 @@ return text.text; json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Read LongPressButton text" - printf '%s\n' "$json" | jq -r '.Result' + printf '%s\n' "$json" | jq -r '.result' } get_drop_zone_status() { @@ -483,7 +483,7 @@ return dropZone.StatusMessage; json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Read DropZone status" - printf '%s\n' "$json" | jq -r '.Result' + printf '%s\n' "$json" | jq -r '.result' } get_virtual_pad_state() { @@ -499,17 +499,17 @@ return pad.Direction.ToString("F3"); json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Read VirtualPad state" - printf '%s\n' "$json" | jq -r '.Result' + printf '%s\n' "$json" | jq -r '.result' } exercise_virtual_pad() { virtual_pad=$(get_element_json "VirtualPadBackground") - pad_x=$(element_field "$virtual_pad" "SimX") - pad_y=$(element_field "$virtual_pad" "SimY") - pad_width=$(printf '%s\n' "$virtual_pad" | jq -r '.BoundsMaxX - .BoundsMinX') - pad_height=$(printf '%s\n' "$virtual_pad" | jq -r '.BoundsMaxY - .BoundsMinY') + pad_x=$(element_field "$virtual_pad" "simX") + pad_y=$(element_field "$virtual_pad" "simY") + pad_width=$(printf '%s\n' "$virtual_pad" | jq -r '.boundsMaxX - .boundsMinX') + pad_height=$(printf '%s\n' "$virtual_pad" | jq -r '.boundsMaxY - .boundsMinY') pad_offset=$(jq -nr --argjson width "$pad_width" --argjson height "$pad_height" '([$width, $height] | min) * 0.28') - target_path=$(element_field "$virtual_pad" "Path") + target_path=$(element_field "$virtual_pad" "path") json=$(run_uloop_json simulate-mouse-ui \ --action DragStart \ @@ -582,11 +582,11 @@ assert_text_equals "$(get_drop_zone_status)" "Dropped: RedBox" "DropZone after R green_box=$(get_element_json "GreenBox") drop_zone=$(get_element_json "DropZone") -green_x=$(element_field "$green_box" "SimX") -green_y=$(element_field "$green_box" "SimY") -drop_x=$(element_field "$drop_zone" "SimX") -drop_y=$(element_field "$drop_zone" "SimY") -drop_path=$(element_field "$drop_zone" "Path") +green_x=$(element_field "$green_box" "simX") +green_y=$(element_field "$green_box" "simY") +drop_x=$(element_field "$drop_zone" "simX") +drop_y=$(element_field "$drop_zone" "simY") +drop_path=$(element_field "$drop_zone" "path") invoke_drag_start "GreenBox" move_x=$(jq -nr --argjson center "$drop_x" '$center + 150') diff --git a/scripts/test-smoke-cli-recovery-readiness.sh b/scripts/test-smoke-cli-recovery-readiness.sh index 3e60780ff0..86f11751cd 100755 --- a/scripts/test-smoke-cli-recovery-readiness.sh +++ b/scripts/test-smoke-cli-recovery-readiness.sh @@ -51,15 +51,15 @@ func main() { fmt.Fprintln(os.Stderr, "{\n \"success\": false,\n \"error\": {\n \"errorCode\": \"UNITY_NOT_REACHABLE\",\n \"message\": \"The Unity CLI Loop server is not reachable for this project.\",\n \"nextActions\": [\n \"If Unity is closed, run `uloop launch`.\"\n ]\n }\n}") os.Exit(1) } - fmt.Println(`{"DisplayedCount":0,"Logs":[],"MaxCount":1,"TotalCount":0}`) + fmt.Println(`{"displayedCount":0,"logs":[],"maxCount":1,"totalCount":0}`) case "compile": if os.Getenv("ULOOP_FAKE_COMPILE_WITHOUT_SUCCESS") == "1" { - fmt.Println(`{"ErrorCount":0,"WarningCount":0}`) + fmt.Println(`{"errorCount":0,"warningCount":0}`) return } - fmt.Println(`{"Success":true,"ErrorCount":0,"WarningCount":0}`) + fmt.Println(`{"success":true,"errorCount":0,"warningCount":0}`) case "execute-dynamic-code": - fmt.Println(`{"Success":true,"Result":"cli-recovery-readiness-e2e"}`) + fmt.Println(`{"success":true,"result":"cli-recovery-readiness-e2e"}`) default: fmt.Fprintf(os.Stderr, "unexpected command: %s\n", commandName) os.Exit(98) @@ -115,7 +115,7 @@ if CALL_LOG="$CALL_LOG" ULOOP_FAKE_COMPILE_WITHOUT_SUCCESS=1 go run "$ROOT_DIR/s --timeout 2 \ --launch-timeout 2 > "$TMP_DIR/missing-success-output.txt" 2>&1; then cat "$TMP_DIR/missing-success-output.txt" - echo "expected compile payload without Success to fail" >&2 + echo "expected compile payload without success to fail" >&2 exit 1 fi grep -F "compile with domain reload wait returned invalid success payload" "$TMP_DIR/missing-success-output.txt" >/dev/null From 4c766ab8a43cd062faf584dee9aae65d05af0da6 Mon Sep 17 00:00:00 2001 From: hatayama <842587+hatayama@users.noreply.github.com> Date: Tue, 16 Jun 2026 21:28:03 +0900 Subject: [PATCH 3/4] Align CLI schema review follow-ups Refresh generated skill docs with lower camel JSON fields, normalize uloop list option metadata, and keep protocol-2 internal control fields strippable while public response keys move to camelCase. --- .agents/skills/uloop-clear-console/SKILL.md | 16 +- .agents/skills/uloop-compile/SKILL.md | 6 +- .../skills/uloop-control-play-mode/SKILL.md | 12 +- .../uloop-execute-dynamic-code/SKILL.md | 24 +- .agents/skills/uloop-focus-window/SKILL.md | 4 +- .agents/skills/uloop-get-logs/SKILL.md | 20 +- .agents/skills/uloop-launch/SKILL.md | 26 +- .agents/skills/uloop-record-input/SKILL.md | 16 +- .agents/skills/uloop-replay-input/SKILL.md | 20 +- .agents/skills/uloop-run-tests/SKILL.md | 30 +- .agents/skills/uloop-screenshot/SKILL.md | 24 +- .../references/annotated-elements.md | 32 +- .../skills/uloop-simulate-keyboard/SKILL.md | 22 +- .../uloop-simulate-mouse-input/SKILL.md | 26 +- .../skills/uloop-simulate-mouse-ui/SKILL.md | 34 +- .../uloop-wait-for-pause-point/SKILL.md | 10 +- .claude/skills/simulate-mouse-demo/SKILL.md | 48 +-- .claude/skills/uloop-clear-console/SKILL.md | 16 +- .claude/skills/uloop-compile/SKILL.md | 6 +- .../skills/uloop-control-play-mode/SKILL.md | 12 +- .../uloop-execute-dynamic-code/SKILL.md | 24 +- .claude/skills/uloop-focus-window/SKILL.md | 4 +- .claude/skills/uloop-get-logs/SKILL.md | 20 +- .claude/skills/uloop-launch/SKILL.md | 26 +- .claude/skills/uloop-record-input/SKILL.md | 16 +- .claude/skills/uloop-replay-input/SKILL.md | 20 +- .claude/skills/uloop-run-tests/SKILL.md | 30 +- .claude/skills/uloop-screenshot/SKILL.md | 24 +- .../references/annotated-elements.md | 32 +- .../skills/uloop-simulate-keyboard/SKILL.md | 22 +- .../uloop-simulate-mouse-input/SKILL.md | 26 +- .../skills/uloop-simulate-mouse-ui/SKILL.md | 34 +- .../uloop-wait-for-pause-point/SKILL.md | 10 +- .codex/skills/uloop-clear-console/SKILL.md | 49 +++ .codex/skills/uloop-compile/SKILL.md | 49 +++ .../skills/uloop-control-play-mode/SKILL.md | 65 +++ .../uloop-execute-dynamic-code/SKILL.md | 89 ++++ .../references/asset-operations.md | 194 +++++++++ .../references/batch-operations.md | 399 +++++++++++++++++ .../references/cleanup-operations.md | 403 ++++++++++++++++++ .../references/material-operations.md | 160 +++++++ .../playmode-automation-powershell.md | 376 ++++++++++++++++ .../references/playmode-automation-zsh.md | 299 +++++++++++++ .../references/playmode-inspection.md | 151 +++++++ .../references/playmode-ui-controls.md | 190 +++++++++ .../references/prefab-operations.md | 221 ++++++++++ .../references/scene-operations.md | 234 ++++++++++ .../references/scriptableobject.md | 216 ++++++++++ .../references/selection-operations.md | 302 +++++++++++++ .../references/undo-operations.md | 210 +++++++++ .../skills/uloop-find-game-objects/SKILL.md | 90 ++++ .codex/skills/uloop-focus-window/SKILL.md | 46 ++ .codex/skills/uloop-get-hierarchy/SKILL.md | 62 +++ .codex/skills/uloop-get-logs/SKILL.md | 64 +++ .codex/skills/uloop-hello-world/SKILL.md | 53 +++ .codex/skills/uloop-launch/SKILL.md | 68 +++ .codex/skills/uloop-record-input/SKILL.md | 55 +++ .../references/deterministic-replay.md | 15 + .codex/skills/uloop-replay-input/SKILL.md | 61 +++ .../references/deterministic-replay.md | 15 + .codex/skills/uloop-run-tests/SKILL.md | 78 ++++ .codex/skills/uloop-screenshot/SKILL.md | 97 +++++ .../references/annotated-elements.md | 37 ++ .../skills/uloop-simulate-keyboard/SKILL.md | 108 +++++ .../uloop-simulate-mouse-input/SKILL.md | 124 ++++++ .../skills/uloop-simulate-mouse-ui/SKILL.md | 143 +++++++ .../uloop-wait-for-pause-point/SKILL.md | 90 ++++ .../ExecuteDynamicCodeUseCaseTests.cs | 25 ++ .../CliOnlyTools~/FocusWindow/Skill/SKILL.md | 4 +- .../CliOnlyTools~/Launch/Skill/SKILL.md | 26 +- .../CliOnlyTools~/PausePoint/Skill/SKILL.md | 10 +- .../ClearConsole/Skill/SKILL.md | 16 +- .../FirstPartyTools/Compile/Skill/SKILL.md | 6 +- .../ControlPlayMode/Skill/SKILL.md | 12 +- .../ExecuteDynamicCodeResponse.cs | 3 + .../ExecuteDynamicCode/Skill/SKILL.md | 24 +- .../FirstPartyTools/GetLogs/Skill/SKILL.md | 20 +- .../RecordInput/Skill/SKILL.md | 16 +- .../ReplayInput/Skill/SKILL.md | 20 +- .../FirstPartyTools/RunTests/Skill/SKILL.md | 30 +- .../FirstPartyTools/Screenshot/Skill/SKILL.md | 24 +- .../Skill/references/annotated-elements.md | 32 +- .../SimulateKeyboard/Skill/SKILL.md | 22 +- .../SimulateMouseInput/Skill/SKILL.md | 26 +- .../SimulateMouseUi/Skill/SKILL.md | 34 +- cli/internal/cli/compile_wait_test.go | 9 + cli/internal/cli/list_output.go | 32 +- cli/internal/cli/list_output_test.go | 28 ++ cli/internal/cli/pause_point_logs.go | 2 +- 89 files changed, 5417 insertions(+), 509 deletions(-) create mode 100644 .codex/skills/uloop-clear-console/SKILL.md create mode 100644 .codex/skills/uloop-compile/SKILL.md create mode 100644 .codex/skills/uloop-control-play-mode/SKILL.md create mode 100644 .codex/skills/uloop-execute-dynamic-code/SKILL.md create mode 100644 .codex/skills/uloop-execute-dynamic-code/references/asset-operations.md create mode 100644 .codex/skills/uloop-execute-dynamic-code/references/batch-operations.md create mode 100644 .codex/skills/uloop-execute-dynamic-code/references/cleanup-operations.md create mode 100644 .codex/skills/uloop-execute-dynamic-code/references/material-operations.md create mode 100644 .codex/skills/uloop-execute-dynamic-code/references/playmode-automation-powershell.md create mode 100644 .codex/skills/uloop-execute-dynamic-code/references/playmode-automation-zsh.md create mode 100644 .codex/skills/uloop-execute-dynamic-code/references/playmode-inspection.md create mode 100644 .codex/skills/uloop-execute-dynamic-code/references/playmode-ui-controls.md create mode 100644 .codex/skills/uloop-execute-dynamic-code/references/prefab-operations.md create mode 100644 .codex/skills/uloop-execute-dynamic-code/references/scene-operations.md create mode 100644 .codex/skills/uloop-execute-dynamic-code/references/scriptableobject.md create mode 100644 .codex/skills/uloop-execute-dynamic-code/references/selection-operations.md create mode 100644 .codex/skills/uloop-execute-dynamic-code/references/undo-operations.md create mode 100644 .codex/skills/uloop-find-game-objects/SKILL.md create mode 100644 .codex/skills/uloop-focus-window/SKILL.md create mode 100644 .codex/skills/uloop-get-hierarchy/SKILL.md create mode 100644 .codex/skills/uloop-get-logs/SKILL.md create mode 100644 .codex/skills/uloop-hello-world/SKILL.md create mode 100644 .codex/skills/uloop-launch/SKILL.md create mode 100644 .codex/skills/uloop-record-input/SKILL.md create mode 100644 .codex/skills/uloop-record-input/references/deterministic-replay.md create mode 100644 .codex/skills/uloop-replay-input/SKILL.md create mode 100644 .codex/skills/uloop-replay-input/references/deterministic-replay.md create mode 100644 .codex/skills/uloop-run-tests/SKILL.md create mode 100644 .codex/skills/uloop-screenshot/SKILL.md create mode 100644 .codex/skills/uloop-screenshot/references/annotated-elements.md create mode 100644 .codex/skills/uloop-simulate-keyboard/SKILL.md create mode 100644 .codex/skills/uloop-simulate-mouse-input/SKILL.md create mode 100644 .codex/skills/uloop-simulate-mouse-ui/SKILL.md create mode 100644 .codex/skills/uloop-wait-for-pause-point/SKILL.md 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..3bac4d65e5 100644 --- a/.agents/skills/uloop-compile/SKILL.md +++ b/.agents/skills/uloop-compile/SKILL.md @@ -44,6 +44,6 @@ uloop compile --stop-on-external-scene-changes ## Output Returns JSON: -- `Success`: boolean -- `ErrorCount`: number -- `WarningCount`: number +- `success`: boolean +- `errorCount`: number +- `warningCount`: number 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-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-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-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-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..9124e6dbd3 100644 --- a/.agents/skills/uloop-wait-for-pause-point/SKILL.md +++ b/.agents/skills/uloop-wait-for-pause-point/SKILL.md @@ -24,15 +24,15 @@ 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. @@ -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..3bac4d65e5 100644 --- a/.claude/skills/uloop-compile/SKILL.md +++ b/.claude/skills/uloop-compile/SKILL.md @@ -44,6 +44,6 @@ uloop compile --stop-on-external-scene-changes ## Output Returns JSON: -- `Success`: boolean -- `ErrorCount`: number -- `WarningCount`: number +- `success`: boolean +- `errorCount`: number +- `warningCount`: number 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-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-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-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-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..9124e6dbd3 100644 --- a/.claude/skills/uloop-wait-for-pause-point/SKILL.md +++ b/.claude/skills/uloop-wait-for-pause-point/SKILL.md @@ -24,15 +24,15 @@ 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. @@ -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..3bac4d65e5 --- /dev/null +++ b/.codex/skills/uloop-compile/SKILL.md @@ -0,0 +1,49 @@ +--- +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 +- `errorCount`: number +- `warningCount`: number 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..3bd98697ab --- /dev/null +++ b/.codex/skills/uloop-execute-dynamic-code/references/material-operations.md @@ -0,0 +1,160 @@ +# 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); + +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); + +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); + +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