Skip to content

fix: quietly save dirty Scenes before CLI Play Mode start#1755

Merged
hatayama merged 1 commit into
feature/pause-playmode-guardsfrom
feat/control-play-mode-quiet-save
Jul 13, 2026
Merged

fix: quietly save dirty Scenes before CLI Play Mode start#1755
hatayama merged 1 commit into
feature/pause-playmode-guardsfrom
feat/control-play-mode-quiet-save

Conversation

@hatayama

@hatayama hatayama commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • control-play-mode --action Play now quietly saves dirty loaded Scenes and the current Prefab Stage before Edit→Play, matching the run-tests save path
  • Extracted shared EditorUnsavedChangesQuietSaver so both tools use the same quiet-save implementation

User Impact

  • Before: Starting Play Mode via CLI with unsaved Scene/Prefab changes could leave the Editor blocked on a save dialog or leave dirty state that later tools trip over
  • After: CLI Play start auto-saves those dirty assets when possible, or fails with an explicit message listing what could not be saved
  • Resume-from-pause does not save (SaveScene is ineffective while already playing)

Changes

  • Add IEditorUnsavedChangesQuietSaver / EditorUnsavedChangesQuietSaver under Common/EditorUtility
  • TestExecutionStateValidationService delegates detect/save to the shared helper
  • ControlPlayModeUseCase runs quiet save only on Edit→Play
  • ControlPlayMode asmdef references EditorUtility
  • Tests for save failure and remaining-dirty failure paths

Verification

  • uloop compile — 0 errors / 0 warnings
  • uloop run-tests filter ControlPlayModeUseCaseTests|TestExecutionStateValidationServiceTests — 24 passed

Test plan

  • CI green on this PR (or local verify if umbrella PR CI is skipped)
  • Manual: dirty a Scene, uloop control-play-mode --action Play → Scene saved, Play starts
  • Manual: resume from pause → no unexpected Scene rewrite

Made with Cursor

Review in cubic

Extract the run-tests quiet-save path into a shared EditorUtility helper and
call it on Edit→Play so control-play-mode no longer leaves dirty scenes that
block later tools or hang on Unity save dialogs. Resume-from-pause skips save
because SaveScene does not work while already playing.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a shared quiet-save service for dirty scenes and prefab stages, blocks PlayMode entry when saving fails or changes remain, and delegates test execution validation to the service.

Changes

Unsaved Editor Change Handling

