chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1511443bffd3044d8a38e727e688c81f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Response schema for ClearConsole command
|
||||
/// Provides type-safe response structure for console clearing operation
|
||||
/// </summary>
|
||||
public class ClearConsoleResponse : BaseToolResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the console clear operation was successful
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of logs that were cleared from the console
|
||||
/// </summary>
|
||||
public int ClearedLogCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Breakdown of cleared logs by type
|
||||
/// </summary>
|
||||
public ClearedLogCounts ClearedCounts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Message describing the clear operation result
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if the operation failed
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new ClearConsoleResponse for successful operation
|
||||
/// </summary>
|
||||
public ClearConsoleResponse(bool success, int clearedLogCount, ClearedLogCounts clearedCounts, string message)
|
||||
{
|
||||
Success = success;
|
||||
ClearedLogCount = clearedLogCount;
|
||||
ClearedCounts = clearedCounts ?? new ClearedLogCounts();
|
||||
Message = message ?? string.Empty;
|
||||
ErrorMessage = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new ClearConsoleResponse for failed operation
|
||||
/// </summary>
|
||||
public ClearConsoleResponse(string errorMessage)
|
||||
{
|
||||
Success = false;
|
||||
ClearedLogCount = 0;
|
||||
ClearedCounts = new ClearedLogCounts();
|
||||
Message = string.Empty;
|
||||
ErrorMessage = errorMessage ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameterless constructor for JSON deserialization
|
||||
/// </summary>
|
||||
public ClearConsoleResponse()
|
||||
{
|
||||
ClearedCounts = new ClearedLogCounts();
|
||||
Message = string.Empty;
|
||||
ErrorMessage = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Breakdown of cleared logs by type
|
||||
/// </summary>
|
||||
public class ClearedLogCounts
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of error logs that were cleared
|
||||
/// </summary>
|
||||
public int ErrorCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of warning logs that were cleared
|
||||
/// </summary>
|
||||
public int WarningCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of info logs that were cleared
|
||||
/// </summary>
|
||||
public int LogCount { get; set; }
|
||||
|
||||
public ClearedLogCounts()
|
||||
{
|
||||
}
|
||||
|
||||
public ClearedLogCounts(int errorCount, int warningCount, int logCount)
|
||||
{
|
||||
ErrorCount = errorCount;
|
||||
WarningCount = warningCount;
|
||||
LogCount = logCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dbbf8d96e00c54171b03c9c0d42822dc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema for ClearConsole command parameters
|
||||
/// Provides type-safe parameter access for clearing Unity Console
|
||||
/// Related classes:
|
||||
/// - ConsoleUtility: Service layer for console operations
|
||||
/// - ClearConsoleResponse: Type-safe response structure
|
||||
/// - ClearConsoleCommand: Command implementation
|
||||
/// </summary>
|
||||
public class ClearConsoleSchema : BaseToolSchema
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to add a confirmation log message after clearing
|
||||
/// </summary>
|
||||
[Description("Whether to add a confirmation log message after clearing")]
|
||||
public bool AddConfirmationMessage { get; set; } = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94a230c702f0e4d75b03dc706886b935
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// ClearConsole tool handler - Type-safe implementation using Schema and Response
|
||||
/// Clears Unity console logs for clean development workflow
|
||||
/// Related classes: ClearConsoleUseCase, ConsoleUtility, ClearConsoleSchema, ClearConsoleResponse
|
||||
/// </summary>
|
||||
[McpTool(Description = "Clear Unity console logs")]
|
||||
public class ClearConsoleTool : AbstractUnityTool<ClearConsoleSchema, ClearConsoleResponse>
|
||||
{
|
||||
public override string ToolName => "clear-console";
|
||||
|
||||
/// <summary>
|
||||
/// Execute console clear tool
|
||||
/// </summary>
|
||||
/// <param name="parameters">Type-safe parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation token for timeout control</param>
|
||||
/// <returns>Clear operation result</returns>
|
||||
protected override async Task<ClearConsoleResponse> ExecuteAsync(ClearConsoleSchema parameters, CancellationToken cancellationToken)
|
||||
{
|
||||
// Create and execute ClearConsoleUseCase instance
|
||||
ClearConsoleUseCase useCase = new();
|
||||
return await useCase.ExecuteAsync(parameters, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b72387c853bad490fb5cb20f551f0781
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc3fddf04059a4303be8a7f27aa53052
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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."
|
||||
---
|
||||
|
||||
# uloop clear-console
|
||||
|
||||
Clear Unity console logs.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
uloop 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 <path>` | Optional. Use only when the target Unity project is not the current directory. |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Clear console
|
||||
uloop clear-console
|
||||
|
||||
# Clear with confirmation
|
||||
uloop clear-console --add-confirmation-message
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Returns JSON with:
|
||||
- `Success` (boolean): Whether the clear operation succeeded
|
||||
- `ClearedLogCount` (number): Total number of log entries that were cleared
|
||||
- `ClearedCounts` (object): Breakdown by log type
|
||||
- `ErrorCount` (number): Errors cleared
|
||||
- `WarningCount` (number): Warnings cleared
|
||||
- `LogCount` (number): Info logs cleared
|
||||
- `Message` (string): Description of the result; carries the failure summary when the operation fails (e.g. `"Failed to clear console: ..."`)
|
||||
- `ErrorMessage` (string): Currently always empty for this tool — read `Message` for failure details
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f1d3cbc9cba645b398b9f4bbe9537c4
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 90328140d69f84f1e925caf7f710c0eb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents information about a Unity component
|
||||
/// </summary>
|
||||
public class ComponentInfo
|
||||
{
|
||||
public string type { get; set; }
|
||||
public string fullTypeName { get; set; }
|
||||
public ComponentPropertyInfo[] properties { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a property of a Unity component
|
||||
/// </summary>
|
||||
public class ComponentPropertyInfo
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string type { get; set; }
|
||||
public object value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8f54071c76614440b5775885e181d95
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8a3c04e7ca654807956364da02c4e85
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,208 @@
|
||||
#nullable enable
|
||||
#if ULOOPMCP_HAS_INPUT_SYSTEM
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
// Shared helper for applying Input System state changes at the correct update phase.
|
||||
// Both keyboard and mouse simulation need frame-precise timing so that
|
||||
// wasPressedThisFrame / wasReleasedThisFrame detect the injected state.
|
||||
internal static class InputSystemUpdateHelper
|
||||
{
|
||||
public static Task ApplyOnNextConfiguredUpdate(Action apply, CancellationToken ct)
|
||||
{
|
||||
InputUpdateType targetUpdateType = InputUpdateTypeResolver.Resolve();
|
||||
if (InputUpdateTypeResolver.RequiresExplicitUpdate())
|
||||
{
|
||||
return ApplyOnExplicitUpdate(apply, targetUpdateType, ct);
|
||||
}
|
||||
|
||||
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
|
||||
CancellationTokenRegistration registration = default;
|
||||
Action? callback = null;
|
||||
|
||||
callback = () =>
|
||||
{
|
||||
InputUpdateType currentUpdateType = InputState.currentUpdateType;
|
||||
if (!InputUpdateTypeResolver.IsMatch(currentUpdateType, targetUpdateType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Assert(callback != null, "callback must be assigned before subscription");
|
||||
InputSystem.onBeforeUpdate -= callback;
|
||||
registration.Dispose();
|
||||
apply();
|
||||
tcs.TrySetResult(true);
|
||||
};
|
||||
|
||||
InputSystem.onBeforeUpdate += callback;
|
||||
if (ct.CanBeCanceled)
|
||||
{
|
||||
registration = ct.Register(() =>
|
||||
{
|
||||
Debug.Assert(callback != null, "callback must be assigned before cancellation");
|
||||
InputSystem.onBeforeUpdate -= callback;
|
||||
tcs.TrySetCanceled(ct);
|
||||
});
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public static int GetMinimumObservationFrameCount()
|
||||
{
|
||||
if (!InputUpdateTypeResolver.RequiresExplicitUpdate())
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
InputUpdateType targetUpdateType = InputUpdateTypeResolver.Resolve();
|
||||
if (targetUpdateType != InputUpdateType.Manual)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Manual-mode projects often call InputSystem.Update from their own Update loop,
|
||||
// so zero-duration taps need one extra frame to remain visible to gameplay code.
|
||||
return 3;
|
||||
}
|
||||
|
||||
public static async Task WaitForObservationFrames(CancellationToken ct)
|
||||
{
|
||||
await WaitForRuntimeFrames(GetMinimumObservationFrameCount(), ct);
|
||||
}
|
||||
|
||||
public static async Task WaitForPressLifetime(float duration, CancellationToken ct)
|
||||
{
|
||||
int minimumObservationFrames = GetMinimumObservationFrameCount();
|
||||
int startFrameCount = Time.frameCount;
|
||||
float startTime = Time.realtimeSinceStartup;
|
||||
float elapsed = 0f;
|
||||
int observedFrames = 0;
|
||||
|
||||
while (observedFrames < minimumObservationFrames || elapsed < duration)
|
||||
{
|
||||
await EditorDelay.DelayFrame(1, ct);
|
||||
observedFrames = Time.frameCount - startFrameCount;
|
||||
elapsed = Time.realtimeSinceStartup - startTime;
|
||||
}
|
||||
}
|
||||
|
||||
public static void RunExplicitUpdate(InputUpdateType targetUpdateType)
|
||||
{
|
||||
InputSettings? settings = InputSystem.settings;
|
||||
if (settings == null)
|
||||
{
|
||||
InputSystem.Update();
|
||||
return;
|
||||
}
|
||||
|
||||
InputSettings.UpdateMode originalUpdateMode = settings.updateMode;
|
||||
InputSettings.UpdateMode targetUpdateMode = GetExplicitUpdateMode(targetUpdateType, originalUpdateMode);
|
||||
if (targetUpdateMode == originalUpdateMode)
|
||||
{
|
||||
InputSystem.Update();
|
||||
return;
|
||||
}
|
||||
|
||||
settings.updateMode = targetUpdateMode;
|
||||
try
|
||||
{
|
||||
InputSystem.Update();
|
||||
}
|
||||
finally
|
||||
{
|
||||
settings.updateMode = originalUpdateMode;
|
||||
}
|
||||
}
|
||||
|
||||
private static Task ApplyOnExplicitUpdate(Action apply, InputUpdateType targetUpdateType, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
|
||||
CancellationTokenRegistration registration = default;
|
||||
Action? callback = null;
|
||||
|
||||
callback = () =>
|
||||
{
|
||||
InputUpdateType currentUpdateType = InputState.currentUpdateType;
|
||||
if (!InputUpdateTypeResolver.IsMatch(currentUpdateType, targetUpdateType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Assert(callback != null, "callback must be assigned before subscription");
|
||||
InputSystem.onBeforeUpdate -= callback;
|
||||
registration.Dispose();
|
||||
apply();
|
||||
tcs.TrySetResult(true);
|
||||
};
|
||||
|
||||
InputSystem.onBeforeUpdate += callback;
|
||||
if (ct.CanBeCanceled)
|
||||
{
|
||||
registration = ct.Register(() =>
|
||||
{
|
||||
Debug.Assert(callback != null, "callback must be assigned before cancellation");
|
||||
InputSystem.onBeforeUpdate -= callback;
|
||||
tcs.TrySetCanceled(ct);
|
||||
});
|
||||
}
|
||||
|
||||
RunExplicitUpdate(targetUpdateType);
|
||||
if (!tcs.Task.IsCompleted)
|
||||
{
|
||||
Debug.Assert(callback != null, "callback must be assigned before explicit update fallback");
|
||||
InputSystem.onBeforeUpdate -= callback;
|
||||
registration.Dispose();
|
||||
apply();
|
||||
RunExplicitUpdate(targetUpdateType);
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static InputSettings.UpdateMode GetExplicitUpdateMode(
|
||||
InputUpdateType targetUpdateType,
|
||||
InputSettings.UpdateMode fallbackUpdateMode)
|
||||
{
|
||||
if (targetUpdateType == InputUpdateType.Dynamic)
|
||||
{
|
||||
return InputSettings.UpdateMode.ProcessEventsInDynamicUpdate;
|
||||
}
|
||||
|
||||
if (targetUpdateType == InputUpdateType.Fixed)
|
||||
{
|
||||
return InputSettings.UpdateMode.ProcessEventsInFixedUpdate;
|
||||
}
|
||||
|
||||
if (targetUpdateType == InputUpdateType.Manual)
|
||||
{
|
||||
return InputSettings.UpdateMode.ProcessEventsManually;
|
||||
}
|
||||
|
||||
return fallbackUpdateMode;
|
||||
}
|
||||
|
||||
private static async Task WaitForRuntimeFrames(int frameCount, CancellationToken ct)
|
||||
{
|
||||
int startFrameCount = Time.frameCount;
|
||||
int observedFrames = 0;
|
||||
|
||||
while (observedFrames < frameCount)
|
||||
{
|
||||
await EditorDelay.DelayFrame(1, ct);
|
||||
observedFrames = Time.frameCount - startFrameCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d2cdea3342ed49e2a971e1766cbafa7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,64 @@
|
||||
#nullable enable
|
||||
#if ULOOPMCP_HAS_INPUT_SYSTEM
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
// Projects can process Input System events in dynamic, fixed, or manual mode.
|
||||
// Input simulation must follow that configured loop to avoid frame mismatches.
|
||||
internal static class InputUpdateTypeResolver
|
||||
{
|
||||
public static InputUpdateType Resolve()
|
||||
{
|
||||
InputSettings? settings = InputSystem.settings;
|
||||
if (settings == null)
|
||||
{
|
||||
return InputUpdateType.Dynamic;
|
||||
}
|
||||
|
||||
InputSettings.UpdateMode updateMode = settings.updateMode;
|
||||
switch (updateMode)
|
||||
{
|
||||
case InputSettings.UpdateMode.ProcessEventsInFixedUpdate:
|
||||
// Paused screens commonly set timeScale to 0, which stops fixed ticks entirely.
|
||||
// Falling back to Dynamic keeps input simulation responsive for those menus.
|
||||
return IsPausedFixedUpdate(settings) ? InputUpdateType.Dynamic : InputUpdateType.Fixed;
|
||||
|
||||
case InputSettings.UpdateMode.ProcessEventsManually:
|
||||
return InputUpdateType.Manual;
|
||||
|
||||
default:
|
||||
return InputUpdateType.Dynamic;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsMatch(InputUpdateType current, InputUpdateType expected)
|
||||
{
|
||||
return (current & expected) == expected;
|
||||
}
|
||||
|
||||
public static bool RequiresExplicitUpdate()
|
||||
{
|
||||
InputSettings? settings = InputSystem.settings;
|
||||
if (settings == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (settings.updateMode == InputSettings.UpdateMode.ProcessEventsManually)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return IsPausedFixedUpdate(settings);
|
||||
}
|
||||
|
||||
private static bool IsPausedFixedUpdate(InputSettings settings)
|
||||
{
|
||||
return settings.updateMode == InputSettings.UpdateMode.ProcessEventsInFixedUpdate && Time.timeScale <= 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67072e4564d6345d58ceff0440cbfeda
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: acb202afcf62f40b88d846c3f1e0c373
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,102 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Compile error or warning information
|
||||
/// </summary>
|
||||
public class CompileIssue
|
||||
{
|
||||
public string Message { get; set; }
|
||||
public string File { get; set; }
|
||||
public int Line { get; set; }
|
||||
|
||||
public CompileIssue(string message, string file, int line)
|
||||
{
|
||||
Message = message;
|
||||
File = file;
|
||||
Line = line;
|
||||
}
|
||||
|
||||
public CompileIssue() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response schema for Compile command
|
||||
/// Provides type-safe response structure
|
||||
/// </summary>
|
||||
public class CompileResponse : BaseToolResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether compilation was successful.
|
||||
/// Null is used when the tool cannot reliably determine the result yet (e.g., forced recompile/domain reload),
|
||||
/// to avoid misleading clients into treating "0 errors" as a confirmed success.
|
||||
/// </summary>
|
||||
public bool? Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of compilation errors.
|
||||
/// Null is used when the tool intentionally does not provide details (e.g., forced recompile),
|
||||
/// because Unity reports errors/warnings after domain reload and clients should fetch logs later.
|
||||
/// </summary>
|
||||
public int? ErrorCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of compilation warnings.
|
||||
/// Null is used when the tool intentionally does not provide details (e.g., forced recompile),
|
||||
/// because Unity reports errors/warnings after domain reload and clients should fetch logs later.
|
||||
/// </summary>
|
||||
public int? WarningCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compilation errors.
|
||||
/// Null is used when the tool intentionally does not provide details (e.g., forced recompile),
|
||||
/// because Unity reports errors/warnings after domain reload and clients should fetch logs later.
|
||||
/// </summary>
|
||||
public CompileIssue[] Errors { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compilation warnings.
|
||||
/// Null is used when the tool intentionally does not provide details (e.g., forced recompile),
|
||||
/// because Unity reports errors/warnings after domain reload and clients should fetch logs later.
|
||||
/// </summary>
|
||||
public CompileIssue[] Warnings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional message for additional information
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unity project root path (from Application.dataPath).
|
||||
/// Set only when WaitForDomainReload=true so that TS/CLI can locate lock files
|
||||
/// in the correct project even when connected to a remote Unity instance.
|
||||
/// </summary>
|
||||
public string ProjectRoot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new CompileResponse
|
||||
/// </summary>
|
||||
public CompileResponse(
|
||||
bool? success,
|
||||
int? errorCount,
|
||||
int? warningCount,
|
||||
CompileIssue[] errors,
|
||||
CompileIssue[] warnings,
|
||||
string message = null
|
||||
)
|
||||
{
|
||||
Success = success;
|
||||
ErrorCount = errorCount;
|
||||
WarningCount = warningCount;
|
||||
Errors = errors;
|
||||
Warnings = warnings;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameterless constructor for JSON deserialization
|
||||
/// </summary>
|
||||
public CompileResponse()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56090f2379ae3428bbe733a4db41e344
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema for Compile command parameters
|
||||
/// Provides type-safe parameter access with default values
|
||||
/// </summary>
|
||||
public class CompileSchema : BaseToolSchema
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to perform forced recompilation
|
||||
/// </summary>
|
||||
[Description("Whether to perform forced recompilation. Note: Force compilation results do not include error/warning messages in the response. Use get-logs tool after execution to retrieve compilation messages.")]
|
||||
public bool ForceRecompile { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to wait for domain reload completion before the caller returns.
|
||||
/// </summary>
|
||||
[Description("Whether to wait for domain reload completion before returning.")]
|
||||
public bool WaitForDomainReload { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Internal request identifier used for delayed result recovery across domain reload.
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public string RequestId { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2581c0d32a90546fd9978a2f938e2f6f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Compile tool handler - Type-safe implementation using Schema and Response
|
||||
/// Handles Unity project compilation with optional force recompile
|
||||
/// Related classes: CompileUseCase, CompilationStateValidationService, CompilationExecutionService
|
||||
/// </summary>
|
||||
[McpTool(Description = "Execute Unity project compilation")]
|
||||
public class CompileTool : AbstractUnityTool<CompileSchema, CompileResponse>
|
||||
{
|
||||
public override string ToolName => "compile";
|
||||
|
||||
/// <summary>
|
||||
/// Execute compile tool - delegates to CompileUseCase
|
||||
/// </summary>
|
||||
/// <param name="parameters">Type-safe parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation token for timeout control</param>
|
||||
/// <returns>Compile result</returns>
|
||||
protected override async Task<CompileResponse> ExecuteAsync(CompileSchema parameters, CancellationToken cancellationToken)
|
||||
{
|
||||
// Create and execute CompileUseCase instance
|
||||
CompileUseCase useCase = new();
|
||||
return await useCase.ExecuteAsync(parameters, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60e9cabfcc7d047cbab5dfd6e9b06904
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1186b025586e04dd794dace399e9ce35
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
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."
|
||||
---
|
||||
|
||||
# uloop compile
|
||||
|
||||
Execute Unity project compilation.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
uloop compile [--force-recompile] [--wait-for-domain-reload]
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `--force-recompile` | boolean | `false` | Force full recompilation (triggers Domain Reload) |
|
||||
| `--wait-for-domain-reload` | boolean | `false` | Wait until Domain Reload completes before returning |
|
||||
|
||||
## Global Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--project-path <path>` | Optional. Use only when the target Unity project is not the current directory. |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Check compilation
|
||||
uloop compile
|
||||
|
||||
# Force full recompilation
|
||||
uloop compile --force-recompile
|
||||
|
||||
# Force recompilation and wait for Domain Reload completion
|
||||
uloop compile --force-recompile true --wait-for-domain-reload true
|
||||
|
||||
# Wait for Domain Reload completion even without force recompilation
|
||||
uloop 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
|
||||
uloop fix
|
||||
```
|
||||
|
||||
This removes any leftover lock files (`compiling.lock`, `domainreload.lock`, `serverstarting.lock`) from the Unity project's Temp directory. Then retry `uloop compile`.
|
||||
|
||||
**Unity Editor not running** (CLI returns a connection failure and no Unity process is alive):
|
||||
|
||||
```bash
|
||||
uloop launch
|
||||
```
|
||||
|
||||
`uloop launch` auto-detects the project at the current working directory and opens it in the matching Unity Editor version. After Unity finishes launching, retry `uloop compile`.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3e7fbf71d98ab425486f6bd68b7a6612
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a76e166c9de484f20b07f88305dd4b6b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public class ControlPlayModeResponse : BaseToolResponse
|
||||
{
|
||||
public bool IsPlaying { get; set; }
|
||||
public bool IsPaused { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 752e3f7c18a5649d8b6a555add78f1d7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public enum PlayModeAction
|
||||
{
|
||||
Play = 0,
|
||||
Stop = 1,
|
||||
Pause = 2
|
||||
}
|
||||
|
||||
public class ControlPlayModeSchema : BaseToolSchema
|
||||
{
|
||||
[Description("Action to perform: Play(0) - Start play mode, Stop(1) - Stop play mode, Pause(2) - Pause play mode")]
|
||||
public PlayModeAction Action { get; set; } = PlayModeAction.Play;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5634451b383ae4ff8b0a1cd00500f342
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
[McpTool(Description = "Control Unity Editor play mode (play/stop/pause)")]
|
||||
public class ControlPlayModeTool : AbstractUnityTool<ControlPlayModeSchema, ControlPlayModeResponse>
|
||||
{
|
||||
public override string ToolName => "control-play-mode";
|
||||
|
||||
protected override async Task<ControlPlayModeResponse> ExecuteAsync(ControlPlayModeSchema parameters, CancellationToken cancellationToken)
|
||||
{
|
||||
ControlPlayModeUseCase useCase = new();
|
||||
return await useCase.ExecuteAsync(parameters, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bdce84d8140ea44e9a5fbc8b1e1cf5d6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8809b197b1dbb42fbbabc04674f5baa9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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."
|
||||
---
|
||||
|
||||
# uloop control-play-mode
|
||||
|
||||
Control Unity Editor play mode (play/stop/pause).
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
uloop control-play-mode [options]
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `--action` | string | `Play` | Action to perform: `Play`, `Stop`, `Pause` |
|
||||
|
||||
## Global Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--project-path <path>` | Optional. Use only when the target Unity project is not the current directory. |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Start play mode
|
||||
uloop control-play-mode --action Play
|
||||
|
||||
# Stop play mode
|
||||
uloop control-play-mode --action Stop
|
||||
|
||||
# Pause play mode
|
||||
uloop 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.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1e1425714444f43d1aab052bf7101893
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4d88782b067f473a8c1bf230936d8f0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,122 @@
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
// Related classes:
|
||||
// - IUnityTool: The interface that this class implements.
|
||||
// - UnityToolRegistry: Registers and manages instances of tool implementations.
|
||||
// - ToolParameterSchemaGenerator: Generates the JSON schema for tool parameters.
|
||||
/// <summary>
|
||||
/// Abstract base class for type-safe Unity tools using Schema and Response types
|
||||
/// </summary>
|
||||
/// <typeparam name="TSchema">Schema type for tool parameters</typeparam>
|
||||
/// <typeparam name="TResponse">Response type for tool results</typeparam>
|
||||
public abstract class AbstractUnityTool<TSchema, TResponse> : IUnityTool
|
||||
where TSchema : BaseToolSchema, new()
|
||||
where TResponse : BaseToolResponse
|
||||
{
|
||||
public abstract string ToolName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Automatically generates parameter schema from TSchema type
|
||||
/// </summary>
|
||||
public virtual ToolParameterSchema ParameterSchema =>
|
||||
ToolParameterSchemaGenerator.FromDto<TSchema>();
|
||||
|
||||
/// <summary>
|
||||
/// Execute tool with type-safe Schema parameters
|
||||
/// Note: This method is called from the main thread context.
|
||||
/// MainThreadSwitcher.SwitchToMainThread() is already handled by the upper layer (JsonRpcProcessor),
|
||||
/// so individual tools do not need to call it again.
|
||||
/// </summary>
|
||||
/// <param name="parameters">Strongly typed parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation token for timeout control</param>
|
||||
/// <returns>Strongly typed tool execution result</returns>
|
||||
protected abstract Task<TResponse> ExecuteAsync(TSchema parameters, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// IUnityTool implementation - converts JToken to Schema and returns BaseToolResponse
|
||||
/// </summary>
|
||||
public async Task<BaseToolResponse> ExecuteAsync(JToken paramsToken)
|
||||
{
|
||||
// Convert JToken to strongly typed Schema
|
||||
TSchema parameters = ConvertToSchema(paramsToken);
|
||||
|
||||
// Execute with type-safe parameters
|
||||
TResponse response = await ExecuteAsync(parameters, CancellationToken.None);
|
||||
|
||||
// Return as BaseToolResponse for IUnityTool interface compatibility
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert JToken to strongly typed Schema with default value fallback
|
||||
/// </summary>
|
||||
private TSchema ConvertToSchema(JToken paramsToken)
|
||||
{
|
||||
if (paramsToken == null || paramsToken.Type == JTokenType.Null)
|
||||
{
|
||||
// Return default instance if no parameters provided
|
||||
return new TSchema();
|
||||
}
|
||||
|
||||
// Create JsonSerializerSettings with CamelCasePropertyNamesContractResolver
|
||||
// This allows TypeScript side to use camelCase while C# uses PascalCase
|
||||
JsonSerializerSettings settings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||
};
|
||||
|
||||
// Create JsonSerializer with custom settings
|
||||
JsonSerializer serializer = JsonSerializer.Create(settings);
|
||||
|
||||
// Try to deserialize from JToken with custom serializer
|
||||
TSchema schema;
|
||||
try
|
||||
{
|
||||
schema = paramsToken.ToObject<TSchema>(serializer);
|
||||
}
|
||||
catch (JsonSerializationException ex)
|
||||
{
|
||||
// Create detailed error message for type mismatches
|
||||
string errorMessage = $"Parameter type mismatch for tool '{ToolName}': {ex.Message}";
|
||||
|
||||
// Check for specific Dictionary<string, object> conversion errors
|
||||
if (ex.Message.Contains("Dictionary") && ex.Message.Contains("Error converting value"))
|
||||
{
|
||||
string received = paramsToken["parameters"]?.ToString() ?? paramsToken["Parameters"]?.ToString() ?? "null";
|
||||
errorMessage =
|
||||
"Parameter 'Parameters' must be an object, not a string. " +
|
||||
"Do: omit 'Parameters' or use {}. " +
|
||||
"Don't: \"{}\" (string). " +
|
||||
$"Received: {received}";
|
||||
}
|
||||
|
||||
throw new ParameterValidationException(errorMessage, ex);
|
||||
}
|
||||
|
||||
// If deserialization returns null, create default instance
|
||||
if (schema == null)
|
||||
{
|
||||
schema = new TSchema();
|
||||
}
|
||||
|
||||
// Apply default values for null properties
|
||||
return ApplyDefaultValues(schema);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply default values to Schema properties if they are null
|
||||
/// Override this method to provide custom default value logic
|
||||
/// </summary>
|
||||
protected virtual TSchema ApplyDefaultValues(TSchema schema)
|
||||
{
|
||||
// Default implementation - return as is
|
||||
// Subclasses can override to apply specific default values
|
||||
return schema;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d4b53392372a454e954428caf82258a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// UseCase base class - Responsible for temporal cohesion
|
||||
/// New instances are created each time and disposed after Execute completion
|
||||
/// Related classes: AbstractUnityTool, BaseToolSchema, BaseToolResponse
|
||||
/// </summary>
|
||||
/// <typeparam name="TSchema">Schema type for tool parameters</typeparam>
|
||||
/// <typeparam name="TResponse">Response type for tool results</typeparam>
|
||||
public abstract class AbstractUseCase<TSchema, TResponse>
|
||||
where TSchema : BaseToolSchema
|
||||
where TResponse : BaseToolResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// UseCase execution method - The only public method
|
||||
/// Responsible for temporal cohesion (executing multiple operations in sequence)
|
||||
/// </summary>
|
||||
/// <param name="parameters">Type-safe parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation control token</param>
|
||||
/// <returns>Type-safe execution result</returns>
|
||||
public abstract Task<TResponse> ExecuteAsync(TSchema parameters, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a74e8d5c037f4277a284f8f686edc05
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Base response class for all Unity MCP tool responses
|
||||
/// </summary>
|
||||
public abstract class BaseToolResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// uLoopMCP server version for CLI version compatibility check
|
||||
/// </summary>
|
||||
public string Ver => McpVersion.VERSION;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a54be1e158eda4064aa08ab9da92d980
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Base schema class for all Unity MCP tool parameters
|
||||
/// </summary>
|
||||
public abstract class BaseToolSchema
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c705d28d63c1b4fb5aa6d7797c95631d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,114 @@
|
||||
using UnityEditor;
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// SessionState management class for forced recompilation
|
||||
/// Preserves request information even when assembly reload occurs
|
||||
/// </summary>
|
||||
public static class CompileSessionState
|
||||
{
|
||||
// Note: Compile requests are now managed via McpSessionManager
|
||||
|
||||
/// <summary>
|
||||
/// Forced recompilation request information
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class CompileRequestInfo
|
||||
{
|
||||
public string requestId;
|
||||
public bool forceRecompile;
|
||||
public string clientEndpoint;
|
||||
public DateTime requestTime;
|
||||
public bool isCompleted;
|
||||
|
||||
public CompileRequestInfo(string requestId, bool forceRecompile, string clientEndpoint)
|
||||
{
|
||||
this.requestId = requestId;
|
||||
this.forceRecompile = forceRecompile;
|
||||
this.clientEndpoint = clientEndpoint;
|
||||
this.requestTime = DateTime.Now;
|
||||
this.isCompleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save forced recompilation request
|
||||
/// </summary>
|
||||
public static void SaveCompileRequest(string requestId, bool forceRecompile, string clientEndpoint = "unknown")
|
||||
{
|
||||
if (!forceRecompile) return; // Don't save normal compilation
|
||||
|
||||
CompileRequestInfo requestInfo = new CompileRequestInfo(requestId, forceRecompile, clientEndpoint);
|
||||
string json = JsonConvert.SerializeObject(requestInfo);
|
||||
|
||||
McpEditorSettings.SetCompileRequestJson(requestId, json);
|
||||
McpEditorSettings.AddPendingCompileRequest(requestId);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get pending request IDs
|
||||
/// </summary>
|
||||
public static string[] GetPendingRequestIds()
|
||||
{
|
||||
return McpEditorSettings.GetPendingCompileRequestIds();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get saved request information
|
||||
/// </summary>
|
||||
public static CompileRequestInfo GetCompileRequest(string requestId)
|
||||
{
|
||||
string json = McpEditorSettings.GetCompileRequestJson(requestId);
|
||||
if (string.IsNullOrEmpty(json)) return null;
|
||||
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<CompileRequestInfo>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark request as completed
|
||||
/// </summary>
|
||||
public static void MarkRequestCompleted(string requestId)
|
||||
{
|
||||
CompileRequestInfo requestInfo = GetCompileRequest(requestId);
|
||||
if (requestInfo == null) return;
|
||||
|
||||
requestInfo.isCompleted = true;
|
||||
string json = JsonConvert.SerializeObject(requestInfo);
|
||||
McpEditorSettings.SetCompileRequestJson(requestId, json);
|
||||
McpEditorSettings.RemovePendingCompileRequest(requestId);
|
||||
|
||||
}
|
||||
|
||||
// Note: RemoveFromPendingRequests is now handled by McpSessionManager
|
||||
|
||||
/// <summary>
|
||||
/// Start forced recompilation
|
||||
/// </summary>
|
||||
public static void StartForceRecompile()
|
||||
{
|
||||
UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(
|
||||
UnityEditor.Compilation.RequestScriptCompilationOptions.CleanBuildCache
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear SessionState
|
||||
/// </summary>
|
||||
public static void ClearAll()
|
||||
{
|
||||
McpEditorSettings.ClearAllCompileRequests();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43fb73081d49d4a309e18955e541493f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Static class for managing custom tools
|
||||
/// Allows users to register and manage their own tools
|
||||
/// </summary>
|
||||
public static class CustomToolManager
|
||||
{
|
||||
private static UnityToolRegistry _sharedRegistry;
|
||||
|
||||
internal static event Action OnToolsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Get shared registry (lazy initialization)
|
||||
/// </summary>
|
||||
private static UnityToolRegistry SharedRegistry
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_sharedRegistry == null)
|
||||
{
|
||||
_sharedRegistry = new UnityToolRegistry();
|
||||
// Standard tools are automatically registered in UnityToolRegistry constructor
|
||||
}
|
||||
return _sharedRegistry;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register custom tool
|
||||
/// </summary>
|
||||
/// <param name="tool">Tool to register</param>
|
||||
public static void RegisterCustomTool(IUnityTool tool)
|
||||
{
|
||||
SharedRegistry.RegisterTool(tool);
|
||||
|
||||
// Notify tool changes for manual registration
|
||||
NotifyToolChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregister custom tool
|
||||
/// </summary>
|
||||
/// <param name="toolName">Name of tool to unregister</param>
|
||||
public static void UnregisterCustomTool(string toolName)
|
||||
{
|
||||
SharedRegistry.UnregisterTool(toolName);
|
||||
|
||||
// Notify tool changes for manual unregistration
|
||||
NotifyToolChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get list of all registered tools (standard + custom)
|
||||
/// </summary>
|
||||
/// <returns>Array of tool information</returns>
|
||||
public static ToolInfo[] GetRegisteredCustomTools()
|
||||
{
|
||||
return SharedRegistry.GetRegisteredTools();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if specified tool is registered
|
||||
/// </summary>
|
||||
/// <param name="toolName">Tool name</param>
|
||||
/// <returns>True if registered</returns>
|
||||
public static bool IsCustomToolRegistered(string toolName)
|
||||
{
|
||||
return SharedRegistry.IsToolRegistered(toolName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get internal registry (for MCP server)
|
||||
/// </summary>
|
||||
/// <returns>UnityToolRegistry instance</returns>
|
||||
internal static UnityToolRegistry GetRegistry()
|
||||
{
|
||||
return SharedRegistry;
|
||||
}
|
||||
|
||||
internal static UnityToolRegistry TryGetRegistry()
|
||||
{
|
||||
return _sharedRegistry;
|
||||
}
|
||||
|
||||
internal static void WarmupRegistry()
|
||||
{
|
||||
_ = SharedRegistry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Debug: Get detailed registry information
|
||||
/// </summary>
|
||||
/// <returns>Debug information</returns>
|
||||
public static string GetDebugInfo()
|
||||
{
|
||||
ToolInfo[] tools = SharedRegistry.GetRegisteredTools();
|
||||
string[] toolNames = new string[tools.Length];
|
||||
for (int i = 0; i < tools.Length; i++)
|
||||
{
|
||||
toolNames[i] = tools[i].Name;
|
||||
}
|
||||
return $"Registry instance: {SharedRegistry.GetHashCode()}, Tools: [{string.Join(", ", toolNames)}]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually notify tool changes to MCP clients
|
||||
/// Public API for users to trigger notifications when needed
|
||||
/// </summary>
|
||||
public static void NotifyToolChanges()
|
||||
{
|
||||
UnityToolRegistry.TriggerToolsChangedNotification();
|
||||
OnToolsChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dd9b6252fa10a4edc9a5f2ee0140365d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Constants for parameter schema property names
|
||||
/// Ensures consistency between Unity and TypeScript sides
|
||||
/// </summary>
|
||||
public static class ParameterSchemaConstants
|
||||
{
|
||||
public const string TYPE_PROPERTY = "Type";
|
||||
public const string DESCRIPTION_PROPERTY = "Description";
|
||||
public const string DEFAULT_VALUE_PROPERTY = "DefaultValue";
|
||||
public const string ENUM_PROPERTY = "Enum";
|
||||
public const string PROPERTIES_PROPERTY = "Properties";
|
||||
public const string REQUIRED_PROPERTY = "Required";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base interface for Unity MCP tool handlers
|
||||
/// Following the Open-Closed principle, when adding new tools,
|
||||
/// create a new class that implements this interface
|
||||
/// </summary>
|
||||
public interface IUnityTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Get tool name
|
||||
/// </summary>
|
||||
string ToolName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get parameter schema information for TypeScript side
|
||||
/// </summary>
|
||||
ToolParameterSchema ParameterSchema { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Execute tool
|
||||
/// </summary>
|
||||
/// <param name="paramsToken">JSON token for parameters</param>
|
||||
/// <returns>Execution result</returns>
|
||||
Task<BaseToolResponse> ExecuteAsync(JToken paramsToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parameter schema definition for tools (immutable)
|
||||
/// </summary>
|
||||
public class ToolParameterSchema
|
||||
{
|
||||
public readonly Dictionary<string, ParameterInfo> Properties;
|
||||
public readonly string[] Required;
|
||||
|
||||
public ToolParameterSchema(Dictionary<string, ParameterInfo> properties = null, string[] required = null)
|
||||
{
|
||||
Properties = properties ?? new Dictionary<string, ParameterInfo>();
|
||||
Required = required ?? new string[0];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual parameter information (immutable)
|
||||
/// </summary>
|
||||
public class ParameterInfo
|
||||
{
|
||||
public readonly string Type;
|
||||
public readonly string Description;
|
||||
public readonly object DefaultValue;
|
||||
public readonly string[] Enum;
|
||||
|
||||
public ParameterInfo(string type, string description, object defaultValue = null, string[] enumValues = null)
|
||||
{
|
||||
Type = type;
|
||||
Description = description;
|
||||
DefaultValue = defaultValue;
|
||||
Enum = enumValues;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f9cf1a64741648cb8533b76ec6f3a87
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute to mark classes for automatic MCP tool registration
|
||||
/// Classes marked with this attribute will be automatically registered as MCP commands
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public class McpToolAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether this tool should only be displayed in development mode
|
||||
/// </summary>
|
||||
public bool DisplayDevelopmentOnly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specific security setting required to execute this command
|
||||
/// </summary>
|
||||
public SecuritySettings RequiredSecuritySetting { get; set; } = SecuritySettings.None;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description of this tool
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize McpTool attribute
|
||||
/// </summary>
|
||||
public McpToolAttribute()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f9f72309eee6a49528d19b182ff51ad6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,64 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
// Instantiates the InputVisualizationCanvas prefab and manages its lifecycle.
|
||||
internal static class OverlayCanvasFactory
|
||||
{
|
||||
private const string CANVAS_PREFAB_PATH = "Packages/io.github.hatayama.uloopmcp/Runtime/Common/InputVisualizationCanvas.prefab";
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetStaticFields()
|
||||
{
|
||||
_instance = null;
|
||||
}
|
||||
|
||||
private static InputVisualizationCanvas _instance;
|
||||
|
||||
public static InputVisualizationCanvas VisualizationCanvas
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureExists();
|
||||
Debug.Assert(_instance != null, "InputVisualizationCanvas instance must exist after EnsureExists");
|
||||
return _instance!;
|
||||
}
|
||||
}
|
||||
|
||||
public static void EnsureExists()
|
||||
{
|
||||
if (_instance != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Domain Reload resets _instance but DontDestroyOnLoad objects survive; reclaim one and destroy duplicates
|
||||
InputVisualizationCanvas[] existing =
|
||||
Object.FindObjectsByType<InputVisualizationCanvas>(FindObjectsSortMode.None);
|
||||
for (int i = 0; i < existing.Length; i++)
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = existing[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
Object.DestroyImmediate(existing[i].gameObject);
|
||||
}
|
||||
}
|
||||
if (_instance != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(CANVAS_PREFAB_PATH);
|
||||
Debug.Assert(prefab != null, $"InputVisualizationCanvas prefab not found at {CANVAS_PREFAB_PATH}");
|
||||
|
||||
GameObject go = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
|
||||
Object.DontDestroyOnLoad(go);
|
||||
_instance = go.GetComponent<InputVisualizationCanvas>();
|
||||
Debug.Assert(_instance != null, "InputVisualizationCanvas component not found on prefab");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76e5da6abcd484cbcbdbbf462a6fdd4b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic data model representing service execution result
|
||||
/// Used in Application Service Layer
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of result data</typeparam>
|
||||
public class ServiceResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether execution was successful
|
||||
/// </summary>
|
||||
public bool Success { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Data of execution result
|
||||
/// </summary>
|
||||
public T Data { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message when execution fails
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create ServiceResult
|
||||
/// </summary>
|
||||
/// <param name="success">Execution result</param>
|
||||
/// <param name="data">Result data</param>
|
||||
/// <param name="errorMessage">Error message (null on success)</param>
|
||||
public ServiceResult(bool success, T data = default, string errorMessage = null)
|
||||
{
|
||||
Success = success;
|
||||
Data = data;
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create success result
|
||||
/// </summary>
|
||||
/// <param name="data">Result data</param>
|
||||
/// <returns>ServiceResult representing success</returns>
|
||||
public static ServiceResult<T> SuccessResult(T data) => new(true, data);
|
||||
|
||||
/// <summary>
|
||||
/// Create failure result
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">Error message</param>
|
||||
/// <returns>ServiceResult representing failure</returns>
|
||||
public static ServiceResult<T> FailureResult(string errorMessage) => new(false, default, errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de2101885e97d4499889ddbb6bb6e358
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,54 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class ToolExecutionAvailability
|
||||
{
|
||||
internal static bool ShouldReportDependencyUnavailableBeforeDisabled(string toolName)
|
||||
{
|
||||
return ShouldReportDependencyUnavailableBeforeDisabled(
|
||||
toolName,
|
||||
isTestFrameworkAvailable: IsTestFrameworkAvailable);
|
||||
}
|
||||
|
||||
internal static bool ShouldReportDependencyUnavailableBeforeDisabled(
|
||||
string toolName,
|
||||
bool isTestFrameworkAvailable)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(toolName), "toolName must not be null or empty");
|
||||
|
||||
return toolName == McpConstants.TOOL_NAME_RUN_TESTS && !isTestFrameworkAvailable;
|
||||
}
|
||||
|
||||
internal static bool ShouldExposeInRegisteredTools(string toolName)
|
||||
{
|
||||
return ShouldExposeInRegisteredTools(
|
||||
toolName,
|
||||
isToolEnabled: ToolSettings.IsToolEnabled(toolName),
|
||||
isTestFrameworkAvailable: IsTestFrameworkAvailable);
|
||||
}
|
||||
|
||||
internal static bool ShouldExposeInRegisteredTools(
|
||||
string toolName,
|
||||
bool isToolEnabled,
|
||||
bool isTestFrameworkAvailable)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(toolName), "toolName must not be null or empty");
|
||||
|
||||
return isToolEnabled ||
|
||||
ShouldReportDependencyUnavailableBeforeDisabled(toolName, isTestFrameworkAvailable);
|
||||
}
|
||||
|
||||
internal static bool IsTestFrameworkAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
#if ULOOPMCP_HAS_TEST_FRAMEWORK
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12a3cd3bb7213f14ab18fa845fe17f13
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,169 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility class to generate ToolParameterSchema from DTO classes
|
||||
/// Eliminates duplication between DTO default values and schema definitions
|
||||
/// </summary>
|
||||
public static class ToolParameterSchemaGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generate ToolParameterSchema from DTO type using reflection
|
||||
/// </summary>
|
||||
/// <typeparam name="TDto">DTO type</typeparam>
|
||||
/// <returns>Generated parameter schema</returns>
|
||||
public static ToolParameterSchema FromDto<TDto>() where TDto : class, new()
|
||||
{
|
||||
Type dtoType = typeof(TDto);
|
||||
Dictionary<string, ParameterInfo> properties = new Dictionary<string, ParameterInfo>();
|
||||
List<string> required = new List<string>();
|
||||
|
||||
// Create instance to get default values
|
||||
TDto defaultInstance = new TDto();
|
||||
|
||||
foreach (PropertyInfo property in dtoType.GetProperties())
|
||||
{
|
||||
// Skip properties without public setters
|
||||
if (!property.CanWrite || property.SetMethod?.IsPublic != true)
|
||||
continue;
|
||||
|
||||
BrowsableAttribute browsableAttribute = property.GetCustomAttribute<BrowsableAttribute>();
|
||||
if (browsableAttribute != null && !browsableAttribute.Browsable)
|
||||
continue;
|
||||
|
||||
// Get JSON property name (fallback to property name)
|
||||
string parameterName = GetJsonPropertyName(property);
|
||||
|
||||
// Get property type information
|
||||
string parameterType = GetParameterType(property.PropertyType);
|
||||
|
||||
// Get description from attributes
|
||||
string description = GetDescription(property);
|
||||
|
||||
// Get default value from instance
|
||||
object defaultValue = property.GetValue(defaultInstance);
|
||||
|
||||
// Get enum values if applicable
|
||||
string[] enumValues = GetEnumValues(property.PropertyType);
|
||||
|
||||
// Check if property is required
|
||||
bool isRequired = IsRequired(property);
|
||||
if (isRequired)
|
||||
{
|
||||
required.Add(parameterName);
|
||||
}
|
||||
|
||||
// Create parameter info
|
||||
ParameterInfo paramInfo = new ParameterInfo(
|
||||
parameterType,
|
||||
description,
|
||||
defaultValue,
|
||||
enumValues
|
||||
);
|
||||
|
||||
properties[parameterName] = paramInfo;
|
||||
}
|
||||
|
||||
return new ToolParameterSchema(properties, required.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get JSON property name from JsonProperty attribute or property name
|
||||
/// </summary>
|
||||
private static string GetJsonPropertyName(PropertyInfo property)
|
||||
{
|
||||
JsonPropertyAttribute jsonAttr = property.GetCustomAttribute<JsonPropertyAttribute>();
|
||||
return jsonAttr?.PropertyName ?? property.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert .NET type to parameter type string
|
||||
/// </summary>
|
||||
private static string GetParameterType(Type type)
|
||||
{
|
||||
// Handle nullable types
|
||||
Type underlyingType = Nullable.GetUnderlyingType(type) ?? type;
|
||||
|
||||
if (underlyingType == typeof(string))
|
||||
return "string";
|
||||
if (underlyingType == typeof(bool))
|
||||
return "boolean";
|
||||
if (underlyingType == typeof(int) || underlyingType == typeof(long) ||
|
||||
underlyingType == typeof(float) || underlyingType == typeof(double) ||
|
||||
underlyingType == typeof(decimal))
|
||||
return "number";
|
||||
if (underlyingType.IsArray || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)))
|
||||
return "array";
|
||||
if (underlyingType.IsEnum)
|
||||
return "string"; // Enums are treated as strings with enum constraints
|
||||
if (IsDictionaryType(underlyingType))
|
||||
return "object";
|
||||
|
||||
return "string"; // Default fallback
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if type is a dictionary type (Dictionary, IDictionary, IReadOnlyDictionary)
|
||||
/// </summary>
|
||||
private static bool IsDictionaryType(Type type)
|
||||
{
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
Type genericDef = type.GetGenericTypeDefinition();
|
||||
if (genericDef == typeof(Dictionary<,>) ||
|
||||
genericDef == typeof(IDictionary<,>) ||
|
||||
genericDef == typeof(IReadOnlyDictionary<,>))
|
||||
return true;
|
||||
}
|
||||
return type.GetInterfaces().Any(i =>
|
||||
i.IsGenericType &&
|
||||
(i.GetGenericTypeDefinition() == typeof(IDictionary<,>) ||
|
||||
i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get description from Description attribute or generate default
|
||||
/// </summary>
|
||||
private static string GetDescription(PropertyInfo property)
|
||||
{
|
||||
DescriptionAttribute descAttr = property.GetCustomAttribute<DescriptionAttribute>();
|
||||
if (descAttr != null)
|
||||
return descAttr.Description;
|
||||
|
||||
// Generate default description
|
||||
return $"Parameter: {property.Name}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get enum values if property type is enum
|
||||
/// </summary>
|
||||
private static string[] GetEnumValues(Type type)
|
||||
{
|
||||
Type underlyingType = Nullable.GetUnderlyingType(type) ?? type;
|
||||
|
||||
if (underlyingType.IsEnum)
|
||||
{
|
||||
return Enum.GetNames(underlyingType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if property is required using custom Required attribute
|
||||
/// For now, returns false as Unity doesn't support DataAnnotations
|
||||
/// Can be extended with custom attributes if needed
|
||||
/// </summary>
|
||||
private static bool IsRequired(PropertyInfo property)
|
||||
{
|
||||
// TODO: Implement custom Required attribute if needed
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9d06a624dd5d4129b95d39ea144a412
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,424 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Stopwatch = System.Diagnostics.Stopwatch;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
// Related classes:
|
||||
// - UnityToolExecutor: Uses this registry to execute _tools.
|
||||
// - IUnityTool: The interface for all _tools stored in this registry.
|
||||
// - AbstractUnityTool: The base class for most tool implementations.
|
||||
// - McpToolAttribute: Attribute used to discover and register _tools automatically.
|
||||
/// <summary>
|
||||
/// Unity MCP tool registry class
|
||||
/// Supports dynamic tool registration, allowing users to add their own _tools
|
||||
/// </summary>
|
||||
public class UnityToolRegistry
|
||||
{
|
||||
private readonly Dictionary<string, IUnityTool> _tools = new();
|
||||
|
||||
/// <summary>
|
||||
/// Singleton instance for global access
|
||||
/// </summary>
|
||||
public static UnityToolRegistry Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor
|
||||
/// Auto-registers standard _tools
|
||||
/// </summary>
|
||||
public UnityToolRegistry()
|
||||
{
|
||||
Instance = this;
|
||||
RegisterDefaultTools();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register standard _tools
|
||||
/// </summary>
|
||||
private void RegisterDefaultTools()
|
||||
{
|
||||
// Register _tools with attribute-based discovery
|
||||
RegisterToolsWithAttributes();
|
||||
|
||||
// Manual registration for _tools without attributes (for backward compatibility)
|
||||
RegisterManualTools();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register _tools using attribute-based discovery
|
||||
/// </summary>
|
||||
private void RegisterToolsWithAttributes()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get all assemblies in the current domain
|
||||
Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
|
||||
|
||||
List<Type> toolTypes = new List<Type>();
|
||||
|
||||
foreach (Assembly assembly in assemblies)
|
||||
{
|
||||
// Find all types with McpTool attribute that implement IUnityTool
|
||||
Type[] types = assembly.GetTypes()
|
||||
.Where(type => type.GetCustomAttribute<McpToolAttribute>() != null)
|
||||
.Where(type => typeof(IUnityTool).IsAssignableFrom(type))
|
||||
.Where(type => !type.IsAbstract && !type.IsInterface)
|
||||
.ToArray();
|
||||
|
||||
toolTypes.AddRange(types);
|
||||
}
|
||||
|
||||
// Register all _tools - filtering will be handled by TypeScript side
|
||||
foreach (Type type in toolTypes)
|
||||
{
|
||||
// Security: Validate type before creating instance
|
||||
if (!IsValidToolType(type))
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"{McpConstants.SECURITY_LOG_PREFIX} Skipping invalid tool type: {type.FullName}");
|
||||
continue;
|
||||
}
|
||||
|
||||
IUnityTool tool = (IUnityTool)Activator.CreateInstance(type);
|
||||
RegisterTool(tool);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register _tools manually (for backward compatibility)
|
||||
/// </summary>
|
||||
private void RegisterManualTools()
|
||||
{
|
||||
// Only register _tools that don't have the McpTool attribute
|
||||
// This prevents double registration
|
||||
|
||||
if (!IsToolTypeRegistered<PingTool>())
|
||||
{
|
||||
RegisterTool(new PingTool());
|
||||
}
|
||||
|
||||
if (!IsToolTypeRegistered<CompileTool>())
|
||||
{
|
||||
RegisterTool(new CompileTool());
|
||||
}
|
||||
|
||||
if (!IsToolTypeRegistered<GetLogsTool>())
|
||||
{
|
||||
RegisterTool(new GetLogsTool());
|
||||
}
|
||||
|
||||
if (!IsToolTypeRegistered<RunTestsTool>())
|
||||
{
|
||||
RegisterTool(new RunTestsTool());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a tool type is already registered
|
||||
/// </summary>
|
||||
private bool IsToolTypeRegistered<T>() where T : IUnityTool
|
||||
{
|
||||
return _tools.Values.Any(tool => tool.GetType() == typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Security: Validate if the type is safe to instantiate
|
||||
/// </summary>
|
||||
private bool IsValidToolType(Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Must implement IUnityTool
|
||||
if (!typeof(IUnityTool).IsAssignableFrom(type))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must not be abstract or interface
|
||||
if (type.IsAbstract || type.IsInterface)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have McpTool attribute
|
||||
if (type.GetCustomAttribute<McpToolAttribute>() == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have parameterless constructor
|
||||
if (type.GetConstructor(Type.EmptyTypes) == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Debug.LogError($"{McpConstants.SECURITY_LOG_PREFIX} Error validating tool type {type?.FullName}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register tool
|
||||
/// </summary>
|
||||
/// <param name="tool">Tool to register</param>
|
||||
public void RegisterTool(IUnityTool tool)
|
||||
{
|
||||
if (tool == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tool));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tool.ToolName))
|
||||
{
|
||||
throw new ArgumentException("Tool name cannot be null or empty", nameof(tool));
|
||||
}
|
||||
|
||||
_tools[tool.ToolName] = tool;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregister tool
|
||||
/// </summary>
|
||||
/// <param name="toolName">Name of tool to unregister</param>
|
||||
public void UnregisterTool(string toolName)
|
||||
{
|
||||
_tools.Remove(toolName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute tool
|
||||
/// </summary>
|
||||
/// <param name="toolName">Tool name</param>
|
||||
/// <param name="paramsToken">Parameters</param>
|
||||
/// <returns>Execution result</returns>
|
||||
/// <exception cref="ArgumentException">When tool is unknown</exception>
|
||||
/// <exception cref="McpSecurityException">When tool is blocked by security settings</exception>
|
||||
public async Task<BaseToolResponse> ExecuteToolAsync(string toolName, JToken paramsToken)
|
||||
{
|
||||
if (!_tools.TryGetValue(toolName, out IUnityTool tool))
|
||||
{
|
||||
throw new ArgumentException($"Unknown tool: {toolName}");
|
||||
}
|
||||
|
||||
if (!ToolSettings.IsToolEnabled(toolName)
|
||||
&& !ToolExecutionAvailability.ShouldReportDependencyUnavailableBeforeDisabled(toolName))
|
||||
{
|
||||
throw new ToolDisabledException(toolName);
|
||||
}
|
||||
|
||||
// Security check - validate tool before execution
|
||||
if (!McpSecurityChecker.IsToolAllowed(toolName))
|
||||
{
|
||||
throw new McpSecurityException(toolName, "Tool is blocked by security settings");
|
||||
}
|
||||
|
||||
Stopwatch mainThreadHopStopwatch = Stopwatch.StartNew();
|
||||
await MainThreadSwitcher.SwitchToMainThread();
|
||||
mainThreadHopStopwatch.Stop();
|
||||
|
||||
Stopwatch toolBodyStopwatch = Stopwatch.StartNew();
|
||||
BaseToolResponse response = await tool.ExecuteAsync(paramsToken);
|
||||
toolBodyStopwatch.Stop();
|
||||
|
||||
AppendExecuteDynamicCodeTimingsIfSupported(
|
||||
response,
|
||||
$"[Perf] RegistryMainThreadHop: {mainThreadHopStopwatch.Elapsed.TotalMilliseconds:F1}ms",
|
||||
$"[Perf] ToolBody: {toolBodyStopwatch.Elapsed.TotalMilliseconds:F1}ms");
|
||||
return response;
|
||||
}
|
||||
|
||||
private static void AppendExecuteDynamicCodeTimingsIfSupported(
|
||||
BaseToolResponse response,
|
||||
params string[] timingEntries)
|
||||
{
|
||||
if (response is not ExecuteDynamicCodeResponse executeDynamicCodeResponse)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (executeDynamicCodeResponse.Timings == null)
|
||||
{
|
||||
executeDynamicCodeResponse.Timings = new List<string>();
|
||||
}
|
||||
|
||||
executeDynamicCodeResponse.Timings.AddRange(timingEntries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get detailed information of registered _tools
|
||||
/// </summary>
|
||||
/// <returns>Array of tool information</returns>
|
||||
public ToolInfo[] GetRegisteredTools()
|
||||
{
|
||||
return _tools.Values
|
||||
.Where(tool => ToolExecutionAvailability.ShouldExposeInRegisteredTools(tool.ToolName))
|
||||
.Select(tool =>
|
||||
{
|
||||
// Check if tool has McpTool attribute with DisplayDevelopmentOnly
|
||||
bool displayDevelopmentOnly = false;
|
||||
McpToolAttribute attribute = tool.GetType().GetCustomAttribute<McpToolAttribute>();
|
||||
if (attribute != null)
|
||||
{
|
||||
displayDevelopmentOnly = attribute.DisplayDevelopmentOnly;
|
||||
}
|
||||
|
||||
// Check security settings
|
||||
bool isAllowed = McpSecurityChecker.IsToolAllowed(tool.ToolName);
|
||||
|
||||
// Get description from attribute
|
||||
string description = attribute?.Description ?? "";
|
||||
|
||||
// Modify description for blocked _tools
|
||||
if (!isAllowed)
|
||||
{
|
||||
description = $"[BLOCKED] {description} - Blocked by security settings";
|
||||
}
|
||||
|
||||
return new ToolInfo(tool.ToolName, description, tool.ParameterSchema, displayDevelopmentOnly);
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all tools including disabled ones. Used by Tool Settings UI.
|
||||
/// </summary>
|
||||
public ToolInfo[] GetAllRegisteredToolInfos()
|
||||
{
|
||||
return _tools.Values.Select(tool =>
|
||||
{
|
||||
McpToolAttribute attribute = tool.GetType().GetCustomAttribute<McpToolAttribute>();
|
||||
string description = attribute?.Description ?? "";
|
||||
bool displayDevelopmentOnly = attribute?.DisplayDevelopmentOnly ?? false;
|
||||
return new ToolInfo(tool.ToolName, description, tool.ParameterSchema, displayDevelopmentOnly);
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public ToolSettingsCatalogItem[] GetToolSettingsCatalog()
|
||||
{
|
||||
return _tools.Values.Select(tool =>
|
||||
{
|
||||
Type toolType = tool.GetType();
|
||||
McpToolAttribute attribute = toolType.GetCustomAttribute<McpToolAttribute>();
|
||||
string description = attribute?.Description ?? "";
|
||||
bool displayDevelopmentOnly = attribute?.DisplayDevelopmentOnly ?? false;
|
||||
bool isThirdParty = IsThirdPartyAssembly(toolType.Assembly.GetName().Name);
|
||||
|
||||
return new ToolSettingsCatalogItem(
|
||||
tool.ToolName,
|
||||
description,
|
||||
displayDevelopmentOnly,
|
||||
isThirdParty);
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a tool belongs to a third-party assembly.
|
||||
/// </summary>
|
||||
public bool IsThirdPartyTool(string toolName)
|
||||
{
|
||||
if (!_tools.TryGetValue(toolName, out IUnityTool tool))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
string assemblyName = tool.GetType().Assembly.GetName().Name;
|
||||
return IsThirdPartyAssembly(assemblyName);
|
||||
}
|
||||
|
||||
private static bool IsThirdPartyAssembly(string assemblyName)
|
||||
{
|
||||
return assemblyName != "uLoopMCP.Editor";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get tool type by name for security checking
|
||||
/// </summary>
|
||||
/// <param name="toolName">Tool name</param>
|
||||
/// <returns>Tool type or null if not found</returns>
|
||||
public Type GetToolType(string toolName)
|
||||
{
|
||||
if (_tools.TryGetValue(toolName, out IUnityTool tool))
|
||||
{
|
||||
return tool.GetType();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if specified tool is registered
|
||||
/// </summary>
|
||||
/// <param name="toolName">Tool name</param>
|
||||
/// <returns>True if registered</returns>
|
||||
public bool IsToolRegistered(string toolName)
|
||||
{
|
||||
return _tools.ContainsKey(toolName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually trigger _tools changed notification
|
||||
/// Used for manual notifications and post-compilation notifications
|
||||
/// </summary>
|
||||
public static void TriggerToolsChangedNotification()
|
||||
{
|
||||
// Call the public method in McpServerController
|
||||
McpServerController.TriggerToolChangeNotification();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class representing tool information
|
||||
/// </summary>
|
||||
public class ToolInfo
|
||||
{
|
||||
[JsonProperty("name")] public string Name { get; }
|
||||
|
||||
[JsonProperty("description")] public string Description { get; }
|
||||
|
||||
[JsonProperty("parameterSchema")] public ToolParameterSchema ParameterSchema { get; }
|
||||
|
||||
[JsonProperty("displayDevelopmentOnly")] public bool DisplayDevelopmentOnly { get; }
|
||||
|
||||
public ToolInfo(string name, string description, ToolParameterSchema parameterSchema, bool displayDevelopmentOnly = false)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
ParameterSchema = parameterSchema;
|
||||
DisplayDevelopmentOnly = displayDevelopmentOnly;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight registry metadata used by Tool Settings UI without generating parameter schemas.
|
||||
/// </summary>
|
||||
public class ToolSettingsCatalogItem
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly string Description;
|
||||
public readonly bool DisplayDevelopmentOnly;
|
||||
public readonly bool IsThirdParty;
|
||||
|
||||
public ToolSettingsCatalogItem(
|
||||
string name,
|
||||
string description,
|
||||
bool displayDevelopmentOnly,
|
||||
bool isThirdParty)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
DisplayDevelopmentOnly = displayDevelopmentOnly;
|
||||
IsThirdParty = isThirdParty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b13155ca2a4cd42b2b0c2997a7925511
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Data model representing validation result
|
||||
/// Used in Application Service Layer
|
||||
/// </summary>
|
||||
public class ValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether validation was successful
|
||||
/// </summary>
|
||||
public bool IsValid { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message when validation fails
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create ValidationResult
|
||||
/// </summary>
|
||||
/// <param name="isValid">Validation result</param>
|
||||
/// <param name="errorMessage">Error message (null on success)</param>
|
||||
public ValidationResult(bool isValid, string errorMessage = null)
|
||||
{
|
||||
IsValid = isValid;
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create success result
|
||||
/// </summary>
|
||||
/// <returns>ValidationResult representing success</returns>
|
||||
public static ValidationResult Success() => new(true);
|
||||
|
||||
/// <summary>
|
||||
/// Create failure result
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">Error message</param>
|
||||
/// <returns>ValidationResult representing failure</returns>
|
||||
public static ValidationResult Failure(string errorMessage) => new(false, errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de99bb9f98ad74025bd8be4e41a953af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dbe57138c4a024b33bfe815d180e9068
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Response for debug sleep tool execution
|
||||
/// Related classes:
|
||||
/// - SleepTool: Implementation that generates this response
|
||||
/// - SleepSchema: Input parameters for the sleep tool
|
||||
/// - BaseToolResponse: Base class providing timing information
|
||||
/// </summary>
|
||||
public class SleepResponse : BaseToolResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Message indicating the result of the sleep operation
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Actual number of seconds the tool slept before completion or timeout
|
||||
/// </summary>
|
||||
public int ActualSleepSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the operation completed successfully or was cancelled
|
||||
/// </summary>
|
||||
public bool WasCancelled { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28eaf066ee0a94a999dd67f9f4baabcd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema for debug sleep tool parameters
|
||||
/// Related classes:
|
||||
/// - SleepTool: Implementation of the debug sleep tool
|
||||
/// - SleepResponse: Response structure for the sleep tool
|
||||
/// </summary>
|
||||
public class SleepSchema : BaseToolSchema
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of seconds to sleep for testing purposes (default: 15)
|
||||
/// </summary>
|
||||
[Description("Number of seconds to sleep for testing purposes (default: 15)")]
|
||||
public int SleepSeconds { get; set; } = 15;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65adc65e8b27d41e889d4ccb67be267b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Debug tool that sleeps for specified duration to test timeout functionality
|
||||
/// Related classes:
|
||||
/// - SleepSchema: Input parameters including sleep duration and timeout
|
||||
/// - SleepResponse: Output containing results and timing information
|
||||
/// - AbstractUnityTool: Base class providing timeout infrastructure
|
||||
/// </summary>
|
||||
[McpTool(
|
||||
DisplayDevelopmentOnly = true,
|
||||
Description = "Debug tool that sleeps for specified duration to test timeout functionality"
|
||||
)]
|
||||
public class SleepTool : AbstractUnityTool<SleepSchema, SleepResponse>
|
||||
{
|
||||
public override string ToolName => "debug-sleep";
|
||||
|
||||
protected override async Task<SleepResponse> ExecuteAsync(SleepSchema parameters, CancellationToken cancellationToken)
|
||||
{
|
||||
int actualSleepSeconds = 0;
|
||||
|
||||
// Sleep 1 second at a time, checking for cancellation between each sleep
|
||||
for (int i = 0; i < parameters.SleepSeconds; i++)
|
||||
{
|
||||
// Check for cancellation before each sleep iteration
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Sleep for 1 second with cancellation support
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
|
||||
actualSleepSeconds = i + 1;
|
||||
}
|
||||
|
||||
return new SleepResponse
|
||||
{
|
||||
Message = $"Successfully slept for {actualSleepSeconds} seconds",
|
||||
ActualSleepSeconds = actualSleepSeconds,
|
||||
WasCancelled = false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3819fd5fb05cb4309a2229a93f79f74a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae802852e2dfd47dc979543d41bb00d2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,31 @@
|
||||
# execute-dynamic-code design notes
|
||||
|
||||
This directory owns the user-facing `execute-dynamic-code` tool contract.
|
||||
The public contract must stay stable even when the internal compiler strategy changes.
|
||||
|
||||
## Fast path intent
|
||||
|
||||
- The preferred compilation path is Unity-bundled Roslyn.
|
||||
- `SharedRoslynCompilerWorkerHost` keeps a warm compiler process so structural cache misses stay fast.
|
||||
- The shared worker is an optimization only. It must not own response shaping, security validation, or tool-specific policy.
|
||||
|
||||
## Prewarm intent
|
||||
|
||||
- Auto prewarm exists to hide first-use compiler startup cost after server startup or recovery.
|
||||
- Prewarm must go through the same runtime facade used by normal requests.
|
||||
- Prewarm must be single-flight. It should never create a parallel execution lane or bypass runtime idleness checks.
|
||||
- Prewarm is allowed to improve latency only. It must not change observable tool behavior.
|
||||
|
||||
## Fallback intent
|
||||
|
||||
- Preferred order is: shared Roslyn worker -> one-shot Roslyn -> AssemblyBuilder fallback.
|
||||
- Fallback changes performance, not semantics.
|
||||
- Any fallback from the ideal fast path is a maintainer-visible error condition and must be logged through `DynamicCompilationHealthMonitor`.
|
||||
|
||||
## Invariants
|
||||
|
||||
- Security validation always runs in Unity after compilation and before execution.
|
||||
- In restricted mode, metadata validation and IL validation must both complete before `Assembly.Load`.
|
||||
- Tool entry points and use cases depend on runtime-facing facades, not compiler backend details.
|
||||
- Worker state is disposable. Domain reload or worker protocol failure must be handled by rebuilding state, not by preserving it.
|
||||
- The compile backend may change, but `ExecuteDynamicCodeResponse` shape and user-facing behavior must remain stable.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a32019de73d24a92b23228d40898acf
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Response for dynamic code execution tool
|
||||
|
||||
/// Related classes: ExecuteDynamicCodeTool, ExecuteDynamicCodeSchema
|
||||
/// </summary>
|
||||
public class ExecuteDynamicCodeResponse : BaseToolResponse
|
||||
{
|
||||
private const bool EmitTimingsInJsonResponses = false;
|
||||
|
||||
/// <summary>Execution success flag</summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>Execution result</summary>
|
||||
public string Result { get; set; }
|
||||
|
||||
/// <summary>Log messages</summary>
|
||||
public List<string> Logs { get; set; } = new();
|
||||
|
||||
/// <summary>Compilation errors</summary>
|
||||
public List<CompilationErrorDto> CompilationErrors { get; set; } = new();
|
||||
|
||||
/// <summary>Error message (on failure)</summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>Current security level</summary>
|
||||
public string SecurityLevel { get; set; }
|
||||
|
||||
/// <summary>Error message (alias for ErrorMessage)</summary>
|
||||
public string Error
|
||||
{
|
||||
get => ErrorMessage;
|
||||
set => ErrorMessage = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Code formatted for compilation
|
||||
/// (After extracting/moving using statements and applying class/method wrapping)
|
||||
/// Allows checking the actual compiled code during debugging
|
||||
/// </summary>
|
||||
public string UpdatedCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Summary of diagnostics (unique count, total count, first error brief)
|
||||
/// </summary>
|
||||
public string DiagnosticsSummary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Structured diagnostics for rich clients (line/column/code/message/hint/suggestions)
|
||||
/// </summary>
|
||||
public List<CompilationErrorDto> Diagnostics { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight internal timings for benchmark comparison.
|
||||
/// </summary>
|
||||
public List<string> Timings { get; set; } = new();
|
||||
|
||||
// Keep timings available in memory for diagnostics and readiness decisions
|
||||
// while avoiding noisy default payloads for normal execute-dynamic-code users.
|
||||
public bool ShouldSerializeTimings()
|
||||
{
|
||||
return EmitTimingsInJsonResponses;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for compilation error information
|
||||
/// </summary>
|
||||
public class CompilationErrorDto
|
||||
{
|
||||
/// <summary>Error message</summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>Line number</summary>
|
||||
public int Line { get; set; }
|
||||
|
||||
/// <summary>Column number</summary>
|
||||
public int Column { get; set; }
|
||||
|
||||
/// <summary>Compiler error code (e.g., CS0103)</summary>
|
||||
public string ErrorCode { get; set; }
|
||||
|
||||
/// <summary>Optional hint for resolving the error</summary>
|
||||
public string Hint { get; set; }
|
||||
|
||||
/// <summary>Suggested fixes (e.g., add using or qualify)</summary>
|
||||
public List<string> Suggestions { get; set; } = new();
|
||||
|
||||
/// <summary>Context lines around the error with a caret pointer</summary>
|
||||
public string Context { get; set; }
|
||||
|
||||
/// <summary>Pointer column for caret rendering (1-based)</summary>
|
||||
public int PointerColumn { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88dbc381725914ce488e21b9b27a2cb8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class ExecuteDynamicCodeResponseTimingAugmenter
|
||||
{
|
||||
public static void AppendTimingEntries(
|
||||
ExecuteDynamicCodeResponse response,
|
||||
double mainThreadWaitMilliseconds,
|
||||
double toolTotalMilliseconds,
|
||||
double requestTotalMilliseconds)
|
||||
{
|
||||
if (response == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.Timings == null)
|
||||
{
|
||||
response.Timings = new List<string>();
|
||||
}
|
||||
|
||||
response.Timings.Add($"[Perf] MainThreadWait: {mainThreadWaitMilliseconds:F1}ms");
|
||||
response.Timings.Add($"[Perf] ToolTotal: {toolTotalMilliseconds:F1}ms");
|
||||
response.Timings.Add($"[Perf] RequestTotal: {requestTotalMilliseconds:F1}ms");
|
||||
response.Timings.AddRange(DynamicCodeStartupTelemetry.CreateTimingEntries());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8683636c860b448cf93c98208c36946b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameter schema for dynamic code execution tool
|
||||
/// Related classes: ExecuteDynamicCodeTool, ExecuteDynamicCodeResponse
|
||||
/// </summary>
|
||||
public class ExecuteDynamicCodeSchema : BaseToolSchema
|
||||
{
|
||||
/// <summary>C# code to execute</summary>
|
||||
[Description(@"Editor automation only — no file I/O, no script authoring.
|
||||
|
||||
Direct statements only (no classes/namespaces/methods); return is optional (auto 'return null;' if omitted).
|
||||
|
||||
You may include using directives at the top; they are hoisted above the wrapper.
|
||||
Example:
|
||||
using UnityEngine;
|
||||
var x = Mathf.PI;
|
||||
return x;
|
||||
|
||||
Do:
|
||||
- Prefab/material wiring (PrefabUtility)
|
||||
- AddComponent + reference wiring (SerializedObject)
|
||||
- Scene/hierarchy edits
|
||||
|
||||
Don’t:
|
||||
- System.IO.* (File/Directory/Path)
|
||||
- AssetDatabase.CreateFolder / file writes
|
||||
- Create/edit .cs/.asmdef (use Terminal/IDE instead)
|
||||
|
||||
Need files/dirs? Run terminal commands.")]
|
||||
public string Code { get; set; } = "";
|
||||
|
||||
/// <summary>Runtime parameters (advanced; usually unnecessary)</summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)]
|
||||
[Description(@"Advanced, optional parameters passed to the snippet.
|
||||
|
||||
- For throwaway snippets, prefer defining locals directly in code
|
||||
- Keys are not preserved; values map to parameters[""param0""], parameters[""param1""], ...
|
||||
- Use when injecting UnityEngine.Object or sensitive values, or reusing a snippet with varying data
|
||||
- Use object values supported by the Editor; prefer fully-qualified type names for ambiguous refs (e.g., UnityEngine.Object)
|
||||
|
||||
- Input type must be an OBJECT. Strings like ""{}"" are invalid.
|
||||
- Do: omit 'Parameters' entirely, or use {}
|
||||
- Don't: ""{}"" (string), ""{""a"":1}"" (string)
|
||||
|
||||
Examples:
|
||||
OK: ""Parameters"": {}
|
||||
OK: (omit the 'Parameters' property)
|
||||
NG: ""Parameters"": ""{}""")]
|
||||
public Dictionary<string, object> Parameters { get; set; } = new();
|
||||
|
||||
/// <summary>Compile only (do not execute)</summary>
|
||||
[Description(@"Compile only (no execution).
|
||||
|
||||
- Uses Roslyn validation to surface diagnostics
|
||||
- For new MonoBehaviours: create .cs → compile (mcp__uLoopMCP__compile with ForceRecompile=false) → ensure ErrorCount=0 → AddComponent")]
|
||||
public bool CompileOnly { get; set; } = false;
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)]
|
||||
public bool YieldToForegroundRequests { get; set; } = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96f1941ea53e64fbf9dca3d1b273ae72
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// MCP Dynamic C# Code Execution Tool.
|
||||
/// Delegates workflow orchestration to the dedicated execute-dynamic-code use case.
|
||||
/// </summary>
|
||||
[McpTool(Description = @"Execute C# code dynamically in Unity Editor for editor automation.
|
||||
|
||||
Use find-game-objects first for basic selected GameObject discovery or property inspection. Use this after built-in inspection tools are not enough or when you need to modify Unity state.
|
||||
|
||||
Direct statements only (no classes/namespaces/methods); return is optional (auto 'return null;' if omitted).
|
||||
|
||||
You may include using directives at the top; they are hoisted above the wrapper.
|
||||
Example:
|
||||
using UnityEngine;
|
||||
var x = Mathf.PI;
|
||||
return x;
|
||||
|
||||
Do:
|
||||
- Prefab/material wiring (PrefabUtility)
|
||||
- AddComponent + reference wiring (SerializedObject)
|
||||
- Scene/hierarchy edits
|
||||
|
||||
Don't:
|
||||
- System.IO.* (File/Directory/Path)
|
||||
- AssetDatabase.CreateFolder / file writes
|
||||
- Create/edit .cs/.asmdef (use Terminal/IDE instead)
|
||||
|
||||
Need files/dirs? Run terminal commands.
|
||||
|
||||
See examples at {project_root}/.claude/skills/unity-cli-loop/uloop-execute-dynamic-code/examples/")]
|
||||
public class ExecuteDynamicCodeTool : AbstractUnityTool<ExecuteDynamicCodeSchema, ExecuteDynamicCodeResponse>
|
||||
{
|
||||
public override string ToolName => "execute-dynamic-code";
|
||||
|
||||
protected override async Task<ExecuteDynamicCodeResponse> ExecuteAsync(
|
||||
ExecuteDynamicCodeSchema parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
IExecuteDynamicCodeUseCase useCase = await DynamicCodeServices.GetExecuteDynamicCodeUseCaseAsync();
|
||||
return await useCase.ExecuteAsync(parameters, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0a0005c984014fe9ae1227c1e458432
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e4545560316c242f08f6034dc0c57177
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 `uloop execute-dynamic-code`: $ARGUMENTS
|
||||
|
||||
For basic selected GameObject discovery or property inspection, use `find-game-objects --search-mode Selected` before this tool. Use this tool after the built-in inspection tools are not enough or when you need to modify Unity state.
|
||||
|
||||
## 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 `uloop execute-dynamic-code --code-file <path>`
|
||||
4. Use `uloop execute-dynamic-code --code '<code>'` only for short one-line snippets
|
||||
5. If execution fails, adjust code and retry
|
||||
6. Report the execution result
|
||||
|
||||
## Parameters
|
||||
|
||||
- `--code '<code>'`: Inline C# statements to execute. Use this for one-line snippets only.
|
||||
- `--code-file <path>`: 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 `uloop execute-dynamic-code --code 'using UnityEngine; return Mathf.PI;'`. PowerShell single-quoted strings can contain normal double quotes, for example `uloop 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 `uloop 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
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2445f075f1af349fc8bf73259f1df493
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5155f736569e440ea4679a66a5ee36b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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<string> paths = new List<string>();
|
||||
|
||||
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<string> paths = new List<string>();
|
||||
|
||||
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<string> paths = new List<string>();
|
||||
|
||||
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<GameObject>(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";
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39fd76f51419343c1bbc8844b9e1f45f
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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<Transform> transforms = new List<Transform>();
|
||||
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<Rigidbody>() == null)
|
||||
{
|
||||
Undo.AddComponent<Rigidbody>(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<Material>(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<ScriptableObject>(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<Rigidbody>();
|
||||
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<Material>(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<MeshRenderer>();
|
||||
if (renderer != null)
|
||||
{
|
||||
Undo.RecordObject(renderer, "");
|
||||
renderer.sharedMaterial = newMat;
|
||||
replaced++;
|
||||
}
|
||||
}
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
return $"Replaced material on {replaced} objects";
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd5beae8d98da49f590990720e8d86be
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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<GameObject>(FindObjectsSortMode.None);
|
||||
List<string> objectsWithMissing = new List<string>();
|
||||
|
||||
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<GameObject>(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<string> missingRefs = new List<string>();
|
||||
|
||||
Component[] components = selected.GetComponents<Component>();
|
||||
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<GameObject>(FindObjectsSortMode.None);
|
||||
List<string> results = new List<string>();
|
||||
|
||||
foreach (GameObject obj in allObjects)
|
||||
{
|
||||
Component[] components = obj.GetComponents<Component>();
|
||||
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<string> usedMaterials = new HashSet<string>();
|
||||
|
||||
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<string> unusedMaterials = new List<string>();
|
||||
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<GameObject>(FindObjectsSortMode.None);
|
||||
List<string> emptyObjects = new List<string>();
|
||||
|
||||
foreach (GameObject obj in allObjects)
|
||||
{
|
||||
Component[] components = obj.GetComponents<Component>();
|
||||
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<GameObject>(FindObjectsSortMode.None);
|
||||
Dictionary<string, int> nameCounts = new Dictionary<string, int>();
|
||||
|
||||
foreach (GameObject obj in allObjects)
|
||||
{
|
||||
if (nameCounts.ContainsKey(obj.name))
|
||||
{
|
||||
nameCounts[obj.name]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
nameCounts[obj.name] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
List<string> duplicates = new List<string>();
|
||||
foreach (KeyValuePair<string, int> 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<GameObject>(FindObjectsSortMode.None);
|
||||
List<string> brokenPrefabs = new List<string>();
|
||||
|
||||
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<GameObject>(FindObjectsSortMode.None);
|
||||
List<string> negativeScale = new List<string>();
|
||||
|
||||
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<GameObject>(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<Component>();
|
||||
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<string> largeMeshes = new List<string>();
|
||||
int threshold = 10000;
|
||||
|
||||
foreach (string guid in meshGuids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
Mesh mesh = AssetDatabase.LoadAssetAtPath<Mesh>(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<string> invalidRefs = new List<string>();
|
||||
|
||||
foreach (string guid in guids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
ScriptableObject so = AssetDatabase.LoadAssetAtPath<ScriptableObject>(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))}";
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13f19aa1f6ea44f119e9e253e14d731c
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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<Material>(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<Material>(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<Material>(matPath);
|
||||
Texture2D tex = AssetDatabase.LoadAssetAtPath<Texture2D>(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<Material>(matPath);
|
||||
|
||||
GameObject selected = Selection.activeGameObject;
|
||||
if (selected == null)
|
||||
{
|
||||
return "No GameObject selected";
|
||||
}
|
||||
|
||||
Renderer renderer = selected.GetComponent<Renderer>();
|
||||
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<Material>(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<string> matchingMaterials = new List<string>();
|
||||
|
||||
foreach (string guid in guids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
Material mat = AssetDatabase.LoadAssetAtPath<Material>(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<Material>(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}";
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b5ad08d159fb4390911b484746791d1
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
uloop 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
|
||||
uloop 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
|
||||
uloop 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;
|
||||
'@
|
||||
|
||||
uloop 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"}
|
||||
'@
|
||||
|
||||
uloop execute-dynamic-code --code $code --parameters $parameters
|
||||
```
|
||||
|
||||
## Click UI Button by Path
|
||||
|
||||
```csharp
|
||||
using UnityEngine.UI;
|
||||
|
||||
Button btn = GameObject.Find("Canvas/StartButton")?.GetComponent<Button>();
|
||||
if (btn == null) return "Button not found";
|
||||
|
||||
btn.onClick.Invoke();
|
||||
return $"Clicked {btn.gameObject.name}";
|
||||
```
|
||||
|
||||
## Click UI Button by Search
|
||||
|
||||
```csharp
|
||||
using UnityEngine.UI;
|
||||
using System.Linq;
|
||||
|
||||
Button[] buttons = Object.FindObjectsByType<Button>(FindObjectsSortMode.None);
|
||||
Button target = buttons.FirstOrDefault(b => b.gameObject.name == "PlayButton");
|
||||
if (target == null) return $"PlayButton not found. Available: {string.Join(", ", buttons.Select(b => b.gameObject.name))}";
|
||||
|
||||
target.onClick.Invoke();
|
||||
return $"Clicked {target.gameObject.name}";
|
||||
```
|
||||
|
||||
## Toggle GameObject Active State
|
||||
|
||||
```csharp
|
||||
GameObject obj = GameObject.Find("Enemy");
|
||||
if (obj == null) return "Enemy not found";
|
||||
|
||||
obj.SetActive(!obj.activeSelf);
|
||||
return $"{obj.name} is now {(obj.activeSelf ? "active" : "inactive")}";
|
||||
```
|
||||
|
||||
## Invoke Method on MonoBehaviour
|
||||
|
||||
```csharp
|
||||
using System.Reflection;
|
||||
|
||||
GameObject player = GameObject.Find("Player");
|
||||
if (player == null) return "Player not found";
|
||||
|
||||
MonoBehaviour script = player.GetComponent("PlayerController") as MonoBehaviour;
|
||||
if (script == null) return "PlayerController not found";
|
||||
|
||||
MethodInfo method = script.GetType().GetMethod("TakeDamage");
|
||||
if (method == null) return "TakeDamage method not found";
|
||||
|
||||
method.Invoke(script, new object[] { 10f });
|
||||
return $"Invoked TakeDamage(10) on {player.name}";
|
||||
```
|
||||
|
||||
## Set Field Value at Runtime
|
||||
|
||||
```csharp
|
||||
using System.Reflection;
|
||||
|
||||
GameObject player = GameObject.Find("Player");
|
||||
if (player == null) return "Player not found";
|
||||
|
||||
MonoBehaviour script = player.GetComponent("PlayerController") as MonoBehaviour;
|
||||
if (script == null) return "PlayerController not found";
|
||||
|
||||
FieldInfo field = script.GetType().GetField("moveSpeed", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (field == null) return "moveSpeed field not found";
|
||||
|
||||
field.SetValue(script, 20f);
|
||||
return $"Set moveSpeed to 20 on {player.name}";
|
||||
```
|
||||
|
||||
## Move Player to Position
|
||||
|
||||
```csharp
|
||||
GameObject player = GameObject.Find("Player");
|
||||
if (player == null) return "Player not found";
|
||||
|
||||
Vector3 targetPos = new Vector3(5f, 0f, 10f);
|
||||
player.transform.position = targetPos;
|
||||
return $"Moved {player.name} to {targetPos}";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Tool Combination Workflows
|
||||
|
||||
Examples of combining `execute-dynamic-code` with other uloop tools for multi-step PlayMode automation.
|
||||
|
||||
## find-game-objects → Click Button
|
||||
|
||||
Use `find-game-objects` to discover buttons with their hierarchy paths, then click one via `execute-dynamic-code`.
|
||||
|
||||
**Step 1**: Find all GameObjects with Button component
|
||||
|
||||
```powershell
|
||||
uloop find-game-objects --required-components UnityEngine.UI.Button
|
||||
```
|
||||
|
||||
**Step 2**: Click the target button using the path from Step 1
|
||||
|
||||
```csharp
|
||||
using UnityEngine.UI;
|
||||
|
||||
// Use the path returned by find-game-objects (e.g. "Canvas/MainMenu/StartButton")
|
||||
GameObject btnObj = GameObject.Find("Canvas/MainMenu/StartButton");
|
||||
if (btnObj == null) return "Button not found at path";
|
||||
|
||||
Button btn = btnObj.GetComponent<Button>();
|
||||
if (btn == null) return "No Button component";
|
||||
|
||||
btn.onClick.Invoke();
|
||||
return $"Clicked {btnObj.name}";
|
||||
```
|
||||
|
||||
## get-hierarchy → Navigate UI and Click
|
||||
|
||||
Use `get-hierarchy` to explore the UI tree structure, then target the right element.
|
||||
|
||||
**Step 1**: Get Canvas hierarchy to understand UI structure
|
||||
|
||||
```powershell
|
||||
uloop get-hierarchy --root-path 'Canvas' --max-depth 3
|
||||
```
|
||||
|
||||
**Step 2**: Based on the hierarchy JSON, click the desired button
|
||||
|
||||
```csharp
|
||||
using UnityEngine.UI;
|
||||
|
||||
// Path identified from hierarchy output
|
||||
GameObject btnObj = GameObject.Find("Canvas/SettingsPanel/AudioTab/MuteToggle");
|
||||
if (btnObj == null) return "MuteToggle not found";
|
||||
|
||||
Toggle toggle = btnObj.GetComponent<Toggle>();
|
||||
if (toggle != null)
|
||||
{
|
||||
toggle.isOn = !toggle.isOn;
|
||||
return $"Toggled {btnObj.name} to {toggle.isOn}";
|
||||
}
|
||||
return "No Toggle component found";
|
||||
```
|
||||
|
||||
## Execute Action → Screenshot to Verify
|
||||
|
||||
Run an action then capture a screenshot to visually confirm the result.
|
||||
|
||||
**Step 1**: Perform the action
|
||||
|
||||
```csharp
|
||||
using UnityEngine.UI;
|
||||
|
||||
Button btn = GameObject.Find("Canvas/PlayButton")?.GetComponent<Button>();
|
||||
if (btn == null) return "PlayButton not found";
|
||||
|
||||
btn.onClick.Invoke();
|
||||
return "Clicked PlayButton";
|
||||
```
|
||||
|
||||
**Step 2**: Capture Game View to verify the result
|
||||
|
||||
```powershell
|
||||
uloop screenshot --window-name Game
|
||||
```
|
||||
|
||||
## Execute Action → Check Logs for Side Effects
|
||||
|
||||
Run an action then inspect Unity Console logs to verify expected behavior.
|
||||
|
||||
**Step 1**: Clear console before the action
|
||||
|
||||
```powershell
|
||||
uloop clear-console
|
||||
```
|
||||
|
||||
**Step 2**: Perform the action
|
||||
|
||||
```csharp
|
||||
using System.Reflection;
|
||||
|
||||
GameObject player = GameObject.Find("Player");
|
||||
if (player == null) return "Player not found";
|
||||
|
||||
MonoBehaviour script = player.GetComponent("PlayerController") as MonoBehaviour;
|
||||
if (script == null) return "PlayerController not found";
|
||||
|
||||
MethodInfo method = script.GetType().GetMethod("TakeDamage");
|
||||
if (method == null) return "TakeDamage method not found";
|
||||
|
||||
method.Invoke(script, new object[] { 50f });
|
||||
return "Invoked TakeDamage(50)";
|
||||
```
|
||||
|
||||
**Step 3**: Check logs for expected output
|
||||
|
||||
```powershell
|
||||
uloop get-logs --log-type Log --search-text 'damage'
|
||||
```
|
||||
|
||||
## Full Automation: Play → Act → Capture → Stop
|
||||
|
||||
End-to-end test flow: start Play mode, perform actions, capture evidence, stop.
|
||||
|
||||
**Step 0**: Clear console to isolate this run
|
||||
|
||||
```powershell
|
||||
uloop clear-console
|
||||
```
|
||||
|
||||
**Step 1**: Start Play mode
|
||||
|
||||
```powershell
|
||||
uloop control-play-mode --action Play
|
||||
```
|
||||
|
||||
**Step 2**: Wait for scene initialization, then find and click a button
|
||||
|
||||
```csharp
|
||||
using UnityEngine.UI;
|
||||
using System.Linq;
|
||||
|
||||
// Scene may need a moment to initialize after Play starts
|
||||
Button[] buttons = Object.FindObjectsByType<Button>(FindObjectsSortMode.None);
|
||||
Button startBtn = buttons.FirstOrDefault(b => b.gameObject.name.Contains("Start"));
|
||||
if (startBtn == null) return $"Start button not found. Available: {string.Join(", ", buttons.Select(b => b.gameObject.name))}";
|
||||
|
||||
startBtn.onClick.Invoke();
|
||||
return $"Clicked {startBtn.gameObject.name}";
|
||||
```
|
||||
|
||||
**Step 3**: Capture screenshot as evidence
|
||||
|
||||
```powershell
|
||||
uloop screenshot --window-name Game
|
||||
```
|
||||
|
||||
**Step 4**: Check logs for errors
|
||||
|
||||
```powershell
|
||||
uloop get-logs --log-type Error
|
||||
```
|
||||
|
||||
**Step 5**: Stop Play mode
|
||||
|
||||
```powershell
|
||||
uloop control-play-mode --action Stop
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86022f911600881458e40f35e562a5db
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,299 @@
|
||||
# PlayMode Automation (zsh)
|
||||
|
||||
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 `zsh`-style usage.
|
||||
|
||||
## 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.
|
||||
|
||||
## zsh 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.
|
||||
|
||||
```zsh
|
||||
uloop execute-dynamic-code --code 'return "Hello from zsh";'
|
||||
```
|
||||
|
||||
### Single quotes inside inline C# code
|
||||
|
||||
If the C# snippet itself contains a single quote, close and reopen the shell string with `'\''`.
|
||||
|
||||
```zsh
|
||||
uloop execute-dynamic-code --code 'char initial = '\''A'\''; return initial.ToString();'
|
||||
```
|
||||
|
||||
### JSON-like values passed via `--parameters`
|
||||
|
||||
Wrap the whole expression in single quotes so the shell does not interpret double quotes.
|
||||
|
||||
```zsh
|
||||
uloop execute-dynamic-code --code 'return parameters["param0"];' --parameters '{"param0":"Hello from zsh"}'
|
||||
```
|
||||
|
||||
## Click UI Button by Path
|
||||
|
||||
```csharp
|
||||
using UnityEngine.UI;
|
||||
|
||||
Button btn = GameObject.Find("Canvas/StartButton")?.GetComponent<Button>();
|
||||
if (btn == null) return "Button not found";
|
||||
|
||||
btn.onClick.Invoke();
|
||||
return $"Clicked {btn.gameObject.name}";
|
||||
```
|
||||
|
||||
## Click UI Button by Search
|
||||
|
||||
```csharp
|
||||
using UnityEngine.UI;
|
||||
using System.Linq;
|
||||
|
||||
Button[] buttons = Object.FindObjectsByType<Button>(FindObjectsSortMode.None);
|
||||
Button target = buttons.FirstOrDefault(b => b.gameObject.name == "PlayButton");
|
||||
if (target == null) return $"PlayButton not found. Available: {string.Join(", ", buttons.Select(b => b.gameObject.name))}";
|
||||
|
||||
target.onClick.Invoke();
|
||||
return $"Clicked {target.gameObject.name}";
|
||||
```
|
||||
|
||||
## Toggle GameObject Active State
|
||||
|
||||
```csharp
|
||||
GameObject obj = GameObject.Find("Enemy");
|
||||
if (obj == null) return "Enemy not found";
|
||||
|
||||
obj.SetActive(!obj.activeSelf);
|
||||
return $"{obj.name} is now {(obj.activeSelf ? "active" : "inactive")}";
|
||||
```
|
||||
|
||||
## Invoke Method on MonoBehaviour
|
||||
|
||||
```csharp
|
||||
using System.Reflection;
|
||||
|
||||
GameObject player = GameObject.Find("Player");
|
||||
if (player == null) return "Player not found";
|
||||
|
||||
MonoBehaviour script = player.GetComponent("PlayerController") as MonoBehaviour;
|
||||
if (script == null) return "PlayerController not found";
|
||||
|
||||
MethodInfo method = script.GetType().GetMethod("TakeDamage");
|
||||
if (method == null) return "TakeDamage method not found";
|
||||
|
||||
method.Invoke(script, new object[] { 10f });
|
||||
return $"Invoked TakeDamage(10) on {player.name}";
|
||||
```
|
||||
|
||||
## Set Field Value at Runtime
|
||||
|
||||
```csharp
|
||||
using System.Reflection;
|
||||
|
||||
GameObject player = GameObject.Find("Player");
|
||||
if (player == null) return "Player not found";
|
||||
|
||||
MonoBehaviour script = player.GetComponent("PlayerController") as MonoBehaviour;
|
||||
if (script == null) return "PlayerController not found";
|
||||
|
||||
FieldInfo field = script.GetType().GetField("moveSpeed", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (field == null) return "moveSpeed field not found";
|
||||
|
||||
field.SetValue(script, 20f);
|
||||
return $"Set moveSpeed to 20 on {player.name}";
|
||||
```
|
||||
|
||||
## Move Player to Position
|
||||
|
||||
```csharp
|
||||
GameObject player = GameObject.Find("Player");
|
||||
if (player == null) return "Player not found";
|
||||
|
||||
Vector3 targetPos = new Vector3(5f, 0f, 10f);
|
||||
player.transform.position = targetPos;
|
||||
return $"Moved {player.name} to {targetPos}";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Tool Combination Workflows
|
||||
|
||||
Examples of combining `execute-dynamic-code` with other uloop tools for multi-step PlayMode automation.
|
||||
|
||||
## find-game-objects → Click Button
|
||||
|
||||
Use `find-game-objects` to discover buttons with their hierarchy paths, then click one via `execute-dynamic-code`.
|
||||
|
||||
**Step 1**: Find all GameObjects with Button component
|
||||
|
||||
```zsh
|
||||
uloop find-game-objects --required-components UnityEngine.UI.Button
|
||||
```
|
||||
|
||||
**Step 2**: Click the target button using the path from Step 1
|
||||
|
||||
```csharp
|
||||
using UnityEngine.UI;
|
||||
|
||||
// Use the path returned by find-game-objects (e.g. "Canvas/MainMenu/StartButton")
|
||||
GameObject btnObj = GameObject.Find("Canvas/MainMenu/StartButton");
|
||||
if (btnObj == null) return "Button not found at path";
|
||||
|
||||
Button btn = btnObj.GetComponent<Button>();
|
||||
if (btn == null) return "No Button component";
|
||||
|
||||
btn.onClick.Invoke();
|
||||
return $"Clicked {btnObj.name}";
|
||||
```
|
||||
|
||||
## get-hierarchy → Navigate UI and Click
|
||||
|
||||
Use `get-hierarchy` to explore the UI tree structure, then target the right element.
|
||||
|
||||
**Step 1**: Get Canvas hierarchy to understand UI structure
|
||||
|
||||
```zsh
|
||||
uloop get-hierarchy --root-path "Canvas" --max-depth 3
|
||||
```
|
||||
|
||||
**Step 2**: Based on the hierarchy JSON, click the desired button
|
||||
|
||||
```csharp
|
||||
using UnityEngine.UI;
|
||||
|
||||
// Path identified from hierarchy output
|
||||
GameObject btnObj = GameObject.Find("Canvas/SettingsPanel/AudioTab/MuteToggle");
|
||||
if (btnObj == null) return "MuteToggle not found";
|
||||
|
||||
Toggle toggle = btnObj.GetComponent<Toggle>();
|
||||
if (toggle != null)
|
||||
{
|
||||
toggle.isOn = !toggle.isOn;
|
||||
return $"Toggled {btnObj.name} to {toggle.isOn}";
|
||||
}
|
||||
return "No Toggle component found";
|
||||
```
|
||||
|
||||
## Execute Action → Screenshot to Verify
|
||||
|
||||
Run an action then capture a screenshot to visually confirm the result.
|
||||
|
||||
**Step 1**: Perform the action
|
||||
|
||||
```csharp
|
||||
using UnityEngine.UI;
|
||||
|
||||
Button btn = GameObject.Find("Canvas/PlayButton")?.GetComponent<Button>();
|
||||
if (btn == null) return "PlayButton not found";
|
||||
|
||||
btn.onClick.Invoke();
|
||||
return "Clicked PlayButton";
|
||||
```
|
||||
|
||||
**Step 2**: Capture Game View to verify the result
|
||||
|
||||
```zsh
|
||||
uloop screenshot --window-name Game
|
||||
```
|
||||
|
||||
## Execute Action → Check Logs for Side Effects
|
||||
|
||||
Run an action then inspect Unity Console logs to verify expected behavior.
|
||||
|
||||
**Step 1**: Clear console before the action
|
||||
|
||||
```zsh
|
||||
uloop clear-console
|
||||
```
|
||||
|
||||
**Step 2**: Perform the action
|
||||
|
||||
```csharp
|
||||
using System.Reflection;
|
||||
|
||||
GameObject player = GameObject.Find("Player");
|
||||
if (player == null) return "Player not found";
|
||||
|
||||
MonoBehaviour script = player.GetComponent("PlayerController") as MonoBehaviour;
|
||||
if (script == null) return "PlayerController not found";
|
||||
|
||||
MethodInfo method = script.GetType().GetMethod("TakeDamage");
|
||||
if (method == null) return "TakeDamage method not found";
|
||||
|
||||
method.Invoke(script, new object[] { 50f });
|
||||
return "Invoked TakeDamage(50)";
|
||||
```
|
||||
|
||||
**Step 3**: Check logs for expected output
|
||||
|
||||
```zsh
|
||||
uloop get-logs --log-type Log --search-text "damage"
|
||||
```
|
||||
|
||||
## Full Automation: Play → Act → Capture → Stop
|
||||
|
||||
End-to-end test flow: start Play mode, perform actions, capture evidence, stop.
|
||||
|
||||
**Step 0**: Clear console to isolate this run
|
||||
|
||||
```zsh
|
||||
uloop clear-console
|
||||
```
|
||||
|
||||
**Step 1**: Start Play mode
|
||||
|
||||
```zsh
|
||||
uloop control-play-mode --action Play
|
||||
```
|
||||
|
||||
**Step 2**: Wait for scene initialization, then find and click a button
|
||||
|
||||
```csharp
|
||||
using UnityEngine.UI;
|
||||
using System.Linq;
|
||||
|
||||
// Scene may need a moment to initialize after Play starts
|
||||
Button[] buttons = Object.FindObjectsByType<Button>(FindObjectsSortMode.None);
|
||||
Button startBtn = buttons.FirstOrDefault(b => b.gameObject.name.Contains("Start"));
|
||||
if (startBtn == null) return $"Start button not found. Available: {string.Join(", ", buttons.Select(b => b.gameObject.name))}";
|
||||
|
||||
startBtn.onClick.Invoke();
|
||||
return $"Clicked {startBtn.gameObject.name}";
|
||||
```
|
||||
|
||||
**Step 3**: Capture screenshot as evidence
|
||||
|
||||
```zsh
|
||||
uloop screenshot --window-name Game
|
||||
```
|
||||
|
||||
**Step 4**: Check logs for errors
|
||||
|
||||
```zsh
|
||||
uloop get-logs --log-type Error
|
||||
```
|
||||
|
||||
**Step 5**: Stop Play mode
|
||||
|
||||
```zsh
|
||||
uloop control-play-mode --action Stop
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user