Skip to content

Commit e940e90

Browse files
committed
fix: address CodeRabbit review findings
- ProcessArgumentListCompat: correct Windows command-line quoting (backslash runs doubled only before quotes/at end; verbatim pass-through when unquoted) - EncryptedFileKeyStore: implement PBKDF2-HMAC-SHA256 manually on 2020.3 (netstandard2.1 lacks 4-arg Rfc2898DeriveBytes) so key derivation stays byte-identical with 2021.2+; 3-arg SHA1 would break existing ciphertext MAC - CompatDropdownField: SetValueWithoutNotify no longer dispatches change events - MCPForUnityEditorWindow: 2020.3 package add/remove serialized via queue (legacy PM accepts one in-flight request per operation) - BuildSettingsHelper: architecture mapping fixed (0 = None, not x86_64) - ManageBuild: 'server' subtarget rejected on <2021.2 instead of silent player - docs: correct packages-lock.json description; test project: align test-framework 1.1.31, portable verify_compile.cmd (auto-detect Unity, exit codes, consistent log), README command block
1 parent 000fde5 commit e940e90

11 files changed

Lines changed: 231 additions & 31 deletions

File tree

MCPForUnity/Editor/Helpers/ProcessArgumentListCompat.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,41 @@ private static string Quote(string arg)
3030
return "\"\"";
3131
}
3232

33+
// Only quote when necessary; otherwise pass through verbatim so
34+
// backslashes in paths (e.g. C:\Program Files\...) are preserved.
3335
if (arg.IndexOfAny(new[] { ' ', '\t', '"' }) < 0)
3436
{
3537
return arg;
3638
}
3739

38-
// Escape embedded quotes the way Windows CreateProcess expects:
39-
// backslashes before a quote are doubled, then the quote escaped.
40-
return "\"" + arg.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
40+
// Standard Windows command-line quoting (the algorithm used by
41+
// .NET's ArgumentList / CommandLineToArgvW): wrap in quotes; for every
42+
// run of backslashes, double them only when immediately followed by a
43+
// quote or at the very end of the argument (before the closing quote).
44+
var sb = new System.Text.StringBuilder();
45+
sb.Append('"');
46+
int backslashes = 0;
47+
foreach (char ch in arg)
48+
{
49+
if (ch == '\\')
50+
{
51+
backslashes++;
52+
continue;
53+
}
54+
if (ch == '"')
55+
{
56+
sb.Append('\\', backslashes * 2 + 1);
57+
sb.Append('"');
58+
backslashes = 0;
59+
continue;
60+
}
61+
sb.Append('\\', backslashes);
62+
backslashes = 0;
63+
sb.Append(ch);
64+
}
65+
sb.Append('\\', backslashes * 2);
66+
sb.Append('"');
67+
return sb.ToString();
4168
}
4269
}
4370
}

MCPForUnity/Editor/Security/SecureKeyStore/EncryptedFileKeyStore.cs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,65 @@ private void DeriveKeys(out byte[] encKey, out byte[] macKey)
9393
byte[] master = LoadOrCreate(Path.Combine(_dir, "secret.bin"), 32);
9494
byte[] salt = LoadOrCreate(Path.Combine(_dir, "salt.bin"), 16);
9595
string password = Convert.ToBase64String(master) + "|" + MachineId();
96-
using (var kdf = new Rfc2898DeriveBytes(password, salt, Iterations))
96+
#if UNITY_2021_2_OR_NEWER
97+
using (var kdf = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256))
9798
{
9899
byte[] material = kdf.GetBytes(64);
99100
encKey = new byte[32];
100101
macKey = new byte[32];
101102
Buffer.BlockCopy(material, 0, encKey, 0, 32);
102103
Buffer.BlockCopy(material, 32, macKey, 0, 32);
103104
}
105+
#else
106+
// Unity 2020.3 (netstandard2.1) has no 4-arg Rfc2898DeriveBytes overload, so
107+
// implement PBKDF2-HMAC-SHA256 (RFC 2898) manually to keep key derivation
108+
// byte-identical with the 2021.2+ path (a 3-arg SHA1 derivation would make
109+
// existing ciphertext fail MAC validation).
110+
byte[] material = Pbkdf2Sha256(password, salt, Iterations, 64);
111+
encKey = new byte[32];
112+
macKey = new byte[32];
113+
Buffer.BlockCopy(material, 0, encKey, 0, 32);
114+
Buffer.BlockCopy(material, 32, macKey, 0, 32);
115+
#endif
104116
}
105117

