Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .agents/skills/uloop-screenshot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ uloop screenshot [--window-name <name>] [--resolution-scale <scale>] [--match-mo

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `--window-name` | string | `Game` | Window name to capture (for example `Game`, `Scene`, `Console`, `Inspector`). Ignored when `--capture-mode rendering`. When the Game tab is Device Simulator and the title is Simulator, default Game falls back to Simulator. |
| `--window-name` | string | `Game` | Window name to capture (for example `Game`, `Scene`, `Console`, `Inspector`). Ignored when the resolved capture mode is rendering (auto during Play Mode resolves to rendering). When the Game tab is Device Simulator and the title is Simulator, default Game falls back to Simulator. |
| `--resolution-scale` | number | `1.0` | Resolution scale (0.1 to 1.0) |
| `--match-mode` | enum | `exact` | Window name matching mode: `exact`, `prefix`, or `contains`. Ignored when `--capture-mode rendering`. |
| `--capture-mode` | enum | `window` | `window` - capture EditorWindow including toolbar, `rendering` - capture game rendering only (PlayMode required), `GameView` - alias for `rendering`. Rendering screenshots return `ScreenshotToInputFormula` for converting raw image pixels before calling simulate-mouse-input (including `--dry-run`) or simulate-mouse-ui. |
| `--match-mode` | enum | `exact` | Window name matching mode: `exact`, `prefix`, or `contains`. Ignored when the resolved capture mode is rendering (auto during Play Mode resolves to rendering). |
| `--capture-mode` | enum | `auto` | `auto` - rendering in PlayMode and window otherwise, `window` - capture EditorWindow including toolbar, `rendering` - capture game rendering only (PlayMode required), `GameView` - alias for `rendering`. Rendering screenshots return `ScreenshotToInputFormula` for converting raw image pixels before calling simulate-mouse-input (including `--dry-run`) or simulate-mouse-ui. |
| `--output-directory` | string | `""` | Output directory path for saving screenshots. When empty, uses default path (.uloop/outputs/Screenshots/). Accepts absolute paths. |
| `--annotate-elements` | flag | - | Annotate interactive UI elements with index labels and interaction hints (A / CLICK, B / DRAG, ...). The response includes an `AnnotatedElements` array with element metadata sorted by z-order. Only works with `--capture-mode rendering` in PlayMode. |
| `--annotate-raycast-grid` | flag | - | Annotate clustered 3D physics collider candidates as `PhysicsCollider` entries in `AnnotatedElements`. Uses `Camera.main` visibility and the same top-left Game View coordinates as `simulate-mouse-input`. Only works with `--capture-mode rendering` in PlayMode. |
| `--annotate-elements` | flag | - | Annotate interactive UI elements with index labels and interaction hints (A / CLICK, B / DRAG, ...). The response includes an `AnnotatedElements` array with element metadata sorted by z-order. Only works when the resolved capture mode is rendering (auto during Play Mode resolves to rendering) in PlayMode. |
| `--annotate-raycast-grid` | flag | - | Annotate clustered 3D physics collider candidates as `PhysicsCollider` entries in `AnnotatedElements`. Uses `Camera.main` visibility and the same top-left Game View coordinates as `simulate-mouse-input`. Only works when the resolved capture mode is rendering (auto during Play Mode resolves to rendering) in PlayMode. |
| `--raycast-layer-mask` | string | `""` | Comma-separated physics layer names to narrow which layers `--annotate-raycast-grid` clusters. Hits are limited to layers also visible to `Camera.main.cullingMask`. When omitted, clusters against `Physics.DefaultRaycastLayers`. |
| `--elements-only` | flag | - | Return only annotated element JSON without capturing a screenshot image. Requires `--annotate-elements` or `--annotate-raycast-grid`, and `--capture-mode rendering` in PlayMode. |
| `--elements-only` | flag | - | Return only annotated element JSON without capturing a screenshot image. Requires `--annotate-elements` or `--annotate-raycast-grid`, and the resolved capture mode to be rendering (auto during Play Mode resolves to rendering) in PlayMode. |

## Match Modes

Expand All @@ -51,6 +51,7 @@ The window name is the text displayed in the window's title bar (tab). Common na
Returns JSON with:

- `ScreenshotCount`: Number of windows captured
- `ResolvedCaptureMode`: `"window"` or `"rendering"` — the mode actually used after resolving `auto`
- `Warning`: Appears only for window captures taken while Play Mode is running with at least one image.
- `Screenshots`: Array of screenshot info, each containing:
- `ImagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. Always open the file named here — the output directory accumulates every past capture, so guessing the newest file with directory listing (`ls -t` or similar) can silently pick a stale screenshot from an earlier run.
Expand Down
13 changes: 7 additions & 6 deletions .claude/skills/uloop-screenshot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ uloop screenshot [--window-name <name>] [--resolution-scale <scale>] [--match-mo

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `--window-name` | string | `Game` | Window name to capture (for example `Game`, `Scene`, `Console`, `Inspector`). Ignored when `--capture-mode rendering`. When the Game tab is Device Simulator and the title is Simulator, default Game falls back to Simulator. |
| `--window-name` | string | `Game` | Window name to capture (for example `Game`, `Scene`, `Console`, `Inspector`). Ignored when the resolved capture mode is rendering (auto during Play Mode resolves to rendering). When the Game tab is Device Simulator and the title is Simulator, default Game falls back to Simulator. |
| `--resolution-scale` | number | `1.0` | Resolution scale (0.1 to 1.0) |
| `--match-mode` | enum | `exact` | Window name matching mode: `exact`, `prefix`, or `contains`. Ignored when `--capture-mode rendering`. |
| `--capture-mode` | enum | `window` | `window` - capture EditorWindow including toolbar, `rendering` - capture game rendering only (PlayMode required), `GameView` - alias for `rendering`. Rendering screenshots return `ScreenshotToInputFormula` for converting raw image pixels before calling simulate-mouse-input (including `--dry-run`) or simulate-mouse-ui. |
| `--match-mode` | enum | `exact` | Window name matching mode: `exact`, `prefix`, or `contains`. Ignored when the resolved capture mode is rendering (auto during Play Mode resolves to rendering). |
| `--capture-mode` | enum | `auto` | `auto` - rendering in PlayMode and window otherwise, `window` - capture EditorWindow including toolbar, `rendering` - capture game rendering only (PlayMode required), `GameView` - alias for `rendering`. Rendering screenshots return `ScreenshotToInputFormula` for converting raw image pixels before calling simulate-mouse-input (including `--dry-run`) or simulate-mouse-ui. |
| `--output-directory` | string | `""` | Output directory path for saving screenshots. When empty, uses default path (.uloop/outputs/Screenshots/). Accepts absolute paths. |
| `--annotate-elements` | flag | - | Annotate interactive UI elements with index labels and interaction hints (A / CLICK, B / DRAG, ...). The response includes an `AnnotatedElements` array with element metadata sorted by z-order. Only works with `--capture-mode rendering` in PlayMode. |
| `--annotate-raycast-grid` | flag | - | Annotate clustered 3D physics collider candidates as `PhysicsCollider` entries in `AnnotatedElements`. Uses `Camera.main` visibility and the same top-left Game View coordinates as `simulate-mouse-input`. Only works with `--capture-mode rendering` in PlayMode. |
| `--annotate-elements` | flag | - | Annotate interactive UI elements with index labels and interaction hints (A / CLICK, B / DRAG, ...). The response includes an `AnnotatedElements` array with element metadata sorted by z-order. Only works when the resolved capture mode is rendering (auto during Play Mode resolves to rendering) in PlayMode. |
| `--annotate-raycast-grid` | flag | - | Annotate clustered 3D physics collider candidates as `PhysicsCollider` entries in `AnnotatedElements`. Uses `Camera.main` visibility and the same top-left Game View coordinates as `simulate-mouse-input`. Only works when the resolved capture mode is rendering (auto during Play Mode resolves to rendering) in PlayMode. |
| `--raycast-layer-mask` | string | `""` | Comma-separated physics layer names to narrow which layers `--annotate-raycast-grid` clusters. Hits are limited to layers also visible to `Camera.main.cullingMask`. When omitted, clusters against `Physics.DefaultRaycastLayers`. |
| `--elements-only` | flag | - | Return only annotated element JSON without capturing a screenshot image. Requires `--annotate-elements` or `--annotate-raycast-grid`, and `--capture-mode rendering` in PlayMode. |
| `--elements-only` | flag | - | Return only annotated element JSON without capturing a screenshot image. Requires `--annotate-elements` or `--annotate-raycast-grid`, and the resolved capture mode to be rendering (auto during Play Mode resolves to rendering) in PlayMode. |

## Match Modes

Expand All @@ -51,6 +51,7 @@ The window name is the text displayed in the window's title bar (tab). Common na
Returns JSON with:

- `ScreenshotCount`: Number of windows captured
- `ResolvedCaptureMode`: `"window"` or `"rendering"` — the mode actually used after resolving `auto`
- `Warning`: Appears only for window captures taken while Play Mode is running with at least one image.
- `Screenshots`: Array of screenshot info, each containing:
- `ImagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. Always open the file named here — the output directory accumulates every past capture, so guessing the newest file with directory listing (`ls -t` or similar) can silently pick a stale screenshot from an earlier run.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ public void FirstPartySchemaEnumProperties_WhenLoaded_ShouldBeZeroBasedAndContig
// Tests that every enum a first-party schema exposes can be resolved by its ordinal.
// The schema cache stores an enum default as a number while listing the members by name,
// so the CLI recovers the name shown in `--help` / `uloop list` by indexing that name
// list (Go enumValueAtIndex). Same-value aliases are allowed only after the canonical
// name: names[ordinal] must be the first declaration of that value (MetadataToken
// order). Gaps, negative values, and a [Flags] enum would make the CLI print the wrong
// default member name.
// list (Go enumValueAtIndex). Enum.GetNames sorts by value, so a same-value alias
// occupies the next index and displaces later canonical names. Members must therefore
// have distinct values. Gaps, negative values, and a [Flags] enum would make the CLI
// print the wrong default member name.
Type[] schemaTypes = FirstPartySchemaTypes();

Assert.That(schemaTypes, Is.Not.Empty);
Expand Down Expand Up @@ -80,8 +80,8 @@ public void FirstPartySchemaEnumProperties_WhenLoaded_ShouldBeZeroBasedAndContig
$"{location} is a [Flags] enum, which cannot be resolved by ordinal");