Layer / File(s) Summary
Quiet-save contract and implementation
Packages/src/Editor/FirstPartyTools/Common/EditorUtility/*
Adds the saver interface and implements detection, saving, failure collection, and display-path formatting for dirty scenes and prefab stages.
PlayMode start save gate
Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeUseCase.cs, Assets/Tests/Editor/ControlPlayModeUseCaseTests.cs
Injects quiet saving into PlayMode start, blocks entry when saves fail or changes remain, and adds coverage for both outcomes.
Test execution validation delegation
Packages/src/Editor/FirstPartyTools/RunTests/TestExecutionStateValidationService.cs
Moves unsaved-change handling behind the injected saver and removes the service’s previous internal save implementation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ControlPlayModeUseCase
  participant IEditorUnsavedChangesQuietSaver
  participant UnityEditor
  ControlPlayModeUseCase->>IEditorUnsavedChangesQuietSaver: SaveUnsavedEditorChanges()
  IEditorUnsavedChangesQuietSaver->>UnityEditor: Save dirty scenes and prefab stage
  IEditorUnsavedChangesQuietSaver-->>ControlPlayModeUseCase: Failed changes
  ControlPlayModeUseCase->>IEditorUnsavedChangesQuietSaver: DetectUnsavedEditorChanges()
  IEditorUnsavedChangesQuietSaver-->>ControlPlayModeUseCase: Remaining changes
  ControlPlayModeUseCase-->>UnityEditor: Block PlayMode start
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: quietly saving dirty Scenes before CLI Play Mode starts.
Description check ✅ Passed The description matches the PR changes, covering the shared quiet-save helper, play-mode blocking, and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/control-play-mode-quiet-save

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hatayama

Copy link
Copy Markdown
Owner Author

LGTM. Verified: EditorUnsavedChangesQuietSaver is a line-for-line extraction of the run-tests save logic (scene loop, prefab-stage save with ClearDirtiness, display-path fallbacks all preserved), so run-tests behavior is unchanged while control-play-mode gains the same path. Placement is correct — quiet save runs only on Edit→Play (!wasPlaying), after the compile-error gate and before entering Play, and resume-from-pause never rewrites assets. Save failure and remaining-dirty both fail fast with explicit item lists instead of a dialog. asmdef wiring checked: the new files land in the existing Common/EditorUtility assembly (GUID d427b32a…), which RunTests already referenced and ControlPlayMode now references. Note: the out success in SavePrefabStage is a verbatim move of pre-existing code calling a Unity API that only offers an out overload — acceptable. Tests (save-failure and remaining-dirty blocking Play) have clear what-comments. Good to merge into the umbrella branch.

@hatayama hatayama merged commit b9a6216 into feature/pause-playmode-guards Jul 13, 2026
1 of 2 checks passed
@hatayama hatayama deleted the feat/control-play-mode-quiet-save branch July 13, 2026 11:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
Packages/src/Editor/FirstPartyTools/Common/EditorUtility/EditorUnsavedChangesQuietSaver.cs (1)

31-93: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate dirty-detection logic between detect/save paths.

AddDirtyLoadedScenes/SaveDirtyLoadedScenes and AddDirtyPrefabStage/SaveDirtyPrefabStage each repeat the same dirty-state guard clauses. Since this class is now the single shared source of truth for both PlayMode start and test execution gating, keeping detect/save logic in sync via duplicated conditions is a maintenance risk if the dirty criteria changes later.

♻️ Suggested consolidation
-        private static void AddDirtyLoadedScenes(List<string> unsavedEditorChanges)
-        {
-            Debug.Assert(unsavedEditorChanges != null, "unsavedEditorChanges must not be null");
-
-            for (int i = 0; i < SceneManager.sceneCount; i++)
-            {
-                Scene scene = SceneManager.GetSceneAt(i);
-                if (!scene.IsValid() || !scene.isLoaded || !scene.isDirty)
-                {
-                    continue;
-                }
-
-                unsavedEditorChanges.Add("Scene: " + GetSceneDisplayPath(scene));
-            }
-        }
-
-        private static void SaveDirtyLoadedScenes(List<string> failedChanges)
-        {
-            Debug.Assert(failedChanges != null, "failedChanges must not be null");
-
-            for (int i = 0; i < SceneManager.sceneCount; i++)
-            {
-                Scene scene = SceneManager.GetSceneAt(i);
-                if (!scene.IsValid() || !scene.isLoaded || !scene.isDirty)
-                {
-                    continue;
-                }
-
-                if (string.IsNullOrEmpty(scene.path) || !EditorSceneManager.SaveScene(scene))
-                {
-                    failedChanges.Add("Scene: " + GetSceneDisplayPath(scene));
-                }
-            }
-        }
+        private static IEnumerable<Scene> GetDirtyLoadedScenes()
+        {
+            for (int i = 0; i < SceneManager.sceneCount; i++)
+            {
+                Scene scene = SceneManager.GetSceneAt(i);
+                if (scene.IsValid() && scene.isLoaded && scene.isDirty)
+                {
+                    yield return scene;
+                }
+            }
+        }
+
+        private static void AddDirtyLoadedScenes(List<string> unsavedEditorChanges)
+        {
+            foreach (Scene scene in GetDirtyLoadedScenes())
+            {
+                unsavedEditorChanges.Add("Scene: " + GetSceneDisplayPath(scene));
+            }
+        }
+
+        private static void SaveDirtyLoadedScenes(List<string> failedChanges)
+        {
+            foreach (Scene scene in GetDirtyLoadedScenes())
+            {
+                if (string.IsNullOrEmpty(scene.path) || !EditorSceneManager.SaveScene(scene))
+                {
+                    failedChanges.Add("Scene: " + GetSceneDisplayPath(scene));
+                }
+            }
+        }

A similar TryGetDirtyPrefabStage(out PrefabStage) helper would remove the duplicated guard in the prefab-stage pair.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/src/Editor/FirstPartyTools/Common/EditorUtility/EditorUnsavedChangesQuietSaver.cs`
around lines 31 - 93, Consolidate the repeated dirty-state checks by introducing
shared helpers for loaded scenes and the current prefab stage, such as a
dirty-scene predicate/lookup and TryGetDirtyPrefabStage(out PrefabStage). Update
AddDirtyLoadedScenes, SaveDirtyLoadedScenes, AddDirtyPrefabStage, and
SaveDirtyPrefabStage to reuse these helpers while preserving their existing
detection and save-failure behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@Packages/src/Editor/FirstPartyTools/Common/EditorUtility/EditorUnsavedChangesQuietSaver.cs`:
- Around line 31-93: Consolidate the repeated dirty-state checks by introducing
shared helpers for loaded scenes and the current prefab stage, such as a
dirty-scene predicate/lookup and TryGetDirtyPrefabStage(out PrefabStage). Update
AddDirtyLoadedScenes, SaveDirtyLoadedScenes, AddDirtyPrefabStage, and
SaveDirtyPrefabStage to reuse these helpers while preserving their existing
detection and save-failure behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e4090505-092e-4879-8ade-cb7bc0d09e0a

📥 Commits

Reviewing files that changed from the base of the PR and between 48c0155 and 4669077.

⛔ Files ignored due to path filters (3)
  • Packages/src/Editor/FirstPartyTools/Common/EditorUtility/EditorUnsavedChangesQuietSaver.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Common/EditorUtility/IEditorUnsavedChangesQuietSaver.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/ControlPlayMode/UnityCLILoop.FirstPartyTools.ControlPlayMode.Editor.asmdef is excluded by none and included by none
📒 Files selected for processing (5)
  • Assets/Tests/Editor/ControlPlayModeUseCaseTests.cs
  • Packages/src/Editor/FirstPartyTools/Common/EditorUtility/EditorUnsavedChangesQuietSaver.cs
  • Packages/src/Editor/FirstPartyTools/Common/EditorUtility/IEditorUnsavedChangesQuietSaver.cs
  • Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeUseCase.cs
  • Packages/src/Editor/FirstPartyTools/RunTests/TestExecutionStateValidationService.cs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant