Skip to content

Commit 1b095fb

Browse files
committed
PatchFix
1 parent 1fba429 commit 1b095fb

5 files changed

Lines changed: 46 additions & 38 deletions

File tree

MCPForUnity/Editor/Tools/ManageScene.cs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,8 @@ private static object CaptureOrbitBatch(SceneCommand cmd)
10971097

10981098
/// <summary>
10991099
/// Captures a single screenshot from a temporary camera placed at view_position and aimed at view_target.
1100-
/// Returns inline base64 PNG and also saves the image to &lt;projectRoot&gt;/Captures/.
1100+
/// Returns inline base64 PNG and also saves the image to the resolved screenshot folder
1101+
/// (caller's <c>output_folder</c> override -> <c>ScreenshotPreferences.DefaultFolder</c> -> built-in <c>Assets/Screenshots</c>).
11011102
/// </summary>
11021103
private static object CapturePositionedScreenshot(SceneCommand cmd)
11031104
{
@@ -1231,20 +1232,13 @@ private static object CapturePositionedScreenshot(SceneCommand cmd)
12311232

12321233
/// <summary>
12331234
/// Resolves the per-call/per-pref/built-in screenshot folder spec to an absolute path.
1234-
/// Falls back to the built-in default when validation fails (used by paths that just want
1235-
/// to surface the folder name in metadata without performing a capture).
1235+
/// Propagates validation errors from <see cref="ScreenshotUtility.ResolveFolderAbsolute"/>
1236+
/// so callers can surface them rather than silently writing somewhere else.
12361237
/// </summary>
12371238
private static string ResolveAbsoluteOutputFolder(string callerOverride)
12381239
{
12391240
string spec = ScreenshotPreferences.Resolve(callerOverride);
1240-
try
1241-
{
1242-
return ScreenshotUtility.ResolveFolderAbsolute(spec).Replace('\\', '/');
1243-
}
1244-
catch
1245-
{
1246-
return ScreenshotUtility.ResolveFolderAbsolute(ScreenshotUtility.DefaultFolder).Replace('\\', '/');
1247-
}
1241+
return ScreenshotUtility.ResolveFolderAbsolute(spec).Replace('\\', '/');
12481242
}
12491243

12501244
private static string GetDirectionLabel(float azimuthDeg)

MCPForUnity/Editor/Tools/ManageUI.cs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,7 @@ private static object RenderUI(JObject @params)
897897
Directory.CreateDirectory(resolvedFolderAbs);
898898
string playFullPath = Path.Combine(resolvedFolderAbs, resolvedPlayName).Replace('\\', '/');
899899
playFullPath = EnsureUniqueFilePath(playFullPath);
900-
string playProjectRelPath = ToProjectRelativePathLocal(playFullPath);
900+
string playProjectRelPath = ScreenshotUtility.ToProjectRelativePath(playFullPath);
901901

902902
// ── Case 1: capture is ready ──────────────────────────────────────
903903
if (s_pendingCaptureDone && s_pendingCaptureTex != null)
@@ -1153,7 +1153,7 @@ private static object RenderUI(JObject @params)
11531153
byte[] png = tex.EncodeToPNG();
11541154
File.WriteAllBytes(fullPath, png);
11551155

1156-
string projectRelPath = ToProjectRelativePathLocal(fullPath);
1156+
string projectRelPath = ScreenshotUtility.ToProjectRelativePath(fullPath);
11571157
if (ScreenshotUtility.IsUnderAssets(projectRelPath))
11581158
AssetDatabase.ImportAsset(projectRelPath, ImportAssetOptions.ForceSynchronousImport);
11591159

@@ -1823,15 +1823,6 @@ private static string EnsureUniqueFilePath(string path)
18231823
return candidate;
18241824
}
18251825

