chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
#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.
|
||||
// Keyboard simulation must follow that configured loop to avoid frame mismatches.
|
||||
internal static class KeyboardInputUpdateTypeResolver
|
||||
{
|
||||
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 keyboard 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 RequiresExplicitManualUpdate(InputUpdateType updateType)
|
||||
{
|
||||
return updateType == InputUpdateType.Manual;
|
||||
}
|
||||
|
||||
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: cb4b715627a724dee9863ab385cc6f89
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,119 @@
|
||||
#if ULOOPMCP_HAS_INPUT_SYSTEM
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
// Tool instances are created fresh per invocation, so held-key state
|
||||
// between KeyDown/KeyUp must be held statically.
|
||||
[InitializeOnLoad]
|
||||
internal static class KeyboardKeyState
|
||||
{
|
||||
private static readonly HashSet<Key> _heldKeys = new();
|
||||
private static readonly HashSet<Key> _transientKeys = new();
|
||||
|
||||
public static bool IsKeyHeld(Key key) => _heldKeys.Contains(key);
|
||||
public static IReadOnlyCollection<Key> HeldKeys => _heldKeys;
|
||||
|
||||
static KeyboardKeyState()
|
||||
{
|
||||
// Guard against duplicate subscriptions on domain reload
|
||||
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
||||
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
||||
}
|
||||
|
||||
public static void SetKeyDown(Key key)
|
||||
{
|
||||
_heldKeys.Add(key);
|
||||
}
|
||||
|
||||
public static void SetKeyUp(Key key)
|
||||
{
|
||||
_heldKeys.Remove(key);
|
||||
}
|
||||
|
||||
public static void RegisterTransientKey(Key key)
|
||||
{
|
||||
_transientKeys.Add(key);
|
||||
}
|
||||
|
||||
public static void UnregisterTransientKey(Key key)
|
||||
{
|
||||
_transientKeys.Remove(key);
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
_heldKeys.Clear();
|
||||
_transientKeys.Clear();
|
||||
}
|
||||
|
||||
// Keyboard keys are stored as a bitfield, so StateEvent.From captures
|
||||
// the entire keyboard state. To support simultaneous key holds, we write
|
||||
// ALL currently held keys into every event — not just the target key.
|
||||
public static void SetKeyState(Keyboard keyboard, Key key, bool pressed)
|
||||
{
|
||||
InputUpdateType updateType = InputUpdateTypeResolver.Resolve();
|
||||
using (StateEvent.From(keyboard, out InputEventPtr eventPtr))
|
||||
{
|
||||
foreach (Key heldKey in _heldKeys)
|
||||
{
|
||||
keyboard[heldKey].WriteValueIntoEvent(1f, eventPtr);
|
||||
}
|
||||
|
||||
foreach (Key transientKey in _transientKeys)
|
||||
{
|
||||
keyboard[transientKey].WriteValueIntoEvent(1f, eventPtr);
|
||||
}
|
||||
|
||||
keyboard[key].WriteValueIntoEvent(pressed ? 1f : 0f, eventPtr);
|
||||
// Updating player state directly avoids editor focus-dependent routing.
|
||||
InputState.Change(keyboard, eventPtr, updateType);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ReleaseAllKeys()
|
||||
{
|
||||
Keyboard keyboard = Keyboard.current;
|
||||
if (keyboard == null)
|
||||
{
|
||||
_heldKeys.Clear();
|
||||
_transientKeys.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Single event with all keys released
|
||||
using (StateEvent.From(keyboard, out InputEventPtr eventPtr))
|
||||
{
|
||||
foreach (Key key in _heldKeys)
|
||||
{
|
||||
keyboard[key].WriteValueIntoEvent(0f, eventPtr);
|
||||
}
|
||||
|
||||
foreach (Key key in _transientKeys)
|
||||
{
|
||||
keyboard[key].WriteValueIntoEvent(0f, eventPtr);
|
||||
}
|
||||
|
||||
InputUpdateType updateType = InputUpdateTypeResolver.Resolve();
|
||||
InputState.Change(keyboard, eventPtr, updateType);
|
||||
}
|
||||
|
||||
_heldKeys.Clear();
|
||||
_transientKeys.Clear();
|
||||
}
|
||||
|
||||
private static void OnPlayModeStateChanged(PlayModeStateChange state)
|
||||
{
|
||||
if (state == PlayModeStateChange.ExitingPlayMode)
|
||||
{
|
||||
ReleaseAllKeys();
|
||||
SimulateKeyboardOverlayState.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7dfe771ac58e34433a0f53e37467165b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
#nullable enable
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public class SimulateKeyboardResponse : BaseToolResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = "";
|
||||
public string Action { get; set; } = "";
|
||||
public string? KeyName { get; set; }
|
||||
|
||||
public SimulateKeyboardResponse()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f1a0b69b098ea4a18bcd8d3b8f1a663d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public class SimulateKeyboardSchema : BaseToolSchema
|
||||
{
|
||||
[Description("Keyboard action: Press(0) - one-shot key tap (Down then Up), KeyDown(1) - hold key down, KeyUp(2) - release held key")]
|
||||
public KeyboardAction Action { get; set; } = KeyboardAction.Press;
|
||||
|
||||
[Description("Key name matching the Input System Key enum (e.g. \"W\", \"Space\", \"LeftShift\", \"A\", \"Enter\"). Case-insensitive.")]
|
||||
public string Key { get; set; } = "";
|
||||
|
||||
[Description("Hold duration in seconds for Press action (default: 0 = one-shot tap). Ignored by KeyDown/KeyUp.")]
|
||||
public float Duration { get; set; } = 0f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55a637b48f2234e4c91cc59d1b7dc629
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,317 @@
|
||||
#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 keyboard key input in PlayMode via Input System. Supports one-shot press, key-down hold, and key-up release for game controls (WASD, Space, etc.). Requires the Input System package (com.unity.inputsystem).")]
|
||||
public class SimulateKeyboardTool : AbstractUnityTool<SimulateKeyboardSchema, SimulateKeyboardResponse>
|
||||
{
|
||||
public override string ToolName => "simulate-keyboard";
|
||||
|
||||
protected override
|
||||
#if !ULOOPMCP_HAS_INPUT_SYSTEM
|
||||
#pragma warning disable CS1998
|
||||
#endif
|
||||
async Task<SimulateKeyboardResponse> ExecuteAsync(
|
||||
SimulateKeyboardSchema parameters,
|
||||
CancellationToken ct)
|
||||
#if !ULOOPMCP_HAS_INPUT_SYSTEM
|
||||
#pragma warning restore CS1998
|
||||
#endif
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
#if !ULOOPMCP_HAS_INPUT_SYSTEM
|
||||
return new SimulateKeyboardResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "simulate-keyboard 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 SimulateKeyboardResponse
|
||||
{
|
||||
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 SimulateKeyboardResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "PlayMode is paused. Resume PlayMode before simulating keyboard input.",
|
||||
Action = parameters.Action.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(parameters.Key))
|
||||
{
|
||||
return new SimulateKeyboardResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "Key parameter is required. Examples: \"W\", \"Space\", \"LeftShift\", \"A\", \"Enter\".",
|
||||
Action = parameters.Action.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
string normalizedKey = NormalizeKeyName(parameters.Key);
|
||||
if (!Enum.TryParse<Key>(normalizedKey, ignoreCase: true, out Key key) || key == Key.None)
|
||||
{
|
||||
return new SimulateKeyboardResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = $"Invalid key name: \"{parameters.Key}\". Use Input System Key enum names (e.g. \"W\", \"Space\", \"LeftShift\", \"A\", \"Enter\").",
|
||||
Action = parameters.Action.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
Keyboard keyboard = Keyboard.current;
|
||||
if (keyboard == null)
|
||||
{
|
||||
return new SimulateKeyboardResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "No keyboard device found in Input System. Ensure the Input System package is properly configured.",
|
||||
Action = parameters.Action.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
VibeLogger.LogInfo(
|
||||
"simulate_keyboard_start",
|
||||
"Keyboard simulation started",
|
||||
new { Action = parameters.Action.ToString(), Key = parameters.Key },
|
||||
correlationId: correlationId
|
||||
);
|
||||
|
||||
EnsureOverlayExists();
|
||||
|
||||
SimulateKeyboardResponse response;
|
||||
|
||||
switch (parameters.Action)
|
||||
{
|
||||
case KeyboardAction.Press:
|
||||
response = await ExecutePress(keyboard, key, parameters.Duration, ct);
|
||||
break;
|
||||
|
||||
case KeyboardAction.KeyDown:
|
||||
response = await ExecuteKeyDown(keyboard, key, ct);
|
||||
break;
|
||||
|
||||
case KeyboardAction.KeyUp:
|
||||
response = await ExecuteKeyUp(keyboard, key, ct);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentException($"Unknown keyboard action: {parameters.Action}");
|
||||
}
|
||||
|
||||
VibeLogger.LogInfo(
|
||||
"simulate_keyboard_complete",
|
||||
$"Keyboard 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();
|
||||
}
|
||||
|
||||
private async Task<SimulateKeyboardResponse> ExecutePress(
|
||||
Keyboard keyboard, Key key, float duration, CancellationToken ct)
|
||||
{
|
||||
if (duration < 0f || float.IsNaN(duration) || float.IsInfinity(duration))
|
||||
{
|
||||
return new SimulateKeyboardResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = $"Duration must be non-negative, got: {duration}",
|
||||
Action = KeyboardAction.Press.ToString(),
|
||||
KeyName = key.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
string keyName = key.ToString();
|
||||
if (KeyboardKeyState.IsKeyHeld(key))
|
||||
{
|
||||
return new SimulateKeyboardResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = $"Key '{keyName}' is already held down. Call KeyUp first.",
|
||||
Action = KeyboardAction.Press.ToString(),
|
||||
KeyName = keyName
|
||||
};
|
||||
}
|
||||
|
||||
SimulateKeyboardOverlayState.ShowPress(keyName);
|
||||
KeyboardKeyState.RegisterTransientKey(key);
|
||||
bool pressWasApplied = false;
|
||||
|
||||
try
|
||||
{
|
||||
await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(() => KeyboardKeyState.SetKeyState(keyboard, key, true), ct);
|
||||
pressWasApplied = true;
|
||||
await InputSystemUpdateHelper.WaitForPressLifetime(duration, ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pressWasApplied)
|
||||
{
|
||||
await ReleaseKeyStateIfPossible(keyboard, key);
|
||||
KeyboardKeyState.UnregisterTransientKey(key);
|
||||
await FinalizePressOverlay(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
KeyboardKeyState.UnregisterTransientKey(key);
|
||||
SimulateKeyboardOverlayState.ClearPress();
|
||||
}
|
||||
}
|
||||
|
||||
string durationText = duration > 0f ? $" for {duration:F1}s" : "";
|
||||
return new SimulateKeyboardResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = $"Pressed '{keyName}'{durationText}",
|
||||
Action = KeyboardAction.Press.ToString(),
|
||||
KeyName = keyName
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<SimulateKeyboardResponse> ExecuteKeyDown(Keyboard keyboard, Key key, CancellationToken ct)
|
||||
{
|
||||
string keyName = key.ToString();
|
||||
|
||||
if (KeyboardKeyState.IsKeyHeld(key))
|
||||
{
|
||||
return new SimulateKeyboardResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = $"Key '{keyName}' is already held down. Call KeyUp first.",
|
||||
Action = KeyboardAction.KeyDown.ToString(),
|
||||
KeyName = keyName
|
||||
};
|
||||
}
|
||||
|
||||
bool keyDownApplied = false;
|
||||
bool committed = false;
|
||||
|
||||
try
|
||||
{
|
||||
await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(() => KeyboardKeyState.SetKeyState(keyboard, key, true), ct);
|
||||
keyDownApplied = true;
|
||||
KeyboardKeyState.SetKeyDown(key);
|
||||
SimulateKeyboardOverlayState.AddHeldKey(keyName);
|
||||
await InputSystemUpdateHelper.WaitForObservationFrames(ct);
|
||||
committed = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (keyDownApplied && !committed)
|
||||
{
|
||||
await RollbackHeldKey(keyboard, key, keyName);
|
||||
}
|
||||
}
|
||||
|
||||
return new SimulateKeyboardResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = $"Key '{keyName}' held down",
|
||||
Action = KeyboardAction.KeyDown.ToString(),
|
||||
KeyName = keyName
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<SimulateKeyboardResponse> ExecuteKeyUp(Keyboard keyboard, Key key, CancellationToken ct)
|
||||
{
|
||||
string keyName = key.ToString();
|
||||
|
||||
if (!KeyboardKeyState.IsKeyHeld(key))
|
||||
{
|
||||
return new SimulateKeyboardResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = $"Key '{keyName}' is not currently held. Call KeyDown first.",
|
||||
Action = KeyboardAction.KeyUp.ToString(),
|
||||
KeyName = keyName
|
||||
};
|
||||
}
|
||||
|
||||
await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(() => KeyboardKeyState.SetKeyState(keyboard, key, false), ct);
|
||||
KeyboardKeyState.SetKeyUp(key);
|
||||
SimulateKeyboardOverlayState.RemoveHeldKey(keyName);
|
||||
await InputSystemUpdateHelper.WaitForObservationFrames(ct);
|
||||
|
||||
return new SimulateKeyboardResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = $"Key '{keyName}' released",
|
||||
Action = KeyboardAction.KeyUp.ToString(),
|
||||
KeyName = keyName
|
||||
};
|
||||
}
|
||||
|
||||
private static string NormalizeKeyName(string keyName)
|
||||
{
|
||||
if (string.Equals(keyName, "Return", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Key.Enter.ToString();
|
||||
}
|
||||
return keyName;
|
||||
}
|
||||
|
||||
private static async Task FinalizePressOverlay(CancellationToken ct)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
SimulateKeyboardOverlayState.ClearPress();
|
||||
return;
|
||||
}
|
||||
|
||||
SimulateKeyboardOverlayState.ReleasePress();
|
||||
await EditorDelay.DelayFrame(1, CancellationToken.None);
|
||||
}
|
||||
|
||||
private static async Task RollbackHeldKey(Keyboard keyboard, Key key, string keyName)
|
||||
{
|
||||
await ReleaseKeyStateIfPossible(keyboard, key);
|
||||
KeyboardKeyState.SetKeyUp(key);
|
||||
SimulateKeyboardOverlayState.RemoveHeldKey(keyName);
|
||||
}
|
||||
|
||||
private static async Task ReleaseKeyStateIfPossible(Keyboard keyboard, Key key)
|
||||
{
|
||||
if (!CanInjectKeyboardState(keyboard))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(() => KeyboardKeyState.SetKeyState(keyboard, key, false), CancellationToken.None);
|
||||
}
|
||||
|
||||
private static bool CanInjectKeyboardState(Keyboard keyboard)
|
||||
{
|
||||
return EditorApplication.isPlaying && !EditorApplication.isPaused && Keyboard.current == keyboard;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1b68e3441c77a41d4aa28bf746973676
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2a2d235036f14d9d8b62acf37f6ed55
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: uloop-simulate-keyboard
|
||||
description: "Simulate keyboard input in PlayMode through Unity Input System. Use for key presses, holds, releases, and game controls such as WASD or Space."
|
||||
context: fork
|
||||
---
|
||||
|
||||
# Task
|
||||
|
||||
Simulate keyboard input on Unity PlayMode: $ARGUMENTS
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not)
|
||||
2. Execute the appropriate `uloop simulate-keyboard` command
|
||||
3. Take a screenshot to verify the result: `uloop screenshot --capture-mode rendering`
|
||||
4. Report what happened
|
||||
|
||||
## Tool Reference
|
||||
|
||||
```bash
|
||||
uloop simulate-keyboard --action <action> --key <key> [options]
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `--action` | enum | `Press` | `Press`, `KeyDown`, `KeyUp` |
|
||||
| `--key` | string | (required) | Key name matching Input System Key enum (e.g. `W`, `Space`, `LeftShift`, `A`, `Enter`). Case-insensitive. |
|
||||
| `--duration` | number | `0` | Hold duration in seconds for Press action (0 = one-shot tap). Ignored by KeyDown/KeyUp. |
|
||||
|
||||
### Actions
|
||||
|
||||
| Action | Behavior | Use Case |
|
||||
|--------|----------|----------|
|
||||
| `Press` | KeyDown → wait → KeyUp | One-shot tap (jump, use item) |
|
||||
| `KeyDown` | KeyDown only (held until KeyUp) | Start continuous movement, hold sprint |
|
||||
| `KeyUp` | KeyUp only (release held key) | Stop movement, release sprint |
|
||||
|
||||
### KeyDown/KeyUp Rules
|
||||
|
||||
- `KeyDown` fails if the key is already held
|
||||
- `KeyUp` fails if the key is not currently held
|
||||
- Multiple keys can be held simultaneously (e.g. W + LeftShift for sprint)
|
||||
- All held keys are automatically released when PlayMode exits
|
||||
- To hold a key for a fixed duration, prefer `--action Press --duration <seconds>` (one-shot, blocks until release). For multi-key holds (e.g. Shift+W), issue separate `KeyDown` calls, then `sleep <seconds>` between them and the `KeyUp` calls.
|
||||
|
||||
### Global Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--project-path <path>` | Optional. Use only when the target Unity project is not the current directory. |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# One-shot key press (tap W once)
|
||||
uloop simulate-keyboard --action Press --key W
|
||||
|
||||
# Jump (tap Space)
|
||||
uloop simulate-keyboard --action Press --key Space
|
||||
|
||||
# Hold W for 2 seconds (move forward)
|
||||
uloop simulate-keyboard --action Press --key W --duration 2.0
|
||||
|
||||
# Sprint forward (hold Shift + W, then release)
|
||||
uloop simulate-keyboard --action KeyDown --key LeftShift
|
||||
uloop simulate-keyboard --action KeyDown --key W
|
||||
uloop screenshot --capture-mode rendering
|
||||
uloop simulate-keyboard --action KeyUp --key W
|
||||
uloop simulate-keyboard --action KeyUp --key LeftShift
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Returns JSON with:
|
||||
- `Success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip)
|
||||
- `Message` (string): Description of what happened or why it failed
|
||||
- `Action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`)
|
||||
- `KeyName` (string, nullable): The key that was acted on; may be `null` when the action could not resolve a key
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Unity must be in **PlayMode**
|
||||
- **Input System package** must be installed (`com.unity.inputsystem`)
|
||||
- Use this only when the project already uses the New Input System.
|
||||
- Game code must read input via Input System API (e.g. `Keyboard.current[Key.W].isPressed`), not legacy `Input.GetKey()`
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42d87015d20c74a5891d205d1a19dfb2
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user