118+
#if !UNITY_2021_2_OR_NEWER
119+
/// <summary>PBKDF2 with HMAC-SHA256 (RFC 2898), matching the .NET 4-arg Rfc2898DeriveBytes output.</summary>
120+
private static byte[] Pbkdf2Sha256(string password, byte[] salt, int iterations, int numBytes)
121+
{
122+
var prf = new System.Security.Cryptography.HMACSHA256(
123+
System.Text.Encoding.UTF8.GetBytes(password));
124+
int hLen = prf.HashSize / 8;
125+
int blocks = (numBytes + hLen - 1) / hLen;
126+
127+
var output = new byte[blocks * hLen];
128+
var saltPlusOne = new byte[salt.Length + 4];
129+
130+
for (int block = 1; block <= blocks; block++)
131+
{
132+
Buffer.BlockCopy(salt, 0, saltPlusOne, 0, salt.Length);
133+
saltPlusOne[salt.Length] = (byte)((block >> 24) & 0xFF);
134+
saltPlusOne[salt.Length + 1] = (byte)((block >> 16) & 0xFF);
135+
saltPlusOne[salt.Length + 2] = (byte)((block >> 8) & 0xFF);
136+
saltPlusOne[salt.Length + 3] = (byte)(block & 0xFF);
137+
138+
byte[] u = prf.ComputeHash(saltPlusOne);
139+
byte[] t = (byte[])u.Clone();
140+
for (int i = 1; i < iterations; i++)
141+
{
142+
u = prf.ComputeHash(u);
143+
for (int j = 0; j < hLen; j++) t[j] ^= u[j];
144+
}
145+
Buffer.BlockCopy(t, 0, output, (block - 1) * hLen, hLen);
146+
}
147+
148+
prf.Dispose();
149+
var result = new byte[numBytes];
150+
Buffer.BlockCopy(output, 0, result, 0, numBytes);
151+
return result;
152+
}
153+
#endif
154+
106155
private byte[] Encrypt(byte[] plaintext)
107156
{
108157
DeriveKeys(out byte[] encKey, out byte[] macKey);

MCPForUnity/Editor/Tools/Build/BuildSettingsHelper.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ public static object ReadProperty(string property, BuildTargetGroup namedTarget)
2626
return new { property, value = PlayerSettings.GetScriptingDefineSymbolsForGroup(namedTarget) };
2727
case "architecture":
2828
var arch = PlayerSettings.GetArchitecture(namedTarget);
29-
string archName = arch switch { 0 => "x86_64", 1 => "arm64", 2 => "universal", _ => "unknown" };
29+
// GetArchitecture returns the BuildTargetGroup's architecture setting,
30+
// where 0 means None (not x86_64); see PlayerSettings.SetArchitecture docs.
31+
string archName = arch switch { 0 => "none", 1 => "arm64", 2 => "universal", _ => "unknown" };
3032
return new { property, value = archName, raw = arch };
3133
default:
3234
return null;
@@ -66,15 +68,14 @@ public static string WriteProperty(string property, string value, BuildTargetGro
6668
case "architecture":
6769
int arch = value.ToLowerInvariant() switch
6870
{
69-
"x86_64" => 0,
7071
"none" => 0,
7172
"default" => 0,
7273
"arm64" => 1,
7374
"universal" => 2,
7475
_ => -1
7576
};
7677
if (arch < 0)
77-
return $"Unknown architecture '{value}'. Valid: x86_64, arm64, universal";
78+
return $"Unknown architecture '{value}'. Valid: none, arm64, universal";
7879
PlayerSettings.SetArchitecture(namedTarget, arch);
7980
return null;
8081
default:

MCPForUnity/Editor/Tools/ManageBuild.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ private static object HandlePlatform(ToolParams p)
250250
EditorUserBuildSettings.standaloneBuildSubtarget = StandaloneBuildSubtarget.Server;
251251
else if (subtargetLower == "player")
252252
EditorUserBuildSettings.standaloneBuildSubtarget = StandaloneBuildSubtarget.Player;
253+
#else
254+
// Unity 2020.3 has no server subtarget; fail loudly instead of silently
255+
// building the player variant.
256+
string subtargetLower = subtargetStr.ToLowerInvariant();
257+
if (subtargetLower == "server")
258+
return new ErrorResponse("subtarget 'server' requires Unity 2021.2 or newer (StandaloneBuildSubtarget API).");
253259
#endif
254260
}
255261

MCPForUnity/Editor/Windows/Components/CompatDropdownField.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,7 @@ public int index
8787
else if (clamped < 0) clamped = 0;
8888
else if (clamped >= m_Choices.Count) clamped = m_Choices.Count - 1;
8989
if (clamped == m_Index) return;
90-
m_Index = clamped;
91-
UpdateValueFromIndex();
90+
SetValueWithoutNotify(clamped >= 0 && clamped < m_Choices.Count ? m_Choices[clamped] : null);
9291
}
9392
}
9493

