Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Assets/Tests/Editor/ToolSkillSynchronizerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,16 @@ public void DetectTargets_WhenOnlyInternalSkillsAreMissing_IgnoresThem()
Assert.That(detectedTargets[0].HasExistingSkills, Is.True);
}

// Tests that Unity-side discovery includes CLI-only skills from the Go CLI package.
[Test]
public void GetSkillSourceInfos_WhenProjectIsCurrentRoot_IncludesCliOnlyGoCliSkills()
{
SkillInstallLayout.SkillSourceInfo[] skillSources = SkillInstallLayout.GetSkillSourceInfos(_projectRoot)
.ToArray();

Assert.That(skillSources.Select(skill => skill.Name), Does.Contain("uloop-launch"));
}

// Tests that internal skill metadata maps back to the hidden tool name only.
[Test]
public void GetInternalSkillToolNames_WhenInternalSkillUsesSkillName_ReturnsToolName()
Expand Down
53 changes: 51 additions & 2 deletions Packages/src/Editor/Api/McpTools/ReplayInput/InputReplayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ internal static class InputReplayer
private static bool _showOverlay;
private static readonly HashSet<Key> _replayHeldKeys = new();
private static readonly HashSet<MouseButton> _replayHeldButtons = new();
private static readonly List<BaseInputModule> _disabledInputModules = new();
private static Vector2? _replayMousePosition;

// InputModule (StandaloneInputModule / InputSystemUIInputModule) ignores injected
// Mouse.current state, so UI interactions must go through ExecuteEvents directly.
// UI replay goes through ExecuteEvents so verification does not depend on
// GameView focus or the active input module's update timing.
private static bool _hasMousePosition;
private static bool _prevLeftButtonHeld;
private static Vector2? _previousReplayMousePosition;
Expand Down Expand Up @@ -115,6 +116,7 @@ public static void StopReplay()
_currentFrame = 0;
_replayHeldKeys.Clear();
_replayHeldButtons.Clear();
RestoreUiInputModules();
ResetUiReplayState();

ReplayInputOverlayState.Clear();
Expand Down Expand Up @@ -445,12 +447,14 @@ private static void ApplyUiEvents()
{
if (!_replayMousePosition.HasValue)
{
RestoreUiInputModules();
return;
}

EventSystem? eventSystem = EventSystem.current;
if (eventSystem == null)
{
RestoreUiInputModules();
return;
}

Expand All @@ -463,6 +467,7 @@ private static void ApplyUiEvents()
bool justPressed = leftHeld && !_prevLeftButtonHeld;
bool justReleased = !leftHeld && _prevLeftButtonHeld;
_prevLeftButtonHeld = leftHeld;
SetUiInputModulesSuppressed(leftHeld || justReleased);

Vector2 gameViewSize = Handles.GetMainGameViewSize();
Vector2 inputPos = new Vector2(screenPos.x, gameViewSize.y - screenPos.y);
Expand Down Expand Up @@ -653,6 +658,50 @@ private static bool DetectMousePositionEvents(InputRecordingData data)
return false;
}

private static void SetUiInputModulesSuppressed(bool suppressed)
{
if (!suppressed)
{
RestoreUiInputModules();
return;
}

EventSystem? eventSystem = EventSystem.current;
if (eventSystem == null)
{
return;
}

BaseInputModule[] modules = eventSystem.GetComponents<BaseInputModule>();
for (int i = 0; i < modules.Length; i++)
{
BaseInputModule module = modules[i];
if (!module.enabled)
{
continue;
}

// Replay synthesizes ExecuteEvents directly; leaving input modules enabled
// lets them consume the same injected Mouse.current state a second time.
module.enabled = false;
_disabledInputModules.Add(module);
}
}

private static void RestoreUiInputModules()
{
for (int i = 0; i < _disabledInputModules.Count; i++)
{
BaseInputModule module = _disabledInputModules[i];
if (module != null)
{
module.enabled = true;
}
}

_disabledInputModules.Clear();
}

