From 7ae6771b30bd74baeae501af41569546983b6722 Mon Sep 17 00:00:00 2001 From: Masamichi Hatayama Date: Sat, 18 Jul 2026 14:58:53 +0900 Subject: [PATCH 1/6] fix: make recovery readiness checks reach Unity connection errors (#1829) --- scripts/smoke-cli-recovery-readiness.go | 23 ++++++++++++++++++-- scripts/test-smoke-cli-recovery-readiness.sh | 2 ++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/scripts/smoke-cli-recovery-readiness.go b/scripts/smoke-cli-recovery-readiness.go index d805fb412..21de34875 100644 --- a/scripts/smoke-cli-recovery-readiness.go +++ b/scripts/smoke-cli-recovery-readiness.go @@ -17,6 +17,7 @@ import ( const ( stateRelativePath = "Temp/UnityCliLoop/server-state.json" + projectRunnerPinPath = ".uloop/project-runner-pin.json" expectedDynamicCodeResult = "cli-recovery-readiness-e2e" e2eDynamicCode = `return "cli-recovery-readiness-e2e";` timeoutExitCode = 124 @@ -61,7 +62,7 @@ func run() error { if err := runLiveRecoverySequence(opts); err != nil { return err } - if err := runStaleRecoveryStateIgnoredSequence(opts.uloopPath, opts.timeout); err != nil { + if err := runStaleRecoveryStateIgnoredSequence(opts.uloopPath, opts.projectPath, opts.timeout); err != nil { return err } @@ -211,7 +212,7 @@ func runLiveRecoverySequence(opts options) error { return assertDynamicCodeResult(dynamicPayload) } -func runStaleRecoveryStateIgnoredSequence(uloopPath string, timeout time.Duration) error { +func runStaleRecoveryStateIgnoredSequence(uloopPath string, sourceProjectPath string, timeout time.Duration) error { projectPath, err := os.MkdirTemp("", "uloop-stale-state-") if err != nil { return err @@ -221,6 +222,10 @@ func runStaleRecoveryStateIgnoredSequence(uloopPath string, timeout time.Duratio if err := createMinimalUnityProject(projectPath); err != nil { return err } + // Why: the dispatcher requires the runner pin before this sequence can reach the connection failure assertion. + if err := copyProjectRunnerPin(sourceProjectPath, projectPath); err != nil { + return err + } if err := writeStaleServerState(projectPath); err != nil { return err } @@ -244,6 +249,20 @@ func runStaleRecoveryStateIgnoredSequence(uloopPath string, timeout time.Duratio return nil } +func copyProjectRunnerPin(sourceProjectPath string, targetProjectPath string) error { + sourcePath := filepath.Join(sourceProjectPath, filepath.FromSlash(projectRunnerPinPath)) + data, err := os.ReadFile(sourcePath) + if err != nil { + return err + } + + targetPath := filepath.Join(targetProjectPath, filepath.FromSlash(projectRunnerPinPath)) + if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { + return err + } + return os.WriteFile(targetPath, data, 0o644) +} + func runUloop(uloopPath string, projectPath string, args []string, timeout time.Duration) commandResult { return runUloopWithEnv(uloopPath, projectPath, args, timeout, nil) } diff --git a/scripts/test-smoke-cli-recovery-readiness.sh b/scripts/test-smoke-cli-recovery-readiness.sh index d7adab044..c90f6adb2 100755 --- a/scripts/test-smoke-cli-recovery-readiness.sh +++ b/scripts/test-smoke-cli-recovery-readiness.sh @@ -22,6 +22,8 @@ cleanup() { trap cleanup EXIT INT TERM mkdir -p "$PROJECT_PATH/Assets" "$PROJECT_PATH/ProjectSettings" +mkdir -p "$PROJECT_PATH/.uloop" +cp "$ROOT_DIR/.uloop/project-runner-pin.json" "$PROJECT_PATH/.uloop/project-runner-pin.json" cat > "$FAKE_ULOOP_SOURCE" <<'EOF' package main From 378a831cc7b6653f5c366951bb501f5be0cef187 Mon Sep 17 00:00:00 2001 From: Masamichi Hatayama Date: Sat, 18 Jul 2026 15:05:32 +0900 Subject: [PATCH 2/6] fix: make input replay E2E parsing match V3 CLI responses (#1830) --- Assets/Tests/Demo/scripts/verify-replay-via-cli.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Assets/Tests/Demo/scripts/verify-replay-via-cli.sh b/Assets/Tests/Demo/scripts/verify-replay-via-cli.sh index e961a5732..87ea5f723 100755 --- a/Assets/Tests/Demo/scripts/verify-replay-via-cli.sh +++ b/Assets/Tests/Demo/scripts/verify-replay-via-cli.sh @@ -66,7 +66,7 @@ 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" fi @@ -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,13 +239,13 @@ 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 fi if [ $((waited % 5)) -eq 0 ]; then - progress=$(echo "$STATUS_RESULT" | grep -o '"progress": *[0-9.]*' | sed 's/.*: *//') + progress=$(echo "$STATUS_RESULT" | grep -o '"Progress": *[0-9.]*' | sed 's/.*: *//') echo " Progress: ${progress:-...}" fi sleep 1 From 9ad54aad7ad864a006174e71948ff928a7858c7f Mon Sep 17 00:00:00 2001 From: Masamichi Hatayama Date: Sat, 18 Jul 2026 15:23:45 +0900 Subject: [PATCH 3/6] fix: keep dynamic-code compiler settings on the Unity main thread (#1831) --- .../ExternalCompilerPathResolverTests.cs | 1 + .../RoslynCompilerOptionsTests.cs | 61 +++++++++++++++++++ .../RoslynCompilerOptionsTests.cs.meta | 2 + .../Compilation/ICompiledAssemblyBuilder.cs | 1 + .../CompiledAssemblyBuilder.cs | 3 + .../DynamicCompilation/DynamicCodeCompiler.cs | 11 +++- .../DynamicCompilationBackend.cs | 2 + .../RoslynCompilerBackend.cs | 26 +------- .../RoslynCompilerOptions.cs | 40 ++++++++++++ .../RoslynCompilerOptions.cs.meta | 2 + 10 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs create mode 100644 Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs create mode 100644 Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs.meta diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs index 6573333fe..e26911308 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs @@ -273,6 +273,7 @@ public async Task CompileAsync_WhenSharedWorkerBuildFails_ShouldFallbackToOneSho dllPath, references, externalCompilerPaths, + new RoslynCompilerOptions(Array.Empty(), false), compileCancellationTokenSource.Token, () => buildStarted = true, () => buildFinished = true, diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs new file mode 100644 index 000000000..5179b0b50 --- /dev/null +++ b/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs @@ -0,0 +1,61 @@ +using System; +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests +{ + /// + /// Verifies the immutable compiler-settings snapshot used by dynamic compilation. + /// + [TestFixture] + public sealed class RoslynCompilerOptionsTests + { + /// + /// Verifies empty and whitespace-only define symbols are excluded while valid symbols are copied. + /// + [Test] + public void Constructor_FiltersWhitespaceAndCopiesDefineSymbols() + { + string[] defineSymbols = + { + "UNITY_EDITOR", + null, + "", + " ", + "CUSTOM_DEFINE" + }; + + RoslynCompilerOptions options = new(defineSymbols, true); + defineSymbols[0] = "MUTATED_AFTER_CAPTURE"; + + Assert.That( + options.DefineSymbols, + Is.EqualTo(new[] { "UNITY_EDITOR", "CUSTOM_DEFINE" })); + Assert.That(options.AllowUnsafeCode, Is.True); + } + + /// + /// Verifies an empty define-symbol collection produces an empty immutable snapshot. + /// + [Test] + public void Constructor_WithEmptyDefineSymbols_CapturesEmptySnapshot() + { + RoslynCompilerOptions options = new(Array.Empty(), false); + + Assert.That(options.DefineSymbols, Is.Empty); + Assert.That(options.AllowUnsafeCode, Is.False); + } + + /// + /// Verifies a null define-symbol collection fails fast at the snapshot boundary. + /// + [Test] + public void Constructor_WithNullDefineSymbols_ThrowsArgumentNullException() + { + Assert.That( + () => new RoslynCompilerOptions(null, false), + Throws.ArgumentNullException); + } + } +} diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs.meta b/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs.meta new file mode 100644 index 000000000..6a552ff7f --- /dev/null +++ b/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c9ebc13404c3436da8f1de5961992daa diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/ICompiledAssemblyBuilder.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/ICompiledAssemblyBuilder.cs index 77d51ba6e..2b0fede68 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/ICompiledAssemblyBuilder.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/ICompiledAssemblyBuilder.cs @@ -10,6 +10,7 @@ public interface ICompiledAssemblyBuilder { Task BuildAsync( DynamicCompilationPlan plan, + RoslynCompilerOptions compilerOptions, CancellationToken ct = default); } } diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/CompiledAssemblyBuilder.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/CompiledAssemblyBuilder.cs index c60309d8b..8b61a2f97 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/CompiledAssemblyBuilder.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/CompiledAssemblyBuilder.cs @@ -64,9 +64,11 @@ public CompiledAssemblyBuilder( public async Task BuildAsync( DynamicCompilationPlan plan, + RoslynCompilerOptions compilerOptions, CancellationToken ct = default) { Debug.Assert(plan != null, "plan must not be null"); + Debug.Assert(compilerOptions != null, "compilerOptions must not be null"); ct.ThrowIfCancellationRequested(); @@ -98,6 +100,7 @@ async Task BuildFunc( resolvedDllPath, resolvedReferences, externalCompilerPaths, + compilerOptions, cancellationToken, () => canDeleteTempFiles = false, () => canDeleteTempFiles = true, diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeCompiler.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeCompiler.cs index b6cdcbb2b..a1c01f483 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeCompiler.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeCompiler.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using UnityEditor; using Debug = UnityEngine.Debug; using Stopwatch = System.Diagnostics.Stopwatch; @@ -61,6 +62,11 @@ public async Task CompileAsync(CompilationRequest request, Ca Debug.Assert(!string.IsNullOrWhiteSpace(request.Code), "request.Code must not be empty"); ct.ThrowIfCancellationRequested(); + await MainThreadSwitcher.SwitchToMainThread(ct); + string[] activeDefineSymbols = EditorUserBuildSettings.activeScriptCompilationDefines ?? Array.Empty(); + RoslynCompilerOptions compilerOptions = new( + activeDefineSymbols, + PlayerSettings.allowUnsafeCode); LastBuildCount = 0; Stopwatch compilerTotalStopwatch = Stopwatch.StartNew(); Stopwatch planStopwatch = Stopwatch.StartNew(); @@ -95,7 +101,10 @@ public async Task CompileAsync(CompilationRequest request, Ca } Stopwatch builderTotalStopwatch = Stopwatch.StartNew(); - CompiledAssemblyBuildResult buildResult = await _assemblyBuilder.BuildAsync(plan, ct).ConfigureAwait(false); + CompiledAssemblyBuildResult buildResult = await _assemblyBuilder.BuildAsync( + plan, + compilerOptions, + ct).ConfigureAwait(false); builderTotalStopwatch.Stop(); LastBuildCount = buildResult.BuildCount; diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCompilationBackend.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCompilationBackend.cs index dbf40e597..cae2b8eab 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCompilationBackend.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCompilationBackend.cs @@ -34,6 +34,7 @@ public Task CompileAsync( string dllPath, List references, ExternalCompilerPaths externalCompilerPaths, + RoslynCompilerOptions compilerOptions, CancellationToken ct, Action markBuildStarted, Action markBuildFinished, @@ -46,6 +47,7 @@ public Task CompileAsync( dllPath, references, externalCompilerPaths, + compilerOptions, ct, markBuildStarted, markBuildFinished, diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs index edebe7b8d..050e331f0 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs @@ -4,7 +4,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using UnityEditor; using UnityEditor.Compilation; using UnityEngine; @@ -52,14 +51,15 @@ public static async Task CompileAsync( string dllPath, List references, ExternalCompilerPaths externalCompilerPaths, + RoslynCompilerOptions compilerOptions, CancellationToken ct, Action markBuildStarted, Action markBuildFinished, Action incrementBuildCount) { string workerRequestFilePath = Path.ChangeExtension(sourcePath, ".worker"); - string[] defineSymbols = GetActiveDefineSymbols(); - bool allowUnsafeCode = PlayerSettings.allowUnsafeCode; + IReadOnlyCollection defineSymbols = compilerOptions.DefineSymbols; + bool allowUnsafeCode = compilerOptions.AllowUnsafeCode; try { @@ -271,26 +271,6 @@ internal static void WriteWorkerRequestFile( File.WriteAllLines(Path.GetFullPath(requestFilePath), lines); } - private static string[] GetActiveDefineSymbols() - { - string[] activeDefines = EditorUserBuildSettings.activeScriptCompilationDefines; - if (activeDefines == null || activeDefines.Length == 0) - { - return Array.Empty(); - } - - List filteredDefines = new(activeDefines.Length); - foreach (string define in activeDefines) - { - if (!string.IsNullOrWhiteSpace(define)) - { - filteredDefines.Add(define); - } - } - - return filteredDefines.ToArray(); - } - private static string BuildDefineOption(IReadOnlyCollection defineSymbols) { string serializedDefines = SerializeDefineSymbols(defineSymbols); diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs new file mode 100644 index 000000000..744800033 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Captures the Unity compiler settings required by the asynchronous Roslyn pipeline. + /// + public sealed class RoslynCompilerOptions + { + public IReadOnlyList DefineSymbols { get; } + + public bool AllowUnsafeCode { get; } + + /// + /// Creates an immutable compiler-settings snapshot for one compilation request. + /// + public RoslynCompilerOptions( + IReadOnlyCollection defineSymbols, + bool allowUnsafeCode) + { + if (defineSymbols == null) + { + throw new ArgumentNullException(nameof(defineSymbols)); + } + + List filteredDefineSymbols = new(defineSymbols.Count); + foreach (string defineSymbol in defineSymbols) + { + if (!string.IsNullOrWhiteSpace(defineSymbol)) + { + filteredDefineSymbols.Add(defineSymbol); + } + } + + DefineSymbols = filteredDefineSymbols.AsReadOnly(); + AllowUnsafeCode = allowUnsafeCode; + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs.meta b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs.meta new file mode 100644 index 000000000..83b65fc5b --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d33068d89875483ba33d9dbc2f5df2dd From 6cc54a0b4ce732ae8ea7336285d5ad66e92c478b Mon Sep 17 00:00:00 2001 From: Masamichi Hatayama Date: Sat, 18 Jul 2026 15:44:47 +0900 Subject: [PATCH 4/6] feat: add Game View size tool (#1832) --- .../skills/uloop-set-game-view-size/SKILL.md | 47 ++++++++ .../skills/uloop-set-game-view-size/SKILL.md | 47 ++++++++ .../FirstPartyTools/SetGameViewSize.meta | 8 ++ .../SetGameViewSizeResponse.cs | 17 +++ .../SetGameViewSizeResponse.cs.meta | 11 ++ .../SetGameViewSize/SetGameViewSizeSchema.cs | 18 ++++ .../SetGameViewSizeSchema.cs.meta | 11 ++ .../SetGameViewSize/SetGameViewSizeTool.cs | 24 +++++ .../SetGameViewSizeTool.cs.meta | 11 ++ .../SetGameViewSize/SetGameViewSizeUseCase.cs | 102 ++++++++++++++++++ .../SetGameViewSizeUseCase.cs.meta | 11 ++ .../SetGameViewSize/Skill.meta | 8 ++ .../SetGameViewSize/Skill/SKILL.md | 47 ++++++++ .../SetGameViewSize/Skill/SKILL.md.meta | 7 ++ ...stPartyTools.SetGameViewSize.Editor.asmdef | 18 ++++ ...tyTools.SetGameViewSize.Editor.asmdef.meta | 7 ++ cli/common/tools/default-tools.json | 17 +++ 17 files changed, 411 insertions(+) create mode 100644 .agents/skills/uloop-set-game-view-size/SKILL.md create mode 100644 .claude/skills/uloop-set-game-view-size/SKILL.md create mode 100644 Packages/src/Editor/FirstPartyTools/SetGameViewSize.meta create mode 100644 Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeResponse.cs create mode 100644 Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeResponse.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeSchema.cs create mode 100644 Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeSchema.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeTool.cs create mode 100644 Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeTool.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeUseCase.cs create mode 100644 Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeUseCase.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/SetGameViewSize/Skill.meta create mode 100644 Packages/src/Editor/FirstPartyTools/SetGameViewSize/Skill/SKILL.md create mode 100644 Packages/src/Editor/FirstPartyTools/SetGameViewSize/Skill/SKILL.md.meta create mode 100644 Packages/src/Editor/FirstPartyTools/SetGameViewSize/UnityCLILoop.FirstPartyTools.SetGameViewSize.Editor.asmdef create mode 100644 Packages/src/Editor/FirstPartyTools/SetGameViewSize/UnityCLILoop.FirstPartyTools.SetGameViewSize.Editor.asmdef.meta diff --git a/.agents/skills/uloop-set-game-view-size/SKILL.md b/.agents/skills/uloop-set-game-view-size/SKILL.md new file mode 100644 index 000000000..2f37ab076 --- /dev/null +++ b/.agents/skills/uloop-set-game-view-size/SKILL.md @@ -0,0 +1,47 @@ +--- +name: uloop-set-game-view-size +toolName: set-game-view-size +description: "Set or inspect the Unity Game View custom rendering resolution." +--- + +# uloop set-game-view-size + +Set or inspect the Unity Game View custom rendering resolution. + +## Usage + +```bash +uloop set-game-view-size [--width --height ] +``` + +## Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--width` | integer | unset | Target Game View rendering width in pixels. Provide with `--height` to change the resolution. | +| `--height` | integer | unset | Target Game View rendering height in pixels. Provide with `--width` to change the resolution. | + +## Output + +Returns JSON containing: + +- `Success`: Whether the request completed. +- `PreviousWidth` / `PreviousHeight`: Resolution before the request. +- `CurrentWidth` / `CurrentHeight`: Resolution after the request. +- `Changed`: Whether the resolution changed. +- `Message`: Human-readable status. + +## Examples + +```bash +# Read the current custom rendering resolution +uloop set-game-view-size + +# Set a Full HD custom rendering resolution +uloop set-game-view-size --width 1920 --height 1080 +``` + +## Notes + +- Width and height must be provided together when changing the resolution. +- The tool uses Unity's public `UnityEditor.PlayModeWindow` API and does not require reflection. diff --git a/.claude/skills/uloop-set-game-view-size/SKILL.md b/.claude/skills/uloop-set-game-view-size/SKILL.md new file mode 100644 index 000000000..2f37ab076 --- /dev/null +++ b/.claude/skills/uloop-set-game-view-size/SKILL.md @@ -0,0 +1,47 @@ +--- +name: uloop-set-game-view-size +toolName: set-game-view-size +description: "Set or inspect the Unity Game View custom rendering resolution." +--- + +# uloop set-game-view-size + +Set or inspect the Unity Game View custom rendering resolution. + +## Usage + +```bash +uloop set-game-view-size [--width --height ] +``` + +## Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--width` | integer | unset | Target Game View rendering width in pixels. Provide with `--height` to change the resolution. | +| `--height` | integer | unset | Target Game View rendering height in pixels. Provide with `--width` to change the resolution. | + +## Output + +Returns JSON containing: + +- `Success`: Whether the request completed. +- `PreviousWidth` / `PreviousHeight`: Resolution before the request. +- `CurrentWidth` / `CurrentHeight`: Resolution after the request. +- `Changed`: Whether the resolution changed. +- `Message`: Human-readable status. + +## Examples + +```bash +# Read the current custom rendering resolution +uloop set-game-view-size + +# Set a Full HD custom rendering resolution +uloop set-game-view-size --width 1920 --height 1080 +``` + +## Notes + +- Width and height must be provided together when changing the resolution. +- The tool uses Unity's public `UnityEditor.PlayModeWindow` API and does not require reflection. diff --git a/Packages/src/Editor/FirstPartyTools/SetGameViewSize.meta b/Packages/src/Editor/FirstPartyTools/SetGameViewSize.meta new file mode 100644 index 000000000..26717bf20 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SetGameViewSize.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 59405e403b6b43069f2860df8030e5a6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeResponse.cs b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeResponse.cs new file mode 100644 index 000000000..4f8011422 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeResponse.cs @@ -0,0 +1,17 @@ +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Carries the resolution before and after a Set Game View Size request. + /// + public sealed class SetGameViewSizeResponse : UnityCliLoopToolResponse + { + public uint PreviousWidth { get; set; } + public uint PreviousHeight { get; set; } + public uint CurrentWidth { get; set; } + public uint CurrentHeight { get; set; } + public bool Changed { get; set; } + public string Message { get; set; } = ""; + } +} diff --git a/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeResponse.cs.meta b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeResponse.cs.meta new file mode 100644 index 000000000..debe10a15 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f481475c0a0b43f5b9906a8eeb369e7e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeSchema.cs b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeSchema.cs new file mode 100644 index 000000000..836289565 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeSchema.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; + +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Describes the optional resolution parameters accepted by the Set Game View Size tool. + /// + public sealed class SetGameViewSizeSchema : UnityCliLoopToolSchema + { + [Description("Target Game View rendering width in pixels. Provide with Height to change the resolution.")] + public int? Width { get; set; } + + [Description("Target Game View rendering height in pixels. Provide with Width to change the resolution.")] + public int? Height { get; set; } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeSchema.cs.meta b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeSchema.cs.meta new file mode 100644 index 000000000..2cc3c87b5 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeSchema.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5d1f930fb6ea4049a4a894f91d750246 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeTool.cs b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeTool.cs new file mode 100644 index 000000000..7e28c4a82 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeTool.cs @@ -0,0 +1,24 @@ +using System.Threading; +using System.Threading.Tasks; + +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Sets or reports the Unity Game View custom rendering resolution. + /// + [UnityCliLoopTool] + public sealed class SetGameViewSizeTool : UnityCliLoopTool + { + public override string ToolName => "set-game-view-size"; + + protected override Task ExecuteAsync( + SetGameViewSizeSchema parameters, + CancellationToken ct) + { + SetGameViewSizeUseCase useCase = new(); + return useCase.ExecuteAsync(parameters, ct); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeTool.cs.meta b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeTool.cs.meta new file mode 100644 index 000000000..22e21c2a4 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeTool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fcf7213d197f44fdafa54e15f239719e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeUseCase.cs b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeUseCase.cs new file mode 100644 index 000000000..3ea9093d1 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeUseCase.cs @@ -0,0 +1,102 @@ +using System.Threading; +using System.Threading.Tasks; +using UnityEditor; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Applies and reports the Unity Game View custom rendering resolution. + /// + public sealed class SetGameViewSizeUseCase + { + private const string RenderingResolutionBaseName = "uloop"; + + /// + /// Reads the current resolution and optionally applies a paired width and height. + /// + public Task ExecuteAsync( + SetGameViewSizeSchema parameters, + CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + + (uint previousWidth, uint previousHeight) = GetRenderingResolution(); + bool hasWidth = parameters.Width.HasValue; + bool hasHeight = parameters.Height.HasValue; + if (hasWidth != hasHeight) + { + return Task.FromResult(CreateResponse( + false, + "Width and Height must be provided together.", + previousWidth, + previousHeight, + previousWidth, + previousHeight)); + } + + if (!hasWidth) + { + return Task.FromResult(CreateResponse( + true, + $"Current Game View rendering resolution is {previousWidth}x{previousHeight}.", + previousWidth, + previousHeight, + previousWidth, + previousHeight)); + } + + if (parameters.Width.Value <= 0 || parameters.Height.Value <= 0) + { + return Task.FromResult(CreateResponse( + false, + "Width and Height must be positive integers.", + previousWidth, + previousHeight, + previousWidth, + previousHeight)); + } + + PlayModeWindow.SetCustomRenderingResolution( + (uint)parameters.Width.Value, + (uint)parameters.Height.Value, + RenderingResolutionBaseName); + + (uint currentWidth, uint currentHeight) = GetRenderingResolution(); + return Task.FromResult(CreateResponse( + true, + $"Game View rendering resolution changed from {previousWidth}x{previousHeight} to {currentWidth}x{currentHeight}.", + previousWidth, + previousHeight, + currentWidth, + currentHeight)); + } + + private static (uint Width, uint Height) GetRenderingResolution() + { + uint width; + uint height; + PlayModeWindow.GetRenderingResolution(out width, out height); + return (width, height); + } + + private static SetGameViewSizeResponse CreateResponse( + bool success, + string message, + uint previousWidth, + uint previousHeight, + uint currentWidth, + uint currentHeight) + { + return new SetGameViewSizeResponse + { + Success = success, + Message = message, + PreviousWidth = previousWidth, + PreviousHeight = previousHeight, + CurrentWidth = currentWidth, + CurrentHeight = currentHeight, + Changed = previousWidth != currentWidth || previousHeight != currentHeight + }; + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeUseCase.cs.meta b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeUseCase.cs.meta new file mode 100644 index 000000000..7024a6358 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/SetGameViewSizeUseCase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eaf2f40386264131ae6a038f4da34aad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/SetGameViewSize/Skill.meta b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/Skill.meta new file mode 100644 index 000000000..dd24a2fa9 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/Skill.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5615f1d8a87645ecbe269d6428fc6744 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/SetGameViewSize/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/Skill/SKILL.md new file mode 100644 index 000000000..2f37ab076 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/Skill/SKILL.md @@ -0,0 +1,47 @@ +--- +name: uloop-set-game-view-size +toolName: set-game-view-size +description: "Set or inspect the Unity Game View custom rendering resolution." +--- + +# uloop set-game-view-size + +Set or inspect the Unity Game View custom rendering resolution. + +## Usage + +```bash +uloop set-game-view-size [--width --height ] +``` + +## Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--width` | integer | unset | Target Game View rendering width in pixels. Provide with `--height` to change the resolution. | +| `--height` | integer | unset | Target Game View rendering height in pixels. Provide with `--width` to change the resolution. | + +## Output + +Returns JSON containing: + +- `Success`: Whether the request completed. +- `PreviousWidth` / `PreviousHeight`: Resolution before the request. +- `CurrentWidth` / `CurrentHeight`: Resolution after the request. +- `Changed`: Whether the resolution changed. +- `Message`: Human-readable status. + +## Examples + +```bash +# Read the current custom rendering resolution +uloop set-game-view-size + +# Set a Full HD custom rendering resolution +uloop set-game-view-size --width 1920 --height 1080 +``` + +## Notes + +- Width and height must be provided together when changing the resolution. +- The tool uses Unity's public `UnityEditor.PlayModeWindow` API and does not require reflection. diff --git a/Packages/src/Editor/FirstPartyTools/SetGameViewSize/Skill/SKILL.md.meta b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/Skill/SKILL.md.meta new file mode 100644 index 000000000..43bf59dce --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/Skill/SKILL.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a72c1303fd8b4665a260bd45df3b67bc +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/SetGameViewSize/UnityCLILoop.FirstPartyTools.SetGameViewSize.Editor.asmdef b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/UnityCLILoop.FirstPartyTools.SetGameViewSize.Editor.asmdef new file mode 100644 index 000000000..eb7854a61 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/UnityCLILoop.FirstPartyTools.SetGameViewSize.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "UnityCLILoop.FirstPartyTools.SetGameViewSize.Editor", + "rootNamespace": "io.github.hatayama.UnityCliLoop.FirstPartyTools", + "references": [ + "GUID:fc3fd32eddbee40e39c2d76dc184957b" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Packages/src/Editor/FirstPartyTools/SetGameViewSize/UnityCLILoop.FirstPartyTools.SetGameViewSize.Editor.asmdef.meta b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/UnityCLILoop.FirstPartyTools.SetGameViewSize.Editor.asmdef.meta new file mode 100644 index 000000000..ce607fb51 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/SetGameViewSize/UnityCLILoop.FirstPartyTools.SetGameViewSize.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 11dcde5fce434d9ebb7704b1f3b39bba +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/cli/common/tools/default-tools.json b/cli/common/tools/default-tools.json index 877cfa314..9e873a08d 100644 --- a/cli/common/tools/default-tools.json +++ b/cli/common/tools/default-tools.json @@ -724,6 +724,23 @@ } } } + }, + { + "name": "set-game-view-size", + "description": "Set or inspect the Unity Game View custom rendering resolution.", + "inputSchema": { + "type": "object", + "properties": { + "Width": { + "type": "integer", + "description": "Target Game View rendering width in pixels. Provide with Height to change the resolution." + }, + "Height": { + "type": "integer", + "description": "Target Game View rendering height in pixels. Provide with Width to change the resolution." + } + } + } } ] } From b8ae2d61a2b4e071236108dd7ca78044f3ac2215 Mon Sep 17 00:00:00 2001 From: Masamichi Hatayama Date: Sat, 18 Jul 2026 15:53:56 +0900 Subject: [PATCH 5/6] fix: use Game View size tool in simulate mouse E2E (#1833) --- scripts/test-simulate-mouse-demo.sh | 179 ++++------------------------ 1 file changed, 20 insertions(+), 159 deletions(-) diff --git a/scripts/test-simulate-mouse-demo.sh b/scripts/test-simulate-mouse-demo.sh index 3e19a4cde..c6a7a1890 100755 --- a/scripts/test-simulate-mouse-demo.sh +++ b/scripts/test-simulate-mouse-demo.sh @@ -6,7 +6,8 @@ set -eu SCENE_PATH="Assets/Scenes/SimulateMouseDemoScene.unity" TMP_DIR="${TMPDIR:-/tmp}/unity-cli-loop-simulate-mouse" ELEMENTS_JSON="$TMP_DIR/simulate-mouse-elements.json" -ORIGINAL_GAME_VIEW_SIZE_INDEX="" +ORIGINAL_GAME_VIEW_WIDTH="" +ORIGINAL_GAME_VIEW_HEIGHT="" PROJECT_PATH="" ULOOP_PATH="${ULOOP_BIN:-uloop}" @@ -17,8 +18,8 @@ fail() { cleanup() { run_uloop control-play-mode --action Stop >/dev/null 2>&1 || true - if [ -n "${ORIGINAL_GAME_VIEW_SIZE_INDEX:-}" ]; then - restore_game_view_size_index "$ORIGINAL_GAME_VIEW_SIZE_INDEX" >/dev/null 2>&1 || true + if [ -n "${ORIGINAL_GAME_VIEW_WIDTH:-}" ] && [ -n "${ORIGINAL_GAME_VIEW_HEIGHT:-}" ]; then + restore_game_view_size "$ORIGINAL_GAME_VIEW_WIDTH" "$ORIGINAL_GAME_VIEW_HEIGHT" >/dev/null 2>&1 || true fi } trap cleanup EXIT INT TERM @@ -133,164 +134,25 @@ return SceneManager.GetActiveScene().path; } select_full_hd_game_view() { - # Unity exposes no public setter for the Game View resolution dropdown. - # This matches the manual "Full HD (1920x1080)" selection before reading UI coordinates. - code=' -using System; -using System.Reflection; -using UnityEditor; -using UnityEngine; -const int Width = 1920; -const int Height = 1080; -EditorApplication.ExecuteMenuItem("Window/General/Game"); -Type gameViewType = typeof(Editor).Assembly.GetType("UnityEditor.GameView"); -Debug.Assert(gameViewType != null, "GameView type must exist."); -UnityEngine.Object[] gameViews = Resources.FindObjectsOfTypeAll(gameViewType); -EditorWindow gameView = null; -for (int i = 0; i < gameViews.Length; i++) -{ - EditorWindow candidate = gameViews[i] as EditorWindow; - if (candidate == null) - { - continue; - } - if (gameView == null || candidate.hasFocus) - { - gameView = candidate; - } -} -if (gameView == null) -{ - gameView = EditorWindow.GetWindow(gameViewType); -} -gameView.Show(); -Type gameViewSizesType = typeof(Editor).Assembly.GetType("UnityEditor.GameViewSizes"); -Debug.Assert(gameViewSizesType != null, "GameViewSizes type must exist."); -Type singletonType = typeof(ScriptableSingleton<>).MakeGenericType(gameViewSizesType); -PropertyInfo instanceProperty = singletonType.GetProperty("instance", BindingFlags.Public | BindingFlags.Static); -Debug.Assert(instanceProperty != null, "GameViewSizes instance property must exist."); -object gameViewSizes = instanceProperty.GetValue(null); -MethodInfo getGroupMethod = gameViewSizesType.GetMethod("GetGroup", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); -Debug.Assert(getGroupMethod != null, "GameViewSizes.GetGroup must exist."); -object group = getGroupMethod.Invoke(gameViewSizes, new object[] { GameViewSizeGroupType.Standalone }); -Debug.Assert(group != null, "Standalone GameViewSize group must exist."); -Type groupType = group.GetType(); -MethodInfo getDisplayTextsMethod = groupType.GetMethod("GetDisplayTexts", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); -Debug.Assert(getDisplayTextsMethod != null, "GameViewSizeGroup.GetDisplayTexts must exist."); -string[] displayTexts = (string[])getDisplayTextsMethod.Invoke(group, null); -int selectedIndex = -1; -for (int i = 0; i < displayTexts.Length; i++) -{ - if (displayTexts[i].Contains("1920x1080") || displayTexts[i].Contains("Full HD")) - { - selectedIndex = i; - break; - } -} -if (selectedIndex < 0) -{ - Type gameViewSizeType = typeof(Editor).Assembly.GetType("UnityEditor.GameViewSize"); - Debug.Assert(gameViewSizeType != null, "GameViewSize type must exist."); - Type gameViewSizeTypeEnum = typeof(Editor).Assembly.GetType("UnityEditor.GameViewSizeType"); - Debug.Assert(gameViewSizeTypeEnum != null, "GameViewSizeType enum must exist."); - object fixedResolution = Enum.Parse(gameViewSizeTypeEnum, "FixedResolution"); - ConstructorInfo constructor = gameViewSizeType.GetConstructor(new Type[] { gameViewSizeTypeEnum, typeof(int), typeof(int), typeof(string) }); - Debug.Assert(constructor != null, "GameViewSize constructor must exist."); - object selectedGameViewSize = constructor.Invoke(new object[] { fixedResolution, Width, Height, "Full HD" }); - MethodInfo addCustomSizeMethod = groupType.GetMethod("AddCustomSize", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - Debug.Assert(addCustomSizeMethod != null, "GameViewSizeGroup.AddCustomSize must exist."); - addCustomSizeMethod.Invoke(group, new object[] { selectedGameViewSize }); - displayTexts = (string[])getDisplayTextsMethod.Invoke(group, null); - selectedIndex = displayTexts.Length - 1; -} -PropertyInfo selectedSizeIndexProperty = gameViewType.GetProperty("selectedSizeIndex", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); -Debug.Assert(selectedSizeIndexProperty != null, "GameView.selectedSizeIndex must exist."); -selectedSizeIndexProperty.SetValue(gameView, selectedIndex, null); -Screen.SetResolution(Width, Height, false); -gameView.Repaint(); -Vector2 currentGameViewSize = Handles.GetMainGameViewSize(); -return displayTexts[selectedIndex] + " / " + currentGameViewSize.x + "x" + currentGameViewSize.y; -' - - 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')" + json=$(run_uloop_json set-game-view-size --width 1920 --height 1080) + assert_json_success "$json" "Set Full HD Game View" + ORIGINAL_GAME_VIEW_WIDTH=$(printf '%s\n' "$json" | jq -r '.PreviousWidth') + ORIGINAL_GAME_VIEW_HEIGHT=$(printf '%s\n' "$json" | jq -r '.PreviousHeight') + current_width=$(printf '%s\n' "$json" | jq -r '.CurrentWidth') + current_height=$(printf '%s\n' "$json" | jq -r '.CurrentHeight') + assert_text_equals "$current_width" "1920" "Game View width" + assert_text_equals "$current_height" "1080" "Game View height" + printf ' %sx%s (previous: %sx%s)\n' \ + "$current_width" "$current_height" \ + "$ORIGINAL_GAME_VIEW_WIDTH" "$ORIGINAL_GAME_VIEW_HEIGHT" sleep 1 } -capture_game_view_size_index() { - code=' -using System; -using System.Reflection; -using UnityEditor; -using UnityEngine; -Type gameViewType = typeof(Editor).Assembly.GetType("UnityEditor.GameView"); -Debug.Assert(gameViewType != null, "GameView type must exist."); -UnityEngine.Object[] gameViews = Resources.FindObjectsOfTypeAll(gameViewType); -EditorWindow gameView = null; -for (int i = 0; i < gameViews.Length; i++) -{ - EditorWindow candidate = gameViews[i] as EditorWindow; - if (candidate == null) - { - continue; - } - if (gameView == null || candidate.hasFocus) - { - gameView = candidate; - } -} -if (gameView == null) -{ - gameView = EditorWindow.GetWindow(gameViewType); -} -PropertyInfo selectedSizeIndexProperty = gameViewType.GetProperty("selectedSizeIndex", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); -Debug.Assert(selectedSizeIndexProperty != null, "GameView.selectedSizeIndex must exist."); -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' -} - -restore_game_view_size_index() { - selected_index=$1 - code=" -using System; -using System.Reflection; -using UnityEditor; -using UnityEngine; -int selectedIndex = $selected_index; -Debug.Assert(selectedIndex >= 0, \"GameView size index must be non-negative.\"); -Type gameViewType = typeof(Editor).Assembly.GetType(\"UnityEditor.GameView\"); -Debug.Assert(gameViewType != null, \"GameView type must exist.\"); -UnityEngine.Object[] gameViews = Resources.FindObjectsOfTypeAll(gameViewType); -EditorWindow gameView = null; -for (int i = 0; i < gameViews.Length; i++) -{ - EditorWindow candidate = gameViews[i] as EditorWindow; - if (candidate == null) - { - continue; - } - if (gameView == null || candidate.hasFocus) - { - gameView = candidate; - } -} -if (gameView == null) -{ - gameView = EditorWindow.GetWindow(gameViewType); -} -PropertyInfo selectedSizeIndexProperty = gameViewType.GetProperty(\"selectedSizeIndex\", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); -Debug.Assert(selectedSizeIndexProperty != null, \"GameView.selectedSizeIndex must exist.\"); -selectedSizeIndexProperty.SetValue(gameView, selectedIndex, null); -gameView.Repaint(); -return selectedIndex.ToString(); -" - - run_uloop_json execute-dynamic-code --code "$code" +restore_game_view_size() { + original_width=$1 + original_height=$2 + json=$(run_uloop_json set-game-view-size --width "$original_width" --height "$original_height") + assert_json_success "$json" "Restore Game View resolution" } capture_annotated_elements() { @@ -557,7 +419,6 @@ wait_play_mode sleep 1 printf '[3/8] Selecting Full HD Game View resolution...\n' -ORIGINAL_GAME_VIEW_SIZE_INDEX=$(capture_game_view_size_index) select_full_hd_game_view printf '[4/8] Reading annotated UI coordinates...\n' From 8e7f9dbdb2a0c7e8df4b22ebc439ccbc651cfad0 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 18 Jul 2026 16:05:31 +0900 Subject: [PATCH 6/6] Use assertions for compiler option preconditions Treat the compiler options snapshot as an internal contract: the only producer normalizes null define symbols before construction. Use the project's assertion convention for that programmer error and keep the remaining tests focused on snapshot behavior. --- .../DynamicCodeToolTests/RoslynCompilerOptionsTests.cs | 10 ---------- .../DynamicCompilation/RoslynCompilerOptions.cs | 7 ++----- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs index 5179b0b50..4145ee079 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs @@ -47,15 +47,5 @@ public void Constructor_WithEmptyDefineSymbols_CapturesEmptySnapshot() Assert.That(options.AllowUnsafeCode, Is.False); } - /// - /// Verifies a null define-symbol collection fails fast at the snapshot boundary. - /// - [Test] - public void Constructor_WithNullDefineSymbols_ThrowsArgumentNullException() - { - Assert.That( - () => new RoslynCompilerOptions(null, false), - Throws.ArgumentNullException); - } } } diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs index 744800033..bdff5e238 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using UnityEngine; namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { @@ -19,10 +19,7 @@ public RoslynCompilerOptions( IReadOnlyCollection defineSymbols, bool allowUnsafeCode) { - if (defineSymbols == null) - { - throw new ArgumentNullException(nameof(defineSymbols)); - } + Debug.Assert(defineSymbols != null, "defineSymbols must not be null"); List filteredDefineSymbols = new(defineSymbols.Count); foreach (string defineSymbol in defineSymbols)