From e00ed289e88e8f8a69a63af94360a94f41440b95 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 6 Oct 2026 22:42:43 +0900 Subject: [PATCH 1/5] Add failing tests for recognizing a Virtual Player project root Hot reload tells a Multiplayer Play Mode Virtual Player to compile first when it cannot find the player's compiled assembly, but a player has no compiled assemblies under its own root, so compiling never changes the answer. These tests pin the recognizer and the reason worded for a player. The helper is a stub that keeps today's behavior, so the Virtual Player cases fail until it is implemented. --- .../HotReloadVirtualPlayerProjectTests.cs | 160 ++++++++++++++++++ ...HotReloadVirtualPlayerProjectTests.cs.meta | 11 ++ .../Shared/HotReloadVirtualPlayerProject.cs | 19 +++ .../HotReloadVirtualPlayerProject.cs.meta | 11 ++ 4 files changed, 201 insertions(+) create mode 100644 Assets/Tests/Editor/HotReload/HotReloadVirtualPlayerProjectTests.cs create mode 100644 Assets/Tests/Editor/HotReload/HotReloadVirtualPlayerProjectTests.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs.meta diff --git a/Assets/Tests/Editor/HotReload/HotReloadVirtualPlayerProjectTests.cs b/Assets/Tests/Editor/HotReload/HotReloadVirtualPlayerProjectTests.cs new file mode 100644 index 0000000000..acdeb9d5ac --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadVirtualPlayerProjectTests.cs @@ -0,0 +1,160 @@ +using System.IO; + +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload +{ + /// + /// EditMode coverage for recognizing a Multiplayer Play Mode Virtual Player by its project root, + /// and for the missing-assembly reason worded for it. + /// + public class HotReloadVirtualPlayerProjectTests + { + private static readonly string VirtualPlayerRoot = + Path.Combine("workspace", "project", "Library", "VP", "mppm0a1b2c3d"); + + private static readonly string OrdinaryProjectRoot = Path.Combine("workspace", "project"); + + /// + /// What: a player directory directly under Library/VP is recognized as a Virtual Player root. + /// + [Test] + public void IsVirtualPlayerProjectRoot_PlayerDirectoryUnderLibraryVP_ReturnsTrue() + { + Assert.That(HotReloadVirtualPlayerProject.IsVirtualPlayerProjectRoot(VirtualPlayerRoot), Is.True); + } + + /// + /// What: a trailing separator does not shift the parent lookup up by one directory. + /// + [Test] + public void IsVirtualPlayerProjectRoot_TrailingSeparator_ReturnsTrue() + { + string rootWithTrailingSeparator = VirtualPlayerRoot + Path.DirectorySeparatorChar; + + Assert.That( + HotReloadVirtualPlayerProject.IsVirtualPlayerProjectRoot(rootWithTrailingSeparator), + Is.True); + } + + /// + /// What: an ordinary project root is not taken for a Virtual Player. + /// + [Test] + public void IsVirtualPlayerProjectRoot_OrdinaryProjectRoot_ReturnsFalse() + { + Assert.That(HotReloadVirtualPlayerProject.IsVirtualPlayerProjectRoot(OrdinaryProjectRoot), Is.False); + } + + /// + /// What: the Library/VP directory itself is not a Virtual Player root. + /// + [Test] + public void IsVirtualPlayerProjectRoot_TheVPDirectoryItself_ReturnsFalse() + { + string virtualPlayersDirectory = Path.Combine("workspace", "project", "Library", "VP"); + + Assert.That( + HotReloadVirtualPlayerProject.IsVirtualPlayerProjectRoot(virtualPlayersDirectory), + Is.False); + } + + /// + /// What: a directory under Library whose parent is not named VP is not a Virtual Player root. + /// + [Test] + public void IsVirtualPlayerProjectRoot_ParentIsNotVP_ReturnsFalse() + { + string projectRoot = Path.Combine("workspace", "project", "Library", "Other", "mppm0a1b2c3d"); + + Assert.That(HotReloadVirtualPlayerProject.IsVirtualPlayerProjectRoot(projectRoot), Is.False); + } + + /// + /// What: a directory under a VP directory that is not inside Library is not a Virtual Player root. + /// + [Test] + public void IsVirtualPlayerProjectRoot_GrandparentIsNotLibrary_ReturnsFalse() + { + string projectRoot = Path.Combine("workspace", "project", "Other", "VP", "mppm0a1b2c3d"); + + Assert.That(HotReloadVirtualPlayerProject.IsVirtualPlayerProjectRoot(projectRoot), Is.False); + } + + /// + /// What: a path without a parent or without a grandparent is answered false instead of throwing. + /// + [Test] + public void IsVirtualPlayerProjectRoot_PathTooShortToHaveAGrandparent_ReturnsFalse() + { + string playerDirectoryOnly = "mppm0a1b2c3d"; + string virtualPlayersAndPlayerDirectory = Path.Combine("VP", "mppm0a1b2c3d"); + + Assert.That( + HotReloadVirtualPlayerProject.IsVirtualPlayerProjectRoot(playerDirectoryOnly), + Is.False, + "A path without a parent must not be taken for a Virtual Player root."); + Assert.That( + HotReloadVirtualPlayerProject.IsVirtualPlayerProjectRoot(virtualPlayersAndPlayerDirectory), + Is.False, + "A path without a grandparent must not be taken for a Virtual Player root."); + } + + /// + /// What: on Windows, a Virtual Player root is recognized with backslashes and with forward slashes. + /// + [Test] + public void IsVirtualPlayerProjectRoot_WindowsSeparators_ReturnsTrue() + { + if (Path.DirectorySeparatorChar != '\\') + { + Assert.Pass("Windows separator handling applies only on Windows."); + return; + } + + Assert.That( + HotReloadVirtualPlayerProject.IsVirtualPlayerProjectRoot(@"C:\workspace\project\Library\VP\mppm0a1b2c3d"), + Is.True, + "Backslash separators must be recognized."); + Assert.That( + HotReloadVirtualPlayerProject.IsVirtualPlayerProjectRoot("C:/workspace/project/Library/VP/mppm0a1b2c3d"), + Is.True, + "Forward-slash separators must be recognized."); + } + + /// + /// What: for a Virtual Player, the reason says hot reload cannot patch it and that a compile + /// brings the edit in, instead of asking for a compile first. + /// + [Test] + public void DescribeMissingCompiledAssembly_VirtualPlayerRoot_SaysHotReloadCannotPatchAVirtualPlayer() + { + string dllPath = Path.Combine(VirtualPlayerRoot, "Library", "ScriptAssemblies", "Sample.dll"); + + string reason = HotReloadVirtualPlayerProject.DescribeMissingCompiledAssembly(VirtualPlayerRoot, dllPath); + + Assert.That(reason, Does.Contain(dllPath)); + Assert.That(reason, Does.Contain("Virtual Player")); + Assert.That(reason, Does.Contain("main Editor")); + Assert.That(reason, Does.Contain("compile")); + Assert.That(reason, Does.Not.Contain("Compile the project first")); + } + + /// + /// What: for an ordinary project, the reason keeps the compile-first text unchanged. + /// + [Test] + public void DescribeMissingCompiledAssembly_OrdinaryRoot_KeepsTheCompileFirstText() + { + string dllPath = Path.Combine(OrdinaryProjectRoot, "Library", "ScriptAssemblies", "Sample.dll"); + + string reason = HotReloadVirtualPlayerProject.DescribeMissingCompiledAssembly(OrdinaryProjectRoot, dllPath); + + Assert.That( + reason, + Is.EqualTo("Compiled assembly not found at '" + dllPath + "'. Compile the project first.")); + } + } +} diff --git a/Assets/Tests/Editor/HotReload/HotReloadVirtualPlayerProjectTests.cs.meta b/Assets/Tests/Editor/HotReload/HotReloadVirtualPlayerProjectTests.cs.meta new file mode 100644 index 0000000000..62a0d7e1b6 --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadVirtualPlayerProjectTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d5931f630b52945939be6449c828de76 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs new file mode 100644 index 0000000000..ac3d84ddcf --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs @@ -0,0 +1,19 @@ +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Recognizes a Multiplayer Play Mode Virtual Player by its project root, and words the + /// missing-assembly reason for it. + /// + internal static class HotReloadVirtualPlayerProject + { + internal static bool IsVirtualPlayerProjectRoot(string projectRoot) + { + return false; + } + + internal static string DescribeMissingCompiledAssembly(string projectRoot, string dllPath) + { + return "Compiled assembly not found at '" + dllPath + "'. Compile the project first."; + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs.meta new file mode 100644 index 0000000000..b3a1270a40 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b89d5225b34994bd3bc23f66877431a7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From f1d8e93faedac15cef8448ad258d8f2118ad3273 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 6 Oct 2026 22:43:49 +0900 Subject: [PATCH 2/5] Tell a Virtual Player that hot reload cannot patch it yet When the compiled assembly is missing and the project root is a Multiplayer Play Mode Virtual Player (
/Library/VP/), the (file) reason now says the player loads the main Editor's script assemblies, so hot reload cannot patch it yet, the edit reaches it through a compile, and a patch applied to the main Editor does not reach it. "Compile the project first" never helps there, because a player has no compiled assemblies under its own root. The player is recognized from the path alone, so projects without Multiplayer Play Mode still compile. Every other project keeps the existing text. --- .../HotReload/HotReloadPatchTargetSupport.cs | 2 +- .../Shared/HotReloadVirtualPlayerProject.cs | 51 ++++++++++++++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadPatchTargetSupport.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadPatchTargetSupport.cs index 23f6cd73a6..8d03315cb3 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadPatchTargetSupport.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadPatchTargetSupport.cs @@ -105,7 +105,7 @@ internal static HotReloadPatchTargetResolution ResolvePatchTarget( outcomes.Add( HotReloadMethodOutcome.Failed( "(file)", - "Compiled assembly not found at '" + home.DllPath + "'. Compile the project first.", + HotReloadVirtualPlayerProject.DescribeMissingCompiledAssembly(projectRoot, home.DllPath), assemblyResolvePath)); return HotReloadPatchTargetResolution.EarlyExit( new HotReloadFileProcessResult(outcomes, warnings, 0)); diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs index ac3d84ddcf..65f29d6279 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs @@ -1,3 +1,8 @@ +using System; +using System.IO; + +using UnityEngine; + namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { /// @@ -6,14 +11,56 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// internal static class HotReloadVirtualPlayerProject { + private const string LibraryDirectoryName = "Library"; + private const string VirtualPlayersDirectoryName = "VP"; + + // A Virtual Player's project root is
/Library/VP/. internal static bool IsVirtualPlayerProjectRoot(string projectRoot) { - return false; + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty."); + + // Why trim first: with a trailing separator, Path.GetDirectoryName returns the same + // directory, so every parent lookup below would land one level too low. + string trimmedRoot = projectRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + string virtualPlayersDirectory = Path.GetDirectoryName(trimmedRoot); + if (string.IsNullOrEmpty(virtualPlayersDirectory)) + { + return false; + } + + if (!string.Equals( + Path.GetFileName(virtualPlayersDirectory), + VirtualPlayersDirectoryName, + StringComparison.Ordinal)) + { + return false; + } + + string libraryDirectory = Path.GetDirectoryName(virtualPlayersDirectory); + if (string.IsNullOrEmpty(libraryDirectory)) + { + return false; + } + + return string.Equals(Path.GetFileName(libraryDirectory), LibraryDirectoryName, StringComparison.Ordinal); } internal static string DescribeMissingCompiledAssembly(string projectRoot, string dllPath) { - return "Compiled assembly not found at '" + dllPath + "'. Compile the project first."; + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty."); + Debug.Assert(!string.IsNullOrEmpty(dllPath), "dllPath must not be null or empty."); + + if (!IsVirtualPlayerProjectRoot(projectRoot)) + { + return "Compiled assembly not found at '" + dllPath + "'. Compile the project first."; + } + + // Why a different reason: a Virtual Player has no compiled assemblies under its own + // root, so "compile first" gives the same answer however many times it is followed. + return "Compiled assembly not found at '" + dllPath + "'. This Editor is a Multiplayer Play Mode " + + "Virtual Player: it loads the script assemblies of the main Editor's project, so hot reload " + + "cannot patch it yet. The edit reaches this player through a compile; a patch applied to the " + + "main Editor does not reach it."; } } } From 6fc544d7745832694b8b605e42456058723e87bf Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 6 Oct 2026 22:47:16 +0900 Subject: [PATCH 3/5] Document that hot reload cannot patch a Virtual Player yet The Multiplayer Play Mode reference gains a Hot reload section: a patch stays in the Editor process it was applied to, so a patch on the main Editor never reaches a Virtual Player, and a player sent hot-reload reports the file as Failed before the CLI's compile fallback brings the edit in. The hot-reload scope reference lists the Virtual Player as a Failed condition. The generated skill copies are regenerated from the sources. --- .../references/multiplayer-play-mode.md | 11 +++++++++++ .../uloop-hot-reload/references/scope-and-limits.md | 1 + .../references/multiplayer-play-mode.md | 11 +++++++++++ .../uloop-hot-reload/references/scope-and-limits.md | 1 + .../Skill/references/multiplayer-play-mode.md | 11 +++++++++++ .../HotReload/Skill/references/scope-and-limits.md | 1 + 6 files changed, 36 insertions(+) diff --git a/.agents/skills/uloop-control-play-mode/references/multiplayer-play-mode.md b/.agents/skills/uloop-control-play-mode/references/multiplayer-play-mode.md index df26774fe1..0e7d046a32 100644 --- a/.agents/skills/uloop-control-play-mode/references/multiplayer-play-mode.md +++ b/.agents/skills/uloop-control-play-mode/references/multiplayer-play-mode.md @@ -47,6 +47,17 @@ uloop --project-path /Library/VP/mppm simulate-keyboard --acti uloop's one-command-at-a-time rule is per Editor, so commands to different players (or to the main Editor) do not block each other. +## Hot reload + +- A hot-reload patch lives in the Editor process it was applied to. A patch applied to the + main Editor does not reach the Virtual Players, and a player's own + `uloop --project-path /Library/VP/mppm hot-reload --status` reports no + active patch. +- Hot reload cannot patch a Virtual Player yet: a player loads the main project's + `Library/ScriptAssemblies` and has none under its own root. `hot-reload --files ...` sent + to a player reports the file as `Failed`, and the CLI then compiles in that player, which + brings the edit in (`Outcome` is `ReplacedByCompile`). + ## Known limitations - `Stop` sent while Virtual Players are still starting (the main Editor is not yet in Play Mode) diff --git a/.agents/skills/uloop-hot-reload/references/scope-and-limits.md b/.agents/skills/uloop-hot-reload/references/scope-and-limits.md index 19f9702124..4284f93130 100644 --- a/.agents/skills/uloop-hot-reload/references/scope-and-limits.md +++ b/.agents/skills/uloop-hot-reload/references/scope-and-limits.md @@ -427,6 +427,7 @@ source on disk. When a run skips a method it had patched before, `Warnings` name | File does not belong to any compiled assembly | Per-file entry with `Method` = `(file)`; only `Assets/` and `Packages/` sources resolve | | Resolved assembly name is missing from CompilationPipeline | Per-file entry with `Method` = `(file)`; Unity may have mapped a not-yet-imported `.asmdef` onto a predefined assembly. Run `uloop compile` first | | Script is not in the last compiled assembly's source list and its assembly membership cannot be confirmed | Per-file entry with `Method` = `(file)`; a new file passed with `--files` is hot-reloadable when its membership in an existing, unchanged compiled assembly is confirmed (`.asmdef` / `.asmref` boundaries are checked when present; a predefined assembly with none also passes), but fails when the Editor is not ready or an `.asmdef` / `.asmref` on its path was added, deleted, or changed since the last import — run `uloop compile` first | +| The Editor is a Multiplayer Play Mode Virtual Player | Per-file entry with `Method` = `(file)`; a Virtual Player has no compiled assemblies under its own project root, so hot reload cannot patch it yet. The CLI's compile fallback brings the edit to that player. A patch applied to the main Editor does not reach Virtual Players (each is a separate Editor process) | | Loaded assembly differs from the one on disk (pending compile) | Run `uloop compile` first, then retry | | Source file fails to parse | Per-file `Failed` entry with `Method` = `(file)` carrying the parse errors; nothing from that file is applied, its earlier patches stay active, and `Success` is false | | Method signature not found in the loaded assembly | Usually a stale assembly; run `uloop compile`. In-file renames and signature changes are classified as added members before reaching this point | diff --git a/.claude/skills/uloop-control-play-mode/references/multiplayer-play-mode.md b/.claude/skills/uloop-control-play-mode/references/multiplayer-play-mode.md index df26774fe1..0e7d046a32 100644 --- a/.claude/skills/uloop-control-play-mode/references/multiplayer-play-mode.md +++ b/.claude/skills/uloop-control-play-mode/references/multiplayer-play-mode.md @@ -47,6 +47,17 @@ uloop --project-path /Library/VP/mppm simulate-keyboard --acti uloop's one-command-at-a-time rule is per Editor, so commands to different players (or to the main Editor) do not block each other. +## Hot reload + +- A hot-reload patch lives in the Editor process it was applied to. A patch applied to the + main Editor does not reach the Virtual Players, and a player's own + `uloop --project-path /Library/VP/mppm hot-reload --status` reports no + active patch. +- Hot reload cannot patch a Virtual Player yet: a player loads the main project's + `Library/ScriptAssemblies` and has none under its own root. `hot-reload --files ...` sent + to a player reports the file as `Failed`, and the CLI then compiles in that player, which + brings the edit in (`Outcome` is `ReplacedByCompile`). + ## Known limitations - `Stop` sent while Virtual Players are still starting (the main Editor is not yet in Play Mode) diff --git a/.claude/skills/uloop-hot-reload/references/scope-and-limits.md b/.claude/skills/uloop-hot-reload/references/scope-and-limits.md index 19f9702124..4284f93130 100644 --- a/.claude/skills/uloop-hot-reload/references/scope-and-limits.md +++ b/.claude/skills/uloop-hot-reload/references/scope-and-limits.md @@ -427,6 +427,7 @@ source on disk. When a run skips a method it had patched before, `Warnings` name | File does not belong to any compiled assembly | Per-file entry with `Method` = `(file)`; only `Assets/` and `Packages/` sources resolve | | Resolved assembly name is missing from CompilationPipeline | Per-file entry with `Method` = `(file)`; Unity may have mapped a not-yet-imported `.asmdef` onto a predefined assembly. Run `uloop compile` first | | Script is not in the last compiled assembly's source list and its assembly membership cannot be confirmed | Per-file entry with `Method` = `(file)`; a new file passed with `--files` is hot-reloadable when its membership in an existing, unchanged compiled assembly is confirmed (`.asmdef` / `.asmref` boundaries are checked when present; a predefined assembly with none also passes), but fails when the Editor is not ready or an `.asmdef` / `.asmref` on its path was added, deleted, or changed since the last import — run `uloop compile` first | +| The Editor is a Multiplayer Play Mode Virtual Player | Per-file entry with `Method` = `(file)`; a Virtual Player has no compiled assemblies under its own project root, so hot reload cannot patch it yet. The CLI's compile fallback brings the edit to that player. A patch applied to the main Editor does not reach Virtual Players (each is a separate Editor process) | | Loaded assembly differs from the one on disk (pending compile) | Run `uloop compile` first, then retry | | Source file fails to parse | Per-file `Failed` entry with `Method` = `(file)` carrying the parse errors; nothing from that file is applied, its earlier patches stay active, and `Success` is false | | Method signature not found in the loaded assembly | Usually a stale assembly; run `uloop compile`. In-file renames and signature changes are classified as added members before reaching this point | diff --git a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/references/multiplayer-play-mode.md b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/references/multiplayer-play-mode.md index df26774fe1..0e7d046a32 100644 --- a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/references/multiplayer-play-mode.md +++ b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/references/multiplayer-play-mode.md @@ -47,6 +47,17 @@ uloop --project-path /Library/VP/mppm simulate-keyboard --acti uloop's one-command-at-a-time rule is per Editor, so commands to different players (or to the main Editor) do not block each other. +## Hot reload + +- A hot-reload patch lives in the Editor process it was applied to. A patch applied to the + main Editor does not reach the Virtual Players, and a player's own + `uloop --project-path /Library/VP/mppm hot-reload --status` reports no + active patch. +- Hot reload cannot patch a Virtual Player yet: a player loads the main project's + `Library/ScriptAssemblies` and has none under its own root. `hot-reload --files ...` sent + to a player reports the file as `Failed`, and the CLI then compiles in that player, which + brings the edit in (`Outcome` is `ReplacedByCompile`). + ## Known limitations - `Stop` sent while Virtual Players are still starting (the main Editor is not yet in Play Mode) diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Skill/references/scope-and-limits.md b/Packages/src/Editor/FirstPartyTools/HotReload/Skill/references/scope-and-limits.md index 19f9702124..4284f93130 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Skill/references/scope-and-limits.md +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Skill/references/scope-and-limits.md @@ -427,6 +427,7 @@ source on disk. When a run skips a method it had patched before, `Warnings` name | File does not belong to any compiled assembly | Per-file entry with `Method` = `(file)`; only `Assets/` and `Packages/` sources resolve | | Resolved assembly name is missing from CompilationPipeline | Per-file entry with `Method` = `(file)`; Unity may have mapped a not-yet-imported `.asmdef` onto a predefined assembly. Run `uloop compile` first | | Script is not in the last compiled assembly's source list and its assembly membership cannot be confirmed | Per-file entry with `Method` = `(file)`; a new file passed with `--files` is hot-reloadable when its membership in an existing, unchanged compiled assembly is confirmed (`.asmdef` / `.asmref` boundaries are checked when present; a predefined assembly with none also passes), but fails when the Editor is not ready or an `.asmdef` / `.asmref` on its path was added, deleted, or changed since the last import — run `uloop compile` first | +| The Editor is a Multiplayer Play Mode Virtual Player | Per-file entry with `Method` = `(file)`; a Virtual Player has no compiled assemblies under its own project root, so hot reload cannot patch it yet. The CLI's compile fallback brings the edit to that player. A patch applied to the main Editor does not reach Virtual Players (each is a separate Editor process) | | Loaded assembly differs from the one on disk (pending compile) | Run `uloop compile` first, then retry | | Source file fails to parse | Per-file `Failed` entry with `Method` = `(file)` carrying the parse errors; nothing from that file is applied, its earlier patches stay active, and `Success` is false | | Method signature not found in the loaded assembly | Usually a stale assembly; run `uloop compile`. In-file renames and signature changes are classified as added members before reaching this point | From 15606bda48f5a3282e77f7a393c8b663d196f7d3 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 6 Oct 2026 23:01:58 +0900 Subject: [PATCH 4/5] Answer false for a project root made only of separators Trimming such a root leaves an empty string, and Path.GetDirectoryName rejects that with ArgumentException in Unity's Mono. The check runs while the missing-assembly reason is being built, so throwing there would end the run instead of reporting the file as Failed. The too-short-path test now covers a separator-only root as well. --- .../HotReload/HotReloadVirtualPlayerProjectTests.cs | 8 +++++++- .../HotReload/Shared/HotReloadVirtualPlayerProject.cs | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Assets/Tests/Editor/HotReload/HotReloadVirtualPlayerProjectTests.cs b/Assets/Tests/Editor/HotReload/HotReloadVirtualPlayerProjectTests.cs index acdeb9d5ac..cbc657a8f9 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadVirtualPlayerProjectTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadVirtualPlayerProjectTests.cs @@ -84,13 +84,15 @@ public void IsVirtualPlayerProjectRoot_GrandparentIsNotLibrary_ReturnsFalse() } /// - /// What: a path without a parent or without a grandparent is answered false instead of throwing. + /// What: a path without a parent or without a grandparent, including one made only of a + /// separator, is answered false instead of throwing. /// [Test] public void IsVirtualPlayerProjectRoot_PathTooShortToHaveAGrandparent_ReturnsFalse() { string playerDirectoryOnly = "mppm0a1b2c3d"; string virtualPlayersAndPlayerDirectory = Path.Combine("VP", "mppm0a1b2c3d"); + string separatorOnly = Path.DirectorySeparatorChar.ToString(); Assert.That( HotReloadVirtualPlayerProject.IsVirtualPlayerProjectRoot(playerDirectoryOnly), @@ -100,6 +102,10 @@ public void IsVirtualPlayerProjectRoot_PathTooShortToHaveAGrandparent_ReturnsFal HotReloadVirtualPlayerProject.IsVirtualPlayerProjectRoot(virtualPlayersAndPlayerDirectory), Is.False, "A path without a grandparent must not be taken for a Virtual Player root."); + Assert.That( + HotReloadVirtualPlayerProject.IsVirtualPlayerProjectRoot(separatorOnly), + Is.False, + "A path made only of a separator must not be taken for a Virtual Player root."); } /// diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs index 65f29d6279..6b9655ead5 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadVirtualPlayerProject.cs @@ -22,6 +22,12 @@ internal static bool IsVirtualPlayerProjectRoot(string projectRoot) // Why trim first: with a trailing separator, Path.GetDirectoryName returns the same // directory, so every parent lookup below would land one level too low. string trimmedRoot = projectRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + // A root made only of separators has no parent, and Path.GetDirectoryName rejects the empty string. + if (trimmedRoot.Length == 0) + { + return false; + } + string virtualPlayersDirectory = Path.GetDirectoryName(trimmedRoot); if (string.IsNullOrEmpty(virtualPlayersDirectory)) { From 5f2adbff303b9fd4aa7c438f13f9c92024c68a49 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 6 Oct 2026 23:10:10 +0900 Subject: [PATCH 5/5] Say that the Virtual Player compile fallback follows --compile-on-skip The two new doc sentences said the CLI always compiles in the player after hot reload fails the file. The fallback follows --compile-on-skip like any unapplied edit: auto holds the compile while that player is in Play Mode (CompileFallback is HeldForPlayMode), and off never runs it, so in those cases the edit has not reached the player. The generated skill copies are regenerated from the sources. --- .../references/multiplayer-play-mode.md | 7 +++++-- .../skills/uloop-hot-reload/references/scope-and-limits.md | 2 +- .../references/multiplayer-play-mode.md | 7 +++++-- .../skills/uloop-hot-reload/references/scope-and-limits.md | 2 +- .../Skill/references/multiplayer-play-mode.md | 7 +++++-- .../HotReload/Skill/references/scope-and-limits.md | 2 +- 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/.agents/skills/uloop-control-play-mode/references/multiplayer-play-mode.md b/.agents/skills/uloop-control-play-mode/references/multiplayer-play-mode.md index 0e7d046a32..b715e2aefe 100644 --- a/.agents/skills/uloop-control-play-mode/references/multiplayer-play-mode.md +++ b/.agents/skills/uloop-control-play-mode/references/multiplayer-play-mode.md @@ -55,8 +55,11 @@ main Editor) do not block each other. active patch. - Hot reload cannot patch a Virtual Player yet: a player loads the main project's `Library/ScriptAssemblies` and has none under its own root. `hot-reload --files ...` sent - to a player reports the file as `Failed`, and the CLI then compiles in that player, which - brings the edit in (`Outcome` is `ReplacedByCompile`). + to a player reports the file as `Failed`. Whether the CLI then compiles in that player + follows `--compile-on-skip`, as for any unapplied edit: when it compiles, the edit comes in + (`Outcome` is `ReplacedByCompile`); when the compile is held (`CompileFallback` is + `HeldForPlayMode`: `auto`, the default, while that player is in Play Mode), the edit has not + reached the player. ## Known limitations diff --git a/.agents/skills/uloop-hot-reload/references/scope-and-limits.md b/.agents/skills/uloop-hot-reload/references/scope-and-limits.md index 4284f93130..eda3049cbc 100644 --- a/.agents/skills/uloop-hot-reload/references/scope-and-limits.md +++ b/.agents/skills/uloop-hot-reload/references/scope-and-limits.md @@ -427,7 +427,7 @@ source on disk. When a run skips a method it had patched before, `Warnings` name | File does not belong to any compiled assembly | Per-file entry with `Method` = `(file)`; only `Assets/` and `Packages/` sources resolve | | Resolved assembly name is missing from CompilationPipeline | Per-file entry with `Method` = `(file)`; Unity may have mapped a not-yet-imported `.asmdef` onto a predefined assembly. Run `uloop compile` first | | Script is not in the last compiled assembly's source list and its assembly membership cannot be confirmed | Per-file entry with `Method` = `(file)`; a new file passed with `--files` is hot-reloadable when its membership in an existing, unchanged compiled assembly is confirmed (`.asmdef` / `.asmref` boundaries are checked when present; a predefined assembly with none also passes), but fails when the Editor is not ready or an `.asmdef` / `.asmref` on its path was added, deleted, or changed since the last import — run `uloop compile` first | -| The Editor is a Multiplayer Play Mode Virtual Player | Per-file entry with `Method` = `(file)`; a Virtual Player has no compiled assemblies under its own project root, so hot reload cannot patch it yet. The CLI's compile fallback brings the edit to that player. A patch applied to the main Editor does not reach Virtual Players (each is a separate Editor process) | +| The Editor is a Multiplayer Play Mode Virtual Player | Per-file entry with `Method` = `(file)`; a Virtual Player has no compiled assemblies under its own project root, so hot reload cannot patch it yet. The edit reaches that player only through a compile: the CLI's compile fallback brings it in when `--compile-on-skip` lets the compile run, and `auto` holds it while that player is in Play Mode (`CompileFallback` is `HeldForPlayMode`). A patch applied to the main Editor does not reach Virtual Players (each is a separate Editor process) | | Loaded assembly differs from the one on disk (pending compile) | Run `uloop compile` first, then retry | | Source file fails to parse | Per-file `Failed` entry with `Method` = `(file)` carrying the parse errors; nothing from that file is applied, its earlier patches stay active, and `Success` is false | | Method signature not found in the loaded assembly | Usually a stale assembly; run `uloop compile`. In-file renames and signature changes are classified as added members before reaching this point | diff --git a/.claude/skills/uloop-control-play-mode/references/multiplayer-play-mode.md b/.claude/skills/uloop-control-play-mode/references/multiplayer-play-mode.md index 0e7d046a32..b715e2aefe 100644 --- a/.claude/skills/uloop-control-play-mode/references/multiplayer-play-mode.md +++ b/.claude/skills/uloop-control-play-mode/references/multiplayer-play-mode.md @@ -55,8 +55,11 @@ main Editor) do not block each other. active patch. - Hot reload cannot patch a Virtual Player yet: a player loads the main project's `Library/ScriptAssemblies` and has none under its own root. `hot-reload --files ...` sent - to a player reports the file as `Failed`, and the CLI then compiles in that player, which - brings the edit in (`Outcome` is `ReplacedByCompile`). + to a player reports the file as `Failed`. Whether the CLI then compiles in that player + follows `--compile-on-skip`, as for any unapplied edit: when it compiles, the edit comes in + (`Outcome` is `ReplacedByCompile`); when the compile is held (`CompileFallback` is + `HeldForPlayMode`: `auto`, the default, while that player is in Play Mode), the edit has not + reached the player. ## Known limitations diff --git a/.claude/skills/uloop-hot-reload/references/scope-and-limits.md b/.claude/skills/uloop-hot-reload/references/scope-and-limits.md index 4284f93130..eda3049cbc 100644 --- a/.claude/skills/uloop-hot-reload/references/scope-and-limits.md +++ b/.claude/skills/uloop-hot-reload/references/scope-and-limits.md @@ -427,7 +427,7 @@ source on disk. When a run skips a method it had patched before, `Warnings` name | File does not belong to any compiled assembly | Per-file entry with `Method` = `(file)`; only `Assets/` and `Packages/` sources resolve | | Resolved assembly name is missing from CompilationPipeline | Per-file entry with `Method` = `(file)`; Unity may have mapped a not-yet-imported `.asmdef` onto a predefined assembly. Run `uloop compile` first | | Script is not in the last compiled assembly's source list and its assembly membership cannot be confirmed | Per-file entry with `Method` = `(file)`; a new file passed with `--files` is hot-reloadable when its membership in an existing, unchanged compiled assembly is confirmed (`.asmdef` / `.asmref` boundaries are checked when present; a predefined assembly with none also passes), but fails when the Editor is not ready or an `.asmdef` / `.asmref` on its path was added, deleted, or changed since the last import — run `uloop compile` first | -| The Editor is a Multiplayer Play Mode Virtual Player | Per-file entry with `Method` = `(file)`; a Virtual Player has no compiled assemblies under its own project root, so hot reload cannot patch it yet. The CLI's compile fallback brings the edit to that player. A patch applied to the main Editor does not reach Virtual Players (each is a separate Editor process) | +| The Editor is a Multiplayer Play Mode Virtual Player | Per-file entry with `Method` = `(file)`; a Virtual Player has no compiled assemblies under its own project root, so hot reload cannot patch it yet. The edit reaches that player only through a compile: the CLI's compile fallback brings it in when `--compile-on-skip` lets the compile run, and `auto` holds it while that player is in Play Mode (`CompileFallback` is `HeldForPlayMode`). A patch applied to the main Editor does not reach Virtual Players (each is a separate Editor process) | | Loaded assembly differs from the one on disk (pending compile) | Run `uloop compile` first, then retry | | Source file fails to parse | Per-file `Failed` entry with `Method` = `(file)` carrying the parse errors; nothing from that file is applied, its earlier patches stay active, and `Success` is false | | Method signature not found in the loaded assembly | Usually a stale assembly; run `uloop compile`. In-file renames and signature changes are classified as added members before reaching this point | diff --git a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/references/multiplayer-play-mode.md b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/references/multiplayer-play-mode.md index 0e7d046a32..b715e2aefe 100644 --- a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/references/multiplayer-play-mode.md +++ b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/references/multiplayer-play-mode.md @@ -55,8 +55,11 @@ main Editor) do not block each other. active patch. - Hot reload cannot patch a Virtual Player yet: a player loads the main project's `Library/ScriptAssemblies` and has none under its own root. `hot-reload --files ...` sent - to a player reports the file as `Failed`, and the CLI then compiles in that player, which - brings the edit in (`Outcome` is `ReplacedByCompile`). + to a player reports the file as `Failed`. Whether the CLI then compiles in that player + follows `--compile-on-skip`, as for any unapplied edit: when it compiles, the edit comes in + (`Outcome` is `ReplacedByCompile`); when the compile is held (`CompileFallback` is + `HeldForPlayMode`: `auto`, the default, while that player is in Play Mode), the edit has not + reached the player. ## Known limitations diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Skill/references/scope-and-limits.md b/Packages/src/Editor/FirstPartyTools/HotReload/Skill/references/scope-and-limits.md index 4284f93130..eda3049cbc 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Skill/references/scope-and-limits.md +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Skill/references/scope-and-limits.md @@ -427,7 +427,7 @@ source on disk. When a run skips a method it had patched before, `Warnings` name | File does not belong to any compiled assembly | Per-file entry with `Method` = `(file)`; only `Assets/` and `Packages/` sources resolve | | Resolved assembly name is missing from CompilationPipeline | Per-file entry with `Method` = `(file)`; Unity may have mapped a not-yet-imported `.asmdef` onto a predefined assembly. Run `uloop compile` first | | Script is not in the last compiled assembly's source list and its assembly membership cannot be confirmed | Per-file entry with `Method` = `(file)`; a new file passed with `--files` is hot-reloadable when its membership in an existing, unchanged compiled assembly is confirmed (`.asmdef` / `.asmref` boundaries are checked when present; a predefined assembly with none also passes), but fails when the Editor is not ready or an `.asmdef` / `.asmref` on its path was added, deleted, or changed since the last import — run `uloop compile` first | -| The Editor is a Multiplayer Play Mode Virtual Player | Per-file entry with `Method` = `(file)`; a Virtual Player has no compiled assemblies under its own project root, so hot reload cannot patch it yet. The CLI's compile fallback brings the edit to that player. A patch applied to the main Editor does not reach Virtual Players (each is a separate Editor process) | +| The Editor is a Multiplayer Play Mode Virtual Player | Per-file entry with `Method` = `(file)`; a Virtual Player has no compiled assemblies under its own project root, so hot reload cannot patch it yet. The edit reaches that player only through a compile: the CLI's compile fallback brings it in when `--compile-on-skip` lets the compile run, and `auto` holds it while that player is in Play Mode (`CompileFallback` is `HeldForPlayMode`). A patch applied to the main Editor does not reach Virtual Players (each is a separate Editor process) | | Loaded assembly differs from the one on disk (pending compile) | Run `uloop compile` first, then retry | | Source file fails to parse | Per-file `Failed` entry with `Method` = `(file)` carrying the parse errors; nothing from that file is applied, its earlier patches stay active, and `Success` is false | | Method signature not found in the loaded assembly | Usually a stale assembly; run `uloop compile`. In-file renames and signature changes are classified as added members before reaching this point |