1826-
private static string ToProjectRelativePathLocal(string normalizedFullPath)
1827-
{
1828-
string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")).Replace('\\', '/');
1829-
string normalizedRoot = projectRoot.EndsWith("/") ? projectRoot : projectRoot + "/";
1830-
return normalizedFullPath.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)
1831-
? normalizedFullPath.Substring(normalizedRoot.Length)
1832-
: normalizedFullPath;
1833-
}
1834-
18351826
private static string ColorToHex(Color c)
18361827
{
18371828
return $"#{ColorUtility.ToHtmlStringRGBA(c)}";

MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -543,8 +543,19 @@ private void OnBrowseScreenshotsFolderClicked()
543543
string normalizedRoot = projectRoot.EndsWith("/") ? projectRoot : projectRoot + "/";
544544
string normalizedPicked = picked.Replace('\\', '/');
545545

546-
if (!normalizedPicked.Equals(projectRoot, StringComparison.OrdinalIgnoreCase) &&
547-
!normalizedPicked.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase))
546+
if (normalizedPicked.Equals(projectRoot, StringComparison.OrdinalIgnoreCase))
547+
{
548+
// Storing "" would wipe the EditorPrefs key (= "unset"), so reject the project
549+
// root rather than silently revert the override the user just chose.
550+
EditorUtility.DisplayDialog(
551+
"Pick a Subfolder",
552+
"Please pick a subfolder of the project (for example 'Assets/Screenshots' or 'Captures'). " +
553+
"Selecting the project root would mix screenshots in with your project files.",
554+
"OK");
555+
return;
556+
}
557+
558+
if (!normalizedPicked.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase))
548559
{
549560
EditorUtility.DisplayDialog(
550561
"Folder Outside Project",
@@ -553,13 +564,11 @@ private void OnBrowseScreenshotsFolderClicked()
553564
return;
554565
}
555566

556-
string projectRelative = normalizedPicked.Equals(projectRoot, StringComparison.OrdinalIgnoreCase)
557-
? string.Empty
558-
: normalizedPicked.Substring(normalizedRoot.Length);
567+
string projectRelative = normalizedPicked.Substring(normalizedRoot.Length);
559568

560569
ScreenshotPreferences.DefaultFolder = projectRelative;
561570
screenshotsFolderOverride?.SetValueWithoutNotify(projectRelative);
562-
McpLog.Info($"Default screenshots folder set to '{(string.IsNullOrEmpty(projectRelative) ? "(project root)" : projectRelative)}'.");
571+
McpLog.Info($"Default screenshots folder set to '{projectRelative}'.");
563572
}
564573

565574
private void OnBrowseDeploySourceClicked()

MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -595,9 +595,12 @@ public static string ResolveFolderAbsolute(string folderOverride)
595595
string fullFolder = Path.GetFullPath(combined).Replace('\\', '/').TrimEnd('/');
596596
string normalizedRoot = projectRoot;
597597

598-
// Reject paths that escape the project root (case-insensitive on Windows).
599-
if (!fullFolder.Equals(normalizedRoot, StringComparison.OrdinalIgnoreCase) &&
600-
!fullFolder.StartsWith(normalizedRoot + "/", StringComparison.OrdinalIgnoreCase))
598+
// Reject paths that escape the project root (case-insensitive on Windows, exact elsewhere).
599+
var rootComparison = Application.platform == RuntimePlatform.WindowsEditor
600+
? StringComparison.OrdinalIgnoreCase
601+
: StringComparison.Ordinal;
602+
if (!fullFolder.Equals(normalizedRoot, rootComparison) &&
603+
!fullFolder.StartsWith(normalizedRoot + "/", rootComparison))
601604
{
602605
throw new InvalidOperationException(
603606
$"Screenshot folder '{folderOverride}' resolves outside the Unity project root ('{fullFolder}'). " +
@@ -607,15 +610,21 @@ public static string ResolveFolderAbsolute(string folderOverride)
607610
return fullFolder;
608611
}
609612

610-
private static string ToProjectRelativePath(string normalizedFullPath)
613+
/// <summary>
614+
/// Converts an absolute filesystem path inside the project to a project-relative path
615+
/// (forward slashes, no leading separator). Returns the input unchanged when it does
616+
/// not live under the project root.
617+
/// </summary>
618+
public static string ToProjectRelativePath(string normalizedFullPath)
611619
{
620+
if (string.IsNullOrEmpty(normalizedFullPath)) return normalizedFullPath;
612621
string projectRoot = GetProjectRootPath();
613-
string relative = normalizedFullPath;
614-
if (relative.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
622+
string normalized = normalizedFullPath.Replace('\\', '/');
623+
if (normalized.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
615624
{
616-
relative = relative.Substring(projectRoot.Length).TrimStart('/');
625+
return normalized.Substring(projectRoot.Length).TrimStart('/');
617626
}
618-
return relative;
627+
return normalized;
619628
}
620629

621630
/// <summary>
@@ -650,8 +659,11 @@ private static string BuildFileName(string fileName)
650659

651660
private static string SanitizeFileName(string fileName)
652661
{
662+
// GetInvalidFileNameChars() doesn't include '\' or '/' on Unix, so a caller-supplied
663+
// name like "foo\bar" would survive and later get spliced into the directory portion.
653664
var invalidChars = Path.GetInvalidFileNameChars();
654-
string cleaned = new string(fileName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
665+
string cleaned = new string(
666+
fileName.Select(ch => invalidChars.Contains(ch) || ch == '/' || ch == '\\' ? '_' : ch).ToArray());
655667

656668
return string.IsNullOrWhiteSpace(cleaned) ? "screenshot" : cleaned;
657669
}

Server/src/services/tools/utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,9 @@ def build_screenshot_params(
483483
if screenshot_file_name:
484484
params["fileName"] = screenshot_file_name
485485
if output_folder:
486-
params["outputFolder"] = output_folder.strip()
486+
trimmed_folder = output_folder.strip()
487+
if trimmed_folder:
488+
params["outputFolder"] = trimmed_folder
487489
coerced_super_size = coerce_int(screenshot_super_size, default=None)
488490
if coerced_super_size is not None:
489491
params["superSize"] = coerced_super_size

0 commit comments

Comments
 (0)