Skip to content

Commit fa282b1

Browse files
authored
Merge pull request CoplayDev#911 from Scriptwonder/bug-fix-and-doc-update
Plugin eviction, CLI camera/graphics, minor fixes
2 parents ad998f1 + abc5832 commit fa282b1

12 files changed

Lines changed: 517 additions & 19 deletions

File tree

MCPForUnity/Editor/Tools/Graphics/SkyboxOps.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ namespace MCPForUnity.Editor.Tools.Graphics
1010
{
1111
internal static class SkyboxOps
1212
{
13+
static Texture CustomReflectionTexture
14+
{
15+
get =>
16+
#if UNITY_2022_1_OR_NEWER
17+
RenderSettings.customReflectionTexture;
18+
#else
19+
RenderSettings.customReflection;
20+
#endif
21+
set {
22+
#if UNITY_2022_1_OR_NEWER
23+
RenderSettings.customReflectionTexture = value;
24+
#else
25+
RenderSettings.customReflection = value;
26+
#endif
27+
}
28+
}
1329
// ---------------------------------------------------------------
1430
// skybox_get — read all environment settings
1531
// ---------------------------------------------------------------
@@ -70,8 +86,8 @@ public static object GetEnvironment(JObject @params)
7086
bounces = RenderSettings.reflectionBounces,
7187
mode = RenderSettings.defaultReflectionMode.ToString(),
7288
resolution = RenderSettings.defaultReflectionResolution,
73-
customCubemap = RenderSettings.customReflectionTexture != null
74-
? AssetDatabase.GetAssetPath(RenderSettings.customReflectionTexture)
89+
customCubemap = CustomReflectionTexture != null
90+
? AssetDatabase.GetAssetPath(CustomReflectionTexture)
7591
: null
7692
},
7793
sun = sun != null
@@ -301,7 +317,7 @@ public static object SetReflection(JObject @params)
301317
{
302318
var cubemap = AssetDatabase.LoadAssetAtPath<Texture>(cubemapPath);
303319
if (cubemap != null)
304-
RenderSettings.customReflectionTexture = cubemap;
320+
CustomReflectionTexture = cubemap;
305321
else
306322
return new ErrorResponse($"Cubemap not found at '{cubemapPath}'.");
307323
}
@@ -318,8 +334,8 @@ public static object SetReflection(JObject @params)
318334
bounces = RenderSettings.reflectionBounces,
319335
mode = RenderSettings.defaultReflectionMode.ToString(),
320336
resolution = RenderSettings.defaultReflectionResolution,
321-
customCubemap = RenderSettings.customReflectionTexture != null
322-
? AssetDatabase.GetAssetPath(RenderSettings.customReflectionTexture)
337+
customCubemap = CustomReflectionTexture != null
338+
? AssetDatabase.GetAssetPath(CustomReflectionTexture)
323339
: null
324340
}
325341
};

