diff --git a/.claude/skills/uloop-clear-console/SKILL.md b/.claude/skills/uloop-clear-console/SKILL.md new file mode 100644 index 00000000..5f9c1daf --- /dev/null +++ b/.claude/skills/uloop-clear-console/SKILL.md @@ -0,0 +1,48 @@ +--- +name: uloop-clear-console +description: "Clear Unity Console entries. Use before compile, tests, or debugging when stale logs would hide the current result." +--- + +# npx --yes uloop-cli@2.2.0 clear-console + +Clear Unity console logs. + +## Usage + +```bash +npx --yes uloop-cli@2.2.0 clear-console [--add-confirmation-message] +``` + +## Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--add-confirmation-message` | boolean | `false` | Add confirmation message after clearing | + +## Global Options + +| Option | Description | +|--------|-------------| +| `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | + +## Examples + +```bash +# Clear console +npx --yes uloop-cli@2.2.0 clear-console + +# Clear with confirmation +npx --yes uloop-cli@2.2.0 clear-console --add-confirmation-message +``` + +## Output + +Returns JSON with: +- `Success` (boolean): Whether the clear operation succeeded +- `ClearedLogCount` (number): Total number of log entries that were cleared +- `ClearedCounts` (object): Breakdown by log type + - `ErrorCount` (number): Errors cleared + - `WarningCount` (number): Warnings cleared + - `LogCount` (number): Info logs cleared +- `Message` (string): Description of the result; carries the failure summary when the operation fails (e.g. `"Failed to clear console: ..."`) +- `ErrorMessage` (string): Currently always empty for this tool — read `Message` for failure details diff --git a/.claude/skills/uloop-compile/SKILL.md b/.claude/skills/uloop-compile/SKILL.md new file mode 100644 index 00000000..21a028a9 --- /dev/null +++ b/.claude/skills/uloop-compile/SKILL.md @@ -0,0 +1,79 @@ +--- +name: uloop-compile +description: "Compile the Unity project and report errors/warnings. Use after C# edits or when a full Domain Reload compile is needed." +--- + +# npx --yes uloop-cli@2.2.0 compile + +Execute Unity project compilation. + +## Usage + +```bash +npx --yes uloop-cli@2.2.0 compile [--force-recompile ] [--wait-for-domain-reload ] +``` + +## Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--force-recompile` | boolean value | `false` | Force full recompilation (triggers Domain Reload). Rarely needed — see "When to use --force-recompile" below. Pass `true` or `false`; bare flags are not accepted. | +| `--wait-for-domain-reload` | boolean value | `false` | Wait until Domain Reload completes before returning. Pass `true` or `false`; bare flags are not accepted. | + +## When to use --force-recompile + +Almost never. Unity itself detects changed files — even when they were edited outside the +Editor, a plain `npx --yes uloop-cli@2.2.0 compile` runs every recompilation the changes require. A forced full +recompile can freeze the Editor for a long time on large projects, and with +`--wait-for-domain-reload true` the response crosses a Domain Reload so `Success` comes back +as `null`, making it useless as a verification step. The only legitimate use: surfacing +warnings hidden by other asmdefs with a full build. + +## Global Options + +| Option | Description | +|--------|-------------| +| `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | + +## Examples + +```bash +# Check compilation +npx --yes uloop-cli@2.2.0 compile + +# Force full recompilation +npx --yes uloop-cli@2.2.0 compile --force-recompile true + +# Force recompilation and wait for Domain Reload completion +npx --yes uloop-cli@2.2.0 compile --force-recompile true --wait-for-domain-reload true + +# Wait for Domain Reload completion even without force recompilation +npx --yes uloop-cli@2.2.0 compile --force-recompile false --wait-for-domain-reload true +``` + +## Output + +Returns JSON: +- `Success`: boolean +- `ErrorCount`: number +- `WarningCount`: number + +## Troubleshooting + +Diagnose the failure mode before retrying. + +**Stale lock files** (CLI hangs or shows "Unity is busy" while Unity Editor *is* running): + +```bash +npx --yes uloop-cli@2.2.0 fix +``` + +This removes any leftover lock files (`compiling.lock`, `domainreload.lock`, `serverstarting.lock`) from the Unity project's Temp directory. Then retry `npx --yes uloop-cli@2.2.0 compile`. + +**Unity Editor not running** (CLI returns a connection failure and no Unity process is alive): + +```bash +npx --yes uloop-cli@2.2.0 launch +``` + +`npx --yes uloop-cli@2.2.0 launch` auto-detects the project at the current working directory and opens it in the matching Unity Editor version. After Unity finishes launching, retry `npx --yes uloop-cli@2.2.0 compile`. diff --git a/.claude/skills/uloop-control-play-mode/SKILL.md b/.claude/skills/uloop-control-play-mode/SKILL.md new file mode 100644 index 00000000..d95f5a6b --- /dev/null +++ b/.claude/skills/uloop-control-play-mode/SKILL.md @@ -0,0 +1,55 @@ +--- +name: uloop-control-play-mode +description: "Control Unity Editor Play Mode. Use to start, stop, or pause Play Mode for runtime behavior checks and frame inspection." +--- + +# npx --yes uloop-cli@2.2.0 control-play-mode + +Control Unity Editor play mode (play/stop/pause). + +## Usage + +```bash +npx --yes uloop-cli@2.2.0 control-play-mode [options] +``` + +## Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--action` | string | `Play` | Action to perform: `Play`, `Stop`, `Pause` | + +## Global Options + +| Option | Description | +|--------|-------------| +| `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | + +## Examples + +```bash +# Start play mode +npx --yes uloop-cli@2.2.0 control-play-mode --action Play + +# Stop play mode +npx --yes uloop-cli@2.2.0 control-play-mode --action Stop + +# Pause play mode +npx --yes uloop-cli@2.2.0 control-play-mode --action Pause +``` + +## Output + +Returns JSON with the current play mode state: +- `IsPlaying`: Whether Unity is currently in play mode +- `IsPaused`: Whether play mode is paused +- `Message`: Description of the action performed + +## Notes + +- Play action starts the game in the Unity Editor (also resumes from pause) +- Stop action exits play mode and returns to edit mode +- Pause action pauses the game while remaining in play mode +- Useful for automated testing workflows + +- PlayMode entry may complete on the next editor frame. If a PlayMode-dependent command reports "PlayMode is not active" immediately after `--action Play`, wait briefly and retry. diff --git a/.claude/skills/uloop-execute-dynamic-code/SKILL.md b/.claude/skills/uloop-execute-dynamic-code/SKILL.md new file mode 100644 index 00000000..7d1c49e5 --- /dev/null +++ b/.claude/skills/uloop-execute-dynamic-code/SKILL.md @@ -0,0 +1,87 @@ +--- +name: uloop-execute-dynamic-code +description: "Execute C# with Unity APIs when existing uloop tools cannot inspect or edit enough. Use for scene, prefab, SerializedObject, AssetDatabase refresh/.meta generation, menu, or PlayMode automation." +context: fork +--- + +# Task + +Execute the following request using `npx --yes uloop-cli@2.2.0 execute-dynamic-code`: $ARGUMENTS + +For basic selected GameObject discovery or property inspection, use `find-game-objects --search-mode Selected` before this tool. Use this tool after the built-in inspection tools are not enough or when you need to modify Unity state. + +## Workflow + +1. Read the relevant reference file(s) from the Code Examples section below +2. Construct C# code based on the reference examples +3. For multiline snippets, write the C# statements to a temporary `.csx` file and execute `npx --yes uloop-cli@2.2.0 execute-dynamic-code --code-file ` +4. Use `npx --yes uloop-cli@2.2.0 execute-dynamic-code --code ''` only for short one-line snippets +5. If execution fails, adjust code and retry +6. Report the execution result + +## Parameters + +- `--code ''`: Inline C# statements to execute. Use this for one-line snippets only. +- `--code-file `: Read C# statements from a UTF-8 file. Prefer this for multiline snippets, especially on PowerShell, because Windows `.cmd` shims can lose lines from multiline inline arguments before `uloop` receives them. +- **Shell quoting**: bash/zsh uses single quotes, for example `npx --yes uloop-cli@2.2.0 execute-dynamic-code --code 'using UnityEngine; return Mathf.PI;'`. PowerShell single-quoted strings can contain normal double quotes, for example `npx --yes uloop-cli@2.2.0 execute-dynamic-code --code 'Debug.Log("Hello!");'`. +- `--parameters {}` (advanced, optional): Pass an object when reusing a snippet with varying data or when keeping values outside the code. Values are exposed as `parameters["param0"]`, `parameters["param1"]`, and so on. Omit this flag for most snippets, and pass an object instead of a JSON string. +- `--compile-only true` (optional): Compile the snippet without executing it. Use this when you want Roslyn diagnostics before running new code. + +## Code Rules + +Write direct statements only — no class/namespace/method wrappers. Return is optional. + +```csharp +using UnityEngine; +float x = Mathf.PI; +return x; +``` + +**Forbidden** — these will be rejected at compile time: `System.IO.*`, `AssetDatabase.CreateFolder`, creating/editing `.cs`/`.asmdef` files. Use terminal commands for file operations instead. + +## Output + +Returns JSON: +- `Success`: boolean — overall execution success +- `Result`: string — value of the snippet's `return` statement (empty when omitted) +- `Logs`: string[] — execution diagnostics from the dynamic-code runner, not Unity Console entries +- `CompilationErrors`: object[] — Roslyn diagnostics with `Message`, `Line`, `Column`, `ErrorCode`, optional `Hint` and `Suggestions` +- `ErrorMessage`: string — top-level failure summary (empty on success) +- `Error`: string — alias of `ErrorMessage` +- `SecurityLevel`: string — dynamic-code security level active for the request +- `UpdatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) +- `DiagnosticsSummary`: string|null — compact summary when diagnostics are available +- `Diagnostics`: object[] — structured diagnostics; same shape as `CompilationErrors`, usually populated together with it + +Use `npx --yes uloop-cli@2.2.0 get-logs` to retrieve `Debug.Log`, `Debug.LogWarning`, and `Debug.LogError` messages emitted by the snippet. On `Success: false`, inspect `CompilationErrors` first. If empty, read `ErrorMessage` (and `Logs` for extra context) — the failure may be a runtime exception, security violation, cancellation, or an "execution in progress" rejection, all of which return empty `CompilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. + +## Code Examples by Category + +For detailed code examples, refer to these files: + +- **Prefab operations**: See [references/prefab-operations.md](references/prefab-operations.md) + - Create prefabs, instantiate, add components, modify properties +- **Material operations**: See [references/material-operations.md](references/material-operations.md) + - Create materials, set shaders/textures, modify properties +- **Asset operations**: See [references/asset-operations.md](references/asset-operations.md) + - Find/search assets, duplicate, move, rename, load +- **ScriptableObject**: See [references/scriptableobject.md](references/scriptableobject.md) + - Create ScriptableObjects, modify with SerializedObject +- **Scene operations**: See [references/scene-operations.md](references/scene-operations.md) + - Create/modify GameObjects, set parents, wire references, load scenes +- **Batch operations**: See [references/batch-operations.md](references/batch-operations.md) + - Bulk modify objects, batch add/remove components, rename, layer/tag/material replacement +- **Cleanup operations**: See [references/cleanup-operations.md](references/cleanup-operations.md) + - Detect broken scripts, missing references, unused materials, empty GameObjects +- **Undo operations**: See [references/undo-operations.md](references/undo-operations.md) + - Undo-aware operations: RecordObject, AddComponent, SetParent, grouping +- **Selection operations**: See [references/selection-operations.md](references/selection-operations.md) + - Get/set selection, multi-select, filter by type/editability +- **PlayMode automation (zsh)**: See [references/playmode-automation-zsh.md](references/playmode-automation-zsh.md) + - Click UI buttons, invoke methods, set fields, tool combination workflows for zsh users +- **PlayMode automation (PowerShell)**: See [references/playmode-automation-powershell.md](references/playmode-automation-powershell.md) + - Click UI buttons, invoke methods, set fields, tool combination workflows for PowerShell users +- **PlayMode UI controls**: See [references/playmode-ui-controls.md](references/playmode-ui-controls.md) + - InputField, Slider, Toggle, Dropdown, drag & drop simulation, list all UI controls +- **PlayMode inspection**: See [references/playmode-inspection.md](references/playmode-inspection.md) + - Scene info, game state via reflection, physics state, raycast checks, GameObject search, position/rotation diff --git a/.claude/skills/uloop-execute-dynamic-code/references/asset-operations.md b/.claude/skills/uloop-execute-dynamic-code/references/asset-operations.md new file mode 100644 index 00000000..31ddf7ad --- /dev/null +++ b/.claude/skills/uloop-execute-dynamic-code/references/asset-operations.md @@ -0,0 +1,194 @@ +# Asset Operations + +Code examples for AssetDatabase operations using `execute-dynamic-code`. + +## Find Assets by Type + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab"); +List paths = new List(); + +foreach (string guid in prefabGuids) +{ + paths.Add(AssetDatabase.GUIDToAssetPath(guid)); +} +return $"Found {paths.Count} prefabs"; +``` + +## Find Assets by Name + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string searchName = "Player"; +string[] guids = AssetDatabase.FindAssets(searchName); +List paths = new List(); + +foreach (string guid in guids) +{ + paths.Add(AssetDatabase.GUIDToAssetPath(guid)); +} +return $"Found {paths.Count} assets matching '{searchName}'"; +``` + +## Find Assets in Folder + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string folder = "Assets/Prefabs"; +string[] guids = AssetDatabase.FindAssets("t:Prefab", new[] { folder }); +List paths = new List(); + +foreach (string guid in guids) +{ + paths.Add(AssetDatabase.GUIDToAssetPath(guid)); +} +return $"Found {paths.Count} prefabs in {folder}"; +``` + +## Duplicate Asset + +```csharp +using UnityEditor; + +string sourcePath = "Assets/Materials/MyMaterial.mat"; +string destPath = "Assets/Materials/MyMaterial_Backup.mat"; + +bool success = AssetDatabase.CopyAsset(sourcePath, destPath); +return success ? $"Copied to {destPath}" : "Copy failed"; +``` + +## Move Asset + +```csharp +using UnityEditor; + +string sourcePath = "Assets/OldFolder/MyAsset.asset"; +string destPath = "Assets/NewFolder/MyAsset.asset"; + +string error = AssetDatabase.MoveAsset(sourcePath, destPath); +return string.IsNullOrEmpty(error) ? $"Moved to {destPath}" : $"Error: {error}"; +``` + +## Rename Asset + +```csharp +using UnityEditor; + +string assetPath = "Assets/Materials/OldName.mat"; +string newName = "NewName"; + +string error = AssetDatabase.RenameAsset(assetPath, newName); +return string.IsNullOrEmpty(error) ? $"Renamed to {newName}" : $"Error: {error}"; +``` + +## Rename Asset (Undo-supported) + +```csharp +using UnityEditor; + +// ObjectNames.SetNameSmart() supports Undo (AssetDatabase.RenameAsset() does NOT) +Object selected = Selection.activeObject; +if (selected == null) +{ + return "No asset selected"; +} + +string oldName = selected.name; +ObjectNames.SetNameSmart(selected, "NewName"); +AssetDatabase.SaveAssets(); +return $"Renamed {oldName} to {selected.name}"; +``` + +## Get Asset Path from Object + +```csharp +using UnityEditor; + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No object selected"; +} + +string path = AssetDatabase.GetAssetPath(selected); +if (string.IsNullOrEmpty(path)) +{ + return "Selected object is not an asset (scene object)"; +} +return $"Asset path: {path}"; +``` + +## Load Asset at Path + +```csharp +using UnityEditor; + +string path = "Assets/Prefabs/Player.prefab"; +GameObject asset = AssetDatabase.LoadAssetAtPath(path); + +if (asset == null) +{ + return $"Asset not found at {path}"; +} +return $"Loaded: {asset.name}"; +``` + +## Get All Assets of Type + +```csharp +using UnityEditor; + +string[] scriptGuids = AssetDatabase.FindAssets("t:MonoScript"); +int count = 0; + +foreach (string guid in scriptGuids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + if (path.StartsWith("Assets/")) + { + count++; + } +} +return $"Found {count} scripts in Assets folder"; +``` + +## Check if Asset Exists + +```csharp +using UnityEditor; + +string path = "Assets/Prefabs/Player.prefab"; +string guid = AssetDatabase.AssetPathToGUID(path); + +bool exists = !string.IsNullOrEmpty(guid); +return exists ? $"Asset exists: {path}" : $"Asset not found: {path}"; +``` + +## Get Asset Dependencies + +```csharp +using UnityEditor; + +string assetPath = "Assets/Prefabs/Player.prefab"; +string[] dependencies = AssetDatabase.GetDependencies(assetPath, true); + +return $"Asset has {dependencies.Length} dependencies"; +``` + +## Refresh AssetDatabase + +Use this after terminal-based asset file changes when Unity needs to import them and generate `.meta` files. + +```csharp +using UnityEditor; + +AssetDatabase.Refresh(); +return "AssetDatabase refreshed"; +``` diff --git a/.claude/skills/uloop-execute-dynamic-code/references/batch-operations.md b/.claude/skills/uloop-execute-dynamic-code/references/batch-operations.md new file mode 100644 index 00000000..875c3fc6 --- /dev/null +++ b/.claude/skills/uloop-execute-dynamic-code/references/batch-operations.md @@ -0,0 +1,399 @@ +# Batch Operations + +Code examples for batch processing using `execute-dynamic-code`. + +## Batch Modify Selected Objects + +```csharp +using UnityEditor; +using System.Collections.Generic; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Modify"); + +foreach (GameObject obj in selected) +{ + Undo.RecordObject(obj.transform, ""); + obj.transform.localScale = Vector3.one * 2; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Scaled {selected.Length} objects (Single undo step)"; +``` + +## Edit Multiple Objects with SerializedObject + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +List transforms = new List(); +foreach (GameObject obj in selected) +{ + transforms.Add(obj.transform); +} + +SerializedObject serializedObj = new SerializedObject(transforms.ToArray()); +SerializedProperty positionProp = serializedObj.FindProperty("m_LocalPosition"); +positionProp.vector3Value = Vector3.zero; +serializedObj.ApplyModifiedProperties(); + +return $"Reset position of {selected.Length} objects"; +``` + +## Batch Add Component + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Add Rigidbody"); + +int addedCount = 0; +foreach (GameObject obj in selected) +{ + if (obj.GetComponent() == null) + { + Undo.AddComponent(obj); + addedCount++; + } +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Added Rigidbody to {addedCount} objects"; +``` + +## Batch Process Assets with StartAssetEditing + +```csharp +using UnityEditor; + +string[] guids = AssetDatabase.FindAssets("t:Material", new[] { "Assets/Materials" }); +if (guids.Length == 0) +{ + return "No materials found"; +} + +AssetDatabase.StartAssetEditing(); +try +{ + int modified = 0; + foreach (string guid in guids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + Material mat = AssetDatabase.LoadAssetAtPath(path); + if (mat != null) + { + mat.color = Color.white; + EditorUtility.SetDirty(mat); + modified++; + } + } + + AssetDatabase.SaveAssets(); + return $"Reset color of {modified} materials"; +} +finally +{ + AssetDatabase.StopAssetEditing(); +} + +``` + +## Batch Rename GameObjects + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Rename"); + +for (int i = 0; i < selected.Length; i++) +{ + Undo.RecordObject(selected[i], ""); + selected[i].name = $"Item_{i:D3}"; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Renamed {selected.Length} objects"; +``` + +## Batch Set Layer + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int layer = LayerMask.NameToLayer("Default"); + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Set Layer"); + +foreach (GameObject obj in selected) +{ + Undo.RecordObject(obj, ""); + obj.layer = layer; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Set layer of {selected.Length} objects to Default"; +``` + +## Batch Set Tag + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Set Tag"); + +foreach (GameObject obj in selected) +{ + Undo.RecordObject(obj, ""); + obj.tag = "Enemy"; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Tagged {selected.Length} objects as Enemy"; +``` + +## Batch Modify ScriptableObjects + +```csharp +using UnityEditor; + +string[] guids = AssetDatabase.FindAssets("t:ScriptableObject", new[] { "Assets/Data" }); +if (guids.Length == 0) +{ + return "No ScriptableObjects found"; +} + +int modified = 0; +foreach (string guid in guids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + ScriptableObject so = AssetDatabase.LoadAssetAtPath(path); + if (so == null) continue; + + SerializedObject serializedObj = new SerializedObject(so); + SerializedProperty prop = serializedObj.FindProperty("isEnabled"); + if (prop != null) + { + prop.boolValue = true; + serializedObj.ApplyModifiedProperties(); + EditorUtility.SetDirty(so); + modified++; + } +} + +AssetDatabase.SaveAssets(); +return $"Enabled {modified} ScriptableObjects"; +``` + +## Batch Remove Component + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Remove Rigidbody"); + +int removedCount = 0; +foreach (GameObject obj in selected) +{ + Rigidbody rb = obj.GetComponent(); + if (rb != null) + { + Undo.DestroyObjectImmediate(rb); + removedCount++; + } +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Removed Rigidbody from {removedCount} objects"; +``` + +## Batch Set Static Flags + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Set Static"); + +foreach (GameObject obj in selected) +{ + Undo.RecordObject(obj, ""); + GameObjectUtility.SetStaticEditorFlags(obj, StaticEditorFlags.BatchingStatic | StaticEditorFlags.OccludeeStatic); +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Set static flags on {selected.Length} objects"; +``` + +## Batch Process with Progress Bar + +```csharp +using UnityEditor; + +string[] guids = AssetDatabase.FindAssets("t:Texture2D"); +if (guids.Length == 0) +{ + return "No textures found"; +} + +int processed = 0; +foreach (string guid in guids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter; + if (importer != null && importer.maxTextureSize > 1024) + { + importer.maxTextureSize = 1024; + importer.SaveAndReimport(); + processed++; + } + + if (processed % 10 == 0) + { + EditorUtility.DisplayProgressBar("Processing Textures", path, (float)processed / guids.Length); + } +} + +EditorUtility.ClearProgressBar(); +return $"Resized {processed} textures to max 1024"; +``` + +## Batch Align Objects + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length < 2) +{ + return "Select at least 2 objects"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Align Objects"); + +float startX = selected[0].transform.position.x; +float spacing = 2f; + +for (int i = 0; i < selected.Length; i++) +{ + Undo.RecordObject(selected[i].transform, ""); + Vector3 pos = selected[i].transform.position; + pos.x = startX + (i * spacing); + selected[i].transform.position = pos; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Aligned {selected.Length} objects with {spacing}m spacing"; +``` + +## Batch Rename Assets (Undo-supported) + +```csharp +using UnityEditor; + +// ObjectNames.SetNameSmart() supports Undo (AssetDatabase.RenameAsset() does NOT) +Object[] selected = Selection.objects; +if (selected.Length == 0) +{ + return "No assets selected"; +} + +for (int i = 0; i < selected.Length; i++) +{ + string newName = $"{i:D3}_{selected[i].name}"; + ObjectNames.SetNameSmart(selected[i], newName); +} + +AssetDatabase.SaveAssets(); +return $"Renamed {selected.Length} assets"; +``` + +## Batch Replace Material + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +string materialPath = "Assets/Materials/NewMaterial.mat"; +Material newMat = AssetDatabase.LoadAssetAtPath(materialPath); +if (newMat == null) +{ + return $"Material not found at {materialPath}"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Replace Material"); + +int replaced = 0; +foreach (GameObject obj in selected) +{ + MeshRenderer renderer = obj.GetComponent(); + if (renderer != null) + { + Undo.RecordObject(renderer, ""); + renderer.sharedMaterial = newMat; + replaced++; + } +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Replaced material on {replaced} objects"; +``` diff --git a/.claude/skills/uloop-execute-dynamic-code/references/cleanup-operations.md b/.claude/skills/uloop-execute-dynamic-code/references/cleanup-operations.md new file mode 100644 index 00000000..40eec9a5 --- /dev/null +++ b/.claude/skills/uloop-execute-dynamic-code/references/cleanup-operations.md @@ -0,0 +1,403 @@ +# Cleanup Operations + +Code examples for project cleanup operations using `execute-dynamic-code`. + +## Detect Missing Scripts on GameObject + +```csharp +using UnityEditor; + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No GameObject selected"; +} + +int missingCount = GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(selected); +return $"{selected.name} has {missingCount} missing script(s)"; +``` + +## Remove Missing Scripts from GameObject + +```csharp +using UnityEditor; + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No GameObject selected"; +} + +int removedCount = GameObjectUtility.RemoveMonoBehavioursWithMissingScript(selected); +return $"Removed {removedCount} missing script(s) from {selected.name}"; +``` + +## Scan Scene for Missing Scripts + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List objectsWithMissing = new List(); + +foreach (GameObject obj in allObjects) +{ + int count = GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(obj); + if (count > 0) + { + objectsWithMissing.Add($"{obj.name} ({count})"); + } +} + +if (objectsWithMissing.Count == 0) +{ + return "No missing scripts found in scene"; +} + +return $"Objects with missing scripts: {string.Join(", ", objectsWithMissing)}"; +``` + +## Remove All Missing Scripts from Scene + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +int totalRemoved = 0; + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Remove All Missing Scripts"); + +foreach (GameObject obj in allObjects) +{ + int removed = GameObjectUtility.RemoveMonoBehavioursWithMissingScript(obj); + totalRemoved += removed; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Removed {totalRemoved} missing scripts from scene"; +``` + +## Detect Missing References in Component + +```csharp +using UnityEditor; + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No GameObject selected"; +} + +List missingRefs = new List(); + +Component[] components = selected.GetComponents(); +foreach (Component comp in components) +{ + if (comp == null) continue; + + SerializedObject so = new SerializedObject(comp); + SerializedProperty prop = so.GetIterator(); + + while (prop.NextVisible(true)) + { + if (prop.propertyType == SerializedPropertyType.ObjectReference) + { + if (prop.objectReferenceValue == null && prop.objectReferenceInstanceIDValue != 0) + { + missingRefs.Add($"{comp.GetType().Name}.{prop.name}"); + } + } + } +} + +if (missingRefs.Count == 0) +{ + return "No missing references found"; +} + +return $"Missing references: {string.Join(", ", missingRefs)}"; +``` + +## Scan Scene for Missing References + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List results = new List(); + +foreach (GameObject obj in allObjects) +{ + Component[] components = obj.GetComponents(); + foreach (Component comp in components) + { + if (comp == null) continue; + + SerializedObject so = new SerializedObject(comp); + SerializedProperty prop = so.GetIterator(); + + while (prop.NextVisible(true)) + { + if (prop.propertyType == SerializedPropertyType.ObjectReference) + { + if (prop.objectReferenceValue == null && prop.objectReferenceInstanceIDValue != 0) + { + results.Add($"{obj.name}/{comp.GetType().Name}.{prop.name}"); + } + } + } + } +} + +if (results.Count == 0) +{ + return "No missing references found in scene"; +} + +return $"Missing references ({results.Count}): {string.Join(", ", results.Take(10))}..."; +``` + +## Find Unused Materials in Project + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string[] materialGuids = AssetDatabase.FindAssets("t:Material"); +HashSet usedMaterials = new HashSet(); + +string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab"); +foreach (string guid in prefabGuids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + string[] deps = AssetDatabase.GetDependencies(path, true); + foreach (string dep in deps) + { + if (dep.EndsWith(".mat")) + { + usedMaterials.Add(dep); + } + } +} + +// This scan only checks prefab dependencies. Verify scene and other asset +// references manually before deleting any reported materials. +List unusedMaterials = new List(); +foreach (string guid in materialGuids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + if (!usedMaterials.Contains(path)) + { + unusedMaterials.Add(path); + } +} + +return $"Found {unusedMaterials.Count} materials not referenced by prefabs. Verify scene and other asset references manually before deleting."; +``` + +## Find Empty GameObjects + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List emptyObjects = new List(); + +foreach (GameObject obj in allObjects) +{ + Component[] components = obj.GetComponents(); + if (components.Length == 1 && obj.transform.childCount == 0) + { + emptyObjects.Add(obj.name); + } +} + +if (emptyObjects.Count == 0) +{ + return "No empty GameObjects found"; +} + +return $"Empty objects ({emptyObjects.Count}): {string.Join(", ", emptyObjects.Take(20))}"; +``` + +## Find Duplicate Names in Hierarchy + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +Dictionary nameCounts = new Dictionary(); + +foreach (GameObject obj in allObjects) +{ + if (nameCounts.ContainsKey(obj.name)) + { + nameCounts[obj.name]++; + } + else + { + nameCounts[obj.name] = 1; + } +} + +List duplicates = new List(); +foreach (KeyValuePair kvp in nameCounts) +{ + if (kvp.Value > 1) + { + duplicates.Add($"{kvp.Key} ({kvp.Value})"); + } +} + +if (duplicates.Count == 0) +{ + return "No duplicate names found"; +} + +return $"Duplicate names: {string.Join(", ", duplicates.Take(15))}"; +``` + +## Check for Broken Prefab Instances + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List brokenPrefabs = new List(); + +foreach (GameObject obj in allObjects) +{ + if (PrefabUtility.IsPartOfPrefabInstance(obj)) + { + GameObject prefabAsset = PrefabUtility.GetCorrespondingObjectFromSource(obj); + if (prefabAsset == null) + { + brokenPrefabs.Add(obj.name); + } + } +} + +if (brokenPrefabs.Count == 0) +{ + return "No broken prefab instances found"; +} + +return $"Broken prefab instances: {string.Join(", ", brokenPrefabs)}"; +``` + +## Find Objects with Negative Scale + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List negativeScale = new List(); + +foreach (GameObject obj in allObjects) +{ + Vector3 scale = obj.transform.localScale; + if (scale.x < 0 || scale.y < 0 || scale.z < 0) + { + negativeScale.Add($"{obj.name} ({scale})"); + } +} + +if (negativeScale.Count == 0) +{ + return "No objects with negative scale found"; +} + +return $"Negative scale objects: {string.Join(", ", negativeScale.Take(10))}"; +``` + +## Remove Empty Leaf GameObjects + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Remove Empty Leaves"); + +int removedCount = 0; +foreach (GameObject obj in allObjects) +{ + if (obj == null) continue; + + Component[] components = obj.GetComponents(); + if (components.Length == 1 && obj.transform.childCount == 0) + { + Undo.DestroyObjectImmediate(obj); + removedCount++; + } +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Removed {removedCount} empty leaf GameObjects"; +``` + +## Find Large Meshes + +```csharp +using UnityEditor; + +string[] meshGuids = AssetDatabase.FindAssets("t:Mesh"); +List largeMeshes = new List(); +int threshold = 10000; + +foreach (string guid in meshGuids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + Mesh mesh = AssetDatabase.LoadAssetAtPath(path); + if (mesh != null && mesh.vertexCount > threshold) + { + largeMeshes.Add($"{path} ({mesh.vertexCount} verts)"); + } +} + +if (largeMeshes.Count == 0) +{ + return $"No meshes with more than {threshold} vertices found"; +} + +return $"Large meshes: {string.Join(", ", largeMeshes.Take(10))}"; +``` + +## Validate Asset References + +```csharp +using UnityEditor; + +string[] guids = AssetDatabase.FindAssets("t:ScriptableObject", new[] { "Assets/Data" }); +List invalidRefs = new List(); + +foreach (string guid in guids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + ScriptableObject so = AssetDatabase.LoadAssetAtPath(path); + if (so == null) continue; + + SerializedObject serializedObj = new SerializedObject(so); + SerializedProperty prop = serializedObj.GetIterator(); + + while (prop.NextVisible(true)) + { + if (prop.propertyType == SerializedPropertyType.ObjectReference) + { + if (prop.objectReferenceValue == null && prop.objectReferenceInstanceIDValue != 0) + { + invalidRefs.Add($"{path}: {prop.name}"); + } + } + } +} + +if (invalidRefs.Count == 0) +{ + return "All asset references are valid"; +} + +return $"Invalid references ({invalidRefs.Count}): {string.Join(", ", invalidRefs.Take(10))}"; +``` diff --git a/.claude/skills/uloop-execute-dynamic-code/references/material-operations.md b/.claude/skills/uloop-execute-dynamic-code/references/material-operations.md new file mode 100644 index 00000000..3bd98697 --- /dev/null +++ b/.claude/skills/uloop-execute-dynamic-code/references/material-operations.md @@ -0,0 +1,160 @@ +# Material Operations + +Code examples for Material operations using `execute-dynamic-code`. + +## Create a New Material + +```csharp +using UnityEditor; + +Shader shader = Shader.Find("Standard"); +Material mat = new Material(shader); +mat.name = "MyMaterial"; +string path = "Assets/Materials/MyMaterial.mat"; +AssetDatabase.CreateAsset(mat, path); +AssetDatabase.SaveAssets(); +return $"Material created at {path}"; +``` + +## Set Material Color + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +Material mat = AssetDatabase.LoadAssetAtPath(matPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} + +mat.SetColor("_Color", new Color(1f, 0.5f, 0f, 1f)); +EditorUtility.SetDirty(mat); +AssetDatabase.SaveAssets(); +return "Material color set to orange"; +``` + +## Set Material Properties (Float, Vector) + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +Material mat = AssetDatabase.LoadAssetAtPath(matPath); + +mat.SetFloat("_Metallic", 0.8f); +mat.SetFloat("_Glossiness", 0.6f); +mat.SetVector("_EmissionColor", new Vector4(1, 1, 0, 1)); + +EditorUtility.SetDirty(mat); +AssetDatabase.SaveAssets(); +return "Material properties updated"; +``` + +## Assign Texture to Material + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +string texPath = "Assets/Textures/MyTexture.png"; + +Material mat = AssetDatabase.LoadAssetAtPath(matPath); +Texture2D tex = AssetDatabase.LoadAssetAtPath(texPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} +if (tex == null) +{ + return $"Texture not found at {texPath}"; +} + +mat.SetTexture("_MainTex", tex); +EditorUtility.SetDirty(mat); +AssetDatabase.SaveAssets(); +return $"Assigned {tex.name} to material"; +``` + +## Assign Material to GameObject + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +Material mat = AssetDatabase.LoadAssetAtPath(matPath); + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No GameObject selected"; +} + +Renderer renderer = selected.GetComponent(); +if (renderer == null) +{ + return "Selected object has no Renderer"; +} + +renderer.sharedMaterial = mat; +EditorUtility.SetDirty(selected); +return $"Assigned {mat.name} to {selected.name}"; +``` + +## Enable/Disable Material Keywords + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +Material mat = AssetDatabase.LoadAssetAtPath(matPath); + +mat.EnableKeyword("_EMISSION"); +mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive; + +EditorUtility.SetDirty(mat); +AssetDatabase.SaveAssets(); +return "Emission enabled on material"; +``` + +## Find All Materials Using a Shader + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string shaderName = "Standard"; +string[] guids = AssetDatabase.FindAssets("t:Material"); +List matchingMaterials = new List(); + +foreach (string guid in guids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + Material mat = AssetDatabase.LoadAssetAtPath(path); + if (mat != null && mat.shader != null && mat.shader.name == shaderName) + { + matchingMaterials.Add(path); + } +} +return $"Found {matchingMaterials.Count} materials using {shaderName}"; +``` + +## Duplicate Material + +```csharp +using UnityEditor; + +string sourcePath = "Assets/Materials/MyMaterial.mat"; +string destPath = "Assets/Materials/MyMaterial_Copy.mat"; + +Material source = AssetDatabase.LoadAssetAtPath(sourcePath); +if (source == null) +{ + return $"Material not found at {sourcePath}"; +} + +Material copy = new Material(source); +AssetDatabase.CreateAsset(copy, destPath); +AssetDatabase.SaveAssets(); +return $"Material duplicated to {destPath}"; +``` diff --git a/.claude/skills/uloop-execute-dynamic-code/references/playmode-automation-powershell.md b/.claude/skills/uloop-execute-dynamic-code/references/playmode-automation-powershell.md new file mode 100644 index 00000000..0956005f --- /dev/null +++ b/.claude/skills/uloop-execute-dynamic-code/references/playmode-automation-powershell.md @@ -0,0 +1,330 @@ +# PlayMode Automation (PowerShell) + +Code examples for runtime automation during Play mode using `execute-dynamic-code`. +These examples manipulate live scene objects while the game is running. +Shell command examples in this file target `PowerShell`. + +## When to use dedicated mouse simulation tools instead + +The examples in this file call UI handlers or runtime methods from C#. +That remains the better choice when you want targeted automation, direct state control, or a quick diagnostic path. +Use dedicated mouse tools only when the input route itself is part of what you need to verify. + +Choose the tool based on what you are trying to validate: + +| Scenario | Recommended tool | Why | +|----------|------------------|-----| +| Verify that a uGUI element responds through the real EventSystem pointer path | `simulate-mouse-ui` | Fires `PointerDown` / `PointerUp` / `PointerClick` / drag events through EventSystem raycasts instead of bypassing the UI input route. | +| Test gameplay that reads `Mouse.current`, button state, delta, or scroll | `simulate-mouse-input` | Injects Input System mouse state into `Mouse.current`, so game code can observe `wasPressedThisFrame`, movement delta, and scroll like player input. This assumes the project uses the New Input System (`Input System Package (New)` or `Both`). If that is not available in the target project, prefer `execute-dynamic-code` for a project-specific workaround instead of changing project settings just to use this tool. | +| Jump straight to a known button callback, invoke a method, inspect state, or set up a test precondition | `execute-dynamic-code` | Best when you intentionally want direct automation without reproducing the full input pipeline. | +| Drive custom runtime behavior that does not map cleanly to the built-in mouse tools | `execute-dynamic-code` | Lets you call project-specific methods, inspect scene objects, and prototype one-off flows immediately. | + +In short: do not default everything to mouse simulation. +Use `execute-dynamic-code` for direct automation and diagnostics, and switch to `simulate-mouse-ui` or `simulate-mouse-input` when reproducing the real input path is the thing you need to test. + +## PowerShell Quoting Notes + +Use these patterns when you need shell-safe inline code: + +### Double quotes inside C# strings + +Single-quote the whole snippet and keep C# string literals unchanged. + +```powershell +npx --yes uloop-cli@2.2.0 execute-dynamic-code --code 'return "Hello from PowerShell";' +``` + +### Single quotes inside inline C# code + +If the C# snippet itself contains a single quote, double it inside the PowerShell single-quoted string. + +```powershell +npx --yes uloop-cli@2.2.0 execute-dynamic-code --code 'char initial = ''A''; return initial.ToString();' +``` + +### JSON-like values passed via `--parameters` + +Wrap the whole expression in single quotes so PowerShell passes the inner double quotes through unchanged. + +```powershell +npx --yes uloop-cli@2.2.0 execute-dynamic-code --code 'return parameters["param0"];' --parameters '{"param0":"Hello from PowerShell"}' +``` + +### Multi-line C# snippets + +Use a here-string when the snippet spans multiple lines. + +```powershell +$code = @' +using UnityEngine; + +GameObject obj = GameObject.Find("Player"); +if (obj == null) return "Player not found"; + +return obj.name; +'@ + +npx --yes uloop-cli@2.2.0 execute-dynamic-code --code $code +``` + +You can combine multi-line code with multi-line parameters the same way. + +```powershell +$code = @' +return parameters["param0"]; +'@ + +$parameters = @' +{"param0":"Hello from PowerShell"} +'@ + +npx --yes uloop-cli@2.2.0 execute-dynamic-code --code $code --parameters $parameters +``` + +## Click UI Button by Path + +```csharp +using UnityEngine.UI; + +Button btn = GameObject.Find("Canvas/StartButton")?.GetComponent