private static void ResetUiReplayState()
{
_replayMousePosition = null;
Expand Down
8 changes: 6 additions & 2 deletions Packages/src/Editor/Config/SkillInstallLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ internal static class SkillInstallLayout
private const ushort CarriageReturnCodeUnit = 0x000D;
private const ushort LineFeedCodeUnit = 0x000A;
private const string CliPackageDirName = "GoCli~";
private const string CliInternalDirName = "internal";
private const string CliPresentationDirName = "presentation";
private const string CliPresentationPackageDirName = "cli";
private const string CliSkillDefinitionsDirName = "skill-definitions";
private const string CliOnlySkillDefinitionsDirName = "cli-only";
private static readonly HashSet<string> ExcludedFileNames = new()
Expand Down Expand Up @@ -660,8 +663,9 @@ private static string GetCliOnlySkillSourceRoot(string projectRoot)
return Path.Combine(
McpConstants.PackageResolvedPath,
CliPackageDirName,
"internal",
"cli",
CliInternalDirName,
CliPresentationDirName,
CliPresentationPackageDirName,
CliSkillDefinitionsDirName,
CliOnlySkillDefinitionsDirName);
}
Expand Down
4 changes: 2 additions & 2 deletions Packages/src/GoCli~/cmd/uloop-core/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"context"
"os"

"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/cli"
"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/app"
)

func main() {
os.Exit(cli.RunProjectLocal(context.Background(), os.Args[1:], os.Stdout, os.Stderr))
os.Exit(app.RunProjectLocal(context.Background(), os.Args[1:], os.Stdout, os.Stderr))
}
4 changes: 2 additions & 2 deletions Packages/src/GoCli~/cmd/uloop-dispatcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"context"
"os"

"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/cli"
"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/app"
)

func main() {
os.Exit(cli.RunLauncher(context.Background(), os.Args[1:], os.Stdout, os.Stderr))
os.Exit(app.RunLauncher(context.Background(), os.Args[1:], os.Stdout, os.Stderr))
}
Binary file modified Packages/src/GoCli~/dist/darwin-amd64/uloop-core
Binary file not shown.
Binary file modified Packages/src/GoCli~/dist/darwin-amd64/uloop-dispatcher
Binary file not shown.
Binary file modified Packages/src/GoCli~/dist/darwin-arm64/uloop-core
Binary file not shown.
Binary file modified Packages/src/GoCli~/dist/darwin-arm64/uloop-dispatcher
Binary file not shown.
Binary file modified Packages/src/GoCli~/dist/windows-amd64/uloop-core.exe
Binary file not shown.
Binary file modified Packages/src/GoCli~/dist/windows-amd64/uloop-dispatcher.exe
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"runtime"
"sort"
"strings"

"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/domain"
)

