chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
#if ULOOPMCP_HAS_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
#endif
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
[McpTool(Description = "Replay recorded input during PlayMode. Loads a JSON recording and injects keyboard/mouse input frame-by-frame with zero CLI overhead.")]
|
||||
public class ReplayInputTool : AbstractUnityTool<ReplayInputSchema, ReplayInputResponse>
|
||||
{
|
||||
public override string ToolName => "replay-input";
|
||||
|
||||
protected override
|
||||
#if !ULOOPMCP_HAS_INPUT_SYSTEM
|
||||
#pragma warning disable CS1998
|
||||
#endif
|
||||
async Task<ReplayInputResponse> ExecuteAsync(
|
||||
ReplayInputSchema parameters,
|
||||
CancellationToken ct)
|
||||
#if !ULOOPMCP_HAS_INPUT_SYSTEM
|
||||
#pragma warning restore CS1998
|
||||
#endif
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
#if !ULOOPMCP_HAS_INPUT_SYSTEM
|
||||
return new ReplayInputResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "replay-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();
|
||||
|
||||
VibeLogger.LogInfo(
|
||||
"replay_input_start",
|
||||
"Replay input started",
|
||||
new { Action = parameters.Action.ToString() },
|
||||
correlationId: correlationId
|
||||
);
|
||||
|
||||
ReplayInputResponse response;
|
||||
|
||||
switch (parameters.Action)
|
||||
{
|
||||
case ReplayInputAction.Start:
|
||||
response = ExecuteStart(parameters);
|
||||
break;
|
||||
|
||||
case ReplayInputAction.Stop:
|
||||
response = ExecuteStop();
|
||||
break;
|
||||
|
||||
case ReplayInputAction.Status:
|
||||
response = ExecuteStatus();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentException($"Unknown replay-input action: {parameters.Action}");
|
||||
}
|
||||
|
||||
VibeLogger.LogInfo(
|
||||
"replay_input_complete",
|
||||
$"Replay input completed: {response.Message}",
|
||||
new { Action = parameters.Action.ToString(), Success = response.Success },
|
||||
correlationId: correlationId
|
||||
);
|
||||
|
||||
await Task.CompletedTask;
|
||||
return response;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if ULOOPMCP_HAS_INPUT_SYSTEM
|
||||
private static ReplayInputResponse ExecuteStart(ReplayInputSchema parameters)
|
||||
{
|
||||
if (!EditorApplication.isPlaying)
|
||||
{
|
||||
return new ReplayInputResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "PlayMode is not active. Use control-play-mode tool to start PlayMode first.",
|
||||
Action = ReplayInputAction.Start.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
if (EditorApplication.isPaused)
|
||||
{
|
||||
return new ReplayInputResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "PlayMode is paused. Resume PlayMode before replaying input.",
|
||||
Action = ReplayInputAction.Start.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
if (InputReplayer.IsReplaying)
|
||||
{
|
||||
return new ReplayInputResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "Already replaying. Stop the current replay first.",
|
||||
Action = ReplayInputAction.Start.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
if (InputRecorder.IsRecording)
|
||||
{
|
||||
return new ReplayInputResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "Cannot replay while recording. Stop the recording first.",
|
||||
Action = ReplayInputAction.Start.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
string inputPath = InputRecordingFileHelper.ResolveLatestRecording(parameters.InputPath);
|
||||
if (string.IsNullOrEmpty(inputPath))
|
||||
{
|
||||
return new ReplayInputResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = $"No recording files found in {RecordInputConstants.DEFAULT_OUTPUT_DIR}/",
|
||||
Action = ReplayInputAction.Start.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
if (!File.Exists(inputPath))
|
||||
{
|
||||
return new ReplayInputResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = $"Recording file not found: {inputPath}",
|
||||
Action = ReplayInputAction.Start.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
InputRecordingData? data = InputRecordingFileHelper.Load(inputPath);
|
||||
|
||||
if (data == null || data.Metadata == null)
|
||||
{
|
||||
return new ReplayInputResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = $"Failed to parse recording file: {inputPath}",
|
||||
Action = ReplayInputAction.Start.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
InputReplayer.StartReplay(data, parameters.Loop, parameters.ShowOverlay);
|
||||
|
||||
int eventCount = data.GetTotalEventCount();
|
||||
|
||||
return new ReplayInputResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = $"Replay started: {eventCount} events across {data.Metadata.TotalFrames} frames" +
|
||||
(parameters.Loop ? " (looping)" : ""),
|
||||
Action = ReplayInputAction.Start.ToString(),
|
||||
InputPath = inputPath,
|
||||
TotalFrames = data.Metadata.TotalFrames,
|
||||
IsReplaying = true
|
||||
};
|
||||
}
|
||||
|
||||
private static ReplayInputResponse ExecuteStop()
|
||||
{
|
||||
if (!InputReplayer.IsReplaying)
|
||||
{
|
||||
return new ReplayInputResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "Not currently replaying.",
|
||||
Action = ReplayInputAction.Stop.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
int stoppedFrame = InputReplayer.CurrentFrame;
|
||||
int totalFrames = InputReplayer.TotalFrames;
|
||||
InputReplayer.StopReplay();
|
||||
|
||||
return new ReplayInputResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = $"Replay stopped at frame {stoppedFrame}/{totalFrames}",
|
||||
Action = ReplayInputAction.Stop.ToString(),
|
||||
CurrentFrame = stoppedFrame,
|
||||
TotalFrames = totalFrames,
|
||||
IsReplaying = false
|
||||
};
|
||||
}
|
||||
|
||||
private static ReplayInputResponse ExecuteStatus()
|
||||
{
|
||||
if (!InputReplayer.IsReplaying)
|
||||
{
|
||||
return new ReplayInputResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "Not replaying.",
|
||||
Action = ReplayInputAction.Status.ToString(),
|
||||
IsReplaying = false
|
||||
};
|
||||
}
|
||||
|
||||
return new ReplayInputResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = $"Replaying: frame {InputReplayer.CurrentFrame}/{InputReplayer.TotalFrames} ({InputReplayer.Progress:P0})",
|
||||
Action = ReplayInputAction.Status.ToString(),
|
||||
CurrentFrame = InputReplayer.CurrentFrame,
|
||||
TotalFrames = InputReplayer.TotalFrames,
|
||||
Progress = InputReplayer.Progress,
|
||||
IsReplaying = true
|
||||
};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user