@@ -110,10 +109,10 @@ public string value
110109
public void SetValueWithoutNotify(string newValue)
111110
{
112111
int newIndex = m_Choices.IndexOf(newValue);
113-
if (newIndex >= 0)
112+
if (newIndex >= 0 && newIndex != m_Index)
114113
{
115114
m_Index = newIndex;
116-
UpdateValueFromIndex();
115+
m_Value = newIndex < m_Choices.Count ? m_Choices[newIndex] : null;
117116
}
118117
}
119118

MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,12 +1058,10 @@ private static void BatchUpmAdd(string[] packageIds, Action onComplete = null)
10581058
var request = UnityEditor.PackageManager.Client.AddAndRemove(packageIds, null);
10591059
PollUpmRequest(request, "install", onComplete);
10601060
#else
1061-
UnityEditor.PackageManager.Requests.AddRequest request = null;
1062-
foreach (var id in packageIds)
1063-
{
1064-
request = UnityEditor.PackageManager.Client.Add(id);
1065-
}
1066-
PollUpmAddRequest(request, onComplete);
1061+
// Unity 2020.3: queue adds serially — PackageManager accepts one in-flight
1062+
// request per operation on legacy editors; starting all at once and polling
1063+
// only the last one would leave earlier packages' results unchecked.
1064+
QueueUpmAdds(new Queue<string>(packageIds), onComplete);
10671065
#endif
10681066
}
10691067

@@ -1074,12 +1072,8 @@ private static void BatchUpmRemove(string[] packageIds, Action onComplete = null
10741072
var request = UnityEditor.PackageManager.Client.AddAndRemove(null, packageIds);
10751073
PollUpmRequest(request, "remove", onComplete);
10761074
#else
1077-
UnityEditor.PackageManager.Requests.RemoveRequest request = null;
1078-
foreach (var id in packageIds)
1079-
{
1080-
request = UnityEditor.PackageManager.Client.Remove(id);
1081-
}
1082-
PollUpmRemoveRequest(request, onComplete);
1075+
// Unity 2020.3: queue removes serially (see BatchUpmAdd).
1076+
QueueUpmRemoves(new Queue<string>(packageIds), onComplete);
10831077
#endif
10841078
}
10851079

@@ -1139,6 +1133,67 @@ private static void PollUpmRemoveRequest(UnityEditor.PackageManager.Requests.Rem
11391133
};
11401134
EditorApplication.update += pollCallback;
11411135
}
1136+
1137+
private static void QueueUpmAdds(Queue<string> queue, Action onComplete)
1138+
{
1139+
if (queue.Count == 0)
1140+
{
1141+
EditorUtility.ClearProgressBar();
1142+
onComplete?.Invoke();
1143+
return;
1144+
}
1145+
string id = queue.Dequeue();
1146+
var request = UnityEditor.PackageManager.Client.Add(id);
1147+
EditorApplication.CallbackFunction poll = null;
1148+
poll = () =>
1149+
{
1150+
if (!request.IsCompleted) return;
1151+
EditorApplication.update -= poll;
1152+
if (request.Status == UnityEditor.PackageManager.StatusCode.Success)
1153+
{
1154+
Debug.Log($"[MCP] Package {id} installed.");
1155+
QueueUpmAdds(queue, onComplete);
1156+
}
1157+
else
1158+
{
1159+
EditorUtility.ClearProgressBar();
1160+
Debug.LogError($"[MCP] Package {id} install failed: {request.Error?.message}");
1161+
onComplete?.Invoke();
1162+
}
1163+
};
1164+
EditorApplication.update += poll;
1165+
}
1166+
1167+
private static void QueueUpmRemoves(Queue<string> queue, Action onComplete)
1168+
{
1169+
if (queue.Count == 0)
1170+
{
1171+
EditorUtility.ClearProgressBar();
1172+
onComplete?.Invoke();
1173+
return;
1174+
}
1175+
string id = queue.Dequeue();
1176+
var request = UnityEditor.PackageManager.Client.Remove(id);
1177+
EditorApplication.CallbackFunction poll = null;
1178+
poll = () =>
1179+
{
1180+
if (!request.IsCompleted) return;
1181+
EditorApplication.update -= poll;
1182+
if (request.Status == UnityEditor.PackageManager.StatusCode.Success)
1183+
{
1184+
Debug.Log($"[MCP] Package {id} removed.");
1185+
QueueUpmRemoves(queue, onComplete);
1186+
}
1187+
else
1188+
{
1189+
EditorUtility.ClearProgressBar();
1190+
Debug.LogError($"[MCP] Package {id} remove failed: {request.Error?.message}");
1191+
onComplete?.Invoke();
1192+
}
1193+
};
1194+
EditorApplication.update += poll;
1195+
}
1196+
11421197
#endif
11431198

11441199
private static void UninstallRoslyn()