// Why MetadataToken order: GetFields does not guarantee declaration order, and
// the CLI indexes Enum.GetNames by the numeric default. The first same-value
// member in declaration order is the canonical name that must occupy that index.
// the CLI indexes Enum.GetNames by the numeric default. Distinct values keep
// names[ordinal] equal to the only member declared at that value.
FieldInfo[] memberFields = propertyType.GetFields(
BindingFlags.Public | BindingFlags.Static)
.OrderBy(field => field.MetadataToken)
Expand All @@ -98,18 +98,23 @@ public void FirstPartySchemaEnumProperties_WhenLoaded_ShouldBeZeroBasedAndContig
value,
Is.GreaterThanOrEqualTo(0),
$"{location} has a negative member value {value}");
distinctValues.Add(value);
if (!canonicalNamesByValue.ContainsKey(value))
{
canonicalNamesByValue[value] = memberField.Name;
}
bool added = distinctValues.Add(value);
Assert.That(
added,
Is.True,
$"{location} reuses value {value} for '{memberField.Name}'; same-value aliases displace later names in Enum.GetNames");
canonicalNamesByValue[value] = memberField.Name;

if (value > maxValue)
{
maxValue = value;
}
}

Assert.That(
distinctValues.Count,
Is.EqualTo(memberFields.Length),
$"{location} has same-value aliases");
Assert.That(
distinctValues.Count,
Is.EqualTo(maxValue + 1),
Expand Down Expand Up @@ -140,9 +145,17 @@ public void FirstPartySchemaEnumProperties_WhenLoaded_ShouldBeZeroBasedAndContig
private static Type[] FirstPartySchemaTypes()
{
return TypeCache.GetTypesDerivedFrom<UnityCliLoopToolSchema>()
.Where(type => type.Assembly.GetName().Name.StartsWith(
"UnityCLILoop.FirstPartyTools",
StringComparison.Ordinal))
.Where(type =>
{
string assemblyName = type.Assembly.GetName().Name;
return assemblyName.StartsWith(
"UnityCLILoop.FirstPartyTools",
StringComparison.Ordinal)
|| string.Equals(
assemblyName,
"UnityCLILoop.ToolContracts",
StringComparison.Ordinal);
})
.ToArray();
}

Expand Down
109 changes: 109 additions & 0 deletions Assets/Tests/Editor/ScreenshotCaptureModeResolverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;
using io.github.hatayama.UnityCliLoop.ToolContracts;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Verifies CaptureMode.auto resolution and wire names for explicit and omitted modes.
/// </summary>
public sealed class ScreenshotCaptureModeResolverTests
{
/// <summary>
/// What: omitted auto in Play Mode resolves to rendering.
/// </summary>
[Test]
public void Resolve_WhenAutoAndPlaying_ReturnsRendering()
{
CaptureMode resolved = ScreenshotCaptureModeResolver.Resolve(CaptureMode.auto, true);

Assert.That(resolved, Is.EqualTo(CaptureMode.rendering));
Assert.That(ScreenshotCaptureModeResolver.ToWireName(resolved), Is.EqualTo("rendering"));
}

/// <summary>
/// What: omitted auto in Edit Mode resolves to window.
/// </summary>
[Test]
public void Resolve_WhenAutoAndNotPlaying_ReturnsWindow()
{
CaptureMode resolved = ScreenshotCaptureModeResolver.Resolve(CaptureMode.auto, false);

Assert.That(resolved, Is.EqualTo(CaptureMode.window));
Assert.That(ScreenshotCaptureModeResolver.ToWireName(resolved), Is.EqualTo("window"));
}

/// <summary>
/// What: explicit window stays window while Play Mode is running.
/// </summary>
[Test]
public void Resolve_WhenWindowAndPlaying_ReturnsWindow()
{
CaptureMode resolved = ScreenshotCaptureModeResolver.Resolve(CaptureMode.window, true);

Assert.That(resolved, Is.EqualTo(CaptureMode.window));
Assert.That(ScreenshotCaptureModeResolver.ToWireName(resolved), Is.EqualTo("window"));
}

/// <summary>
/// What: explicit window stays window while Play Mode is stopped.
/// </summary>
[Test]
public void Resolve_WhenWindowAndNotPlaying_ReturnsWindow()
{
CaptureMode resolved = ScreenshotCaptureModeResolver.Resolve(CaptureMode.window, false);

Assert.That(resolved, Is.EqualTo(CaptureMode.window));
Assert.That(ScreenshotCaptureModeResolver.ToWireName(resolved), Is.EqualTo("window"));
}

/// <summary>
/// What: explicit rendering stays rendering while Play Mode is running.
/// </summary>
[Test]
public void Resolve_WhenRenderingAndPlaying_ReturnsRendering()
{
CaptureMode resolved = ScreenshotCaptureModeResolver.Resolve(CaptureMode.rendering, true);

Assert.That(resolved, Is.EqualTo(CaptureMode.rendering));
Assert.That(ScreenshotCaptureModeResolver.ToWireName(resolved), Is.EqualTo("rendering"));
}

/// <summary>
/// What: explicit rendering stays rendering while Play Mode is stopped.
/// </summary>
[Test]
public void Resolve_WhenRenderingAndNotPlaying_ReturnsRendering()
{
CaptureMode resolved = ScreenshotCaptureModeResolver.Resolve(CaptureMode.rendering, false);

Assert.That(resolved, Is.EqualTo(CaptureMode.rendering));
Assert.That(ScreenshotCaptureModeResolver.ToWireName(resolved), Is.EqualTo("rendering"));
}

/// <summary>
/// What: explicit GameView is a rendering request even while Play Mode is running.
/// </summary>
[Test]
public void Resolve_WhenGameViewAndPlaying_ReturnsRendering()
{
CaptureMode resolved = ScreenshotCaptureModeResolver.Resolve(CaptureMode.GameView, true);

Assert.That(resolved, Is.EqualTo(CaptureMode.rendering));
Assert.That(ScreenshotCaptureModeResolver.ToWireName(resolved), Is.EqualTo("rendering"));
}

/// <summary>
/// What: explicit GameView is a rendering request even while Play Mode is stopped.
/// </summary>
[Test]
public void Resolve_WhenGameViewAndNotPlaying_ReturnsRendering()
{
CaptureMode resolved = ScreenshotCaptureModeResolver.Resolve(CaptureMode.GameView, false);

Assert.That(resolved, Is.EqualTo(CaptureMode.rendering));
Assert.That(ScreenshotCaptureModeResolver.ToWireName(resolved), Is.EqualTo("rendering"));
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/ScreenshotCaptureModeResolverTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions Assets/Tests/Editor/ScreenshotResponseWarningContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,25 @@ public void ScreenshotResponse_WhenWarningIsSet_SerializesExactChromeWarningSent
Is.EqualTo(
"This window capture includes Unity Editor chrome. If you wanted the Game View image (typical during Play Mode), re-run with --capture-mode rendering."));
}

/// <summary>
/// What: ResolvedCaptureMode is always present on the wire as window or rendering.
/// </summary>
[Test]
public void ScreenshotResponse_WhenResolvedCaptureModeIsSet_SerializesExactWireName()
{
ScreenshotResponse response = new ScreenshotResponse
{
ResolvedCaptureMode = "rendering"
};

string json = JsonConvert.SerializeObject(
response,
Formatting.None,
JsonRpcResponseSerializer.Settings);
JObject parsed = JObject.Parse(json);

Assert.That(parsed.Value<string>("ResolvedCaptureMode"), Is.EqualTo("rendering"));
}
}
}
Loading
Loading