diff --git a/Assets/Tests/Editor/ControlPlayModeUseCaseTests.cs b/Assets/Tests/Editor/ControlPlayModeUseCaseTests.cs
index 236bbd264..c359930f5 100644
--- a/Assets/Tests/Editor/ControlPlayModeUseCaseTests.cs
+++ b/Assets/Tests/Editor/ControlPlayModeUseCaseTests.cs
@@ -493,6 +493,125 @@ public async Task ExecuteAsync_WhenPlayResumesFromPause_OnlyClearsPauseAndReport
Assert.That(quietSaver.SaveCallCount, Is.EqualTo(0));
}
+ ///
+ /// What: Play while already running and not paused is a no-op with the already-running message.
+ ///
+ [Test]
+ public async Task ExecuteAsync_WhenPlayWhileAlreadyRunning_ReportsNoOpWithoutSideEffects()
+ {
+ FakeControlPlayModeEditorStateService editorState = new(isPlaying: true, isPaused: false);
+ StubEditorUnsavedChangesQuietSaver quietSaver = new(
+ saveFailures: System.Array.Empty(),
+ remainingAfterSave: System.Array.Empty());
+ ControlPlayModeUseCase useCase = new ControlPlayModeUseCase(
+ new StubCompilationFailureProvider(System.Array.Empty()),
+ new StubCompilationFailureGate(false),
+ quietSaver,
+ editorState,
+ new StubDomainReloadDropStateProvider());
+ ControlPlayModeSchema schema = new ControlPlayModeSchema
+ {
+ Action = PlayModeAction.Play,
+ };
+
+ ControlPlayModeResponse response = await useCase.ExecuteAsync(schema, CancellationToken.None);
+
+ Assert.That(
+ response.Message,
+ Is.EqualTo("Play mode was already running; nothing to start or resume."));
+ Assert.That(response.Changed, Is.False);
+ Assert.That(response.ResumedFromPause, Is.False);
+ Assert.That(response.Warning, Is.Empty);
+ Assert.That(response.IsPlaying, Is.True);
+ Assert.That(response.IsPaused, Is.False);
+ Assert.That(editorState.IsPlayingSetCount, Is.EqualTo(0));
+ Assert.That(editorState.IsPausedSetCount, Is.EqualTo(0));
+ Assert.That(quietSaver.SaveCallCount, Is.EqualTo(0));
+ }
+
+ ///
+ /// What: Resume while already running and not paused uses the same no-op contract as Play.
+ ///
+ [Test]
+ public async Task ExecuteAsync_WhenResumeWhileAlreadyRunning_ReportsNoOpWithoutSideEffects()
+ {
+ FakeControlPlayModeEditorStateService editorState = new(isPlaying: true, isPaused: false);
+ StubEditorUnsavedChangesQuietSaver quietSaver = new(
+ saveFailures: System.Array.Empty(),
+ remainingAfterSave: System.Array.Empty());
+ ControlPlayModeUseCase useCase = new ControlPlayModeUseCase(
+ new StubCompilationFailureProvider(System.Array.Empty()),
+ new StubCompilationFailureGate(false),
+ quietSaver,
+ editorState,
+ new StubDomainReloadDropStateProvider());
+ ControlPlayModeSchema schema = new ControlPlayModeSchema
+ {
+ Action = PlayModeAction.Resume,
+ };
+
+ ControlPlayModeResponse response = await useCase.ExecuteAsync(schema, CancellationToken.None);
+
+ Assert.That(
+ response.Message,
+ Is.EqualTo("Play mode was already running; nothing to start or resume."));
+ Assert.That(response.Changed, Is.False);
+ Assert.That(response.ResumedFromPause, Is.False);
+ Assert.That(response.Warning, Is.Empty);
+ Assert.That(response.IsPlaying, Is.True);
+ Assert.That(response.IsPaused, Is.False);
+ Assert.That(editorState.IsPlayingSetCount, Is.EqualTo(0));
+ Assert.That(editorState.IsPausedSetCount, Is.EqualTo(0));
+ Assert.That(quietSaver.SaveCallCount, Is.EqualTo(0));
+ }
+
+ ///
+ /// What: a failed compile gate does not replace the already-running no-op while Play is live.
+ ///
+ [Test]
+ public async Task ExecuteAsync_WhenPlayWhileAlreadyRunningAndCompileFailed_ReportsNoOpNotCompileBlock()
+ {
+ FakeControlPlayModeEditorStateService editorState = new(isPlaying: true, isPaused: false);
+ StubEditorUnsavedChangesQuietSaver quietSaver = new(
+ saveFailures: System.Array.Empty(),
+ remainingAfterSave: System.Array.Empty());
+ ControlPlayModeCompileError[] compileErrors =
+ {
+ new ControlPlayModeCompileError
+ {
+ Message = "CS1525: invalid expression",
+ File = "Assets/Scripts/Sample.cs",
+ Line = 3
+ }
+ };
+ ControlPlayModeUseCase useCase = new ControlPlayModeUseCase(
+ new StubCompilationFailureProvider(compileErrors),
+ new StubCompilationFailureGate(true),
+ quietSaver,
+ editorState,
+ new StubDomainReloadDropStateProvider());
+ ControlPlayModeSchema schema = new ControlPlayModeSchema
+ {
+ Action = PlayModeAction.Play,
+ };
+
+ ControlPlayModeResponse response = await useCase.ExecuteAsync(schema, CancellationToken.None);
+
+ Assert.That(
+ response.Message,
+ Is.EqualTo("Play mode was already running; nothing to start or resume."));
+ Assert.That(response.Changed, Is.False);
+ Assert.That(response.ResumedFromPause, Is.False);
+ Assert.That(response.Warning, Is.Empty);
+ Assert.That(response.IsPlaying, Is.True);
+ Assert.That(response.IsPaused, Is.False);
+ Assert.That(response.BlockedByCompileErrors, Is.False);
+ Assert.That(response.CompileErrorCount, Is.EqualTo(0));
+ Assert.That(editorState.IsPlayingSetCount, Is.EqualTo(0));
+ Assert.That(editorState.IsPausedSetCount, Is.EqualTo(0));
+ Assert.That(quietSaver.SaveCallCount, Is.EqualTo(0));
+ }
+
[Test]
public async Task ExecuteAsync_WhenPlayStartsFreshSession_SetsPlayingAndReportsWarning()
{
diff --git a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeConstants.cs b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeConstants.cs
index 29154e236..4d5a6dbb8 100644
--- a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeConstants.cs
+++ b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeConstants.cs
@@ -10,5 +10,8 @@ internal static class ControlPlayModeConstants
internal const string StoppedByCliRunTestsCancel = "cli-run-tests-cancel";
internal const string StoppedByScriptCompilation = "script-compilation";
internal const string StoppedByUnknown = "unknown";
+
+ internal const string AlreadyRunningPlayMessage =
+ "Play mode was already running; nothing to start or resume.";
}
}
diff --git a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeUseCase.cs b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeUseCase.cs
index 8d7a5483c..cfdd3fa3c 100644
--- a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeUseCase.cs
+++ b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeUseCase.cs
@@ -173,6 +173,16 @@ private ControlPlayModeActionResult ExecutePlayModeStart(bool wasPaused, bool wa
true);
}
+ // Why: already-running Play used to report "Play mode started" even when Changed
+ // was false, which made a no-op look like a new session.
+ if (wasPlaying && !wasPaused)
+ {
+ return ControlPlayModeActionResult.FromState(
+ ControlPlayModeConstants.AlreadyRunningPlayMessage,
+ false,
+ false);
+ }
+
// Why only when entering Play from Edit: SaveScene does not work while already playing,
// and resume-from-pause must not rewrite Scene assets.
if (!wasPlaying)