const (
Expand All @@ -29,50 +31,35 @@ var excludedProjectSearchDirs = map[string]bool{
"obj": true,
}

type Endpoint struct {
Network string
Address string
}

type RequestMetadata struct {
ExpectedProjectRoot string `json:"expectedProjectRoot"`
}

type Connection struct {
Endpoint Endpoint
ProjectRoot string
RequestMetadata *RequestMetadata
}

func ResolveConnection(startPath string, explicitProjectPath string) (Connection, error) {
func ResolveConnection(startPath string, explicitProjectPath string) (domain.Connection, error) {
projectRoot, err := resolveProjectRoot(startPath, explicitProjectPath)
if err != nil {
return Connection{}, err
return domain.Connection{}, err
}

canonicalProjectRoot, err := filepath.EvalSymlinks(projectRoot)
if err != nil {
return Connection{}, err
return domain.Connection{}, err
}
canonicalProjectRoot = trimTrailingSeparators(canonicalProjectRoot)

return Connection{
return domain.Connection{
Endpoint: CreateEndpoint(canonicalProjectRoot),
ProjectRoot: canonicalProjectRoot,
RequestMetadata: createRequestMetadata(canonicalProjectRoot),
}, nil
}

func CreateEndpoint(canonicalProjectRoot string) Endpoint {
func CreateEndpoint(canonicalProjectRoot string) domain.Endpoint {
endpointName := createEndpointName(canonicalProjectRoot)
if runtime.GOOS == "windows" {
return Endpoint{
return domain.Endpoint{
Network: "pipe",
Address: fmt.Sprintf(`%s-%s`, windowsPipePrefix, endpointName),
}
}

return Endpoint{
return domain.Endpoint{
Network: "unix",
Address: filepath.Join(unixSocketDir, endpointName+".sock"),
}
Expand Down Expand Up @@ -197,8 +184,8 @@ func resolveProjectRoot(startPath string, explicitProjectPath string) (string, e
return projectRoot, nil
}

func createRequestMetadata(projectRoot string) *RequestMetadata {
return &RequestMetadata{
func createRequestMetadata(projectRoot string) *domain.RequestMetadata {
return &domain.RequestMetadata{
ExpectedProjectRoot: projectRoot,
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"runtime"
"strings"
"testing"

"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/domain"
)

func TestCreateEndpointUsesStableProjectHash(t *testing.T) {
Expand Down Expand Up @@ -121,7 +123,7 @@ func createUnityProject(t *testing.T, projectRoot string) {
}
}

func assertProjectConnection(t *testing.T, connection Connection, projectRoot string) {
func assertProjectConnection(t *testing.T, connection domain.Connection, projectRoot string) {
t.Helper()

canonicalProjectRoot, err := filepath.EvalSymlinks(projectRoot)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,25 @@ import (
"fmt"
"time"

"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/framing"
"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/project"
"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/adapters/framing"
"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/domain"
)

const requestTimeout = 180 * time.Second

type Client struct {
connection project.Connection
connection domain.Connection
requestID int
}

type ProgressFunc func(message string)

type SendOutcome struct {
Result json.RawMessage
RequestDispatched bool
}
type ProgressFunc = func(message string)

type rpcRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params map[string]any `json:"params"`
ID int `json:"id"`
Uloop *project.RequestMetadata `json:"x-uloop,omitempty"`
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params map[string]any `json:"params"`
ID int `json:"id"`
Uloop *domain.RequestMetadata `json:"x-uloop,omitempty"`
}

type rpcResponse struct {
Expand Down Expand Up @@ -70,7 +65,7 @@ func (err *RPCError) Error() string {
return fmt.Sprintf("unity error: %s", err.Message)
}

func NewClient(connection project.Connection) *Client {
func NewClient(connection domain.Connection) *Client {
return &Client{connection: connection}
}

Expand All @@ -83,13 +78,13 @@ func (client *Client) SendWithProgress(ctx context.Context, method string, param
return outcome.Result, err
}

func (client *Client) SendWithProgressOutcome(ctx context.Context, method string, params map[string]any, progress ProgressFunc) (SendOutcome, error) {
func (client *Client) SendWithProgressOutcome(ctx context.Context, method string, params map[string]any, progress ProgressFunc) (domain.UnitySendOutcome, error) {
ctx, cancel := context.WithTimeout(ctx, requestTimeout)
defer cancel()

conn, err := dialEndpoint(ctx, client.connection.Endpoint)
if err != nil {
return SendOutcome{}, formatConnectionAttemptError(client.connection, err)
return domain.UnitySendOutcome{}, formatConnectionAttemptError(client.connection, err)
}
defer func() {
_ = conn.Close()
Expand All @@ -110,17 +105,17 @@ func (client *Client) SendWithProgressOutcome(ctx context.Context, method string

payload, err := json.Marshal(request)
if err != nil {
return SendOutcome{}, err
return domain.UnitySendOutcome{}, err
}

if deadline, ok := ctx.Deadline(); ok {
_ = conn.SetDeadline(deadline)
}

if err := framing.Write(conn, payload); err != nil {
return SendOutcome{}, err
return domain.UnitySendOutcome{}, err
}
outcome := SendOutcome{RequestDispatched: true}
outcome := domain.UnitySendOutcome{RequestDispatched: true}

responsePayload, err := framing.Read(bufio.NewReader(conn))
if err != nil {
Expand All @@ -146,7 +141,7 @@ func (client *Client) SendWithProgressOutcome(ctx context.Context, method string
return outcome, nil
}

func formatConnectionAttemptError(connection project.Connection, err error) error {
func formatConnectionAttemptError(connection domain.Connection, err error) error {
return &ConnectionAttemptError{
ProjectRoot: connection.ProjectRoot,
Endpoint: connection.Endpoint.Address,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import (
"errors"
"testing"

"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/project"
"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/domain"
)

func TestFormatConnectionAttemptErrorExplainsDialFailureWithoutDisconnectClaim(t *testing.T) {
connection := project.Connection{
Endpoint: project.Endpoint{
connection := domain.Connection{
Endpoint: domain.Endpoint{
Network: "unix",
Address: "/tmp/uloop/UnityCliLoop-sample.sock",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (
"context"
"net"

"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/project"
"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/domain"
)

func dialEndpoint(ctx context.Context, endpoint project.Endpoint) (net.Conn, error) {
func dialEndpoint(ctx context.Context, endpoint domain.Endpoint) (net.Conn, error) {
dialer := net.Dialer{}
return dialer.DialContext(ctx, endpoint.Network, endpoint.Address)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"net"

"github.com/Microsoft/go-winio"
"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/project"
"github.com/hatayama/unity-cli-loop/Packages/src/GoCli/internal/domain"
)

func dialEndpoint(ctx context.Context, endpoint project.Endpoint) (net.Conn, error) {
func dialEndpoint(ctx context.Context, endpoint domain.Endpoint) (net.Conn, error) {
return winio.DialPipeContext(ctx, endpoint.Address)
}
Loading