TestProjects/Unity2020Compat/Packages/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"com.unity.ide.rider": "2.0.7",
66
"com.unity.ide.visualstudio": "2.0.12",
77
"com.unity.ide.vscode": "1.2.4",
8-
"com.unity.test-framework": "1.1.29",
8+
"com.unity.test-framework": "1.1.31",
99
"com.unity.textmeshpro": "3.0.6",
1010
"com.unity.timeline": "1.4.8",
1111
"com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.11",
@@ -43,4 +43,4 @@
4343
"com.unity.modules.wind": "1.0.0",
4444
"com.unity.modules.xr": "1.0.0"
4545
}
46-
}
46+
}

TestProjects/Unity2020Compat/Packages/packages-lock.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,4 +409,4 @@
409409
}
410410
}
411411
}
412-
}
412+
}

TestProjects/Unity2020Compat/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010

1111
```cmd
1212
cd /d <repo>\TestProjects\Unity2020Compat
13-
"C:\SoftWare\Unity\2020.3.24f1\Editor\Unity.exe" -batchmode -nographics -quit -projectPath "%~dp0" -logFile compile.log
13+
verify_compile.cmd rem auto-detects Unity 2020.3
14+
verify_compile.cmd C:\path\to\Unity.exe rem or pass the editor explicitly
1415
```
1516

1617
通过标准:`compile.log` 中无 `error CS`,结尾出现 `Exiting batchmode successfully now!`
Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,66 @@
11
@echo off
2-
cd /d F:\AIProjects\Unity-shikiMCP\TestProjects\Unity2020Compat
3-
"C:\SoftWare\Unity\2020.3.24f1\Editor\Unity.exe" -batchmode -nographics -quit -projectPath "F:\AIProjects\Unity-shikiMCP\TestProjects\Unity2020Compat" -logFile "F:\AIProjects\Unity-shikiMCP\TestProjects\Unity2020Compat\compile_check.log"
4-
echo UNITY_EXIT=%ERRORLEVEL%
2+
rem ============================================================
3+
rem Unity 2020.3 compile verification for MCP for Unity
4+
rem
5+
rem Usage:
6+
rem verify_compile.cmd (uses UNITY_EDITOR env var or auto-detect)
7+
rem verify_compile.cmd <path\to\Unity.exe>
8+
rem
9+
rem Exit codes: 0 = compile OK, 1 = compile errors found, 2 = Unity not found
10+
rem ============================================================
11+
setlocal
12+
13+
set "PROJECT_DIR=%~dp0"
14+
set "LOG_FILE=%PROJECT_DIR%compile_check.log"
15+
set "UNITY_EXE="
16+
17+
if not "%~1"=="" (
18+
set "UNITY_EXE=%~1"
19+
) else if defined UNITY_EDITOR (
20+
set "UNITY_EXE=%UNITY_EDITOR%"
21+
) else (
22+
for %%E in (
23+
"C:\SoftWare\Unity\2020.3.24f1\Editor\Unity.exe"
24+
"C:\Program Files\Unity\Hub\Editor\2020.3.*\Editor\Unity.exe"
25+
"%ProgramFiles%\Unity\Hub\Editor\2020.3.*\Editor\Unity.exe"
26+
) do (
27+
if not defined UNITY_EXE if exist %%E set "UNITY_EXE=%%E"
28+
)
29+
)
30+
31+
if not defined UNITY_EXE (
32+
echo [verify] Unity 2020.3 executable not found.
33+
echo [verify] Pass the path: verify_compile.cmd C:\path\to\Unity.exe
34+
exit /b 2
35+
)
36+
if not exist "%UNITY_EXE%" (
37+
echo [verify] Unity executable not found: %UNITY_EXE%
38+
exit /b 2
39+
)
40+
41+
echo [verify] Unity: %UNITY_EXE%
42+
echo [verify] Project: %PROJECT_DIR%
43+
del /q "%LOG_FILE%" 2>nul
44+
45+
"%UNITY_EXE%" -batchmode -nographics -quit -projectPath "%PROJECT_DIR%" -logFile "%LOG_FILE%"
46+
set "UNITY_STATUS=%ERRORLEVEL%"
47+
48+
if not exist "%LOG_FILE%" (
49+
echo [verify] No log file produced (Unity may have failed to start).
50+
exit /b 1
51+
)
52+
53+
findstr /c:"error CS" "%LOG_FILE%" >nul
54+
if not errorlevel 1 (
55+
echo [verify] FAILED: compiler errors found in %LOG_FILE%
56+
exit /b 1
57+
)
58+
59+
findstr /c:"Exiting batchmode successfully now!" "%LOG_FILE%" >nul
60+
if errorlevel 1 (
61+
echo [verify] FAILED: Unity did not exit cleanly (status %UNITY_STATUS%).
62+
exit /b 1
63+
)
64+
65+
echo [verify] PASS: 0 compiler errors, clean batchmode exit.
66+
exit /b 0

0 commit comments

Comments
 (0)