MCPForUnity/Editor/Tools/ManageScript.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2741,6 +2741,7 @@ private static void CheckDuplicateMethodSignatures(string contents, System.Colle
27412741
foreach (Match sm in sigMatches)
27422742
{
27432743
string methodName = sm.Groups[1].Value;
2744+
if (IsCSharpKeyword(methodName)) continue;
27442745
int paramCount = CountTopLevelParams(sm.Groups[2].Value);
27452746
string paramTypes = ExtractParamTypes(sm.Groups[2].Value);
27462747
string containingType = containingTypeArr[sm.Index];
@@ -2752,6 +2753,21 @@ private static void CheckDuplicateMethodSignatures(string contents, System.Colle
27522753
}
27532754
}
27542755

2756+
private static readonly System.Collections.Generic.HashSet<string> CSharpKeywords =
2757+
new System.Collections.Generic.HashSet<string>(System.StringComparer.Ordinal)
2758+
{
2759+
"if", "else", "for", "foreach", "while", "do", "switch", "case",
2760+
"try", "catch", "finally", "throw", "return", "yield", "await",
2761+
"lock", "using", "fixed", "checked", "unchecked", "typeof", "sizeof",
2762+
"nameof", "default", "new", "stackalloc", "when", "in", "is", "as",
2763+
"ref", "out", "params", "this", "base", "null", "true", "false",
2764+
"get", "set", "var", "dynamic", "where", "from", "select", "group",
2765+
"into", "orderby", "join", "let", "on", "equals", "by", "ascending",
2766+
"descending"
2767+
};
2768+
2769+
private static bool IsCSharpKeyword(string name) => CSharpKeywords.Contains(name);
2770+
27552771
/// <summary>
27562772
/// Validates semantic rules and common coding issues
27572773
/// </summary>

Server/src/cli/CLI_USAGE_GUIDE.md

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,12 +604,205 @@ unity-mcp animation set-parameter "Character" "IsRunning" true --type bool
604604
unity-mcp animation set-parameter "Character" "Jump" "" --type trigger
605605
```
606606

607+
### Camera Commands
608+
609+
```bash
610+
# Check Cinemachine availability
611+
unity-mcp camera ping
612+
613+
# List all cameras in scene
614+
unity-mcp camera list
615+
616+
# Create cameras (plain or with Cinemachine presets)
617+
unity-mcp camera create # Basic camera
618+
unity-mcp camera create --name "FollowCam" --preset follow --follow "Player" --look-at "Player"
619+
unity-mcp camera create --preset third_person --follow "Player" --fov 50
620+
unity-mcp camera create --preset dolly --look-at "Player"
621+
unity-mcp camera create --preset top_down --follow "Player"
622+
unity-mcp camera create --preset side_scroller --follow "Player"
623+
unity-mcp camera create --preset static --fov 40
624+
625+
# Set targets on existing camera
626+
unity-mcp camera set-target "FollowCam" --follow "Player" --look-at "Enemy"
627+
628+
# Lens settings
629+
unity-mcp camera set-lens "MainCam" --fov 60 --near 0.1 --far 1000
630+
unity-mcp camera set-lens "OrthoCamera" --ortho-size 10
631+
632+
# Priority (higher = preferred by CinemachineBrain)
633+
unity-mcp camera set-priority "FollowCam" --priority 15
634+
635+
# Cinemachine Body/Aim/Noise configuration
636+
unity-mcp camera set-body "FollowCam" --body-type "CinemachineFollow"
637+
unity-mcp camera set-body "FollowCam" --body-type "CinemachineFollow" --props '{"TrackerSettings": {"BindingMode": 1}}'
638+
unity-mcp camera set-aim "FollowCam" --aim-type "CinemachineRotationComposer"
639+
unity-mcp camera set-noise "FollowCam" --amplitude 1.5 --frequency 0.5
640+
641+
# Extensions
642+
unity-mcp camera add-extension "FollowCam" CinemachineConfiner3D
643+
unity-mcp camera remove-extension "FollowCam" CinemachineConfiner3D
644+
645+
# Brain (ensure Brain exists on main camera, set default blend)
646+
unity-mcp camera ensure-brain
647+
unity-mcp camera ensure-brain --blend-style "EaseInOut" --blend-duration 1.5
648+
unity-mcp camera brain-status
649+
unity-mcp camera set-blend --style "Cut" --duration 0
650+
651+
# Force/release camera override
652+
unity-mcp camera force "FollowCam"
653+
unity-mcp camera release
654+
655+
# Screenshots
656+
unity-mcp camera screenshot
657+
unity-mcp camera screenshot --file-name "my_capture" --super-size 2
658+
unity-mcp camera screenshot --camera-ref "SecondCamera" --include-image
659+
unity-mcp camera screenshot --max-resolution 256
660+
unity-mcp camera screenshot --batch surround --max-resolution 256
661+
unity-mcp camera screenshot --batch orbit --look-at "Player"
662+
unity-mcp camera screenshot-multiview --look-at "Player" --max-resolution 480
663+
```
664+
665+
### Graphics Commands
666+
667+
```bash
668+
# Check graphics system status
669+
unity-mcp graphics ping
670+
671+
# --- Volumes ---
672+
# Create a Volume (global or local)
673+
unity-mcp graphics volume-create --name "PostProcessing" --global
674+
unity-mcp graphics volume-create --name "LocalFog" --local --weight 0.8 --priority 1
675+
676+
# Add/remove/configure effects on a Volume
677+
unity-mcp graphics volume-add-effect --target "PostProcessing" --effect "Bloom"
678+
unity-mcp graphics volume-set-effect --target "PostProcessing" --effect "Bloom" -p intensity 1.5 -p threshold 0.9
679+
unity-mcp graphics volume-remove-effect --target "PostProcessing" --effect "Bloom"
680+
unity-mcp graphics volume-info --target "PostProcessing"
681+
unity-mcp graphics volume-set-properties --target "PostProcessing" --weight 0.5 --priority 2 --local
682+
unity-mcp graphics volume-list-effects
683+
unity-mcp graphics volume-create-profile --path "Assets/Profiles/MyProfile.asset" --name "MyProfile"
684+
685+
# --- Render Pipeline ---
686+
unity-mcp graphics pipeline-info
687+
unity-mcp graphics pipeline-settings
688+
unity-mcp graphics pipeline-set-quality --level "High"
689+
unity-mcp graphics pipeline-set-settings -s renderScale 1.5 -s msaaSampleCount 4
690+
691+
# --- Light Baking ---
692+
unity-mcp graphics bake-start
693+
unity-mcp graphics bake-start --sync # Wait for completion
694+
unity-mcp graphics bake-status
695+
unity-mcp graphics bake-cancel
696+
unity-mcp graphics bake-clear
697+
unity-mcp graphics bake-settings
698+
unity-mcp graphics bake-set-settings -s lightmapResolution 64 -s directSamples 32
699+
unity-mcp graphics bake-reflection-probe --target "ReflectionProbe1"
700+
unity-mcp graphics bake-create-probes --name "LightProbes" --spacing 5
701+
unity-mcp graphics bake-create-reflection --name "ReflProbe" --resolution 512 --mode Realtime
702+
703+
# --- Rendering Stats ---
704+
unity-mcp graphics stats
705+
unity-mcp graphics stats-memory
706+
unity-mcp graphics stats-debug-mode --mode "Wireframe"
707+
708+
# --- URP Renderer Features ---
709+
unity-mcp graphics feature-list
710+
unity-mcp graphics feature-add --type "ScreenSpaceAmbientOcclusion" --name "SSAO"
711+
unity-mcp graphics feature-remove --name "SSAO"
712+
unity-mcp graphics feature-configure --name "SSAO" -p Intensity 1.5 -p Radius 0.3
713+
unity-mcp graphics feature-reorder --order "0,2,1,3"
714+
unity-mcp graphics feature-toggle --name "SSAO" --active
715+
unity-mcp graphics feature-toggle --name "SSAO" --inactive
716+
717+
# --- Skybox & Environment ---
718+
unity-mcp graphics skybox-info
719+
unity-mcp graphics skybox-set-material --material "Assets/Materials/NightSky.mat"
720+
unity-mcp graphics skybox-set-properties -p _Tint "0.5,0.5,1,1" -p _Exposure 1.2
721+
unity-mcp graphics skybox-set-ambient --mode Flat --color "0.2,0.2,0.3"
722+
unity-mcp graphics skybox-set-ambient --mode Trilight --color "0.4,0.6,0.8" --equator-color "0.3,0.3,0.3" --ground-color "0.1,0.1,0.1"
723+
unity-mcp graphics skybox-set-fog --enable --mode ExponentialSquared --color "0.7,0.8,0.9" --density 0.02
724+
unity-mcp graphics skybox-set-fog --disable
725+
unity-mcp graphics skybox-set-reflection --intensity 1.0 --bounces 2 --mode Custom --resolution 256
726+
unity-mcp graphics skybox-set-sun --target "DirectionalLight"
727+
```
728+
729+
### Package Commands
730+
731+
```bash
732+
# Check package manager status
733+
unity-mcp packages ping
734+
735+
# List installed packages
736+
unity-mcp packages list
737+
738+
# Search Unity registry
739+
unity-mcp packages search "cinemachine"
740+
unity-mcp packages search "probuilder"
741+
742+
# Get package details
743+
unity-mcp packages info "com.unity.cinemachine"
744+
745+
# Install / remove packages
746+
unity-mcp packages add "com.unity.cinemachine"
747+
unity-mcp packages add "com.unity.cinemachine@4.1.1"
748+
unity-mcp packages remove "com.unity.cinemachine"
749+
unity-mcp packages remove "com.unity.cinemachine" --force # Skip confirmation
750+
751+
# Embed package for local editing
752+
unity-mcp packages embed "com.unity.cinemachine"
753+
754+
# Force package re-resolution
755+
unity-mcp packages resolve
756+
757+
# Check async operation status
758+
unity-mcp packages status <job_id>
759+
760+
# Scoped registries
761+
unity-mcp packages list-registries
762+
unity-mcp packages add-registry "My Registry" --url "https://registry.example.com" -s "com.example"
763+
unity-mcp packages remove-registry "My Registry"
764+
```
765+
766+
### Texture Commands
767+
768+
```bash
769+
# Create procedural textures
770+
unity-mcp texture create "Assets/Textures/Red.png" --width 128 --height 128 --color "1,0,0,1"
771+
unity-mcp texture create "Assets/Textures/Check.png" --pattern checkerboard --palette "1,0,0,1;0,0,1,1"
772+
unity-mcp texture create "Assets/Textures/Brick.png" --width 256 --height 256 --pattern brick
773+
unity-mcp texture create "Assets/Textures/Grid.png" --pattern grid --width 512 --height 512
774+
775+
# Available patterns: checkerboard, stripes, stripes_h, stripes_v, stripes_diag, dots, grid, brick
776+
777+
# Create from image file
778+
unity-mcp texture create "Assets/Textures/Photo.png" --image-path "/path/to/source.png"
779+
780+
# Create with custom import settings
781+
unity-mcp texture create "Assets/Textures/Normal.png" --import-settings '{"textureType": "NormalMap", "filterMode": "Trilinear"}'
782+
783+
# Create sprites (auto-configures import settings for 2D)
784+
unity-mcp texture sprite "Assets/Sprites/Player.png" --width 32 --height 32 --color "0,0.5,1,1"
785+
unity-mcp texture sprite "Assets/Sprites/Tile.png" --pattern checkerboard --ppu 16 --pivot "0.5,0"
786+
787+
# Modify existing texture pixels
788+
unity-mcp texture modify "Assets/Textures/Existing.png" --set-pixels '{"x":0,"y":0,"width":16,"height":16,"color":[1,0,0,1]}'
789+
790+
# Delete texture
791+
unity-mcp texture delete "Assets/Textures/Old.png"
792+
unity-mcp texture delete "Assets/Textures/Old.png" --force
793+
```
794+
607795
### Code Commands
608796

609797
```bash
610798
# Read source files
611799
unity-mcp code read "Assets/Scripts/Player.cs"
612800
unity-mcp code read "Assets/Scripts/Player.cs" --start-line 10 --line-count 20
801+
802+
# Search with regex
803+
unity-mcp code search "class.*Player" "Assets/Scripts/Player.cs"
804+
unity-mcp code search "TODO|FIXME" "Assets/Scripts/Utils.cs"
805+
unity-mcp code search "void Update" "Assets/Scripts/Game.cs" --max-results 20
613806
```
614807

615808
### Raw Commands
@@ -622,6 +815,9 @@ unity-mcp raw manage_scene '{"action": "get_active"}'
622815
unity-mcp raw manage_gameobject '{"action": "create", "name": "Test"}'
623816
unity-mcp raw manage_components '{"action": "add", "target": "Test", "componentType": "Rigidbody"}'
624817
unity-mcp raw manage_editor '{"action": "play"}'
818+
unity-mcp raw manage_camera '{"action": "screenshot", "include_image": true}'
819+
unity-mcp raw manage_graphics '{"action": "volume_get_info", "target": "PostProcessing"}'
820+
unity-mcp raw manage_packages '{"action": "list_packages"}'
625821
```
626822

627823
---

Server/src/services/tools/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ def extract_screenshot_images(response: dict[str, Any]) -> "ToolResult | None":
406406
"""If a Unity response contains inline base64 images, return a ToolResult
407407
with TextContent + ImageContent blocks. Returns None for normal text-only responses.
408408
409-
Shared by manage_scene and manage_camera screenshot handling.
409+
Shared screenshot handling (used by manage_camera).
410410
"""
411411
from fastmcp.server.server import ToolResult
412412
from mcp.types import TextContent, ImageContent
@@ -476,7 +476,7 @@ def build_screenshot_params(
476476
"""Populate screenshot-related keys in *params* dict. Returns an error dict
477477
if validation fails, or None on success.
478478
479-
Shared by manage_scene and manage_camera screenshot handling.
479+
Shared screenshot handling (used by manage_camera).
480480
"""
481481
if screenshot_file_name:
482482
params["fileName"] = screenshot_file_name

Server/src/transport/plugin_hub.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,8 +426,37 @@ async def _handle_register(self, websocket: WebSocket, payload: RegisterMessage)
426426
response = RegisteredMessage(session_id=session_id)
427427
await websocket.send_json(response.model_dump())
428428

429-
session = await registry.register(session_id, project_name, project_hash, unity_version, project_path, user_id=user_id)
429+
session, evicted_session_id = await registry.register(session_id, project_name, project_hash, unity_version, project_path, user_id=user_id)
430+
evicted_ws = None
430431
async with lock:
432+
# Clean up the evicted session's connection, ping loop, and pending commands
433+
# so they don't linger as orphans after a domain-reload reconnection race.
434+
if evicted_session_id:
435+
evicted_ws = cls._connections.pop(evicted_session_id, None)
436+
old_ping = cls._ping_tasks.pop(evicted_session_id, None)
437+
if old_ping and not old_ping.done():
438+
old_ping.cancel()
439+
cls._last_pong.pop(evicted_session_id, None)
440+
cancelled_commands = []
441+
for command_id, entry in list(cls._pending.items()):
442+
if entry.get("session_id") == evicted_session_id:
443+
future = entry.get("future")
444+
if future and not future.done():
445+
future.set_exception(
446+
PluginDisconnectedError(
447+
f"Unity plugin session {evicted_session_id} superseded by {session_id}"
448+
)
449+
)
450+
cancelled_commands.append(command_id)
451+
cls._pending.pop(command_id, None)
452+
if cancelled_commands:
453+
logger.info(
454+
"Evicted session %s: cancelled pending commands %s",
455+
evicted_session_id,
456+
cancelled_commands,
457+
)
458+
logger.info(f"Evicted previous session {evicted_session_id} for same instance")
459+
431460
cls._connections[session.session_id] = websocket
432461
# Initialize last pong time and start ping loop for this session
433462
cls._last_pong[session_id] = time.monotonic()
@@ -439,6 +468,17 @@ async def _handle_register(self, websocket: WebSocket, payload: RegisterMessage)
439468
ping_task = asyncio.create_task(cls._ping_loop(session_id, websocket))
440469
cls._ping_tasks[session_id] = ping_task
441470

471+
# Close evicted WebSocket outside the lock to avoid blocking
472+
if evicted_ws is not None:
473+
try:
474+
await evicted_ws.close(code=1001)
475+
except Exception:
476+
logger.debug(
477+
"Failed to close evicted WebSocket for session %s",
478+
evicted_session_id,
479+
exc_info=True,
480+
)
481+
442482
if user_id:
443483
logger.info(f"Plugin registered: {project_name} ({project_hash}) for user {user_id}")
444484
else:

0 commit comments

Comments
 (0)