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,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

View File

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

View File

@@ -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

View File

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