From 17ef367e5cdf4ec0cd0dcf658a519abf6aff15a7 Mon Sep 17 00:00:00 2001 From: yhc5091 Date: Sat, 15 Aug 2026 12:04:46 +0900 Subject: [PATCH] feat(screenshot)!: default to a 1024px JPEG for agent-facing captures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A screenshot is the heaviest single response an agent can request: a 1920x1080 PNG bills at roughly 2,040 image tokens, against about 576 for the same frame capped at 1024px. The lightweight options existed but were opt-in, so the common caller paid full price for fidelity no vision model can use. `screenshot` now defaults to JPEG quality 75 capped at 1024px wide. Three rules keep that from surprising anyone: - an explicit `--format` always wins; - with no `--format`, a `--path` ending in `.png` selects PNG — writing JPEG bytes into a file the caller named `.png` is a worse outcome than the tokens the default saves; - an explicit `--width`/`--height` suppresses the cap. That gate predates this change, which is why sized captures do not silently shrink. `--max-width 0` is the opt-out for the cap alone and travels as a -1 wire sentinel, because `maxWidth == 0` already means "unspecified" and the wire model cannot carry a nullable int. ScreenshotDefaults lives in Runtime/Protocol so the CLI and the bridge resolve the defaults from one place and the resolution is unit-tested; resolving the cap once in Handle also collapses the ShouldApplyMaxWidth gate that was repeated at each capture site. Protocol 7 -> 8: the default output of an existing command changed and maxWidth gained a sentinel, so a mismatched CLI should be routed to a matching install rather than silently return different images. Verified live against the sample project (Unity 6000.3.13f1, headless): with the Game View at 3600x2110, an option-free capture returned 1024x600 jpg / 10 KB against 3600x2110 png / 104 KB before. Format selection, the .png path rule, the explicit-size bypass and the negative --max-width rejection all confirmed against a running editor. Closes #127 Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + CLAUDE.md | 2 +- README.md | 9 +- cli/UnityCli.Cli/Models/ParsedCommand.cs | 19 ++- .../Services/CliArgumentParser.cs | 4 +- docs/cli-reference.md | 2 +- .../CliArgumentParserTests.cs | 33 +++++ .../ProtocolConstantsTests.cs | 4 +- .../ScreenshotDefaultsTests.cs | 100 ++++++++++++++ tools/skills/unity-cli-operator/SKILL.md | 2 +- .../references/command-flows.md | 10 +- .../references/qa-testing.md | 10 +- .../com.yhc509.unity-cli-bridge/CHANGELOG.md | 1 + .../Editor/ScreenshotCommandHandler.cs | 59 ++++---- .../Runtime/Protocol/CliCommandCatalog.cs | 4 +- .../Runtime/Protocol/ProtocolConstants.cs | 2 +- .../Runtime/Protocol/ScreenshotDefaults.cs | 129 ++++++++++++++++++ .../Protocol/ScreenshotDefaults.cs.meta | 2 + .../SkillTemplates~/SKILL.md | 2 +- .../references/command-flows.md | 10 +- .../SkillTemplates~/references/qa-testing.md | 10 +- 21 files changed, 345 insertions(+), 70 deletions(-) create mode 100644 tests/UnityCli.Cli.Tests/ScreenshotDefaultsTests.cs create mode 100644 unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/ScreenshotDefaults.cs create mode 100644 unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/ScreenshotDefaults.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 111b489..d9a3b3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - The bridge can now be kept from starting, for build machines that have no use for it. Set `UNITY_CLI_BRIDGE_DISABLE=1` or pass `-noUnityCliBridge` on the Editor command line and the bridge builds nothing at all — no socket, no instance registration, no token file, no console warnings if any of that would have failed. A CI job that only opens the Editor to produce a player build gets a quiet log and no registry lock to contend over with parallel builds. ### Changed +- **`screenshot` now defaults to a JPEG capped at 1024px wide** (quality 75) instead of a full-resolution PNG. A capture is by far the most expensive thing an AI agent can ask for — a 1920×1080 PNG bills at roughly 2,040 image tokens, against about 576 for the same frame capped at 1024px — and the capped JPEG is still perfectly readable for checking UI state, which is what nearly every capture is for. Three things keep the change from surprising anyone: `--format png` still gives you lossless, a `--path` ending in `.png` selects PNG on its own, and an explicit `--width`/`--height` is left exactly as you asked. `--max-width 0` turns the cap off while keeping the automatic size. Tap and dump coordinates are unaffected — they scale from the last capture's size, so a downscaled screenshot's coordinates still land correctly. - Unity Recorder is no longer installed alongside the package. Only the `record` commands and `qa run-sequence --record` ever used it, so projects that do not record no longer carry the dependency. If you do record, add `com.unity.recorder` to your project — `record start` now fails with that exact instruction when it is missing, and `record stop` / `record status` keep working on recordings you already have. `com.unity.test-framework`, which the test commands genuinely require, is now declared outright instead of arriving as a side effect of the Recorder dependency. ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index defb0fd..1671602 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,7 +87,7 @@ Tests live in `tests/UnityCli.Cli.Tests/` (xUnit, `.NET`-testable surface only). - **Scene paths:** Format `/Root[0]/Child[0]` with array notation for sibling indexing; `/` is the virtual scene root. - **Scene/prefab node flags:** Convenience commands that point at a hierarchy node use `--node`; JSON patch specs still use `target`/`parent`. - **Prefab editing:** Based on `SerializedProperty.propertyPath` (run `prefab inspect --with-values` to verify paths before patching). -- **Screenshot lightweight options:** `screenshot` defaults to PNG for compatibility; use `--format jpg|jpeg`, `--quality 1-100`, and `--max-width ` to reduce agent-facing image size without changing explicit `--width`/`--height` behavior. +- **Screenshot agent-facing defaults:** `screenshot` defaults to **JPEG quality 75 downscaled to 1024px wide** — a capture is the heaviest single response an agent can request (~2,040 image tokens at 1080p PNG vs ~576 capped), so the default is tuned for the common caller rather than for archival fidelity. `ScreenshotDefaults` (`Runtime/Protocol/`) owns the resolution so the CLI and the bridge agree and it stays unit-tested. Three rules keep the default from surprising anyone: an explicit `--format` always wins; with no `--format`, a `--path` ending in `.png` selects PNG (writing JPEG bytes into a file the caller named `.png` is worse than the tokens saved); and an explicit `--width`/`--height` suppresses the cap entirely — that gate predates the default and is why sized captures did not silently shrink. `--max-width 0` is the opt-out for the cap alone, and travels as the `MaxWidthUncapped` (-1) wire sentinel because `maxWidth == 0` already means "unspecified" (no `Nullable` on the wire). Downscaling does not break QA coordinates: `qa tap`/`ui-dump`/`world-dump` scale from `LastCapturedWidth`/`Height`. - **Record:** Play Mode 전용 Unity Recorder 기반 mp4. `record start`는 `STARTED+recordingId`를 즉시 반환하고, `--duration` 또는 600초 안전캡으로 자동 stop한다. `--wait`는 CLI가 `record status` sidecar를 폴링한다. force-rule 없음. 글로벌 busy에 참여하지 않고 자체 single-flight만 사용한다. - **Console/test/list trims:** `read-console --no-stacktrace`, `test list --no-detail`, `test run/results --failures-only`, and `instances list --brief` are opt-in response trims only. Defaults preserve full output. Test failure trimming never changes summary counts or cached result files. - **Test runner:** EditMode는 동기, PlayMode는 비동기(STARTED+runId 즉시 반환). `--wait`로 CLI 측 폴링. `--no-domain-reload`는 PlayMode 전용 속도 옵션이며 정합성 결정과 분리. 동시 실행 1회(`TEST_RUN_IN_PROGRESS` 거부). non-`Completed` 결과는 error envelope/exit code 1로 반환한다. force-rule 없음. 결과는 `Library/com.yhc509.unity-cli-bridge/test-runs/.json`. diff --git a/README.md b/README.md index 0be3e6b..87c7601 100644 --- a/README.md +++ b/README.md @@ -156,8 +156,9 @@ unity-cli play / pause / stop # Play Mode control unity-cli compile # Trigger recompile unity-cli compile --wait # Wait until compile/import finishes and bridge is reachable unity-cli refresh --wait # Refresh assets and wait for Editor readiness -unity-cli screenshot --path /tmp/shot.png # Game View capture (default), or --view scene -unity-cli screenshot --format jpg --quality 70 --max-width 1024 --path /tmp/shot.jpg +unity-cli screenshot --path /tmp/shot.jpg # Game View capture (default), or --view scene + # Defaults: JPEG q75, downscaled to 1024px wide +unity-cli screenshot --format png --max-width 0 --path /tmp/shot.png # Lossless, full resolution unity-cli record start --duration 5 --wait --path /tmp/play.mp4 unity-cli record start # Manual recording; stop with record stop unity-cli record status @@ -289,8 +290,8 @@ A typical AI repair loop is: make a focused code change, `unity-cli refresh`, ru ```bash unity-cli qa click --qa-id StartButton -unity-cli screenshot --view game --path /tmp/qa-reference.png -unity-cli screenshot --view game --format jpg --quality 70 --max-width 1024 --path /tmp/qa-reference.jpg +unity-cli screenshot --view game --path /tmp/qa-reference.jpg +unity-cli screenshot --view game --format png --max-width 0 --path /tmp/qa-reference.png unity-cli qa ui-dump --json unity-cli qa ui-dump --text Start --interactable-only --limit 20 --omit-rect --json unity-cli qa world-dump --json diff --git a/cli/UnityCli.Cli/Models/ParsedCommand.cs b/cli/UnityCli.Cli/Models/ParsedCommand.cs index a87f632..eea111b 100644 --- a/cli/UnityCli.Cli/Models/ParsedCommand.cs +++ b/cli/UnityCli.Cli/Models/ParsedCommand.cs @@ -383,7 +383,7 @@ private string BuildArgumentsJson() height = ScreenshotHeight ?? 0, format = ScreenshotFormat, quality = ScreenshotQuality ?? 0, - maxWidth = ScreenshotMaxWidth ?? 0, + maxWidth = ResolveScreenshotMaxWidth(), }, CommandKind.PackageList => new PackageListArgs { @@ -749,6 +749,23 @@ private string BuildRawArgumentsJson(JsonElement root) : ScreenshotView; } + /// + /// Maps --max-width onto the wire, where 0 means "unspecified, apply the default cap". + /// A caller who passed --max-width 0 is asking for the opposite — no cap at all — so it + /// travels as the uncapped sentinel rather than as a value the bridge would read as silence. + /// + private int ResolveScreenshotMaxWidth() + { + if (ScreenshotMaxWidth is null) + { + return 0; + } + + return ScreenshotMaxWidth.Value > 0 + ? ScreenshotMaxWidth.Value + : ScreenshotDefaults.MaxWidthUncapped; + } + private string? BuildAssetCreateOptionsJson() { var options = new Dictionary(AssetCustomOptions, StringComparer.OrdinalIgnoreCase); diff --git a/cli/UnityCli.Cli/Services/CliArgumentParser.cs b/cli/UnityCli.Cli/Services/CliArgumentParser.cs index c2134d4..45d4cbc 100644 --- a/cli/UnityCli.Cli/Services/CliArgumentParser.cs +++ b/cli/UnityCli.Cli/Services/CliArgumentParser.cs @@ -613,7 +613,9 @@ private static void ParseCommandOptions(ParsedCommand parsed, Queue toke parsed.ScreenshotQuality = RequireScreenshotQuality(RequireValue(tokens, "--quality")); break; case CommandKind.Screenshot when token == "--max-width": - parsed.ScreenshotMaxWidth = RequireInt(RequireValue(tokens, "--max-width"), "--max-width"); + // 0 is allowed and means "no cap" — the way to opt out of the default downscale + // without having to state an explicit --width/--height. + parsed.ScreenshotMaxWidth = RequireInt(RequireValue(tokens, "--max-width"), "--max-width", minimumValue: 0); break; case CommandKind.ExecuteCode when token == "--code": parsed.ExecuteCodeSnippet = RequireValue(tokens, "--code"); diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 1667354..c86d866 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -39,7 +39,7 @@ Commands for editor state, compilation, play state, menus, arbitrary code execut | `editor launch` | `editor launch [--gui] [--nographics] [--no-wait] [--timeout ] [--editor-path ]` | local | `None` | Launches the Unity Editor for the selected project (headless -batchmode by default, GPU kept for rendering commands). Idempotent: reuses a live instance when one is already running. Waits for bridge readiness unless --no-wait. | | `editor stop` | `editor stop [--force] [--no-wait] [--timeout ]` | live | `OnDestructiveOp` | Gracefully quits the running editor for the selected project. Refuses with EDITOR_DIRTY when unsaved scene/prefab-stage changes exist; --force discards them. Waits for process exit unless --no-wait. | | `execute-menu` | `execute-menu (--path "Menu/Item" \| --list "Prefix")` | live | `None` | Executes a Unity menu item or lists registered menu items matching a prefix in a running editor. | -| `screenshot` | `screenshot [--view game\|scene (default: game) \| --camera ] [--path ] [--width N] [--height N] [--format png\|jpg\|jpeg] [--quality 1-100] [--max-width N]` | live | `None` | Captures a screenshot from the Game View, Scene View, or a named camera. Defaults to Game View; encoding defaults to PNG. Use --format jpg with --quality to reduce file size, and --max-width to downscale proportionally when --width/--height are not specified. The response includes image size, actual saved format, and screen-space metadata (`screenWidth`, `screenHeight`, `imageOrigin`, `coordinateOrigin`) for QA coordinate alignment. In Play Mode, --view game can downscale the native Game View capture but does not upscale it. | +| `screenshot` | `screenshot [--view game\|scene (default: game) \| --camera ] [--path ] [--width N] [--height N] [--format png\|jpg\|jpeg] [--quality 1-100] [--max-width N\|0]` | live | `None` | Captures a screenshot from the Game View, Scene View, or a named camera. Defaults to Game View, JPEG at quality 75, and a 1024px width cap — agent-friendly defaults that cut image tokens by roughly 72% at 1080p. Override with --format png for lossless, --quality for the JPEG setting, --max-width N for a different cap, or --max-width 0 for no cap. The cap only applies when neither --width nor --height is given, and --path with a .png extension selects PNG when --format is omitted. The response includes image size, actual saved format, and screen-space metadata (`screenWidth`, `screenHeight`, `imageOrigin`, `coordinateOrigin`) for QA coordinate alignment; qa tap/ui-dump scale from the last captured size, so downscaled shots keep their coordinates usable. In Play Mode, --view game can downscale the native Game View capture but does not upscale it. | | `record start` | `record start [--path ] [--fps (default: 30)] [--max-width ] [--duration ] [--wait]` | live | `None` | Starts recording the Game View to an mp4 file via Unity Recorder. Returns immediately with a recordingId. Requires Play Mode. | | `record stop` | `record stop` | live | `None` | Stops the active recording, finalizes the mp4, and returns the output path. | | `record status` | `record status [--recording-id ]` | live | `None` | Reports whether a recording is active and the result of a finished recording. | diff --git a/tests/UnityCli.Cli.Tests/CliArgumentParserTests.cs b/tests/UnityCli.Cli.Tests/CliArgumentParserTests.cs index c3531df..a822807 100644 --- a/tests/UnityCli.Cli.Tests/CliArgumentParserTests.cs +++ b/tests/UnityCli.Cli.Tests/CliArgumentParserTests.cs @@ -1487,6 +1487,39 @@ public void ToEnvelope_Screenshot_IncludesLightweightOptions() Assert.Equal(1024, arguments.GetProperty("maxWidth").GetInt32()); } + [Fact] + public void ToEnvelope_Screenshot_WithoutMaxWidth_LeavesTheDefaultToTheBridge() + { + var parsed = CliArgumentParser.Parse(["screenshot"]); + + using var document = JsonDocument.Parse(parsed.ToEnvelope().argumentsJson); + + Assert.Equal(0, document.RootElement.GetProperty("maxWidth").GetInt32()); + } + + [Fact] + public void ToEnvelope_Screenshot_WithZeroMaxWidth_SendsTheUncappedSentinel() + { + // 0 on the wire already means "unspecified", so an explicit opt-out has to travel as + // something the bridge cannot read as silence. + var parsed = CliArgumentParser.Parse(["screenshot", "--max-width", "0"]); + + using var document = JsonDocument.Parse(parsed.ToEnvelope().argumentsJson); + + Assert.Equal(0, parsed.ScreenshotMaxWidth); + Assert.Equal( + ScreenshotDefaults.MaxWidthUncapped, + document.RootElement.GetProperty("maxWidth").GetInt32()); + } + + [Fact] + public void Parse_Screenshot_RejectsNegativeMaxWidth() + { + Assert.Throws(() => CliArgumentParser.Parse([ + "screenshot", "--max-width", "-1" + ])); + } + [Fact] public void Parse_Screenshot_AcceptsCameraName() { diff --git a/tests/UnityCli.Cli.Tests/ProtocolConstantsTests.cs b/tests/UnityCli.Cli.Tests/ProtocolConstantsTests.cs index 050684b..ae45e5f 100644 --- a/tests/UnityCli.Cli.Tests/ProtocolConstantsTests.cs +++ b/tests/UnityCli.Cli.Tests/ProtocolConstantsTests.cs @@ -5,9 +5,9 @@ namespace UnityCli.Cli.Tests; public sealed class ProtocolConstantsTests { [Fact] - public void ProtocolVersion_BumpedToSeven_ForHeadlessEditorCommands() + public void ProtocolVersion_BumpedToEight_ForAgentFacingScreenshotDefaults() { - Assert.Equal("7", ProtocolConstants.ProtocolVersion); + Assert.Equal("8", ProtocolConstants.ProtocolVersion); } [Fact] diff --git a/tests/UnityCli.Cli.Tests/ScreenshotDefaultsTests.cs b/tests/UnityCli.Cli.Tests/ScreenshotDefaultsTests.cs new file mode 100644 index 0000000..89e68da --- /dev/null +++ b/tests/UnityCli.Cli.Tests/ScreenshotDefaultsTests.cs @@ -0,0 +1,100 @@ +using UnityCli.Protocol; +using Xunit; + +namespace UnityCli.Cli.Tests; + +public class ScreenshotDefaultsTests +{ + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void TryResolveFormat_WithNoRequestAndNoPath_DefaultsToJpg(string? requestedFormat) + { + Assert.True(ScreenshotDefaults.TryResolveFormat(requestedFormat!, null!, out string format)); + Assert.Equal(ScreenshotDefaults.FormatJpg, format); + } + + [Theory] + [InlineData("/tmp/shot.png")] + [InlineData("/tmp/shot.PNG")] + [InlineData(" /tmp/nested.dir/shot.Png ")] + public void TryResolveFormat_WithPngOutputPath_KeepsPng(string outputPath) + { + // Writing JPEG bytes into a file the caller named .png would be a worse outcome than the + // tokens the default saves, so an explicit extension outranks the default. + Assert.True(ScreenshotDefaults.TryResolveFormat(null!, outputPath, out string format)); + Assert.Equal(ScreenshotDefaults.FormatPng, format); + } + + [Theory] + [InlineData("/tmp/shot.jpg")] + [InlineData("/tmp/shot.jpeg")] + [InlineData("/tmp/shot.webp")] + [InlineData("/tmp/shot")] + [InlineData(".png")] + public void TryResolveFormat_WithNonPngOutputPath_UsesTheJpgDefault(string outputPath) + { + Assert.True(ScreenshotDefaults.TryResolveFormat(null!, outputPath, out string format)); + Assert.Equal(ScreenshotDefaults.FormatJpg, format); + } + + [Theory] + [InlineData("png", "png")] + [InlineData("PNG", "png")] + [InlineData(" jpg ", "jpg")] + [InlineData("jpeg", "jpg")] + [InlineData("JPEG", "jpg")] + public void TryResolveFormat_WithExplicitFormat_OverridesTheOutputExtension(string requested, string expected) + { + Assert.True(ScreenshotDefaults.TryResolveFormat(requested, "/tmp/shot.png", out string format)); + Assert.Equal(expected, format); + } + + [Theory] + [InlineData("bmp")] + [InlineData("gif")] + [InlineData("jpgg")] + public void TryResolveFormat_WithUnknownFormat_Fails(string requested) + { + Assert.False(ScreenshotDefaults.TryResolveFormat(requested, null!, out _)); + } + + [Fact] + public void ResolveMaxWidth_WithNothingSpecified_AppliesTheDefaultCap() + { + Assert.Equal(ScreenshotDefaults.DefaultMaxWidth, ScreenshotDefaults.ResolveMaxWidth(0, 0, 0)); + } + + [Fact] + public void ResolveMaxWidth_WithExplicitCap_UsesIt() + { + Assert.Equal(640, ScreenshotDefaults.ResolveMaxWidth(640, 0, 0)); + } + + [Fact] + public void ResolveMaxWidth_WithUncappedSentinel_DisablesTheCap() + { + Assert.Equal(0, ScreenshotDefaults.ResolveMaxWidth(ScreenshotDefaults.MaxWidthUncapped, 0, 0)); + } + + [Theory] + [InlineData(1920, 0)] + [InlineData(0, 1080)] + [InlineData(1920, 1080)] + public void ResolveMaxWidth_WithAnExplicitSize_LeavesTheSizeAlone(int width, int height) + { + // The explicit-size gate predates the default cap; keeping it is what stops this change + // from silently shrinking captures that already state the size they want. + Assert.Equal(0, ScreenshotDefaults.ResolveMaxWidth(0, width, height)); + Assert.Equal(0, ScreenshotDefaults.ResolveMaxWidth(512, width, height)); + } + + [Theory] + [InlineData("jpg", ".jpg")] + [InlineData("png", ".png")] + public void FileExtension_MatchesTheResolvedFormat(string format, string expected) + { + Assert.Equal(expected, ScreenshotDefaults.FileExtension(format)); + } +} diff --git a/tools/skills/unity-cli-operator/SKILL.md b/tools/skills/unity-cli-operator/SKILL.md index 52ea247..1f051ba 100644 --- a/tools/skills/unity-cli-operator/SKILL.md +++ b/tools/skills/unity-cli-operator/SKILL.md @@ -84,7 +84,7 @@ for (int i = 0; i < workItems.Count; i++) - scene path는 `/Root[0]/Child[0]` 형식으로 쓰고 `/`는 virtual scene root로 본다. - root prefab 이름은 Unity 저장 규칙 때문에 파일 이름으로 정규화된다고 가정한다. - `screenshot`은 `--view` 생략 시 game이 기본이다. Scene View가 필요하면 `--view scene`을 명시한다. -- **에이전트가 읽을 스크린샷은 `--format jpg --quality 75 --max-width 1024`를 기본으로 붙인다.** 기본 PNG full-resolution은 이미지 토큰을 크게 소비한다(1080p 기준 ~72% 절약). lossless가 필요할 때만 `--format png`. +- **스크린샷은 옵션 없이 그대로 찍으면 된다.** 기본값이 이미 JPEG quality 75 + 1024px 가로 축소라 에이전트가 읽기 좋은 크기로 나온다(1080p PNG 대비 이미지 토큰 ~72% 절약). lossless 원본이 필요할 때만 `--format png --max-width 0`을 붙인다. `--path`가 `.png`로 끝나면 `--format` 없이도 PNG로 저장된다. - Play Mode 영상을 남겨야 하면 `record start --duration N --wait --path /tmp/out.mp4`를 쓴다. 수동 녹화는 `record start` 후 `record status`, `record stop` 순서로 종료한다. `record start`는 Play Mode 전용이고 `com.unity.recorder`가 설치된 프로젝트에서만 동작한다 — 미설치면 `RECORD_FAILED`와 함께 `unity-cli package add --name com.unity.recorder` 안내가 돌아오므로 그대로 설치한 뒤 재시도한다. - `qa tap --x --y`에는 `screenshot`에서 확인한 이미지 좌표를 그대로 넣는다. 응답의 `imageOrigin`은 `top-left`, `coordinateOrigin`은 `bottom-left`다. - `qa click`, `qa tap`, `qa swipe`는 기본 좌클릭/좌드래그이며, 우클릭 입력 경로를 검증할 때는 `--button right`를 붙인다. diff --git a/tools/skills/unity-cli-operator/references/command-flows.md b/tools/skills/unity-cli-operator/references/command-flows.md index 0713e8b..b699e08 100644 --- a/tools/skills/unity-cli-operator/references/command-flows.md +++ b/tools/skills/unity-cli-operator/references/command-flows.md @@ -200,14 +200,14 @@ ucli material info --project "$PROJECT" --path Assets/Materials/MyMat.mat --omit ## 스크린샷 ```bash -# Game View 캡처 — 에이전트가 읽을 캡처는 jpg + max-width로 토큰 절약 (--view 생략 시 game이 기본) -ucli screenshot --project "$PROJECT" --path /tmp/capture.jpg --format jpg --quality 75 --max-width 1024 --output compact +# Game View 캡처 — 기본값이 이미 jpg q75 + 1024px 축소다 (--view 생략 시 game이 기본) +ucli screenshot --project "$PROJECT" --path /tmp/capture.jpg --output compact -# lossless가 필요할 때만 PNG -ucli screenshot --project "$PROJECT" --path /tmp/capture.png --output compact +# lossless 원본이 필요할 때만 +ucli screenshot --project "$PROJECT" --path /tmp/capture.png --format png --max-width 0 --output compact # Scene View 캡처 -ucli screenshot --project "$PROJECT" --path /tmp/scene.png --view scene --output compact +ucli screenshot --project "$PROJECT" --path /tmp/scene.jpg --view scene --output compact ``` ## 테스트 러너 diff --git a/tools/skills/unity-cli-operator/references/qa-testing.md b/tools/skills/unity-cli-operator/references/qa-testing.md index a975d65..ce3ad0d 100644 --- a/tools/skills/unity-cli-operator/references/qa-testing.md +++ b/tools/skills/unity-cli-operator/references/qa-testing.md @@ -12,7 +12,7 @@ status → play → (입력 시뮬레이션) → 검증 (로그 + 스크린샷) 2. `play`로 Play Mode 진입 (자동으로 `runInBackground = true` 설정됨) 3. QA 커맨드로 입력 시뮬레이션 4. **로그 검증**: `read-console --type error --limit N --no-stacktrace` + `read-console --type log --limit N --no-stacktrace` -5. **시각 검증**: `screenshot --view game --format jpg --quality 75 --max-width 1024 --path /tmp/qa-check.jpg` 후 이미지 확인 (lossless가 필요할 때만 `--format png`) +5. **시각 검증**: `screenshot --view game --path /tmp/qa-check.jpg` 후 이미지 확인 (기본이 jpg q75 + 1024px 축소, lossless 원본은 `--format png --max-width 0`) 6. `stop`으로 Play Mode 종료 (`runInBackground` 원래값 복원됨) ## 입력 방식 선택 @@ -135,10 +135,10 @@ ucli read-console --type error --limit 5 --no-stacktrace --project "$P" --json `screenshot --view game`으로 Game View를 캡처해서 시각적 변화를 확인한다. ```bash -# 토큰 절약: 에이전트가 읽을 스크린샷은 jpg + max-width를 기본으로 한다 -ucli screenshot --view game --format jpg --quality 75 --max-width 1024 --path /tmp/qa-check.jpg --project "$P" --json -# lossless가 필요할 때만 PNG -ucli screenshot --view game --path /tmp/qa-check.png --project "$P" --json +# 기본값이 이미 jpg q75 + 1024px 축소라 옵션 없이 그대로 찍으면 된다 +ucli screenshot --view game --path /tmp/qa-check.jpg --project "$P" --json +# lossless 원본이 필요할 때만 +ucli screenshot --view game --format png --max-width 0 --path /tmp/qa-check.png --project "$P" --json # 이후 이미지를 Read 도구로 확인 ``` diff --git a/unity-package/com.yhc509.unity-cli-bridge/CHANGELOG.md b/unity-package/com.yhc509.unity-cli-bridge/CHANGELOG.md index 111b489..d9a3b3f 100644 --- a/unity-package/com.yhc509.unity-cli-bridge/CHANGELOG.md +++ b/unity-package/com.yhc509.unity-cli-bridge/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - The bridge can now be kept from starting, for build machines that have no use for it. Set `UNITY_CLI_BRIDGE_DISABLE=1` or pass `-noUnityCliBridge` on the Editor command line and the bridge builds nothing at all — no socket, no instance registration, no token file, no console warnings if any of that would have failed. A CI job that only opens the Editor to produce a player build gets a quiet log and no registry lock to contend over with parallel builds. ### Changed +- **`screenshot` now defaults to a JPEG capped at 1024px wide** (quality 75) instead of a full-resolution PNG. A capture is by far the most expensive thing an AI agent can ask for — a 1920×1080 PNG bills at roughly 2,040 image tokens, against about 576 for the same frame capped at 1024px — and the capped JPEG is still perfectly readable for checking UI state, which is what nearly every capture is for. Three things keep the change from surprising anyone: `--format png` still gives you lossless, a `--path` ending in `.png` selects PNG on its own, and an explicit `--width`/`--height` is left exactly as you asked. `--max-width 0` turns the cap off while keeping the automatic size. Tap and dump coordinates are unaffected — they scale from the last capture's size, so a downscaled screenshot's coordinates still land correctly. - Unity Recorder is no longer installed alongside the package. Only the `record` commands and `qa run-sequence --record` ever used it, so projects that do not record no longer carry the dependency. If you do record, add `com.unity.recorder` to your project — `record start` now fails with that exact instruction when it is missing, and `record stop` / `record status` keep working on recordings you already have. `com.unity.test-framework`, which the test commands genuinely require, is now declared outright instead of arriving as a side effect of the Recorder dependency. ### Fixed diff --git a/unity-package/com.yhc509.unity-cli-bridge/Editor/ScreenshotCommandHandler.cs b/unity-package/com.yhc509.unity-cli-bridge/Editor/ScreenshotCommandHandler.cs index 3c35d4d..68a0005 100644 --- a/unity-package/com.yhc509.unity-cli-bridge/Editor/ScreenshotCommandHandler.cs +++ b/unity-package/com.yhc509.unity-cli-bridge/Editor/ScreenshotCommandHandler.cs @@ -10,9 +10,7 @@ namespace UnityCliBridge.Bridge.Editor { internal sealed class ScreenshotCommandHandler { - private const string FormatPng = "png"; - private const string FormatJpg = "jpg"; - private const int DefaultJpegQuality = 75; + private const string FormatJpg = ScreenshotDefaults.FormatJpg; internal static int LastCapturedWidth { get; private set; } internal static int LastCapturedHeight { get; private set; } @@ -57,12 +55,16 @@ public string Handle(string command, string argumentsJson) string outputPath; int capturedWidth; int capturedHeight; - string format = NormalizeScreenshotFormat(args.format); + string format = NormalizeScreenshotFormat(args.format, args.outputPath); int jpegQuality = NormalizeJpegQuality(args.quality); + // Resolved once here so every capture path shares one answer; the explicit-size gate + // that used to live at each call site is folded into ResolveMaxWidth. + int maxWidth = ScreenshotDefaults.ResolveMaxWidth(args.maxWidth, args.width, args.height); + if (!string.IsNullOrWhiteSpace(args.camera)) { - var result = CaptureFromCamera(args.camera!, args.width, args.height, args.maxWidth, format, jpegQuality); + var result = CaptureFromCamera(args.camera!, args.width, args.height, maxWidth, format, jpegQuality); outputPath = result.path; capturedWidth = result.width; capturedHeight = result.height; @@ -70,7 +72,7 @@ public string Handle(string command, string argumentsJson) else { string view = string.IsNullOrWhiteSpace(args.view) ? "game" : args.view!; - var result = CaptureView(view, args.width, args.height, args.maxWidth, format, jpegQuality); + var result = CaptureView(view, args.width, args.height, maxWidth, format, jpegQuality); outputPath = result.path; capturedWidth = result.width; capturedHeight = result.height; @@ -145,8 +147,7 @@ public string Handle(string command, string argumentsJson) throw new CommandFailureException("SCREENSHOT_FAILED", "Scene View 캡처를 위한 카메라가 없습니다.", false, null); } - int effectiveMaxWidth = ShouldApplyMaxWidth(requestedWidth, requestedHeight) ? maxWidth : 0; - var result = CaptureCameraToPath(camera, width, height, effectiveMaxWidth, format, jpegQuality, tempPath); + var result = CaptureCameraToPath(camera, width, height, maxWidth, format, jpegQuality, tempPath); return (tempPath, result.width, result.height); } @@ -181,8 +182,7 @@ public string Handle(string command, string argumentsJson) } string tempPath = CreateTempScreenshotPath(format); - int effectiveMaxWidth = ShouldApplyMaxWidth(requestedWidth, requestedHeight) ? maxWidth : 0; - var result = CaptureCameraToPath(camera, width, height, effectiveMaxWidth, format, jpegQuality, tempPath); + var result = CaptureCameraToPath(camera, width, height, maxWidth, format, jpegQuality, tempPath); return (tempPath, result.width, result.height); } @@ -265,7 +265,7 @@ private static (int width, int height, bool shouldResize) ResolvePlayModeGameVie return (capturedTexture.width, capturedTexture.height, false); } - if (ShouldApplyMaxWidth(requestedWidth, requestedHeight) && maxWidth > 0 && capturedTexture.width > maxWidth) + if (maxWidth > 0 && capturedTexture.width > maxWidth) { return (maxWidth, CalculateAspectFitHeight(capturedTexture.width, capturedTexture.height, maxWidth), true); } @@ -398,8 +398,7 @@ private static (string path, int width, int height) CaptureGameViewFromCamera( height = gameView.height; } - int effectiveMaxWidth = ShouldApplyMaxWidth(requestedWidth, requestedHeight) ? maxWidth : 0; - var result = CaptureCameraToPath(camera, width, height, effectiveMaxWidth, format, jpegQuality, path); + var result = CaptureCameraToPath(camera, width, height, maxWidth, format, jpegQuality, path); return (path, result.width, result.height); } @@ -500,36 +499,25 @@ private static int CalculateAspectFitHeight(int width, int height, int maxWidth) return Mathf.Max(1, Mathf.RoundToInt(height * maxWidth / (float)width)); } - private static bool ShouldApplyMaxWidth(int requestedWidth, int requestedHeight) + private static string NormalizeScreenshotFormat(string? format, string? outputPath) { - return requestedWidth <= 0 && requestedHeight <= 0; - } - - private static string NormalizeScreenshotFormat(string? format) - { - if (string.IsNullOrWhiteSpace(format)) + if (ScreenshotDefaults.TryResolveFormat(format, outputPath, out string resolved)) { - return FormatPng; + return resolved; } - return format!.Trim().ToLowerInvariant() switch - { - FormatPng => FormatPng, - FormatJpg => FormatJpg, - "jpeg" => FormatJpg, - _ => throw new CommandFailureException( - "SCREENSHOT_INVALID_FORMAT", - "screenshot format은 png, jpg, jpeg 중 하나여야 합니다.", - false, - null), - }; + throw new CommandFailureException( + "SCREENSHOT_INVALID_FORMAT", + "screenshot format은 png, jpg, jpeg 중 하나여야 합니다.", + false, + null); } private static int NormalizeJpegQuality(int quality) { if (quality == 0) { - return DefaultJpegQuality; + return ScreenshotDefaults.DefaultJpegQuality; } if (quality >= 1 && quality <= 100) @@ -546,8 +534,9 @@ private static int NormalizeJpegQuality(int quality) private static string CreateTempScreenshotPath(string format) { - string extension = string.Equals(format, FormatJpg, StringComparison.Ordinal) ? ".jpg" : ".png"; - return Path.Combine(Path.GetTempPath(), $"puc-screenshot-{Guid.NewGuid():N}{extension}"); + return Path.Combine( + Path.GetTempPath(), + $"puc-screenshot-{Guid.NewGuid():N}{ScreenshotDefaults.FileExtension(format)}"); } private static void WriteTextureToPath(Texture2D texture, string path, string format, int jpegQuality) diff --git a/unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/CliCommandCatalog.cs b/unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/CliCommandCatalog.cs index 0805292..4b4e69e 100644 --- a/unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/CliCommandCatalog.cs +++ b/unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/CliCommandCatalog.cs @@ -192,8 +192,8 @@ public static class CliCommandCatalog notes: new[] { "Use --list to inspect registered menu item paths before executing one." }), new CliCommandDescriptor( "screenshot", - "screenshot [--view game|scene (default: game) | --camera ] [--path ] [--width N] [--height N] [--format png|jpg|jpeg] [--quality 1-100] [--max-width N]", - "Captures a screenshot from the Game View, Scene View, or a named camera. Defaults to Game View; encoding defaults to PNG. Use --format jpg with --quality to reduce file size, and --max-width to downscale proportionally when --width/--height are not specified. The response includes image size, actual saved format, and screen-space metadata (`screenWidth`, `screenHeight`, `imageOrigin`, `coordinateOrigin`) for QA coordinate alignment. In Play Mode, --view game can downscale the native Game View capture but does not upscale it.", + "screenshot [--view game|scene (default: game) | --camera ] [--path ] [--width N] [--height N] [--format png|jpg|jpeg] [--quality 1-100] [--max-width N|0]", + "Captures a screenshot from the Game View, Scene View, or a named camera. Defaults to Game View, JPEG at quality 75, and a 1024px width cap — agent-friendly defaults that cut image tokens by roughly 72% at 1080p. Override with --format png for lossless, --quality for the JPEG setting, --max-width N for a different cap, or --max-width 0 for no cap. The cap only applies when neither --width nor --height is given, and --path with a .png extension selects PNG when --format is omitted. The response includes image size, actual saved format, and screen-space metadata (`screenWidth`, `screenHeight`, `imageOrigin`, `coordinateOrigin`) for QA coordinate alignment; qa tap/ui-dump scale from the last captured size, so downscaled shots keep their coordinates usable. In Play Mode, --view game can downscale the native Game View capture but does not upscale it.", CliCommandGroup.EditorControl, ProtocolConstants.CommandScreenshot, canUseLocal: false, diff --git a/unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/ProtocolConstants.cs b/unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/ProtocolConstants.cs index 15c9220..f50014b 100644 --- a/unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/ProtocolConstants.cs +++ b/unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/ProtocolConstants.cs @@ -10,7 +10,7 @@ namespace UnityCli.Protocol public static class ProtocolConstants { public const string AppName = "unity-cli"; - public const string ProtocolVersion = "7"; + public const string ProtocolVersion = "8"; public const int DefaultLiveTimeoutMs = 30_000; public const int DefaultTimeoutMs = DefaultLiveTimeoutMs; public const int DefaultExecuteTimeoutMs = 30_000; diff --git a/unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/ScreenshotDefaults.cs b/unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/ScreenshotDefaults.cs new file mode 100644 index 0000000..d6299c4 --- /dev/null +++ b/unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/ScreenshotDefaults.cs @@ -0,0 +1,129 @@ +using System; + +namespace UnityCli.Protocol +{ + /// + /// Encoding and downscale defaults for screenshot. + /// + /// A screenshot is the heaviest single response an agent can ask for: a 1920x1080 PNG bills at + /// roughly 2,000 image tokens, so a handful of captures costs more than every text response in + /// the session combined. The defaults here trade a lossless full-resolution image — which almost + /// no caller needs and no vision model can use at full detail anyway — for a JPEG capped at + /// , which is still wide enough to read UI text. + /// + /// Every knob stays overridable: --format png restores lossless, --width/ + /// --height take precedence over the cap, and --max-width 0 turns the cap off + /// while leaving the size auto-detected. + /// + /// Kept free of Unity types so both the CLI and the bridge resolve the defaults from one place, + /// and so the resolution is unit-testable. + /// + public static class ScreenshotDefaults + { + public const string FormatPng = "png"; + public const string FormatJpg = "jpg"; + + /// Visually lossless enough for UI verification at a fraction of the PNG size. + public const int DefaultJpegQuality = 75; + + /// + /// Wide enough to keep UI text legible, narrow enough that a 16:9 capture bills at roughly + /// 576 image tokens instead of 2,040. + /// + public const int DefaultMaxWidth = 1024; + + /// + /// Wire sentinel for "the caller explicitly asked for no downscale cap". The wire model + /// cannot use Nullable<int> (Unity's JsonUtility drops it), and 0 already + /// means "unspecified", so the opt-out has to be a distinct value. + /// + public const int MaxWidthUncapped = -1; + + /// + /// Resolves the encoding to write. An explicit format always wins; otherwise an explicit + /// output extension decides, because writing JPEG bytes into a file the caller named + /// .png is a worse outcome than the tokens the default saves. + /// + /// Value of --format; null or blank when unspecified. + /// Value of --path; null or blank when unspecified. + /// False when is set but not a known format. + public static bool TryResolveFormat(string requestedFormat, string outputPath, out string format) + { + if (!IsBlank(requestedFormat)) + { + return TryNormalizeFormat(requestedFormat, out format); + } + + format = HasExtension(outputPath, ".png") ? FormatPng : FormatJpg; + return true; + } + + /// Normalizes an explicitly requested format, folding jpeg into jpg. + public static bool TryNormalizeFormat(string requestedFormat, out string format) + { + string normalized = IsBlank(requestedFormat) + ? string.Empty + : requestedFormat.Trim().ToLowerInvariant(); + + switch (normalized) + { + case FormatPng: + format = FormatPng; + return true; + case FormatJpg: + case "jpeg": + format = FormatJpg; + return true; + default: + format = FormatPng; + return false; + } + } + + /// + /// Resolves the downscale cap to apply, where 0 means "do not downscale". + /// + /// An explicit --width/--height already states the size the caller wants, so + /// the cap stays out of its way — that gate predates the default and is why raising it does + /// not silently shrink sized captures. + /// + /// Wire maxWidth: 0 unspecified, negative uncapped. + public static int ResolveMaxWidth(int requestedMaxWidth, int requestedWidth, int requestedHeight) + { + if (requestedMaxWidth < 0) + { + return 0; + } + + if (requestedWidth > 0 || requestedHeight > 0) + { + return 0; + } + + return requestedMaxWidth > 0 ? requestedMaxWidth : DefaultMaxWidth; + } + + /// File extension matching the resolved format, dot included. + public static string FileExtension(string format) + { + return string.Equals(format, FormatJpg, StringComparison.Ordinal) ? ".jpg" : ".png"; + } + + private static bool HasExtension(string path, string extension) + { + if (IsBlank(path)) + { + return false; + } + + string trimmed = path.Trim(); + return trimmed.Length > extension.Length + && trimmed.EndsWith(extension, StringComparison.OrdinalIgnoreCase); + } + + private static bool IsBlank(string value) + { + return value == null || value.Trim().Length == 0; + } + } +} diff --git a/unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/ScreenshotDefaults.cs.meta b/unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/ScreenshotDefaults.cs.meta new file mode 100644 index 0000000..582d389 --- /dev/null +++ b/unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/ScreenshotDefaults.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 83a62c1513afe4c8fafee6472c727c62 \ No newline at end of file diff --git a/unity-package/com.yhc509.unity-cli-bridge/SkillTemplates~/SKILL.md b/unity-package/com.yhc509.unity-cli-bridge/SkillTemplates~/SKILL.md index 2e4954c..79635c3 100644 --- a/unity-package/com.yhc509.unity-cli-bridge/SkillTemplates~/SKILL.md +++ b/unity-package/com.yhc509.unity-cli-bridge/SkillTemplates~/SKILL.md @@ -66,7 +66,7 @@ unity-cli editor stop --project # graceful 종료 (미저장 변 - scene path는 `/Root[0]/Child[0]` 형식으로 쓰고 `/`는 virtual scene root로 본다. - root prefab 이름은 Unity 저장 규칙 때문에 파일 이름으로 정규화된다고 가정한다. - `screenshot`은 `--view` 생략 시 game이 기본이다. Scene View가 필요하면 `--view scene`을 명시한다. -- **에이전트가 읽을 스크린샷은 `--format jpg --quality 75 --max-width 1024`를 기본으로 붙인다.** 기본 PNG full-resolution은 이미지 토큰을 크게 소비한다(1080p 기준 ~72% 절약). lossless가 필요할 때만 `--format png`. +- **스크린샷은 옵션 없이 그대로 찍으면 된다.** 기본값이 이미 JPEG quality 75 + 1024px 가로 축소라 에이전트가 읽기 좋은 크기로 나온다(1080p PNG 대비 이미지 토큰 ~72% 절약). lossless 원본이 필요할 때만 `--format png --max-width 0`을 붙인다. `--path`가 `.png`로 끝나면 `--format` 없이도 PNG로 저장된다. - Play Mode 영상을 남겨야 하면 `record start --duration N --wait --path /tmp/out.mp4`를 쓴다. 수동 녹화는 `record start` 후 `record status`, `record stop` 순서로 종료한다. `record start`는 Play Mode 전용이고 `com.unity.recorder`가 설치된 프로젝트에서만 동작한다 — 미설치면 `RECORD_FAILED`와 함께 `unity-cli package add --name com.unity.recorder` 안내가 돌아오므로 그대로 설치한 뒤 재시도한다. - `qa tap --x --y`에는 `screenshot`에서 확인한 이미지 좌표를 그대로 넣는다. 응답의 `imageOrigin`은 `top-left`, `coordinateOrigin`은 `bottom-left`다. - 별도 Y-flip이나 해상도 스케일 변환은 하지 않는다. Bridge가 마지막 `screenshot` 크기 또는 명시한 `--screenshot-width`/`--screenshot-height`를 기준으로 내부 처리한다. diff --git a/unity-package/com.yhc509.unity-cli-bridge/SkillTemplates~/references/command-flows.md b/unity-package/com.yhc509.unity-cli-bridge/SkillTemplates~/references/command-flows.md index 0713e8b..b699e08 100644 --- a/unity-package/com.yhc509.unity-cli-bridge/SkillTemplates~/references/command-flows.md +++ b/unity-package/com.yhc509.unity-cli-bridge/SkillTemplates~/references/command-flows.md @@ -200,14 +200,14 @@ ucli material info --project "$PROJECT" --path Assets/Materials/MyMat.mat --omit ## 스크린샷 ```bash -# Game View 캡처 — 에이전트가 읽을 캡처는 jpg + max-width로 토큰 절약 (--view 생략 시 game이 기본) -ucli screenshot --project "$PROJECT" --path /tmp/capture.jpg --format jpg --quality 75 --max-width 1024 --output compact +# Game View 캡처 — 기본값이 이미 jpg q75 + 1024px 축소다 (--view 생략 시 game이 기본) +ucli screenshot --project "$PROJECT" --path /tmp/capture.jpg --output compact -# lossless가 필요할 때만 PNG -ucli screenshot --project "$PROJECT" --path /tmp/capture.png --output compact +# lossless 원본이 필요할 때만 +ucli screenshot --project "$PROJECT" --path /tmp/capture.png --format png --max-width 0 --output compact # Scene View 캡처 -ucli screenshot --project "$PROJECT" --path /tmp/scene.png --view scene --output compact +ucli screenshot --project "$PROJECT" --path /tmp/scene.jpg --view scene --output compact ``` ## 테스트 러너 diff --git a/unity-package/com.yhc509.unity-cli-bridge/SkillTemplates~/references/qa-testing.md b/unity-package/com.yhc509.unity-cli-bridge/SkillTemplates~/references/qa-testing.md index a975d65..ce3ad0d 100644 --- a/unity-package/com.yhc509.unity-cli-bridge/SkillTemplates~/references/qa-testing.md +++ b/unity-package/com.yhc509.unity-cli-bridge/SkillTemplates~/references/qa-testing.md @@ -12,7 +12,7 @@ status → play → (입력 시뮬레이션) → 검증 (로그 + 스크린샷) 2. `play`로 Play Mode 진입 (자동으로 `runInBackground = true` 설정됨) 3. QA 커맨드로 입력 시뮬레이션 4. **로그 검증**: `read-console --type error --limit N --no-stacktrace` + `read-console --type log --limit N --no-stacktrace` -5. **시각 검증**: `screenshot --view game --format jpg --quality 75 --max-width 1024 --path /tmp/qa-check.jpg` 후 이미지 확인 (lossless가 필요할 때만 `--format png`) +5. **시각 검증**: `screenshot --view game --path /tmp/qa-check.jpg` 후 이미지 확인 (기본이 jpg q75 + 1024px 축소, lossless 원본은 `--format png --max-width 0`) 6. `stop`으로 Play Mode 종료 (`runInBackground` 원래값 복원됨) ## 입력 방식 선택 @@ -135,10 +135,10 @@ ucli read-console --type error --limit 5 --no-stacktrace --project "$P" --json `screenshot --view game`으로 Game View를 캡처해서 시각적 변화를 확인한다. ```bash -# 토큰 절약: 에이전트가 읽을 스크린샷은 jpg + max-width를 기본으로 한다 -ucli screenshot --view game --format jpg --quality 75 --max-width 1024 --path /tmp/qa-check.jpg --project "$P" --json -# lossless가 필요할 때만 PNG -ucli screenshot --view game --path /tmp/qa-check.png --project "$P" --json +# 기본값이 이미 jpg q75 + 1024px 축소라 옵션 없이 그대로 찍으면 된다 +ucli screenshot --view game --path /tmp/qa-check.jpg --project "$P" --json +# lossless 원본이 필요할 때만 +ucli screenshot --view game --format png --max-width 0 --path /tmp/qa-check.png --project "$P" --json # 이후 이미지를 Read 도구로 확인 ```