chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖

This commit is contained in:
Liubing\LB
2026-09-28 18:14:59 +08:00
parent 0eb43b2b40
commit c4ae02def9
952 changed files with 70137 additions and 11 deletions

View File

@@ -0,0 +1,234 @@
#if ULOOPMCP_HAS_INPUT_SYSTEM
#nullable enable
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.LowLevel;
namespace io.github.hatayama.uLoopMCP
{
// Tool instances are created fresh per invocation, so held-button state
// between Click/LongPress must be held statically.
[InitializeOnLoad]
internal static class MouseInputState
{
private static readonly HashSet<MouseButton> _heldButtons = new();
// Pending reset callbacks for per-frame values (delta, scroll).
// Tracked so we can unsubscribe on PlayMode exit to prevent leaks.
private static Action? _pendingDeltaReset;
private static Action? _pendingScrollReset;
public static bool IsButtonHeld(MouseButton button) => _heldButtons.Contains(button);
static MouseInputState()
{
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}
public static void SetButtonDown(MouseButton button)
{
_heldButtons.Add(button);
}
public static void SetButtonUp(MouseButton button)
{
_heldButtons.Remove(button);
}
// StateEvent.From captures the full mouse state snapshot.
// All currently held buttons must be written into every event
// to avoid accidentally releasing them.
public static void SetButtonState(Mouse mouse, MouseButton button, bool pressed)
{
Debug.Assert(mouse != null, "mouse must not be null");
ApplyStateEvent(mouse!, eventPtr =>
{
GetButtonControl(mouse!, button).WriteValueIntoEvent(pressed ? 1f : 0f, eventPtr);
});
}
public static void SetPositionState(Mouse mouse, Vector2 position)
{
Debug.Assert(mouse != null, "mouse must not be null");
ApplyStateEvent(mouse!, eventPtr =>
{
mouse!.position.WriteValueIntoEvent(position, eventPtr);
});
}
public static void SetDeltaState(Mouse mouse, Vector2 delta)
{
InjectDelta(mouse, delta);
SchedulePerFrameReset(mouse!, mouse!.delta, isDelta: true);
}
// Inject delta without scheduling a reset. Used by SmoothDelta which
// manages its own lifecycle — resetting only after the final frame.
public static void InjectDelta(Mouse mouse, Vector2 delta)
{
Debug.Assert(mouse != null, "mouse must not be null");
ApplyStateEvent(mouse!, eventPtr =>
{
mouse!.delta.WriteValueIntoEvent(delta, eventPtr);
});
}
public static void SetScrollState(Mouse mouse, Vector2 scroll)
{
Debug.Assert(mouse != null, "mouse must not be null");
ApplyStateEvent(mouse!, eventPtr =>
{
mouse!.scroll.WriteValueIntoEvent(scroll, eventPtr);
});
SchedulePerFrameReset(mouse!, mouse!.scroll, isDelta: false);
}
public static void ReleaseAllButtons()
{
Mouse? mouse = Mouse.current;
if (mouse == null)
{
_heldButtons.Clear();
ClearPendingResets();
return;
}
using (StateEvent.From(mouse, out InputEventPtr eventPtr))
{
foreach (MouseButton button in _heldButtons)
{
GetButtonControl(mouse, button).WriteValueIntoEvent(0f, eventPtr);
}
InputUpdateType updateType = InputUpdateTypeResolver.Resolve();
InputState.Change(mouse, eventPtr, updateType);
}
_heldButtons.Clear();
ClearPendingResets();
}
private static void ApplyStateEvent(Mouse mouse, Action<InputEventPtr> writePayload)
{
InputUpdateType updateType = InputUpdateTypeResolver.Resolve();
using (StateEvent.From(mouse, out InputEventPtr eventPtr))
{
foreach (MouseButton heldButton in _heldButtons)
{
GetButtonControl(mouse, heldButton).WriteValueIntoEvent(1f, eventPtr);
}
writePayload(eventPtr);
InputState.Change(mouse, eventPtr, updateType);
}
}
// delta and scroll are per-frame values; leaving them non-zero causes
// accumulation across frames. Reset on the next input update.
private static void SchedulePerFrameReset(
Mouse mouse,
InputControl<Vector2> control,
bool isDelta)
{
// Remove previous pending reset to avoid stacking callbacks
Action? previousReset = isDelta ? _pendingDeltaReset : _pendingScrollReset;
if (previousReset != null)
{
InputSystem.onBeforeUpdate -= previousReset;
}
// Capture the target update type so the reset only fires in the same
// Input System update phase that gameplay code reads, preventing an
// Editor or BeforeRender update from clearing the value prematurely.
InputUpdateType targetUpdateType = InputUpdateTypeResolver.Resolve();
Action? resetCallback = null;
resetCallback = () =>
{
InputUpdateType currentUpdateType = InputState.currentUpdateType;
if (!InputUpdateTypeResolver.IsMatch(currentUpdateType, targetUpdateType))
{
return;
}
Debug.Assert(resetCallback != null, "resetCallback must be assigned before subscription");
InputSystem.onBeforeUpdate -= resetCallback;
if (isDelta && _pendingDeltaReset == resetCallback)
{
_pendingDeltaReset = null;
}
else if (!isDelta && _pendingScrollReset == resetCallback)
{
_pendingScrollReset = null;
}
if (Mouse.current != mouse)
{
return;
}
ApplyStateEvent(mouse, eventPtr =>
{
control.WriteValueIntoEvent(Vector2.zero, eventPtr);
});
};
if (isDelta)
{
_pendingDeltaReset = resetCallback;
}
else
{
_pendingScrollReset = resetCallback;
}
InputSystem.onBeforeUpdate += resetCallback;
}
private static void ClearPendingResets()
{
if (_pendingDeltaReset != null)
{
InputSystem.onBeforeUpdate -= _pendingDeltaReset;
_pendingDeltaReset = null;
}
if (_pendingScrollReset != null)
{
InputSystem.onBeforeUpdate -= _pendingScrollReset;
_pendingScrollReset = null;
}
}
internal static ButtonControl GetButtonControl(Mouse mouse, MouseButton button)
{
switch (button)
{
case MouseButton.Right:
return mouse.rightButton;
case MouseButton.Middle:
return mouse.middleButton;
default:
Debug.Assert(button == MouseButton.Left, $"Unexpected MouseButton value: {button}");
return mouse.leftButton;
}
}
private static void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.ExitingPlayMode)
{
ReleaseAllButtons();
SimulateMouseInputOverlayState.Clear();
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cfbc61ded72be44e1b330f1439aedf49
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
#nullable enable
namespace io.github.hatayama.uLoopMCP
{
public class SimulateMouseInputResponse : BaseToolResponse
{
public bool Success { get; set; }
public string Message { get; set; } = "";
public string Action { get; set; } = "";
public string? Button { get; set; }
public float? PositionX { get; set; }
public float? PositionY { get; set; }
public SimulateMouseInputResponse()
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cb634f2ab3ed54b9187f316f8757c8a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
using System.ComponentModel;
namespace io.github.hatayama.uLoopMCP
{
public class SimulateMouseInputSchema : BaseToolSchema
{
[Description("Mouse input action: Click(0) - inject left/right/middle button press+release, LongPress(1) - inject button hold for Duration seconds, MoveDelta(2) - inject mouse delta for camera/look (one-shot), Scroll(3) - inject scroll wheel, SmoothDelta(4) - inject mouse delta smoothly over Duration seconds")]
public MouseInputAction Action { get; set; } = MouseInputAction.Click;
[Description("Target X position in screen pixels (origin: top-left). Used by Click and LongPress to set Mouse.current.position.")]
public float X { get; set; } = 0f;
[Description("Target Y position in screen pixels (origin: top-left). Used by Click and LongPress to set Mouse.current.position.")]
public float Y { get; set; } = 0f;
[Description("Mouse button: Left(0, default), Right(1), Middle(2). Used by Click and LongPress.")]
public MouseButton Button { get; set; } = MouseButton.Left;
[Description("Hold duration in seconds for LongPress, or minimum hold time for Click (0 = one-shot tap). Also used as total duration for SmoothDelta.")]
public float Duration { get; set; } = 0f;
[Description("Delta X in pixels for MoveDelta/SmoothDelta action. Positive = right.")]
public float DeltaX { get; set; } = 0f;
[Description("Delta Y in pixels for MoveDelta/SmoothDelta action. Positive = up (Unity screen space).")]
public float DeltaY { get; set; } = 0f;
[Description("Horizontal scroll delta for Scroll action (default: 0).")]
public float ScrollX { get; set; } = 0f;
[Description("Vertical scroll delta for Scroll action. Positive = up, negative = down. Typically 120 per notch.")]
public float ScrollY { get; set; } = 0f;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 31532fd15780c48929585624ec776aae
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,389 @@
#nullable enable
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
#if ULOOPMCP_HAS_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
namespace io.github.hatayama.uLoopMCP
{
[McpTool(Description = "Simulate mouse input in PlayMode via Input System. Injects button clicks, mouse delta, and scroll wheel directly into Mouse.current for game logic that reads Input System (e.g. WasPressedThisFrame). Requires the Input System package.")]
public class SimulateMouseInputTool : AbstractUnityTool<SimulateMouseInputSchema, SimulateMouseInputResponse>
{
public override string ToolName => "simulate-mouse-input";
protected override
#if !ULOOPMCP_HAS_INPUT_SYSTEM
#pragma warning disable CS1998
#endif
async Task<SimulateMouseInputResponse> ExecuteAsync(
SimulateMouseInputSchema parameters,
CancellationToken ct)
#if !ULOOPMCP_HAS_INPUT_SYSTEM
#pragma warning restore CS1998
#endif
{
ct.ThrowIfCancellationRequested();
#if !ULOOPMCP_HAS_INPUT_SYSTEM
return new SimulateMouseInputResponse
{
Success = false,
Message = "simulate-mouse-input requires the Input System package (com.unity.inputsystem). Install it via Package Manager and set Active Input Handling to 'Input System Package (New)' or 'Both' in Player Settings.",
Action = parameters.Action.ToString()
};
#else
string correlationId = McpConstants.GenerateCorrelationId();
if (!EditorApplication.isPlaying)
{
return new SimulateMouseInputResponse
{
Success = false,
Message = "PlayMode is not active. Use control-play-mode tool to start PlayMode first.",
Action = parameters.Action.ToString()
};
}
if (EditorApplication.isPaused)
{
return new SimulateMouseInputResponse
{
Success = false,
Message = "PlayMode is paused. Resume PlayMode before simulating mouse input.",
Action = parameters.Action.ToString()
};
}
if (!System.Enum.IsDefined(typeof(MouseInputAction), parameters.Action))
{
return new SimulateMouseInputResponse
{
Success = false,
Message = $"Invalid Action value: {(int)parameters.Action}. Use Click, LongPress, MoveDelta, Scroll, or SmoothDelta.",
Action = parameters.Action.ToString()
};
}
if (!System.Enum.IsDefined(typeof(MouseButton), parameters.Button))
{
return new SimulateMouseInputResponse
{
Success = false,
Message = $"Invalid Button value: {(int)parameters.Button}. Use Left, Right, or Middle.",
Action = parameters.Action.ToString()
};
}
Mouse? mouse = Mouse.current;
if (mouse == null)
{
return new SimulateMouseInputResponse
{
Success = false,
Message = "No mouse device found in Input System. Ensure the Input System package is properly configured.",
Action = parameters.Action.ToString()
};
}
VibeLogger.LogInfo(
"simulate_mouse_input_start",
"Mouse input simulation started",
new { Action = parameters.Action.ToString(), Button = parameters.Button.ToString() },
correlationId: correlationId
);
EnsureOverlayExists();
SimulateMouseInputResponse response;
switch (parameters.Action)
{
case MouseInputAction.Click:
response = await ExecuteClick(mouse, parameters, ct);
break;
case MouseInputAction.LongPress:
response = await ExecuteLongPress(mouse, parameters, ct);
break;
case MouseInputAction.MoveDelta:
response = await ExecuteMoveDelta(mouse, parameters, ct);
break;
case MouseInputAction.Scroll:
response = await ExecuteScroll(mouse, parameters, ct);
break;
case MouseInputAction.SmoothDelta:
response = await ExecuteSmoothDelta(mouse, parameters, ct);
break;
default:
throw new ArgumentException($"Unknown mouse input action: {parameters.Action}");
}
VibeLogger.LogInfo(
"simulate_mouse_input_complete",
$"Mouse input simulation completed: {response.Message}",
new { Action = parameters.Action.ToString(), Success = response.Success },
correlationId: correlationId
);
return response;
#endif
}
#if ULOOPMCP_HAS_INPUT_SYSTEM
private static void EnsureOverlayExists()
{
OverlayCanvasFactory.EnsureExists();
}
// Input coordinates use top-left origin; Unity Screen space uses bottom-left origin.
// Uses Screen.height (runtime resolution) because Mouse.current.position is in
// runtime screen space, not the editor Game view target resolution.
private static Vector2 InputToScreen(Vector2 inputPos)
{
return new Vector2(inputPos.x, Screen.height - inputPos.y);
}
private async Task<SimulateMouseInputResponse> ExecuteClick(
Mouse mouse, SimulateMouseInputSchema parameters, CancellationToken ct)
{
if (parameters.Duration < 0f || float.IsNaN(parameters.Duration) || float.IsInfinity(parameters.Duration))
{
return new SimulateMouseInputResponse
{
Success = false,
Message = $"Duration must be non-negative, got: {parameters.Duration}",
Action = MouseInputAction.Click.ToString()
};
}
Vector2 inputPos = new Vector2(parameters.X, parameters.Y);
Vector2 screenPos = InputToScreen(inputPos);
MouseButton button = parameters.Button;
string buttonName = button.ToString();
// Set mouse position before clicking
await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(
() => MouseInputState.SetPositionState(mouse, screenPos), ct);
// Press button
MouseInputState.SetButtonDown(button);
SimulateMouseInputOverlayState.SetButtonHeld(button, true);
bool pressWasApplied = false;
try
{
await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(
() => MouseInputState.SetButtonState(mouse, button, true), ct);
pressWasApplied = true;
await InputSystemUpdateHelper.WaitForPressLifetime(parameters.Duration, ct);
}
finally
{
if (pressWasApplied)
{
await ReleaseButtonIfPossible(mouse, button);
MouseInputState.SetButtonUp(button);
}
else
{
MouseInputState.SetButtonUp(button);
}
SimulateMouseInputOverlayState.SetButtonHeld(button, false);
}
string durationText = parameters.Duration > 0f ? $" for {parameters.Duration:F1}s" : "";
return new SimulateMouseInputResponse
{
Success = true,
Message = $"Clicked {buttonName} at ({inputPos.x:F1}, {inputPos.y:F1}){durationText}",
Action = MouseInputAction.Click.ToString(),
Button = buttonName,
PositionX = inputPos.x,
PositionY = inputPos.y
};
}
private async Task<SimulateMouseInputResponse> ExecuteLongPress(
Mouse mouse, SimulateMouseInputSchema parameters, CancellationToken ct)
{
if (parameters.Duration <= 0f || float.IsNaN(parameters.Duration) || float.IsInfinity(parameters.Duration))
{
return new SimulateMouseInputResponse
{
Success = false,
Message = $"Duration must be positive for LongPress, got: {parameters.Duration}",
Action = MouseInputAction.LongPress.ToString()
};
}
Vector2 inputPos = new Vector2(parameters.X, parameters.Y);
Vector2 screenPos = InputToScreen(inputPos);
MouseButton button = parameters.Button;
string buttonName = button.ToString();
// Set mouse position before pressing
await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(
() => MouseInputState.SetPositionState(mouse, screenPos), ct);
// Press button
MouseInputState.SetButtonDown(button);
SimulateMouseInputOverlayState.SetButtonHeld(button, true);
bool pressWasApplied = false;
try
{
await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(
() => MouseInputState.SetButtonState(mouse, button, true), ct);
pressWasApplied = true;
// Hold for at least the minimum observation frames so the press
// is visible to game code, then continue until duration elapses.
await InputSystemUpdateHelper.WaitForPressLifetime(parameters.Duration, ct);
}
finally
{
if (pressWasApplied)
{
await ReleaseButtonIfPossible(mouse, button);
}
MouseInputState.SetButtonUp(button);
SimulateMouseInputOverlayState.SetButtonHeld(button, false);
}
return new SimulateMouseInputResponse
{
Success = true,
Message = $"Long-pressed {buttonName} at ({inputPos.x:F1}, {inputPos.y:F1}) for {parameters.Duration:F1}s",
Action = MouseInputAction.LongPress.ToString(),
Button = buttonName,
PositionX = inputPos.x,
PositionY = inputPos.y
};
}
private async Task<SimulateMouseInputResponse> ExecuteMoveDelta(
Mouse mouse, SimulateMouseInputSchema parameters, CancellationToken ct)
{
Vector2 delta = new Vector2(parameters.DeltaX, parameters.DeltaY);
SimulateMouseInputOverlayState.SetMoveDelta(delta);
await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(
() => MouseInputState.SetDeltaState(mouse, delta), ct);
await InputSystemUpdateHelper.WaitForObservationFrames(ct);
return new SimulateMouseInputResponse
{
Success = true,
Message = $"Mouse delta injected: ({parameters.DeltaX:F1}, {parameters.DeltaY:F1})",
Action = MouseInputAction.MoveDelta.ToString()
};
}
private async Task<SimulateMouseInputResponse> ExecuteScroll(
Mouse mouse, SimulateMouseInputSchema parameters, CancellationToken ct)
{
Vector2 scroll = new Vector2(parameters.ScrollX, parameters.ScrollY);
int scrollDir = parameters.ScrollY > 0f ? 1 : parameters.ScrollY < 0f ? -1 : 0;
SimulateMouseInputOverlayState.SetScrollDirection(scrollDir);
await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(
() => MouseInputState.SetScrollState(mouse, scroll), ct);
await InputSystemUpdateHelper.WaitForObservationFrames(ct);
return new SimulateMouseInputResponse
{
Success = true,
Message = $"Scroll injected: ({parameters.ScrollX:F1}, {parameters.ScrollY:F1})",
Action = MouseInputAction.Scroll.ToString()
};
}
// Distributes totalDelta across frames over duration for human-like smooth movement.
// Uses ApplyOnNextConfiguredUpdate per frame so the delta is visible to game code
// in the same Input System update cycle. Resets delta to zero only after the final frame.
private async Task<SimulateMouseInputResponse> ExecuteSmoothDelta(
Mouse mouse, SimulateMouseInputSchema parameters, CancellationToken ct)
{
if (parameters.Duration <= 0f || float.IsNaN(parameters.Duration) || float.IsInfinity(parameters.Duration))
{
return new SimulateMouseInputResponse
{
Success = false,
Message = $"Duration must be positive for SmoothDelta, got: {parameters.Duration}",
Action = MouseInputAction.SmoothDelta.ToString()
};
}
Vector2 totalDelta = new Vector2(parameters.DeltaX, parameters.DeltaY);
float duration = parameters.Duration;
float startTime = Time.realtimeSinceStartup;
float previousT = 0f;
while (true)
{
float elapsed = Time.realtimeSinceStartup - startTime;
float t = Mathf.Clamp01(elapsed / duration);
float frameFraction = t - previousT;
Vector2 frameDelta = totalDelta * frameFraction;
SimulateMouseInputOverlayState.SetMoveDelta(frameDelta);
await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(
() => MouseInputState.InjectDelta(mouse, frameDelta), ct);
previousT = t;
if (t >= 1f)
{
break;
}
// Explicit/manual update completes synchronously, so an extra
// frame delay is needed to prevent the loop from collapsing into
// a single burst. Dynamic/Fixed modes already yield naturally via
// ApplyOnNextConfiguredUpdate's onBeforeUpdate callback.
if (InputUpdateTypeResolver.RequiresExplicitUpdate())
{
await EditorDelay.DelayFrame(1, ct);
}
}
// Reset delta to zero after the smooth operation completes
await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(
() => MouseInputState.InjectDelta(mouse, Vector2.zero), ct);
return new SimulateMouseInputResponse
{
Success = true,
Message = $"Smooth delta ({parameters.DeltaX:F1}, {parameters.DeltaY:F1}) over {duration:F2}s",
Action = MouseInputAction.SmoothDelta.ToString()
};
}
private static async Task ReleaseButtonIfPossible(Mouse mouse, MouseButton button)
{
if (!CanInjectMouseState(mouse))
{
return;
}
await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(
() => MouseInputState.SetButtonState(mouse, button, false), CancellationToken.None);
}
private static bool CanInjectMouseState(Mouse mouse)
{
return EditorApplication.isPlaying && !EditorApplication.isPaused && Mouse.current == mouse;
}
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d275634f15e8543e191b4c6bd9fc8f69
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7108d27f0ac0149519466969a0afac8c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,109 @@
---
name: uloop-simulate-mouse-input
description: "Simulate Mouse.current input in PlayMode through Unity Input System. Use for gameplay clicks, mouse delta, or scroll; use simulate-mouse-ui for EventSystem UI elements."
context: fork
---
# Task
Simulate mouse input via Input System in Unity PlayMode: $ARGUMENTS
## Workflow
1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not)
2. For Click/LongPress: determine the target screen position (use `uloop screenshot` to find coordinates)
3. Execute the appropriate `uloop simulate-mouse-input` command
4. Take a screenshot to verify the result: `uloop screenshot --capture-mode rendering`
5. Report what happened
## Tool Reference
```bash
uloop simulate-mouse-input --action <action> [options]
```
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `--action` | enum | `Click` | `Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, `Scroll` |
| `--x` | number | `0` | Target X position (origin: top-left). Used by Click and LongPress. |
| `--y` | number | `0` | Target Y position (origin: top-left). Used by Click and LongPress. |
| `--button` | enum | `Left` | Mouse button: `Left`, `Right`, `Middle`. Used by Click and LongPress. |
| `--duration` | number | `0` | Hold duration for LongPress, or interpolation duration for SmoothDelta (seconds). For Click, 0 = one-shot tap. |
| `--delta-x` | number | `0` | Delta X in pixels for MoveDelta/SmoothDelta. Positive = right. |
| `--delta-y` | number | `0` | Delta Y in pixels for MoveDelta/SmoothDelta. Positive = up. |
| `--scroll-x` | number | `0` | Horizontal scroll delta for Scroll action. |
| `--scroll-y` | number | `0` | Vertical scroll delta for Scroll action. Typically 120 per notch. |
### Actions
| Action | What it injects | Description |
|--------|----------------|-------------|
| `Click` | Mouse.current button press → release | Inject a button click so game logic detects `wasPressedThisFrame` |
| `LongPress` | Mouse.current button press → hold → release | Hold a button for `--duration` seconds |
| `MoveDelta` | Mouse.current.delta | Inject mouse movement delta one-shot (e.g. for FPS camera look) |
| `SmoothDelta` | Mouse.current.delta (per-frame) | Inject mouse delta smoothly over `--duration` seconds (human-like camera pan) |
| `Scroll` | Mouse.current.scroll | Inject scroll wheel input (e.g. for hotbar or zoom) |
### Global Options (optional)
| Option | Description |
|--------|-------------|
| `--project-path <path>` | Optional. Use only when the target Unity project is not the current directory. |
## When to use this vs simulate-mouse-ui
| Scenario | Tool |
|----------|------|
| Click a Unity UI Button (IPointerClickHandler) | `simulate-mouse-ui` |
| Destroy a block in Minecraft (reads `Mouse.current.leftButton`) | `simulate-mouse-input` when the project uses the New Input System |
| Place a block with right-click | `simulate-mouse-input --button Right` when the project uses the New Input System |
| Drag a UI slider | `simulate-mouse-ui --action Drag` |
| Look around with mouse (FPS camera) | `simulate-mouse-input --action MoveDelta` when the project uses the New Input System |
| Scroll hotbar slots | `simulate-mouse-input --action Scroll` when the project uses the New Input System |
## Examples
```bash
# Left-click at screen center (for game logic)
uloop simulate-mouse-input --action Click --x 400 --y 300
# Right-click at screen center (e.g. place block)
uloop simulate-mouse-input --action Click --x 400 --y 300 --button Right
# Hold left-click for 2 seconds (e.g. mine block)
uloop simulate-mouse-input --action LongPress --x 400 --y 300 --duration 2.0
# Look right (FPS camera)
uloop simulate-mouse-input --action MoveDelta --delta-x 100 --delta-y 0
# Scroll up (e.g. previous hotbar slot)
uloop simulate-mouse-input --action Scroll --scroll-y 120
# Scroll down (e.g. next hotbar slot)
uloop simulate-mouse-input --action Scroll --scroll-y -120
# Smooth camera pan right over 0.5 seconds
uloop simulate-mouse-input --action SmoothDelta --delta-x 300 --delta-y 0 --duration 0.5
```
## Prerequisites
- Unity must be in **PlayMode**
- **Input System package** must be installed (`com.unity.inputsystem`)
- Game code must read input via Input System API (e.g. `Mouse.current.leftButton.wasPressedThisFrame`)
- Use this only when the project already uses the New Input System.
## Output
Returns JSON with:
- `Success`: Whether the operation succeeded
- `Message`: Status message
- `Action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`)
- `Button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise)
- `PositionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`)
- `PositionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`)
These are the only six fields. There is no `DeltaX`, `DeltaY`, `ScrollX`, `ScrollY`, `Duration`, or hit-element field in the response — only the issued action, button, and target position are echoed back. Verify visual outcome with a follow-up screenshot.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8db6cab1b94614865a037994fb75e71a
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: