Compare commits

...

1 Commits
dev ... master

Author SHA1 Message Date
716f55c138 Add player skin override and update game assets
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- Добавлен компонент подмены спрайтов игрока перед началом игры.
- Обновлены сцена уровня, префабы, шейдеры, настройки импорта и данные призов.
- Зафиксирован embedded-пакет Unity MCP 9.6.8 и обновлены связанные файлы проекта.
2026-09-22 13:21:18 +03:00
591 changed files with 78799 additions and 582 deletions

File diff suppressed because one or more lines are too long

View File

@ -297,7 +297,7 @@ MonoBehaviour:
m_NumColorKeys: 3
m_NumAlphaKeys: 3
overrideTexture: 1
texture: {fileID: 0}
texture: {fileID: 2800000, guid: 375a132719b8f114aadcfda6bf62f8b1, type: 3}
--- !u!114 &2117915465640148810
MonoBehaviour:
m_ObjectHideFlags: 0

View File

@ -8,6 +8,14 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: -7905401754613951787, guid: 6331eaa4049f7fc4a8107649fc474455, type: 3}
propertyPath: jumpVFX
value:
objectReference: {fileID: 8926484042661614526, guid: 2a6298d2789c9b14faf02ffd58da2493, type: 3}
- target: {fileID: -7905401754613951787, guid: 6331eaa4049f7fc4a8107649fc474455, type: 3}
propertyPath: landingVFX
value:
objectReference: {fileID: 8926484042661614526, guid: 2a6298d2789c9b14faf02ffd58da2493, type: 3}
- target: {fileID: 1088233528469808677, guid: 6331eaa4049f7fc4a8107649fc474455, type: 3}
propertyPath: m_Color.b
value: 1

View File

@ -37,7 +37,7 @@ TextureImporter:
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
filterMode: 0
aniso: 1
mipBias: 0
wrapU: 1

View File

@ -500,6 +500,11 @@ Transform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1818583591}
- {fileID: 874137942}
- {fileID: 440292227}
- {fileID: 1064847901}
- {fileID: 1038585760}
- {fileID: 1917762239}
m_Father: {fileID: 144963232}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &9184405
@ -138694,6 +138699,105 @@ SpriteRenderer:
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_SpriteSortPoint: 0
--- !u!1 &440292226
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 440292227}
- component: {fileID: 440292229}
- component: {fileID: 440292228}
m_Layer: 0
m_Name: Cave
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &440292227
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 440292226}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 158, y: 7, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8883396}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &440292228
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 440292226}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b5c91a3a7ea71314e9c60d5e48b397d4, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Game2D.Behaviour.EntitySpawnerZone
prefabs:
- prefab: {fileID: 3466985052531008679, guid: d720d0e3523106548b2837f548fa96bd, type: 3}
weight: 0
- prefab: {fileID: 3321014767926845978, guid: acfc0078c078bda4bb708ac4d22f60df, type: 3}
weight: 0
- prefab: {fileID: 6296299153439293608, guid: dd9539b93ce25584fafe89eeb707878f, type: 3}
weight: 0
spawnerNameFilter: Enemy
--- !u!61 &440292229
BoxCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 440292226}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 41, y: 27}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0, y: 0}
oldSize: {x: 0, y: 0}
newSize: {x: 0, y: 0}
adaptiveTilingThreshold: 0
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Size: {x: 109, y: 73}
m_EdgeRadius: 0
--- !u!1 &440338873
GameObject:
m_ObjectHideFlags: 0
@ -164920,6 +165024,103 @@ SpriteRenderer:
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_SpriteSortPoint: 0
--- !u!1 &874137941
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 874137942}
- component: {fileID: 874137944}
- component: {fileID: 874137943}
m_Layer: 0
m_Name: NoProjectiles_1
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &874137942
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 874137941}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 267, y: 5, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8883396}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &874137943
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 874137941}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b5c91a3a7ea71314e9c60d5e48b397d4, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Game2D.Behaviour.EntitySpawnerZone
prefabs:
- prefab: {fileID: 3321014767926845978, guid: acfc0078c078bda4bb708ac4d22f60df, type: 3}
weight: 0
- prefab: {fileID: 5620532696342616547, guid: 64b2e8b5daaf53048a7ae0709db68282, type: 3}
weight: 0
spawnerNameFilter: Enemy
--- !u!61 &874137944
BoxCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 874137941}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 9.5, y: 3}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0, y: 0}
oldSize: {x: 0, y: 0}
newSize: {x: 0, y: 0}
adaptiveTilingThreshold: 0
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Size: {x: 32, y: 19}
m_EdgeRadius: 0
--- !u!4 &874646962 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 5491244471745524865, guid: 1c7455363049dbc4996e240f0bcdceff, type: 3}
@ -173328,6 +173529,103 @@ SpriteRenderer:
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_SpriteSortPoint: 0
--- !u!1 &1038585759
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1038585760}
- component: {fileID: 1038585762}
- component: {fileID: 1038585761}
m_Layer: 0
m_Name: NoProjectilesCave
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1038585760
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1038585759}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 172, y: 27, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8883396}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1038585761
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1038585759}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b5c91a3a7ea71314e9c60d5e48b397d4, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Game2D.Behaviour.EntitySpawnerZone
prefabs:
- prefab: {fileID: 3321014767926845978, guid: acfc0078c078bda4bb708ac4d22f60df, type: 3}
weight: 0
- prefab: {fileID: 3466985052531008679, guid: d720d0e3523106548b2837f548fa96bd, type: 3}
weight: 0
spawnerNameFilter: Enemy
--- !u!61 &1038585762
BoxCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1038585759}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 5.5, y: 6}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0, y: 0}
oldSize: {x: 0, y: 0}
newSize: {x: 0, y: 0}
adaptiveTilingThreshold: 0
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Size: {x: 40, y: 13}
m_EdgeRadius: 0
--- !u!1 &1040802952
GameObject:
m_ObjectHideFlags: 0
@ -174566,6 +174864,105 @@ SpriteRenderer:
m_SpriteTileMode: 0
m_WasSpriteAssigned: 1
m_SpriteSortPoint: 0
--- !u!1 &1064847900
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1064847901}
- component: {fileID: 1064847903}
- component: {fileID: 1064847902}
m_Layer: 0
m_Name: Cave_1
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1064847901
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1064847900}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 217, y: -68, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8883396}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1064847902
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1064847900}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b5c91a3a7ea71314e9c60d5e48b397d4, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Game2D.Behaviour.EntitySpawnerZone
prefabs:
- prefab: {fileID: 3466985052531008679, guid: d720d0e3523106548b2837f548fa96bd, type: 3}
weight: 0
- prefab: {fileID: 3321014767926845978, guid: acfc0078c078bda4bb708ac4d22f60df, type: 3}
weight: 0
- prefab: {fileID: 6296299153439293608, guid: dd9539b93ce25584fafe89eeb707878f, type: 3}
weight: 0
spawnerNameFilter: Enemy
--- !u!61 &1064847903
BoxCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1064847900}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 66, y: 44}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0, y: 0}
oldSize: {x: 0, y: 0}
newSize: {x: 0, y: 0}
adaptiveTilingThreshold: 0
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Size: {x: 159, y: 39}
m_EdgeRadius: 0
--- !u!1 &1067041237
GameObject:
m_ObjectHideFlags: 0
@ -504034,6 +504431,7 @@ MonoBehaviour:
weight: 0
- prefab: {fileID: 5620532696342616547, guid: 64b2e8b5daaf53048a7ae0709db68282, type: 3}
weight: 0
spawnerNameFilter: Enemy
--- !u!61 &1818583593
BoxCollider2D:
m_ObjectHideFlags: 0
@ -509171,6 +509569,103 @@ PrefabInstance:
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: e5783e81b8b0fd648974f0ec7ebde930, type: 3}
--- !u!1 &1917762238
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1917762239}
- component: {fileID: 1917762241}
- component: {fileID: 1917762240}
m_Layer: 0
m_Name: NoProjectilesCave_1
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1917762239
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1917762238}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 298, y: -27, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8883396}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1917762240
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1917762238}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b5c91a3a7ea71314e9c60d5e48b397d4, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Game2D.Behaviour.EntitySpawnerZone
prefabs:
- prefab: {fileID: 3321014767926845978, guid: acfc0078c078bda4bb708ac4d22f60df, type: 3}
weight: 0
- prefab: {fileID: 3466985052531008679, guid: d720d0e3523106548b2837f548fa96bd, type: 3}
weight: 0
spawnerNameFilter: Enemy
--- !u!61 &1917762241
BoxCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1917762238}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 14, y: 5.5}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0, y: 0}
oldSize: {x: 0, y: 0}
newSize: {x: 0, y: 0}
adaptiveTilingThreshold: 0
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Size: {x: 29, y: 12}
m_EdgeRadius: 0
--- !u!1 &1918398179
GameObject:
m_ObjectHideFlags: 0

View File

@ -0,0 +1,226 @@
using System.Collections.Generic;
using RR.Game2D.Core.Managers;
using UnityEngine;
using UnityEngine.U2D.Animation;
namespace RR.Game2D.Behaviour
{
/// <summary>
/// Заменяет спрайты созданного игрока спрайтами из совместимого prefab-скина.
/// Компонент действует только в собственной сцене и не изменяет prefab игрока.
/// </summary>
[DisallowMultipleComponent]
public sealed class PlayerSkinOverride : MonoBehaviour
{
[Header("Скин игрока")]
[Tooltip("Prefab с той же иерархией SpriteRenderer + SpriteSkin, что и у игрока.")]
[SerializeField] private GameObject skinPrefab;
private GameManager _subscribedManager;
private GameObject _lastAppliedPlayer;
private string _lastReportedProblem;
private void OnEnable()
{
SubscribeToPlayerSpawn();
TryApplyToCurrentPlayer();
}
private void Start()
{
// Повторная попытка нужна, если компонент включился раньше GameManager.
SubscribeToPlayerSpawn();
TryApplyToCurrentPlayer();
}
private void OnDisable()
{
UnsubscribeFromPlayerSpawn();
_lastAppliedPlayer = null;
}
private void SubscribeToPlayerSpawn()
{
GameManager manager = GameManager.Instance;
if (_subscribedManager == manager) return;
UnsubscribeFromPlayerSpawn();
if (manager == null) return;
_subscribedManager = manager;
_subscribedManager.PlayerSpawned += OnPlayerSpawned;
}
private void UnsubscribeFromPlayerSpawn()
{
if (_subscribedManager != null)
_subscribedManager.PlayerSpawned -= OnPlayerSpawned;
_subscribedManager = null;
}
private void TryApplyToCurrentPlayer()
{
GameObject player = _subscribedManager != null ? _subscribedManager.PlayerObj : null;
if (player == null || player.scene != gameObject.scene) return;
ApplySkin(player);
}
private void OnPlayerSpawned(GameObject player)
{
if (player == null || player.scene != gameObject.scene) return;
ApplySkin(player);
}
private void ApplySkin(GameObject player)
{
if (_lastAppliedPlayer == player) return;
if (HasAnotherOverrideInScene())
{
ReportProblem("В сцене найдено несколько активных PlayerSkinOverride. Скин не применён.", true);
return;
}
if (skinPrefab == null)
{
ReportProblem("Prefab скина не назначен. Используется стандартный скин игрока.", false);
return;
}
if (skinPrefab.scene.IsValid())
{
ReportProblem("В поле Skin Prefab должен быть назначен prefab-ассет, а не объект сцены.", true);
return;
}
if (!TryCollectParts(skinPrefab, "скина", true, out Dictionary<string, SpriteRenderer> skinParts))
return;
if (!TryCollectParts(player, "игрока", false, out Dictionary<string, SpriteRenderer> playerParts))
return;
if (skinParts.Count != playerParts.Count)
{
ReportProblem(
$"Структура скина несовместима с игроком: частей в скине — {skinParts.Count}, " +
$"частей у игрока — {playerParts.Count}. Скин не применён.",
true);
return;
}
foreach (KeyValuePair<string, SpriteRenderer> skinPart in skinParts)
{
if (!playerParts.ContainsKey(skinPart.Key))
{
ReportProblem(
$"У игрока отсутствует визуальная часть \"{skinPart.Key}\". Скин не применён.",
true);
return;
}
}
// Замена выполняется только после полной проверки, чтобы не получить частично применённый скин.
foreach (KeyValuePair<string, SpriteRenderer> skinPart in skinParts)
playerParts[skinPart.Key].sprite = skinPart.Value.sprite;
_lastAppliedPlayer = player;
_lastReportedProblem = null;
}
private bool TryCollectParts(
GameObject root,
string rootDescription,
bool requireSprite,
out Dictionary<string, SpriteRenderer> parts)
{
parts = new Dictionary<string, SpriteRenderer>();
SpriteSkin[] spriteSkins = root.GetComponentsInChildren<SpriteSkin>(true);
if (spriteSkins.Length == 0)
{
ReportProblem(
$"В иерархии {rootDescription} \"{root.name}\" не найдено ни одного SpriteSkin.",
true);
return false;
}
foreach (SpriteSkin spriteSkin in spriteSkins)
{
SpriteRenderer spriteRenderer = spriteSkin.GetComponent<SpriteRenderer>();
string relativePath = GetRelativePath(root.transform, spriteSkin.transform);
if (spriteRenderer == null)
{
ReportProblem(
$"На части {rootDescription} \"{relativePath}\" отсутствует SpriteRenderer.",
true);
return false;
}
if (requireSprite && spriteRenderer.sprite == null)
{
ReportProblem(
$"На части {rootDescription} \"{relativePath}\" не назначен Sprite.",
true);
return false;
}
if (!parts.TryAdd(relativePath, spriteRenderer))
{
ReportProblem(
$"В иерархии {rootDescription} повторяется путь \"{relativePath}\".",
true);
return false;
}
}
return true;
}
private bool HasAnotherOverrideInScene()
{
PlayerSkinOverride[] overrides = FindObjectsByType<PlayerSkinOverride>(
FindObjectsInactive.Exclude,
FindObjectsSortMode.None);
foreach (PlayerSkinOverride candidate in overrides)
{
if (candidate != this && candidate.gameObject.scene == gameObject.scene)
return true;
}
return false;
}
private static string GetRelativePath(Transform root, Transform child)
{
if (root == child) return string.Empty;
Stack<string> names = new();
Transform current = child;
while (current != null && current != root)
{
names.Push(current.name);
current = current.parent;
}
return string.Join("/", names);
}
private void ReportProblem(string message, bool isError)
{
if (_lastReportedProblem == message) return;
_lastReportedProblem = message;
string formattedMessage = $"[{nameof(PlayerSkinOverride)}] {message}";
if (isError)
Debug.LogError(formattedMessage, this);
else
Debug.LogWarning(formattedMessage, this);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3784dd4dcbda97c44a85ea59fdee2076

View File

@ -90,9 +90,6 @@
{
"m_Id": "0d74e8c23ef1428eaf8c38a4c033c563"
},
{
"m_Id": "ea6c916c3dd64bcaa527955c3fbb4f62"
},
{
"m_Id": "79f96f582c954fcc9e238541746f9671"
},
@ -137,6 +134,12 @@
},
{
"m_Id": "b1f7ddf2f27e4298b62af4614df310c0"
},
{
"m_Id": "73ce726c69084867adf2c184625cead4"
},
{
"m_Id": "55f2c3eb7ea742a5a0959c2495bd8554"
}
],
"m_GroupDatas": [
@ -149,6 +152,20 @@
],
"m_StickyNoteDatas": [],
"m_Edges": [
{
"m_OutputSlot": {
"m_Node": {
"m_Id": "0d74e8c23ef1428eaf8c38a4c033c563"
},
"m_SlotId": 2
},
"m_InputSlot": {
"m_Node": {
"m_Id": "55f2c3eb7ea742a5a0959c2495bd8554"
},
"m_SlotId": 0
}
},
{
"m_OutputSlot": {
"m_Node": {
@ -163,34 +180,6 @@
"m_SlotId": 1
}
},
{
"m_OutputSlot": {
"m_Node": {
"m_Id": "0d74e8c23ef1428eaf8c38a4c033c563"
},
"m_SlotId": 2
},
"m_InputSlot": {
"m_Node": {
"m_Id": "ea6c916c3dd64bcaa527955c3fbb4f62"
},
"m_SlotId": 1
}
},
{
"m_OutputSlot": {
"m_Node": {
"m_Id": "0d74e8c23ef1428eaf8c38a4c033c563"
},
"m_SlotId": 2
},
"m_InputSlot": {
"m_Node": {
"m_Id": "node_cartoon_fire_custom"
},
"m_SlotId": 0
}
},
{
"m_OutputSlot": {
"m_Node": {
@ -303,6 +292,20 @@
"m_SlotId": 0
}
},
{
"m_OutputSlot": {
"m_Node": {
"m_Id": "55f2c3eb7ea742a5a0959c2495bd8554"
},
"m_SlotId": 2
},
"m_InputSlot": {
"m_Node": {
"m_Id": "node_cartoon_fire_custom"
},
"m_SlotId": 0
}
},
{
"m_OutputSlot": {
"m_Node": {
@ -317,6 +320,20 @@
"m_SlotId": 0
}
},
{
"m_OutputSlot": {
"m_Node": {
"m_Id": "73ce726c69084867adf2c184625cead4"
},
"m_SlotId": 2
},
"m_InputSlot": {
"m_Node": {
"m_Id": "55f2c3eb7ea742a5a0959c2495bd8554"
},
"m_SlotId": 1
}
},
{
"m_OutputSlot": {
"m_Node": {
@ -443,6 +460,20 @@
"m_SlotId": 1
}
},
{
"m_OutputSlot": {
"m_Node": {
"m_Id": "96abc08641fe4f6ebd03b1ebec52bf9c"
},
"m_SlotId": 1
},
"m_InputSlot": {
"m_Node": {
"m_Id": "73ce726c69084867adf2c184625cead4"
},
"m_SlotId": 1
}
},
{
"m_OutputSlot": {
"m_Node": {
@ -835,8 +866,8 @@
"m_ObjectId": "06f798f4e4d445d0a65b4f9f8db515b7",
"m_Title": "Texture",
"m_Position": {
"x": -2312.999755859375,
"y": -619.9999389648438
"x": -2313.0,
"y": -620.0
}
}
@ -911,10 +942,10 @@
"m_Expanded": true,
"m_Position": {
"serializedVersion": "2",
"x": -1011.0,
"y": 40.99998092651367,
"width": 207.9998779296875,
"height": 302.0
"x": -896.9999389648438,
"y": 46.99999237060547,
"width": 207.99993896484376,
"height": 301.9999694824219
}
},
"m_Slots": [
@ -1311,6 +1342,31 @@
"m_LiteralMode": false
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot",
"m_ObjectId": "214ec3bef4c24c5891c0ec6d43a0174c",
"m_Id": 0,
"m_DisplayName": "Edge",
"m_SlotType": 0,
"m_Hidden": false,
"m_ShaderOutputName": "Edge",
"m_StageCapability": 3,
"m_Value": {
"x": 0.30000001192092898,
"y": 1.0,
"z": 1.0,
"w": 1.0
},
"m_DefaultValue": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_LiteralMode": false
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.DynamicValueMaterialSlot",
@ -1375,6 +1431,31 @@
"m_LiteralMode": false
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot",
"m_ObjectId": "2d3c080fc37743f181887d546bb76525",
"m_Id": 0,
"m_DisplayName": "A",
"m_SlotType": 0,
"m_Hidden": false,
"m_ShaderOutputName": "A",
"m_StageCapability": 3,
"m_Value": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_DefaultValue": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_LiteralMode": false
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
@ -1929,6 +2010,49 @@
]
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.AddNode",
"m_ObjectId": "55f2c3eb7ea742a5a0959c2495bd8554",
"m_Group": {
"m_Id": ""
},
"m_Name": "Add",
"m_DrawState": {
"m_Expanded": true,
"m_Position": {
"serializedVersion": "2",
"x": -583.0,
"y": 340.0,
"width": 208.0,
"height": 301.99993896484377
}
},
"m_Slots": [
{
"m_Id": "2d3c080fc37743f181887d546bb76525"
},
{
"m_Id": "f6735ec548a245e5a45c3f09b4cec75c"
},
{
"m_Id": "ee24c03a60844a788a4edf26d75da532"
}
],
"synonyms": [
"addition",
"sum",
"plus"
],
"m_Precision": 0,
"m_PreviewExpanded": true,
"m_DismissedVersion": 0,
"m_PreviewMode": 0,
"m_CustomColors": {
"m_SerializableColors": []
}
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.BlockNode",
@ -1985,31 +2109,6 @@
"m_Channel": 0
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot",
"m_ObjectId": "5bd1434cd77d429cb2d3da6afd79db01",
"m_Id": 1,
"m_DisplayName": "In",
"m_SlotType": 0,
"m_Hidden": false,
"m_ShaderOutputName": "In",
"m_StageCapability": 3,
"m_Value": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_DefaultValue": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_LiteralMode": false
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.UVMaterialSlot",
@ -2133,31 +2232,6 @@
"m_Labels": []
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot",
"m_ObjectId": "5f4a817aae964d7eaabc6ba9e11664c1",
"m_Id": 2,
"m_DisplayName": "Out",
"m_SlotType": 1,
"m_Hidden": false,
"m_ShaderOutputName": "Out",
"m_StageCapability": 3,
"m_Value": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_DefaultValue": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_LiteralMode": false
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
@ -2239,8 +2313,8 @@
"m_ObjectId": "69f637288cb54756bdd28eab6e8ffd25",
"m_Title": "Noise",
"m_Position": {
"x": -2807.999755859375,
"y": 231.99996948242188
"x": -2808.0,
"y": 232.0
}
}
@ -2364,6 +2438,45 @@
}
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.StepNode",
"m_ObjectId": "73ce726c69084867adf2c184625cead4",
"m_Group": {
"m_Id": ""
},
"m_Name": "Step",
"m_DrawState": {
"m_Expanded": true,
"m_Position": {
"serializedVersion": "2",
"x": -841.0,
"y": 651.0,
"width": 208.0,
"height": 302.0
}
},
"m_Slots": [
{
"m_Id": "214ec3bef4c24c5891c0ec6d43a0174c"
},
{
"m_Id": "7a71d71bd4d749c6bce8d9f021960d7b"
},
{
"m_Id": "a5e7ca9d891d4ac8b8321ce2febf7251"
}
],
"synonyms": [],
"m_Precision": 0,
"m_PreviewExpanded": true,
"m_DismissedVersion": 0,
"m_PreviewMode": 0,
"m_CustomColors": {
"m_SerializableColors": []
}
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
@ -2461,6 +2574,31 @@
}
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot",
"m_ObjectId": "7a71d71bd4d749c6bce8d9f021960d7b",
"m_Id": 1,
"m_DisplayName": "In",
"m_SlotType": 0,
"m_Hidden": false,
"m_ShaderOutputName": "In",
"m_StageCapability": 3,
"m_Value": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_DefaultValue": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_LiteralMode": false
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
@ -3356,6 +3494,31 @@
"m_LiteralMode": false
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot",
"m_ObjectId": "a5e7ca9d891d4ac8b8321ce2febf7251",
"m_Id": 2,
"m_DisplayName": "Out",
"m_SlotType": 1,
"m_Hidden": false,
"m_ShaderOutputName": "Out",
"m_StageCapability": 3,
"m_Value": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_DefaultValue": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_LiteralMode": false
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
@ -3894,10 +4057,10 @@
"m_Expanded": true,
"m_Position": {
"serializedVersion": "2",
"x": -305.99981689453127,
"y": 209.00001525878907,
"width": 119.99990844726563,
"height": 149.0000457763672
"x": 100.00003051757813,
"y": 144.9999542236328,
"width": 119.99989318847656,
"height": 149.0000762939453
}
},
"m_Slots": [
@ -3995,10 +4158,10 @@
"m_Expanded": true,
"m_Position": {
"serializedVersion": "2",
"x": -536.9998779296875,
"y": 56.00006103515625,
"width": 207.99993896484376,
"height": 302.0
"x": -214.00006103515626,
"y": 9.000008583068848,
"width": 208.00003051757813,
"height": 301.9999694824219
}
},
"m_Slots": [
@ -4227,31 +4390,6 @@
"m_LiteralMode": false
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot",
"m_ObjectId": "d73462583fb1450a82199f7998a145c5",
"m_Id": 0,
"m_DisplayName": "Edge",
"m_SlotType": 0,
"m_Hidden": false,
"m_ShaderOutputName": "Edge",
"m_StageCapability": 3,
"m_Value": {
"x": 1.0,
"y": 1.0,
"z": 1.0,
"w": 1.0
},
"m_DefaultValue": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_LiteralMode": false
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
@ -4724,45 +4862,6 @@
"m_Labels": []
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.StepNode",
"m_ObjectId": "ea6c916c3dd64bcaa527955c3fbb4f62",
"m_Group": {
"m_Id": ""
},
"m_Name": "Step",
"m_DrawState": {
"m_Expanded": true,
"m_Position": {
"serializedVersion": "2",
"x": -769.9999389648438,
"y": 191.99998474121095,
"width": 208.0,
"height": 302.0
}
},
"m_Slots": [
{
"m_Id": "d73462583fb1450a82199f7998a145c5"
},
{
"m_Id": "5bd1434cd77d429cb2d3da6afd79db01"
},
{
"m_Id": "5f4a817aae964d7eaabc6ba9e11664c1"
}
],
"synonyms": [],
"m_Precision": 0,
"m_PreviewExpanded": true,
"m_DismissedVersion": 0,
"m_PreviewMode": 0,
"m_CustomColors": {
"m_SerializableColors": []
}
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot",
@ -4957,6 +5056,31 @@
}
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot",
"m_ObjectId": "ee24c03a60844a788a4edf26d75da532",
"m_Id": 2,
"m_DisplayName": "Out",
"m_SlotType": 1,
"m_Hidden": false,
"m_ShaderOutputName": "Out",
"m_StageCapability": 3,
"m_Value": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_DefaultValue": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_LiteralMode": false
}
{
"m_SGVersion": 1,
"m_Type": "UnityEditor.ShaderGraph.NoiseNode",
@ -5024,6 +5148,31 @@
"m_Labels": []
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot",
"m_ObjectId": "f6735ec548a245e5a45c3f09b4cec75c",
"m_Id": 1,
"m_DisplayName": "B",
"m_SlotType": 0,
"m_Hidden": false,
"m_ShaderOutputName": "B",
"m_StageCapability": 3,
"m_Value": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_DefaultValue": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 0.0
},
"m_LiteralMode": false
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.Vector2MaterialSlot",

View File

@ -1,359 +0,0 @@
{
"m_SGVersion": 3,
"m_Type": "UnityEditor.ShaderGraph.GraphData",
"m_ObjectId": "bfd41be4768743d49abea9ce9a041ce1",
"m_Properties": [],
"m_Keywords": [],
"m_Dropdowns": [],
"m_CategoryData": [
{
"m_Id": "10f6db50c33e4ea2bb7a18d474e4bbff"
}
],
"m_Nodes": [
{
"m_Id": "94c186bebeef4d2b9f136112253d8cd5"
},
{
"m_Id": "252fbabf290644db92ce5bee37d0512a"
},
{
"m_Id": "2dcf088eb2824593aefd458b9ff69f99"
},
{
"m_Id": "cdcab024229546818a2d8b4d465e82e6"
}
],
"m_GroupDatas": [],
"m_StickyNoteDatas": [],
"m_Edges": [],
"m_VertexContext": {
"m_Position": {
"x": 0.0,
"y": 0.0
},
"m_Blocks": [
{
"m_Id": "94c186bebeef4d2b9f136112253d8cd5"
},
{
"m_Id": "252fbabf290644db92ce5bee37d0512a"
},
{
"m_Id": "2dcf088eb2824593aefd458b9ff69f99"
}
]
},
"m_FragmentContext": {
"m_Position": {
"x": 0.0,
"y": 200.0
},
"m_Blocks": [
{
"m_Id": "cdcab024229546818a2d8b4d465e82e6"
}
]
},
"m_PreviewData": {
"serializedMesh": {
"m_SerializedMesh": "{\"mesh\":{\"instanceID\":0}}",
"m_Guid": ""
},
"preventRotation": false
},
"m_Path": "Shader Graphs",
"m_GraphPrecision": 1,
"m_PreviewMode": 2,
"m_OutputNode": {
"m_Id": ""
},
"m_SubDatas": [],
"m_ActiveTargets": [
{
"m_Id": "54c208f34b824035815c4e6157e4325e"
}
]
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.CategoryData",
"m_ObjectId": "10f6db50c33e4ea2bb7a18d474e4bbff",
"m_Name": "",
"m_ChildObjectList": []
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.BlockNode",
"m_ObjectId": "252fbabf290644db92ce5bee37d0512a",
"m_Group": {
"m_Id": ""
},
"m_Name": "VertexDescription.Normal",
"m_DrawState": {
"m_Expanded": true,
"m_Position": {
"serializedVersion": "2",
"x": 0.0,
"y": 0.0,
"width": 0.0,
"height": 0.0
}
},
"m_Slots": [
{
"m_Id": "65da9c62784d4cb19d46bba53309634c"
}
],
"synonyms": [],
"m_Precision": 0,
"m_PreviewExpanded": true,
"m_DismissedVersion": 0,
"m_PreviewMode": 0,
"m_CustomColors": {
"m_SerializableColors": []
},
"m_SerializedDescriptor": "VertexDescription.Normal"
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.BlockNode",
"m_ObjectId": "2dcf088eb2824593aefd458b9ff69f99",
"m_Group": {
"m_Id": ""
},
"m_Name": "VertexDescription.Tangent",
"m_DrawState": {
"m_Expanded": true,
"m_Position": {
"serializedVersion": "2",
"x": 0.0,
"y": 0.0,
"width": 0.0,
"height": 0.0
}
},
"m_Slots": [
{
"m_Id": "81df142a9ee645e08cfc667785bf4b7e"
}
],
"synonyms": [],
"m_Precision": 0,
"m_PreviewExpanded": true,
"m_DismissedVersion": 0,
"m_PreviewMode": 0,
"m_CustomColors": {
"m_SerializableColors": []
},
"m_SerializedDescriptor": "VertexDescription.Tangent"
}
{
"m_SGVersion": 1,
"m_Type": "UnityEditor.Rendering.Universal.ShaderGraph.UniversalTarget",
"m_ObjectId": "54c208f34b824035815c4e6157e4325e",
"m_Datas": [],
"m_ActiveSubTarget": {
"m_Id": "70ee05b7f8f24361a49d9b6ebda7422a"
},
"m_AllowMaterialOverride": false,
"m_SurfaceType": 0,
"m_ZTestMode": 4,
"m_ZWriteControl": 0,
"m_AlphaMode": 0,
"m_RenderFace": 2,
"m_AlphaClip": false,
"m_CastShadows": true,
"m_ReceiveShadows": true,
"m_DisableTint": false,
"m_Sort3DAs2DCompatible": false,
"m_AdditionalMotionVectorMode": 0,
"m_AlembicMotionVectors": false,
"m_SupportsLODCrossFade": false,
"m_CustomEditorGUI": "",
"m_SupportVFX": false
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.PositionMaterialSlot",
"m_ObjectId": "5b35b4eb69ed4bcaa6689ec1d77efabe",
"m_Id": 0,
"m_DisplayName": "Position",
"m_SlotType": 0,
"m_Hidden": false,
"m_ShaderOutputName": "Position",
"m_StageCapability": 1,
"m_Value": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"m_DefaultValue": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"m_Labels": [],
"m_Space": 0
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.NormalMaterialSlot",
"m_ObjectId": "65da9c62784d4cb19d46bba53309634c",
"m_Id": 0,
"m_DisplayName": "Normal",
"m_SlotType": 0,
"m_Hidden": false,
"m_ShaderOutputName": "Normal",
"m_StageCapability": 1,
"m_Value": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"m_DefaultValue": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"m_Labels": [],
"m_Space": 0
}
{
"m_SGVersion": 2,
"m_Type": "UnityEditor.Rendering.Universal.ShaderGraph.UniversalUnlitSubTarget",
"m_ObjectId": "70ee05b7f8f24361a49d9b6ebda7422a",
"m_KeepLightingVariants": false,
"m_DefaultDecalBlending": true,
"m_DefaultSSAO": true
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.TangentMaterialSlot",
"m_ObjectId": "81df142a9ee645e08cfc667785bf4b7e",
"m_Id": 0,
"m_DisplayName": "Tangent",
"m_SlotType": 0,
"m_Hidden": false,
"m_ShaderOutputName": "Tangent",
"m_StageCapability": 1,
"m_Value": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"m_DefaultValue": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"m_Labels": [],
"m_Space": 0
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.BlockNode",
"m_ObjectId": "94c186bebeef4d2b9f136112253d8cd5",
"m_Group": {
"m_Id": ""
},
"m_Name": "VertexDescription.Position",
"m_DrawState": {
"m_Expanded": true,
"m_Position": {
"serializedVersion": "2",
"x": 0.0,
"y": 0.0,
"width": 0.0,
"height": 0.0
}
},
"m_Slots": [
{
"m_Id": "5b35b4eb69ed4bcaa6689ec1d77efabe"
}
],
"synonyms": [],
"m_Precision": 0,
"m_PreviewExpanded": true,
"m_DismissedVersion": 0,
"m_PreviewMode": 0,
"m_CustomColors": {
"m_SerializableColors": []
},
"m_SerializedDescriptor": "VertexDescription.Position"
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.BlockNode",
"m_ObjectId": "cdcab024229546818a2d8b4d465e82e6",
"m_Group": {
"m_Id": ""
},
"m_Name": "SurfaceDescription.BaseColor",
"m_DrawState": {
"m_Expanded": true,
"m_Position": {
"serializedVersion": "2",
"x": 0.0,
"y": 0.0,
"width": 0.0,
"height": 0.0
}
},
"m_Slots": [
{
"m_Id": "eb00736542bf494b858ac57f9e4a3d72"
}
],
"synonyms": [],
"m_Precision": 0,
"m_PreviewExpanded": true,
"m_DismissedVersion": 0,
"m_PreviewMode": 0,
"m_CustomColors": {
"m_SerializableColors": []
},
"m_SerializedDescriptor": "SurfaceDescription.BaseColor"
}
{
"m_SGVersion": 0,
"m_Type": "UnityEditor.ShaderGraph.ColorRGBMaterialSlot",
"m_ObjectId": "eb00736542bf494b858ac57f9e4a3d72",
"m_Id": 0,
"m_DisplayName": "Base Color",
"m_SlotType": 0,
"m_Hidden": false,
"m_ShaderOutputName": "BaseColor",
"m_StageCapability": 2,
"m_Value": {
"x": 0.5,
"y": 0.5,
"z": 0.5
},
"m_DefaultValue": {
"x": 0.5,
"y": 0.5,
"z": 0.5
},
"m_Labels": [],
"m_ColorMode": 0,
"m_DefaultColor": {
"r": 0.5,
"g": 0.5,
"b": 0.5,
"a": 1.0
}
}

View File

@ -1,18 +0,0 @@
fileFormatVersion: 2
guid: ea1ea5e43c81c7b4285ed77336f302bf
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
useAsTemplate: 0
exposeTemplateAsShader: 0
template:
name:
category:
description:
icon: {instanceID: 0}
thumbnail: {instanceID: 0}

View File

@ -1,6 +1,8 @@
<Solution>
<Project Path="MCPForUnity.Runtime.csproj" />
<Project Path="VFXGraph.OutputEventHandlers.csproj" />
<Project Path="Assembly-CSharp.csproj" />
<Project Path="MCPForUnity.Editor.csproj" />
<Project Path="Assembly-CSharp-Editor.csproj" />
<Project Path="VFXGraph.OutputEventHandlers.Editor.csproj" />
</Solution>

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 31e7fac5858840340a75cc6df0ad3d9e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")]

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: be61633e00d934610ac1ff8192ffbe3d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c9d47f01d06964ee7843765d1bd71205
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 59ff83375c2c74c8385c4a22549778dd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Models;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class AntigravityConfigurator : JsonFileMcpConfigurator
{
public AntigravityConfigurator() : base(new McpClient
{
name = "Antigravity",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"),
HttpUrlProperty = "serverUrl",
DefaultUnityFields = { { "disabled", false } },
StripEnvWhenNotRequired = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Antigravity",
"Click the more_horiz menu in the Agent pane > MCP Servers",
"Select 'Install' for Unity MCP or use the Configure button above",
"Restart Antigravity if necessary"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 331b33961513042e3945d0a1d06615b5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Models;
using MCPForUnity.Editor.Services;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class CherryStudioConfigurator : JsonFileMcpConfigurator
{
public const string ClientName = "Cherry Studio";
public CherryStudioConfigurator() : base(new McpClient
{
name = ClientName,
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Cherry Studio", "config"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Cherry Studio", "config"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Cherry Studio", "config"),
SupportsHttpTransport = false
})
{ }
public override bool SupportsAutoConfigure => false;
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Cherry Studio",
"Go to Settings (⚙️) → MCP Server",
"Click 'Add Server' button",
"For STDIO mode (recommended):",
" - Name: unity-mcp",
" - Type: STDIO",
" - Command: uvx",
" - Arguments: Copy from the Manual Configuration JSON below",
"Click Save and restart Cherry Studio",
"",
"Note: Cherry Studio uses UI-based configuration.",
"Use the manual snippet below as reference for the values to enter."
};
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
client.SetStatus(McpStatus.NotConfigured, "Cherry Studio requires manual UI configuration");
return client.status;
}
public override void Configure()
{
throw new InvalidOperationException(
"Cherry Studio uses UI-based configuration. " +
"Please use the Manual Configuration snippet and Installation Steps to configure manually."
);
}
public override string GetManualSnippet()
{
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
if (useHttp)
{
return "# Cherry Studio does not support WebSocket transport.\n" +
"# Cherry Studio supports STDIO and SSE transports.\n" +
"# \n" +
"# To use Cherry Studio:\n" +
"# 1. Switch transport to 'Stdio' in Advanced Settings below\n" +
"# 2. Return to this configuration screen\n" +
"# 3. Copy the STDIO configuration snippet that will appear\n" +
"# \n" +
"# OPTION 2: SSE mode (future support)\n" +
"# Note: Unity MCP does not currently have an SSE endpoint.\n" +
"# This may be added in a future update.";
}
return base.GetManualSnippet() + "\n\n" +
"# Cherry Studio Configuration Instructions:\n" +
"# Cherry Studio uses UI-based configuration, not a JSON file.\n" +
"# \n" +
"# To configure:\n" +
"# 1. Open Cherry Studio\n" +
"# 2. Go to Settings (⚙️) → MCP Server\n" +
"# 3. Click 'Add Server'\n" +
"# 4. Enter the following values from the JSON above:\n" +
"# - Name: unity-mcp\n" +
"# - Type: STDIO\n" +
"# - Command: (copy 'command' value from JSON)\n" +
"# - Arguments: (copy 'args' array values, space-separated or as individual entries)\n" +
"# - Active: true\n" +
"# 5. Click Save\n" +
"# 6. Restart Cherry Studio";
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6de06c6bb0399154d840a1e4c84be869
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Claude Code configurator using the CLI-based registration (claude mcp add/remove).
/// This integrates with Claude Code's native MCP management.
/// </summary>
public class ClaudeCodeConfigurator : ClaudeCliMcpConfigurator
{
public ClaudeCodeConfigurator() : base(new McpClient
{
name = "Claude Code",
SupportsHttpTransport = true,
})
{ }
public override bool SupportsSkills => true;
public override string GetSkillInstallPath()
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(userHome, ".claude", "skills", "unity-mcp-skill");
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Claude CLI is installed (comes with Claude Code)",
"Click Configure to add UnityMCP via 'claude mcp add'",
"The server will be automatically available in Claude Code",
"Use Unregister to remove via 'claude mcp remove'"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d0d22681fc594475db1c189f2d9abdf7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Models;
using MCPForUnity.Editor.Services;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class ClaudeDesktopConfigurator : JsonFileMcpConfigurator
{
public const string ClientName = "Claude Desktop";
public ClaudeDesktopConfigurator() : base(new McpClient
{
name = ClientName,
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Claude", "claude_desktop_config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Claude", "claude_desktop_config.json"),
SupportsHttpTransport = false,
StripEnvWhenNotRequired = true
})
{ }
public override bool SupportsSkills => true;
public override string GetSkillInstallPath()
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(userHome, ".claude", "skills", "unity-mcp-skill");
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Claude Desktop",
"Go to Settings > Developer > Edit Config\nOR open the config path",
"Paste the configuration JSON",
"Save and restart Claude Desktop"
};
public override void Configure()
{
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
if (useHttp)
{
throw new InvalidOperationException("Claude Desktop does not support HTTP transport. Switch to stdio in settings before configuring.");
}
base.Configure();
}
public override string GetManualSnippet()
{
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
if (useHttp)
{
return "# Claude Desktop does not support HTTP transport.\n" +
"# In Connect tab, change the Transport option from HTTP to stdio, then regenerate.";
}
return base.GetManualSnippet();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d5e5d87c9db57495f842dc366f1ebd65
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class ClineConfigurator : JsonFileMcpConfigurator
{
public ClineConfigurator() : base(new McpClient
{
name = "Cline",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
DefaultUnityFields = { { "disabled", false }, { "autoApprove", new object[] { } } }
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Cline in VS Code",
"Click the MCP Servers icon in the Cline pane",
"Go to Configure tab and click 'Configure MCP Servers'\nOR open the config file at the path above",
"Paste the configuration JSON into the mcpServers object",
"Save and restart VS Code"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6b8abf0951c7413d9ff97a053b0adf2d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Configures the CodeBuddy CLI (~/.codebuddy.json) MCP settings.
/// </summary>
public class CodeBuddyCliConfigurator : JsonFileMcpConfigurator
{
public CodeBuddyCliConfigurator() : base(new McpClient
{
name = "CodeBuddy CLI",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"),
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install CodeBuddy CLI and ensure '~/.codebuddy.json' exists",
"Click Configure to add the UnityMCP entry (or manually edit the file above)",
"Restart your CLI session if needed"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 923728a98c8c74cfaa6e9203c408f34e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class CodexConfigurator : CodexMcpConfigurator
{
public CodexConfigurator() : base(new McpClient
{
name = "Codex",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml")
})
{ }
public override bool SupportsSkills => true;
public override string GetSkillInstallPath()
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(userHome, ".codex", "skills", "unity-mcp-skill");
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Run 'codex config edit' in a terminal\nOR open the config file at the path above",
"Paste the configuration TOML",
"Save and restart Codex"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7037ef8b168e49f79247cb31c3be75a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class CopilotCliConfigurator : JsonFileMcpConfigurator
{
public CopilotCliConfigurator() : base(new McpClient
{
name = "GitHub Copilot CLI",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json")
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install GitHub Copilot CLI (https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)",
"Open or create mcp-config.json at the path above",
"Paste the configuration JSON (or use /mcp add in the CLI)",
"Restart your Copilot CLI session"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 14a4b9a7f749248d496466c2a3a53e56
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class CursorConfigurator : JsonFileMcpConfigurator
{
public CursorConfigurator() : base(new McpClient
{
name = "Cursor",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json")
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Cursor",
"Go to File > Preferences > Cursor Settings > MCP > Add new global MCP server\nOR open the config file at the path above",
"Paste the configuration JSON",
"Save and restart Cursor"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b708eda314746481fb8f4a1fb0652b03
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Models;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class GeminiCliConfigurator : JsonFileMcpConfigurator
{
public GeminiCliConfigurator() : base(new McpClient
{
name = "Gemini CLI",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "settings.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "settings.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "settings.json"),
HttpUrlProperty = "httpUrl",
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Gemini CLI is installed (see https://geminicli.com/docs/get-started/installation/)",
"Click Register to add UnityMCP via 'gemini mcp add'",
"The server will be automatically available in Gemini CLI",
"Use Unregister to remove via 'gemini mcp remove'"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c5e9bbb45e552453ab5cb557a22d43e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class KiloCodeConfigurator : JsonFileMcpConfigurator
{
public KiloCodeConfigurator() : base(new McpClient
{
name = "Kilo Code",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"),
IsVsCodeLayout = false
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install Kilo Code extension in VS Code",
"Open Kilo Code settings (gear icon in sidebar)",
"Navigate to MCP Servers section and click 'Edit Global MCP Settings'\nOR open the config file at the path above",
"Paste the configuration JSON into the mcpServers object",
"Save and restart VS Code"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3286d62ffe5644f5ea60488fd7e6513d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class KiroConfigurator : JsonFileMcpConfigurator
{
public KiroConfigurator() : base(new McpClient
{
name = "Kiro",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
EnsureEnvObject = true,
DefaultUnityFields = { { "disabled", false } }
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Kiro",
"Go to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config\nOR open the config file at the path above",
"Paste the configuration JSON",
"Save and restart Kiro"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e9b73ff071a6043dda1f2ec7d682ef71
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,398 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
using MCPForUnity.Editor.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Configurator for OpenClaw via the openclaw-mcp-bridge plugin.
/// OpenClaw stores config at ~/.openclaw/openclaw.json.
/// </summary>
public class OpenClawConfigurator : McpClientConfiguratorBase
{
private const string PluginName = "openclaw-mcp-bridge";
private const string ServerName = "unityMCP";
private const string HttpTransportName = "http";
private const string StdioTransportName = "stdio";
private const string StdioUrl = "stdio://local";
public OpenClawConfigurator() : base(new McpClient
{
name = "OpenClaw",
windowsConfigPath = BuildConfigPath(),
macConfigPath = BuildConfigPath(),
linuxConfigPath = BuildConfigPath()
})
{ }
private static string BuildConfigPath()
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".openclaw",
"openclaw.json");
}
public override string GetConfigPath() => CurrentOsPath();
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
try
{
string path = GetConfigPath();
if (!File.Exists(path))
{
client.SetStatus(McpStatus.NotConfigured);
client.configuredTransport = ConfiguredTransport.Unknown;
return client.status;
}
JObject root = LoadConfig(path);
JObject pluginEntry = root["plugins"]?["entries"]?[PluginName] as JObject;
JObject unityServer = FindUnityServer(pluginEntry?["config"]?["servers"]);
if (pluginEntry == null || unityServer == null)
{
client.SetStatus(McpStatus.MissingConfig);
client.configuredTransport = ConfiguredTransport.Unknown;
return client.status;
}
if (!IsEnabled(pluginEntry) || !IsEnabled(unityServer))
{
client.SetStatus(McpStatus.NotConfigured);
client.configuredTransport = ConfiguredTransport.Unknown;
return client.status;
}
bool matches = ServerMatchesCurrentEndpoint(unityServer);
if (matches)
{
client.SetStatus(McpStatus.Configured);
client.configuredTransport = ResolveTransport(unityServer);
return client.status;
}
if (attemptAutoRewrite)
{
Configure();
}
else
{
client.SetStatus(McpStatus.IncorrectPath);
client.configuredTransport = ConfiguredTransport.Unknown;
}
}
catch (Exception ex)
{
client.SetStatus(McpStatus.Error, ex.Message);
client.configuredTransport = ConfiguredTransport.Unknown;
}
return client.status;
}
public override void Configure()
{
if (EditorPrefs.GetBool(EditorPrefKeys.LockCursorConfig, false))
return;
string path = GetConfigPath();
McpConfigurationHelper.EnsureConfigDirectoryExists(path);
JObject root = File.Exists(path) ? LoadConfig(path) : new JObject();
JObject plugins = root["plugins"] as JObject ?? new JObject();
root["plugins"] = plugins;
JObject entries = plugins["entries"] as JObject ?? new JObject();
plugins["entries"] = entries;
JObject pluginEntry = entries[PluginName] as JObject ?? new JObject();
entries[PluginName] = pluginEntry;
pluginEntry["enabled"] = true;
JObject pluginConfig = pluginEntry["config"] as JObject ?? new JObject();
pluginEntry["config"] = pluginConfig;
pluginConfig.Remove("timeout"); // removed in openclaw-mcp-bridge v2+
pluginConfig.Remove("retries"); // removed in openclaw-mcp-bridge v2+
pluginConfig["servers"] = UpsertUnityServer(pluginConfig["servers"]);
McpConfigurationHelper.WriteAtomicFile(path, root.ToString(Formatting.Indented));
client.SetStatus(McpStatus.Configured);
client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
}
public override string GetManualSnippet()
{
JObject snippet = new JObject
{
["plugins"] = new JObject
{
["entries"] = new JObject
{
[PluginName] = new JObject
{
["enabled"] = true,
["config"] = new JObject
{
["servers"] = new JObject
{
[ServerName] = BuildUnityServerEntry()
}
}
}
}
}
};
return snippet.ToString(Formatting.Indented);
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install OpenClaw",
"Install the bridge plugin: npm install -g openclaw-mcp-bridge (or pnpm add -g openclaw-mcp-bridge)",
"In MCP for Unity, choose OpenClaw and click Configure",
"OpenClaw uses the currently selected MCP for Unity transport (HTTP or stdio)",
"OpenClaw exposes a proxy tool such as unityMCP__call for Unity MCP access",
"Restart OpenClaw if the plugin does not hot-reload the new config"
};
private JObject LoadConfig(string path)
{
string text = File.ReadAllText(path);
if (string.IsNullOrWhiteSpace(text))
{
return new JObject();
}
try
{
return JsonConvert.DeserializeObject<JObject>(text) ?? new JObject();
}
catch (JsonException ex)
{
throw new InvalidOperationException(
$"OpenClaw config contains non-JSON content and cannot be safely auto-edited: {ex.Message}");
}
}
private JObject FindUnityServer(JToken serversToken)
{
if (serversToken is JObject serverMap)
{
return serverMap[ServerName] as JObject;
}
if (serversToken is JArray legacyServers)
{
foreach (JToken token in legacyServers)
{
JObject server = token as JObject;
if (server == null)
{
continue;
}
string name = server["name"]?.ToString();
if (string.Equals(name, ServerName, StringComparison.OrdinalIgnoreCase))
{
return server;
}
}
}
return null;
}
private JObject UpsertUnityServer(JToken serversToken)
{
JObject servers = NormalizeServers(serversToken);
JObject entry = servers[ServerName] as JObject ?? new JObject();
JObject desiredEntry = BuildUnityServerEntry();
entry.Remove("name");
entry.Remove("prefix");
entry.Remove("healthCheck");
entry.Remove("command");
entry.Remove("args");
entry.Remove("env");
entry.Remove("connectTimeoutMs");
foreach (var property in desiredEntry.Properties())
{
entry[property.Name] = property.Value.DeepClone();
}
servers[ServerName] = entry;
return servers;
}
private static JObject NormalizeServers(JToken serversToken)
{
if (serversToken is JObject serverMap)
{
return serverMap;
}
var normalized = new JObject();
if (!(serversToken is JArray legacyServers))
{
return normalized;
}
foreach (JToken token in legacyServers)
{
if (!(token is JObject legacyServer))
{
continue;
}
string name = legacyServer["name"]?.ToString();
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
normalized[name] = legacyServer;
}
return normalized;
}
private static JObject BuildUnityServerEntry()
{
ConfiguredTransport transport = HttpEndpointUtility.GetCurrentServerTransport();
if (transport == ConfiguredTransport.Stdio)
{
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
if (string.IsNullOrWhiteSpace(uvxPath))
{
throw new InvalidOperationException("uvx not found. Install uv/uvx or set the override in Advanced Settings.");
}
var args = new JArray();
foreach (string value in AssetPathUtility.GetUvxDevFlagsList())
{
args.Add(value);
}
foreach (string value in AssetPathUtility.GetBetaServerFromArgsList())
{
args.Add(value);
}
args.Add(packageName);
args.Add("--transport");
args.Add("stdio");
return new JObject
{
["enabled"] = true,
["url"] = StdioUrl,
["transport"] = StdioTransportName,
["command"] = uvxPath,
["args"] = args,
["toolPrefix"] = ServerName,
["requestTimeoutMs"] = 60000,
["connectTimeoutMs"] = 15000
};
}
return new JObject
{
["enabled"] = true,
["url"] = HttpEndpointUtility.GetMcpRpcUrl(),
["transport"] = HttpTransportName,
["toolPrefix"] = ServerName,
["requestTimeoutMs"] = 30000
};
}
private bool ServerMatchesCurrentEndpoint(JObject server)
{
if (server == null)
{
return false;
}
ConfiguredTransport expectedTransport = HttpEndpointUtility.GetCurrentServerTransport();
ConfiguredTransport configuredTransport = ResolveTransport(server);
if (configuredTransport != expectedTransport)
{
return false;
}
if (configuredTransport == ConfiguredTransport.Stdio)
{
string configuredUrl = server["url"]?.ToString();
string command = server["command"]?.ToString();
if (!UrlsEqual(configuredUrl, StdioUrl) || string.IsNullOrWhiteSpace(command))
{
return false;
}
// Validate the --from package source hasn't drifted (e.g. stable vs prerelease switch)
string[] args = (server["args"] as JArray)?.ToObject<string[]>();
string configuredSource = McpConfigurationHelper.ExtractUvxUrl(args);
string expectedSource = GetExpectedPackageSourceForValidation();
if (!string.IsNullOrEmpty(configuredSource) && !string.IsNullOrEmpty(expectedSource) &&
!McpConfigurationHelper.PathsEqual(configuredSource, expectedSource))
{
return false;
}
}
else
{
string configuredUrl = server["url"]?.ToString();
if (string.IsNullOrWhiteSpace(configuredUrl) ||
(!UrlsEqual(configuredUrl, HttpEndpointUtility.GetLocalMcpRpcUrl()) &&
!UrlsEqual(configuredUrl, HttpEndpointUtility.GetRemoteMcpRpcUrl())))
{
return false;
}
}
string toolPrefix = server["toolPrefix"]?.ToString();
return string.IsNullOrWhiteSpace(toolPrefix) ||
string.Equals(toolPrefix, ServerName, StringComparison.OrdinalIgnoreCase);
}
private static bool IsEnabled(JObject entry)
{
JToken enabledToken = entry["enabled"];
return enabledToken == null || enabledToken.Type != JTokenType.Boolean || enabledToken.Value<bool>();
}
private ConfiguredTransport ResolveTransport(JObject server)
{
string configuredTransport = server?["transport"]?.ToString();
string configuredUrl = server?["url"]?.ToString();
if (string.Equals(configuredTransport, StdioTransportName, StringComparison.OrdinalIgnoreCase) ||
UrlsEqual(configuredUrl, StdioUrl))
{
return ConfiguredTransport.Stdio;
}
if (UrlsEqual(configuredUrl, HttpEndpointUtility.GetRemoteMcpRpcUrl()))
{
return ConfiguredTransport.HttpRemote;
}
if (UrlsEqual(configuredUrl, HttpEndpointUtility.GetLocalMcpRpcUrl()))
{
return ConfiguredTransport.Http;
}
return ConfiguredTransport.Unknown;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 19f29c88761345158fc766d24e7c18f3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Configurator for OpenCode (opencode.ai) - a Go-based terminal AI coding assistant.
/// OpenCode uses ~/.config/opencode/opencode.json with a custom "mcp" format.
/// </summary>
public class OpenCodeConfigurator : McpClientConfiguratorBase
{
private const string ServerName = "unityMCP";
private const string SchemaUrl = "https://opencode.ai/config.json";
private const string RemoteType = "remote";
private const string LocalType = "local";
public OpenCodeConfigurator() : base(new McpClient
{
name = "OpenCode",
windowsConfigPath = BuildConfigPath(),
macConfigPath = BuildConfigPath(),
linuxConfigPath = BuildConfigPath()
})
{ }
private static string BuildConfigPath()
{
string xdgConfigHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
string configBase = !string.IsNullOrEmpty(xdgConfigHome)
? xdgConfigHome
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
return Path.Combine(configBase, "opencode", "opencode.json");
}
public override string GetConfigPath() => CurrentOsPath();
/// <summary>
/// Attempts to load and parse the config file.
/// Returns null if file doesn't exist or cannot be read.
/// Returns parsed JObject if valid JSON found.
/// Logs warning if file exists but contains malformed JSON.
/// </summary>
private JObject TryLoadConfig(string path)
{
if (!File.Exists(path))
return null;
string content;
try
{
content = File.ReadAllText(path);
}
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"[OpenCodeConfigurator] Failed to read config file {path}: {ex.Message}");
return null;
}
try
{
return JsonConvert.DeserializeObject<JObject>(content) ?? new JObject();
}
catch (JsonException ex)
{
// Malformed JSON - log warning and return null.
// When Configure() receives null, it will do: TryLoadConfig(path) ?? new JObject()
// This creates a fresh empty JObject, which replaces the entire file with only the unityMCP section.
// Existing config sections are lost. To preserve sections, a different recovery strategy
// (e.g., line-by-line parsing, JSON repair, or manual user intervention) would be needed.
UnityEngine.Debug.LogWarning($"[OpenCodeConfigurator] Malformed JSON in {path}: {ex.Message}");
return null;
}
}
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
try
{
string path = GetConfigPath();
var config = TryLoadConfig(path);
if (config == null)
{
client.SetStatus(McpStatus.NotConfigured);
return client.status;
}
var unityMcp = config["mcp"]?[ServerName] as JObject;
if (unityMcp == null)
{
client.SetStatus(McpStatus.NotConfigured);
return client.status;
}
if (EntryMatchesCurrentTransport(unityMcp))
{
client.SetStatus(McpStatus.Configured);
}
else if (attemptAutoRewrite)
{
Configure();
}
else
{
client.SetStatus(McpStatus.IncorrectPath);
}
}
catch (Exception ex)
{
client.SetStatus(McpStatus.Error, ex.Message);
}
return client.status;
}
public override void Configure()
{
try
{
string path = GetConfigPath();
McpConfigurationHelper.EnsureConfigDirectoryExists(path);
// Load existing config or start fresh, preserving all other properties and MCP servers
var config = TryLoadConfig(path) ?? new JObject();
// Only add $schema if creating a new file
if (!File.Exists(path))
{
config["$schema"] = SchemaUrl;
}
// Preserve existing mcp section and only update our server entry
var mcpSection = config["mcp"] as JObject ?? new JObject();
config["mcp"] = mcpSection;
mcpSection[ServerName] = BuildServerEntry();
McpConfigurationHelper.WriteAtomicFile(path, JsonConvert.SerializeObject(config, Formatting.Indented));
client.SetStatus(McpStatus.Configured);
}
catch (Exception ex)
{
client.SetStatus(McpStatus.Error, ex.Message);
}
}
public override string GetManualSnippet()
{
var snippet = new JObject
{
["mcp"] = new JObject { [ServerName] = BuildServerEntry() }
};
return JsonConvert.SerializeObject(snippet, Formatting.Indented);
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install OpenCode (https://opencode.ai)",
"Click Configure to add Unity MCP to ~/.config/opencode/opencode.json",
"Restart OpenCode",
"The Unity MCP server should be detected automatically"
};
private static JObject BuildServerEntry()
{
if (HttpEndpointUtility.GetCurrentServerTransport() == ConfiguredTransport.Stdio)
{
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
if (string.IsNullOrWhiteSpace(uvxPath))
{
throw new InvalidOperationException("uvx not found. Install uv/uvx or set the override in Advanced Settings.");
}
var command = new JArray { uvxPath };
foreach (string value in AssetPathUtility.GetUvxDevFlagsList())
{
command.Add(value);
}
foreach (string value in AssetPathUtility.GetBetaServerFromArgsList())
{
command.Add(value);
}
command.Add(packageName);
command.Add("--transport");
command.Add("stdio");
return new JObject
{
["type"] = LocalType,
["command"] = command,
["enabled"] = true
};
}
return new JObject
{
["type"] = RemoteType,
["url"] = HttpEndpointUtility.GetMcpRpcUrl(),
["enabled"] = true
};
}
private bool EntryMatchesCurrentTransport(JObject entry)
{
string entryType = entry["type"]?.ToString();
ConfiguredTransport expected = HttpEndpointUtility.GetCurrentServerTransport();
if (expected == ConfiguredTransport.Stdio)
{
return string.Equals(entryType, LocalType, StringComparison.OrdinalIgnoreCase)
&& entry["command"] is JArray;
}
return string.Equals(entryType, RemoteType, StringComparison.OrdinalIgnoreCase)
&& UrlsEqual(entry["url"]?.ToString(), HttpEndpointUtility.GetMcpRpcUrl());
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 489f99ffb7e6743e88e3203552c8b37b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Qwen Code MCP client configurator.
/// Qwen Code uses a JSON-based configuration file with mcpServers section.
/// Config path: ~/.qwen/settings.json
///
/// Qwen Code supports both stdio (uvx) and HTTP transport modes.
/// Default: stdio mode (works without Unity Editor for basic operations)
/// HTTP mode: requires Unity Editor running with MCP HTTP server started
/// </summary>
public class QwenCodeConfigurator : JsonFileMcpConfigurator
{
public QwenCodeConfigurator() : base(new McpClient
{
name = "Qwen Code",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".qwen", "settings.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".qwen", "settings.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".qwen", "settings.json"),
SupportsHttpTransport = true,
// Default to stdio transport for Qwen Code (like Cursor)
// User can switch to HTTP in Unity: Window > MCP for Unity > Settings
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Qwen Code is installed (npm install -g @qwen-code/qwen-code or download from https://github.com/QwenLM/qwen-code)",
"Open Qwen Code",
"Click 'Auto Configure' to automatically add UnityMCP to settings.json",
"OR click 'Manual Setup' to copy the configuration JSON",
"Open ~/.qwen/settings.json and paste the configuration",
"Save and restart Qwen Code",
"Use /mcp command in Qwen Code to verify Unity MCP is connected",
"Note: For full functionality, open Unity Editor and start HTTP server"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 46891bcdb00e468cbd04afbfb8f3095e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class RiderConfigurator : JsonFileMcpConfigurator
{
public RiderConfigurator() : base(new McpClient
{
name = "Rider GitHub Copilot",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "github-copilot", "intellij", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "github-copilot", "intellij", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "github-copilot", "intellij", "mcp.json"),
IsVsCodeLayout = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install GitHub Copilot plugin in Rider",
"Open or create mcp.json at the path above",
"Paste the configuration JSON",
"Save and restart Rider"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2511b0d05271d486bb61f8cc9fd11363
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class TraeConfigurator : JsonFileMcpConfigurator
{
public TraeConfigurator() : base(new McpClient
{
name = "Trae",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Trae", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Trae", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Trae", "mcp.json"),
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Trae and go to Settings > MCP",
"Select Add Server > Add Manually",
"Paste the JSON or point to the mcp.json file\n"+
"Windows: %AppData%\\Trae\\mcp.json\n" +
"macOS: ~/Library/Application Support/Trae/mcp.json\n" +
"Linux: ~/.config/Trae/mcp.json\n",
"Save and restart Trae"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b3ab39e22ae0948ab94beae307f9902e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class VSCodeConfigurator : JsonFileMcpConfigurator
{
public VSCodeConfigurator() : base(new McpClient
{
name = "VSCode GitHub Copilot",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "mcp.json"),
IsVsCodeLayout = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install GitHub Copilot extension",
"Open or create mcp.json at the path above",
"Paste the configuration JSON",
"Save and restart VSCode"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bcc7ead475a4d4ea2978151c217757b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class VSCodeInsidersConfigurator : JsonFileMcpConfigurator
{
public VSCodeInsidersConfigurator() : base(new McpClient
{
name = "VSCode Insiders GitHub Copilot",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code - Insiders", "User", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code - Insiders", "User", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code - Insiders", "User", "mcp.json"),
IsVsCodeLayout = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install GitHub Copilot extension in VS Code Insiders",
"Open or create mcp.json at the path above",
"Paste the configuration JSON",
"Save and restart VS Code Insiders"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c4a1b0d3b34489cbf0f8c40c49c4f3b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class WindsurfConfigurator : JsonFileMcpConfigurator
{
public WindsurfConfigurator() : base(new McpClient
{
name = "Windsurf",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
HttpUrlProperty = "serverUrl",
DefaultUnityFields = { { "disabled", false } },
StripEnvWhenNotRequired = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Windsurf",
"Go to File > Preferences > Windsurf Settings > MCP > Manage MCPs > View raw config\nOR open the config file at the path above",
"Paste the configuration JSON",
"Save and restart Windsurf"
};
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b528971e189f141d38db577f155bd222
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,53 @@
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients
{
/// <summary>
/// Contract for MCP client configurators. Each client is responsible for
/// status detection, auto-configure, and manual snippet/steps.
/// </summary>
public interface IMcpClientConfigurator
{
/// <summary>Stable identifier (e.g., "cursor").</summary>
string Id { get; }
/// <summary>Display name shown in the UI.</summary>
string DisplayName { get; }
/// <summary>Current status cached by the configurator.</summary>
McpStatus Status { get; }
/// <summary>
/// The transport type the client is currently configured for.
/// Returns Unknown if the client is not configured or the transport cannot be determined.
/// </summary>
ConfiguredTransport ConfiguredTransport { get; }
/// <summary>True if this client supports auto-configure.</summary>
bool SupportsAutoConfigure { get; }
/// <summary>Label to show on the configure button for the current state.</summary>
string GetConfigureActionLabel();
/// <summary>Returns the platform-specific config path (or message for CLI-managed clients).</summary>
string GetConfigPath();
/// <summary>Checks and updates status; returns current status.</summary>
McpStatus CheckStatus(bool attemptAutoRewrite = true);
/// <summary>Runs auto-configuration (register/write file/CLI etc.).</summary>
void Configure();
/// <summary>Returns the manual configuration snippet (JSON/TOML/commands).</summary>
string GetManualSnippet();
/// <summary>Returns ordered human-readable installation steps.</summary>
System.Collections.Generic.IList<string> GetInstallationSteps();
/// <summary>True if this client supports skill installation/sync.</summary>
bool SupportsSkills { get; }
/// <summary>Returns the absolute path where skills should be installed, or null if unsupported.</summary>
string GetSkillInstallPath();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f5a5078d9e6e14027a1abfebf4018634
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d408fd7733cb4a1eb80f785307db2ff
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Clients
{
/// <summary>
/// Central registry that auto-discovers configurators via TypeCache.
/// </summary>
public static class McpClientRegistry
{
private static List<IMcpClientConfigurator> cached;
public static IReadOnlyList<IMcpClientConfigurator> All
{
get
{
if (cached == null)
{
cached = BuildRegistry();
}
return cached;
}
}
private static List<IMcpClientConfigurator> BuildRegistry()
{
var configurators = new List<IMcpClientConfigurator>();
foreach (var type in TypeCache.GetTypesDerivedFrom<IMcpClientConfigurator>())
{
if (type.IsAbstract || !type.IsClass || !type.IsPublic)
continue;
// Require a public parameterless constructor
if (type.GetConstructor(Type.EmptyTypes) == null)
continue;
try
{
if (Activator.CreateInstance(type) is IMcpClientConfigurator instance)
{
configurators.Add(instance);
}
}
catch (Exception ex)
{
McpLog.Warn($"UnityMCP: Failed to instantiate configurator {type.Name}: {ex.Message}");
}
}
// Alphabetical order by display name
configurators = configurators.OrderBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase).ToList();
return configurators;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4ce08555f995e4e848a826c63f18cb35
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f7e009cbf3e74f6c987331c2b438ec59
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,10 @@
namespace MCPForUnity.Editor.Constants
{
/// <summary>
/// Protocol-level constants for API key authentication.
/// </summary>
internal static class AuthConstants
{
internal const string ApiKeyHeader = "X-API-Key";
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 96844bc39e9a94cf18b18f8127f3854f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,75 @@
namespace MCPForUnity.Editor.Constants
{
/// <summary>
/// Centralized list of EditorPrefs keys used by the MCP for Unity package.
/// Keeping them in one place avoids typos and simplifies migrations.
/// </summary>
internal static class EditorPrefKeys
{
internal const string UseHttpTransport = "MCPForUnity.UseHttpTransport";
internal const string HttpTransportScope = "MCPForUnity.HttpTransportScope"; // "local" | "remote"
internal const string LastLocalHttpServerPid = "MCPForUnity.LocalHttpServer.LastPid";
internal const string LastLocalHttpServerPort = "MCPForUnity.LocalHttpServer.LastPort";
internal const string LastLocalHttpServerStartedUtc = "MCPForUnity.LocalHttpServer.LastStartedUtc";
internal const string LastLocalHttpServerPidArgsHash = "MCPForUnity.LocalHttpServer.LastPidArgsHash";
internal const string LastLocalHttpServerPidFilePath = "MCPForUnity.LocalHttpServer.LastPidFilePath";
internal const string LastLocalHttpServerInstanceToken = "MCPForUnity.LocalHttpServer.LastInstanceToken";
internal const string DebugLogs = "MCPForUnity.DebugLogs";
internal const string ValidationLevel = "MCPForUnity.ValidationLevel";
internal const string UnitySocketPort = "MCPForUnity.UnitySocketPort";
internal const string ResumeHttpAfterReload = "MCPForUnity.ResumeHttpAfterReload";
internal const string ResumeStdioAfterReload = "MCPForUnity.ResumeStdioAfterReload";
internal const string UvxPathOverride = "MCPForUnity.UvxPath";
internal const string ClaudeCliPathOverride = "MCPForUnity.ClaudeCliPath";
internal const string ClientProjectDirOverride = "MCPForUnity.ClientProjectDir";
internal const string HttpBaseUrl = "MCPForUnity.HttpUrl";
internal const string HttpRemoteBaseUrl = "MCPForUnity.HttpRemoteUrl";
internal const string SessionId = "MCPForUnity.SessionId";
internal const string WebSocketUrlOverride = "MCPForUnity.WebSocketUrl";
internal const string GitUrlOverride = "MCPForUnity.GitUrlOverride";
internal const string DevModeForceServerRefresh = "MCPForUnity.DevModeForceServerRefresh";
internal const string ProjectScopedToolsLocalHttp = "MCPForUnity.ProjectScopedTools.LocalHttp";
internal const string AllowLanHttpBind = "MCPForUnity.Security.AllowLanHttpBind";
internal const string AllowInsecureRemoteHttp = "MCPForUnity.Security.AllowInsecureRemoteHttp";
internal const string PackageDeploySourcePath = "MCPForUnity.PackageDeploy.SourcePath";
internal const string PackageDeployLastBackupPath = "MCPForUnity.PackageDeploy.LastBackupPath";
internal const string PackageDeployLastTargetPath = "MCPForUnity.PackageDeploy.LastTargetPath";
internal const string PackageDeployLastSourcePath = "MCPForUnity.PackageDeploy.LastSourcePath";
internal const string ServerSrc = "MCPForUnity.ServerSrc";
internal const string UseEmbeddedServer = "MCPForUnity.UseEmbeddedServer";
internal const string LockCursorConfig = "MCPForUnity.LockCursorConfig";
internal const string AutoRegisterEnabled = "MCPForUnity.AutoRegisterEnabled";
internal const string ToolEnabledPrefix = "MCPForUnity.ToolEnabled.";
internal const string ToolFoldoutStatePrefix = "MCPForUnity.ToolFoldout.";
internal const string ResourceEnabledPrefix = "MCPForUnity.ResourceEnabled.";
internal const string ResourceFoldoutStatePrefix = "MCPForUnity.ResourceFoldout.";
internal const string EditorWindowActivePanel = "MCPForUnity.EditorWindow.ActivePanel";
internal const string LastSelectedClientId = "MCPForUnity.LastSelectedClientId";
internal const string SetupCompleted = "MCPForUnity.SetupCompleted";
internal const string SetupDismissed = "MCPForUnity.SetupDismissed";
internal const string CustomToolRegistrationEnabled = "MCPForUnity.CustomToolRegistrationEnabled";
internal const string LastUpdateCheck = "MCPForUnity.LastUpdateCheck";
internal const string LatestKnownVersion = "MCPForUnity.LatestKnownVersion";
internal const string LastAssetStoreUpdateCheck = "MCPForUnity.LastAssetStoreUpdateCheck";
internal const string LatestKnownAssetStoreVersion = "MCPForUnity.LatestKnownAssetStoreVersion";
internal const string LastStdIoUpgradeVersion = "MCPForUnity.LastStdIoUpgradeVersion";
internal const string TelemetryDisabled = "MCPForUnity.TelemetryDisabled";
internal const string CustomerUuid = "MCPForUnity.CustomerUUID";
internal const string ApiKey = "MCPForUnity.ApiKey";
internal const string AutoStartOnLoad = "MCPForUnity.AutoStartOnLoad";
internal const string BatchExecuteMaxCommands = "MCPForUnity.BatchExecute.MaxCommands";
internal const string LogRecordEnabled = "MCPForUnity.LogRecordEnabled";
internal const string ExecuteCodeCompiler = "MCPForUnity.ExecuteCode.Compiler";
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7317786cfb9304b0db20ca73a774b9fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,14 @@
namespace MCPForUnity.Editor.Constants
{
/// <summary>
/// Constants for health check status values.
/// Used for coordinating health state between Connection and Advanced sections.
/// </summary>
public static class HealthStatus
{
public const string Unknown = "Unknown";
public const string Healthy = "Healthy";
public const string PingFailed = "Ping Failed";
public const string Unhealthy = "Unhealthy";
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c15ed2426f43860479f1b8a99a343d16
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 221a4d6e595be6897a5b17b77aedd4d0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Dependencies.PlatformDetectors;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Dependencies
{
/// <summary>
/// Main orchestrator for dependency validation and management
/// </summary>
public static class DependencyManager
{
private static readonly List<IPlatformDetector> _detectors = new List<IPlatformDetector>
{
new WindowsPlatformDetector(),
new MacOSPlatformDetector(),
new LinuxPlatformDetector()
};
private static IPlatformDetector _currentDetector;
/// <summary>
/// Get the platform detector for the current operating system
/// </summary>
public static IPlatformDetector GetCurrentPlatformDetector()
{
if (_currentDetector == null)
{
_currentDetector = _detectors.FirstOrDefault(d => d.CanDetect);
if (_currentDetector == null)
{
throw new PlatformNotSupportedException($"No detector available for current platform: {RuntimeInformation.OSDescription}");
}
}
return _currentDetector;
}
/// <summary>
/// Perform a comprehensive dependency check
/// </summary>
public static DependencyCheckResult CheckAllDependencies()
{
var result = new DependencyCheckResult();
try
{
var detector = GetCurrentPlatformDetector();
McpLog.Info($"Checking dependencies on {detector.PlatformName}...", always: false);
// Check Python
var pythonStatus = detector.DetectPython();
result.Dependencies.Add(pythonStatus);
// Check uv
var uvStatus = detector.DetectUv();
result.Dependencies.Add(uvStatus);
// Generate summary and recommendations
result.GenerateSummary();
GenerateRecommendations(result, detector);
McpLog.Info($"Dependency check completed. System ready: {result.IsSystemReady}", always: false);
}
catch (Exception ex)
{
McpLog.Error($"Error during dependency check: {ex.Message}");
result.Summary = $"Dependency check failed: {ex.Message}";
result.IsSystemReady = false;
}
return result;
}
/// <summary>
/// Get installation recommendations for the current platform
/// </summary>
public static string GetInstallationRecommendations()
{
try
{
var detector = GetCurrentPlatformDetector();
return detector.GetInstallationRecommendations();
}
catch (Exception ex)
{
return $"Error getting installation recommendations: {ex.Message}";
}
}
/// <summary>
/// Get platform-specific installation URLs
/// </summary>
public static (string pythonUrl, string uvUrl) GetInstallationUrls()
{
try
{
var detector = GetCurrentPlatformDetector();
return (detector.GetPythonInstallUrl(), detector.GetUvInstallUrl());
}
catch
{
return ("https://python.org/downloads/", "https://docs.astral.sh/uv/getting-started/installation/");
}
}
private static void GenerateRecommendations(DependencyCheckResult result, IPlatformDetector detector)
{
var missing = result.GetMissingDependencies();
if (missing.Count == 0)
{
result.RecommendedActions.Add("All dependencies are available. You can start using MCP for Unity.");
return;
}
foreach (var dep in missing)
{
if (dep.Name == "Python")
{
result.RecommendedActions.Add($"Install Python 3.10+ from: {detector.GetPythonInstallUrl()}");
}
else if (dep.Name == "uv Package Manager")
{
result.RecommendedActions.Add($"Install uv package manager from: {detector.GetUvInstallUrl()}");
}
else if (dep.Name == "MCP Server")
{
result.RecommendedActions.Add("MCP Server will be installed automatically when needed.");
}
}
if (result.GetMissingRequired().Count > 0)
{
result.RecommendedActions.Add("Use the Setup Window (Window > MCP for Unity > Local Setup Window) for guided installation.");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4a6d2236d370b4f1db4d0e3d3ce0dcac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4c0f2e87395b4c6c9df8c21b6d0fae13
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MCPForUnity.Editor.Dependencies.Models
{
/// <summary>
/// Result of a comprehensive dependency check
/// </summary>
[Serializable]
public class DependencyCheckResult
{
/// <summary>
/// List of all dependency statuses checked
/// </summary>
public List<DependencyStatus> Dependencies { get; set; }
/// <summary>
/// Overall system readiness for MCP operations
/// </summary>
public bool IsSystemReady { get; set; }
/// <summary>
/// Whether all required dependencies are available
/// </summary>
public bool AllRequiredAvailable => Dependencies?.Where(d => d.IsRequired).All(d => d.IsAvailable) ?? false;
/// <summary>
/// Whether any optional dependencies are missing
/// </summary>
public bool HasMissingOptional => Dependencies?.Where(d => !d.IsRequired).Any(d => !d.IsAvailable) ?? false;
/// <summary>
/// Summary message about the dependency state
/// </summary>
public string Summary { get; set; }
/// <summary>
/// Recommended next steps for the user
/// </summary>
public List<string> RecommendedActions { get; set; }
/// <summary>
/// Timestamp when this check was performed
/// </summary>
public DateTime CheckedAt { get; set; }
public DependencyCheckResult()
{
Dependencies = new List<DependencyStatus>();
RecommendedActions = new List<string>();
CheckedAt = DateTime.UtcNow;
}
/// <summary>
/// Get dependencies by availability status
/// </summary>
public List<DependencyStatus> GetMissingDependencies()
{
return Dependencies?.Where(d => !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
}
/// <summary>
/// Get missing required dependencies
/// </summary>
public List<DependencyStatus> GetMissingRequired()
{
return Dependencies?.Where(d => d.IsRequired && !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
}
/// <summary>
/// Generate a user-friendly summary of the dependency state
/// </summary>
public void GenerateSummary()
{
var missing = GetMissingDependencies();
var missingRequired = GetMissingRequired();
if (missing.Count == 0)
{
Summary = "All dependencies are available and ready.";
IsSystemReady = true;
}
else if (missingRequired.Count == 0)
{
Summary = $"System is ready. {missing.Count} optional dependencies are missing.";
IsSystemReady = true;
}
else
{
Summary = $"System is not ready. {missingRequired.Count} required dependencies are missing.";
IsSystemReady = false;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f6df82faa423f4e9ebb13a3dcee8ba19
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,65 @@
using System;
namespace MCPForUnity.Editor.Dependencies.Models
{
/// <summary>
/// Represents the status of a dependency check
/// </summary>
[Serializable]
public class DependencyStatus
{
/// <summary>
/// Name of the dependency being checked
/// </summary>
public string Name { get; set; }
/// <summary>
/// Whether the dependency is available and functional
/// </summary>
public bool IsAvailable { get; set; }
/// <summary>
/// Version information if available
/// </summary>
public string Version { get; set; }
/// <summary>
/// Path to the dependency executable/installation
/// </summary>
public string Path { get; set; }
/// <summary>
/// Additional details about the dependency status
/// </summary>
public string Details { get; set; }
/// <summary>
/// Error message if dependency check failed
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// Whether this dependency is required for basic functionality
/// </summary>
public bool IsRequired { get; set; }
/// <summary>
/// Suggested installation method or URL
/// </summary>
public string InstallationHint { get; set; }
public DependencyStatus(string name, bool isRequired = true)
{
Name = name;
IsRequired = isRequired;
IsAvailable = false;
}
public override string ToString()
{
var status = IsAvailable ? "✓" : "✗";
var version = !string.IsNullOrEmpty(Version) ? $" ({Version})" : "";
return $"{status} {Name}{version}";
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ddeeeca2f876f4083a84417404175199
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bdbaced669d14798a4ceeebfbff2b22c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,45 @@
using MCPForUnity.Editor.Dependencies.Models;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
/// <summary>
/// Interface for platform-specific dependency detection
/// </summary>
public interface IPlatformDetector
{
/// <summary>
/// Platform name this detector handles
/// </summary>
string PlatformName { get; }
/// <summary>
/// Whether this detector can run on the current platform
/// </summary>
bool CanDetect { get; }
/// <summary>
/// Detect Python installation on this platform
/// </summary>
DependencyStatus DetectPython();
/// <summary>
/// Detect uv package manager on this platform
/// </summary>
DependencyStatus DetectUv();
/// <summary>
/// Get platform-specific installation recommendations
/// </summary>
string GetInstallationRecommendations();
/// <summary>
/// Get platform-specific Python installation URL
/// </summary>
string GetPythonInstallUrl();
/// <summary>
/// Get platform-specific uv installation URL
/// </summary>
string GetUvInstallUrl();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 67d73d0e8caef4e60942f4419c6b76bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,207 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
/// <summary>
/// Linux-specific dependency detection
/// </summary>
public class LinuxPlatformDetector : PlatformDetectorBase
{
public override string PlatformName => "Linux";
public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
public override DependencyStatus DetectPython()
{
var status = new DependencyStatus("Python", isRequired: true)
{
InstallationHint = GetPythonInstallUrl()
};
try
{
// Try running python directly first
if (TryValidatePython("python3", out string version, out string fullPath) ||
TryValidatePython("python", out version, out fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} in PATH";
return status;
}
// Fallback: try 'which' command
if (TryFindInPath("python3", out string pathResult) ||
TryFindInPath("python", out pathResult))
{
if (TryValidatePython(pathResult, out version, out fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} in PATH";
return status;
}
}
status.ErrorMessage = "Python not found in PATH";
status.Details = "Install Python 3.10+ and ensure it's added to PATH.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting Python: {ex.Message}";
}
return status;
}
public override string GetPythonInstallUrl()
{
return "https://www.python.org/downloads/source/";
}
public override string GetUvInstallUrl()
{
return "https://docs.astral.sh/uv/getting-started/installation/#linux";
}
public override string GetInstallationRecommendations()
{
return @"Linux Installation Recommendations:
1. Python: Install via package manager or pyenv
- Ubuntu/Debian: sudo apt install python3 python3-pip
- Fedora/RHEL: sudo dnf install python3 python3-pip
- Arch: sudo pacman -S python python-pip
- Or use pyenv: https://github.com/pyenv/pyenv
2. uv Package Manager: Install via curl
- Run: curl -LsSf https://astral.sh/uv/install.sh | sh
- Or download from: https://github.com/astral-sh/uv/releases
3. MCP Server: Will be installed automatically by MCP for Unity
Note: Make sure ~/.local/bin is in your PATH for user-local installations.";
}
public override DependencyStatus DetectUv()
{
// First, honor overrides and cross-platform resolution via the base implementation
var status = base.DetectUv();
if (status.IsAvailable)
{
return status;
}
// If the user configured an override path but fallback was not used, keep the base result
// (failure typically means the override path is invalid and no system fallback found)
if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback)
{
return status;
}
try
{
string augmentedPath = BuildAugmentedPath();
// Try uv first, then uvx, using ExecPath.TryRun for proper timeout handling
if (TryValidateUvWithPath("uv", augmentedPath, out string version, out string fullPath) ||
TryValidateUvWithPath("uvx", augmentedPath, out version, out fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found uv {version} in PATH";
status.ErrorMessage = null;
return status;
}
status.ErrorMessage = "uv not found in PATH";
status.Details = "Install uv package manager and ensure it's added to PATH.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting uv: {ex.Message}";
}
return status;
}
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
string augmentedPath = BuildAugmentedPath();
// First, try to resolve the absolute path for better UI/logging display
string commandToRun = pythonPath;
if (TryFindInPath(pythonPath, out string resolvedPath))
{
commandToRun = resolvedPath;
}
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr,
5000, augmentedPath))
return false;
// Check stdout first, then stderr (some Python distributions output to stderr)
string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim();
if (output.StartsWith("Python "))
{
version = output.Substring(7);
fullPath = commandToRun;
if (TryParseVersion(version, out var major, out var minor))
{
return major > 3 || (major == 3 && minor >= 10);
}
}
}
catch
{
// Ignore validation errors
}
return false;
}
protected string BuildAugmentedPath()
{
var additions = GetPathAdditions();
if (additions.Length == 0) return null;
// Only return the additions - ExecPath.TryRun will prepend to existing PATH
return string.Join(Path.PathSeparator, additions);
}
private string[] GetPathAdditions()
{
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return new[]
{
"/usr/local/bin",
"/usr/bin",
"/bin",
"/snap/bin",
Path.Combine(homeDir, ".local", "bin")
};
}
protected override bool TryFindInPath(string executable, out string fullPath)
{
fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath());
return !string.IsNullOrEmpty(fullPath);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b682b492eb80d4ed6834b76f72c9f0f3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,206 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
/// <summary>
/// macOS-specific dependency detection
/// </summary>
public class MacOSPlatformDetector : PlatformDetectorBase
{
public override string PlatformName => "macOS";
public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
public override DependencyStatus DetectPython()
{
var status = new DependencyStatus("Python", isRequired: true)
{
InstallationHint = GetPythonInstallUrl()
};
try
{
// 1. Try 'which' command with augmented PATH (prioritizing Homebrew)
if (TryFindInPath("python3", out string pathResult) ||
TryFindInPath("python", out pathResult))
{
if (TryValidatePython(pathResult, out string version, out string fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} at {fullPath}";
return status;
}
}
// 2. Fallback: Try running python directly from PATH
if (TryValidatePython("python3", out string v, out string p) ||
TryValidatePython("python", out v, out p))
{
status.IsAvailable = true;
status.Version = v;
status.Path = p;
status.Details = $"Found Python {v} in PATH";
return status;
}
status.ErrorMessage = "Python not found in PATH or standard locations";
status.Details = "Install Python 3.10+ via Homebrew ('brew install python3') and ensure it's in your PATH.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting Python: {ex.Message}";
}
return status;
}
public override string GetPythonInstallUrl()
{
return "https://www.python.org/downloads/macos/";
}
public override string GetUvInstallUrl()
{
return "https://docs.astral.sh/uv/getting-started/installation/#macos";
}
public override string GetInstallationRecommendations()
{
return @"macOS Installation Recommendations:
1. Python: Install via Homebrew (recommended) or python.org
- Homebrew: brew install python3
- Direct download: https://python.org/downloads/macos/
2. uv Package Manager: Install via curl or Homebrew
- Curl: curl -LsSf https://astral.sh/uv/install.sh | sh
- Homebrew: brew install uv
3. MCP Server: Will be installed automatically by MCP for Unity Bridge
Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH.";
}
public override DependencyStatus DetectUv()
{
// First, honor overrides and cross-platform resolution via the base implementation
var status = base.DetectUv();
if (status.IsAvailable)
{
return status;
}
// If the user configured an override path but fallback was not used, keep the base result
// (failure typically means the override path is invalid and no system fallback found)
if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback)
{
return status;
}
try
{
string augmentedPath = BuildAugmentedPath();
// Try uv first, then uvx, using ExecPath.TryRun for proper timeout handling
if (TryValidateUvWithPath("uv", augmentedPath, out string version, out string fullPath) ||
TryValidateUvWithPath("uvx", augmentedPath, out version, out fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found uv {version} in PATH";
status.ErrorMessage = null;
return status;
}
status.ErrorMessage = "uv not found in PATH";
status.Details = "Install uv package manager and ensure it's added to PATH.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting uv: {ex.Message}";
}
return status;
}
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
string augmentedPath = BuildAugmentedPath();
// First, try to resolve the absolute path for better UI/logging display
string commandToRun = pythonPath;
if (TryFindInPath(pythonPath, out string resolvedPath))
{
commandToRun = resolvedPath;
}
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr,
5000, augmentedPath))
return false;
// Check stdout first, then stderr (some Python distributions output to stderr)
string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim();
if (output.StartsWith("Python "))
{
version = output.Substring(7);
fullPath = commandToRun;
if (TryParseVersion(version, out var major, out var minor))
{
return major > 3 || (major == 3 && minor >= 10);
}
}
}
catch
{
// Ignore validation errors
}
return false;
}
protected string BuildAugmentedPath()
{
var additions = GetPathAdditions();
if (additions.Length == 0) return null;
// Only return the additions - ExecPath.TryRun will prepend to existing PATH
return string.Join(Path.PathSeparator, additions);
}
private string[] GetPathAdditions()
{
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return new[]
{
Path.Combine(homeDir, ".pyenv", "shims"), // pyenv: Python/uv when Unity is launched from Dock/Spotlight
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
"/bin",
Path.Combine(homeDir, ".local", "bin")
};
}
protected override bool TryFindInPath(string executable, out string fullPath)
{
fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath());
return !string.IsNullOrEmpty(fullPath);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c6f602b0a8ca848859197f9a949a7a5d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,137 @@
using System;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
/// <summary>
/// Base class for platform-specific dependency detection
/// </summary>
public abstract class PlatformDetectorBase : IPlatformDetector
{
public abstract string PlatformName { get; }
public abstract bool CanDetect { get; }
public abstract DependencyStatus DetectPython();
public abstract string GetPythonInstallUrl();
public abstract string GetUvInstallUrl();
public abstract string GetInstallationRecommendations();
public virtual DependencyStatus DetectUv()
{
var status = new DependencyStatus("uv Package Manager", isRequired: true)
{
InstallationHint = GetUvInstallUrl()
};
try
{
// Get uv path from PathResolverService (respects override)
string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
// Verify uv executable and get version
if (MCPServiceLocator.Paths.TryValidateUvxExecutable(uvxPath, out string version))
{
status.IsAvailable = true;
status.Version = version;
status.Path = uvxPath;
// Check if we used fallback from override to system path
if (MCPServiceLocator.Paths.HasUvxPathFallback)
{
status.Details = $"Found uv {version} (fallback to system path)";
status.ErrorMessage = "Override path not found, using system path";
}
else
{
status.Details = MCPServiceLocator.Paths.HasUvxPathOverride
? $"Found uv {version} (override path)"
: $"Found uv {version} in system path";
}
return status;
}
status.ErrorMessage = "uvx not found";
status.Details = "Install uv package manager or configure path override in Advanced Settings.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting uvx: {ex.Message}";
}
return status;
}
protected bool TryParseVersion(string version, out int major, out int minor)
{
major = 0;
minor = 0;
try
{
var parts = version.Split('.');
if (parts.Length >= 2)
{
return int.TryParse(parts[0], out major) && int.TryParse(parts[1], out minor);
}
}
catch
{
// Ignore parsing errors
}
return false;
}
// In PlatformDetectorBase.cs
protected bool TryValidateUvWithPath(string command, string augmentedPath, out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
string commandToRun = command;
if (TryFindInPath(command, out string resolvedPath))
{
commandToRun = resolvedPath;
}
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr,
5000, augmentedPath))
return false;
string output = string.IsNullOrWhiteSpace(stdout) ? stderr.Trim() : stdout.Trim();
if (output.StartsWith("uvx ") || output.StartsWith("uv "))
{
int spaceIndex = output.IndexOf(' ');
if (spaceIndex >= 0)
{
var remainder = output.Substring(spaceIndex + 1).Trim();
int nextSpace = remainder.IndexOf(' ');
int parenIndex = remainder.IndexOf('(');
int endIndex = Math.Min(
nextSpace >= 0 ? nextSpace : int.MaxValue,
parenIndex >= 0 ? parenIndex : int.MaxValue
);
version = endIndex < int.MaxValue ? remainder.Substring(0, endIndex).Trim() : remainder;
fullPath = commandToRun;
return true;
}
}
}
catch
{
// Ignore validation errors
}
return false;
}
// Add abstract method for subclasses to implement
protected abstract bool TryFindInPath(string executable, out string fullPath);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 44d715aedea2b8b41bf914433bbb2c49
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,297 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
/// <summary>
/// Windows-specific dependency detection
/// </summary>
public class WindowsPlatformDetector : PlatformDetectorBase
{
public override string PlatformName => "Windows";
public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public override DependencyStatus DetectPython()
{
var status = new DependencyStatus("Python", isRequired: true)
{
InstallationHint = GetPythonInstallUrl()
};
try
{
// Try running python directly first (works with Windows App Execution Aliases)
if (TryValidatePython("python3.exe", out string version, out string fullPath) ||
TryValidatePython("python.exe", out version, out fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} in PATH";
return status;
}
// Fallback: try 'where' command
if (TryFindInPath("python3.exe", out string pathResult) ||
TryFindInPath("python.exe", out pathResult))
{
if (TryValidatePython(pathResult, out version, out fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} in PATH";
return status;
}
}
// Fallback: try to find python via uv
if (TryFindPythonViaUv(out version, out fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} via uv";
return status;
}
status.ErrorMessage = "Python not found in PATH";
status.Details = "Install Python 3.10+ and ensure it's added to PATH.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting Python: {ex.Message}";
}
return status;
}
public override string GetPythonInstallUrl()
{
return "https://apps.microsoft.com/store/detail/python-313/9NCVDN91XZQP";
}
public override string GetUvInstallUrl()
{
return "https://docs.astral.sh/uv/getting-started/installation/#windows";
}
public override string GetInstallationRecommendations()
{
return @"Windows Installation Recommendations:
1. Python: Install from Microsoft Store or python.org
- Microsoft Store: Search for 'Python 3.10' or higher
- Direct download: https://python.org/downloads/windows/
2. uv Package Manager: Install via PowerShell
- Run: powershell -ExecutionPolicy ByPass -c ""irm https://astral.sh/uv/install.ps1 | iex""
- Or download from: https://github.com/astral-sh/uv/releases
3. MCP Server: Will be installed automatically by MCP for Unity Bridge";
}
public override DependencyStatus DetectUv()
{
// First, honor overrides and cross-platform resolution via the base implementation
var status = base.DetectUv();
if (status.IsAvailable)
{
return status;
}
// If the user configured an override path but fallback was not used, keep the base result
// (failure typically means the override path is invalid and no system fallback found)
if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback)
{
return status;
}
try
{
string augmentedPath = BuildAugmentedPath();
// try to find uv
if (TryValidateUvWithPath("uv.exe", augmentedPath, out string uvVersion, out string uvPath))
{
status.IsAvailable = true;
status.Version = uvVersion;
status.Path = uvPath;
status.Details = $"Found uv {uvVersion} at {uvPath}";
return status;
}
// try to find uvx
if (TryValidateUvWithPath("uvx.exe", augmentedPath, out string uvxVersion, out string uvxPath))
{
status.IsAvailable = true;
status.Version = uvxVersion;
status.Path = uvxPath;
status.Details = $"Found uvx {uvxVersion} at {uvxPath} (fallback)";
return status;
}
status.ErrorMessage = "uv not found in PATH";
status.Details = "Install uv package manager and ensure it's added to PATH.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting uv: {ex.Message}";
}
return status;
}
private bool TryFindPythonViaUv(out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
string augmentedPath = BuildAugmentedPath();
// Try to list installed python versions via uvx
if (!ExecPath.TryRun("uv", "python list", null, out string stdout, out string stderr, 5000, augmentedPath))
return false;
var lines = stdout.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.Contains("<download available>")) continue;
var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 2)
{
string potentialPath = parts[parts.Length - 1];
if (File.Exists(potentialPath) &&
(potentialPath.EndsWith("python.exe") || potentialPath.EndsWith("python3.exe")))
{
if (TryValidatePython(potentialPath, out version, out fullPath))
{
return true;
}
}
}
}
}
catch
{
// Ignore errors if uv is not installed or fails
}
return false;
}
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
string augmentedPath = BuildAugmentedPath();
// First, try to resolve the absolute path for better UI/logging display
string commandToRun = pythonPath;
if (TryFindInPath(pythonPath, out string resolvedPath))
{
commandToRun = resolvedPath;
}
// Run 'python --version' to get the version
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr, 5000, augmentedPath))
return false;
// Check stdout first, then stderr (some Python distributions output to stderr)
string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim();
if (output.StartsWith("Python "))
{
version = output.Substring(7);
fullPath = commandToRun;
if (TryParseVersion(version, out var major, out var minor))
{
return major > 3 || (major == 3 && minor >= 10);
}
}
}
catch
{
// Ignore validation errors
}
return false;
}
protected override bool TryFindInPath(string executable, out string fullPath)
{
fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath());
return !string.IsNullOrEmpty(fullPath);
}
protected string BuildAugmentedPath()
{
var additions = GetPathAdditions();
if (additions.Length == 0) return null;
// Only return the additions - ExecPath.TryRun will prepend to existing PATH
return string.Join(Path.PathSeparator, additions);
}
private string[] GetPathAdditions()
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var additions = new List<string>();
// uv common installation paths
if (!string.IsNullOrEmpty(localAppData))
additions.Add(Path.Combine(localAppData, "Programs", "uv"));
if (!string.IsNullOrEmpty(programFiles))
additions.Add(Path.Combine(programFiles, "uv"));
// npm global paths
if (!string.IsNullOrEmpty(appData))
additions.Add(Path.Combine(appData, "npm"));
if (!string.IsNullOrEmpty(localAppData))
additions.Add(Path.Combine(localAppData, "npm"));
// Python common paths
if (!string.IsNullOrEmpty(localAppData))
additions.Add(Path.Combine(localAppData, "Programs", "Python"));
// Instead of hardcoded versions, enumerate existing directories
if (!string.IsNullOrEmpty(programFiles))
{
try
{
var pythonDirs = Directory.GetDirectories(programFiles, "Python3*")
.OrderByDescending(d => d); // Newest first
foreach (var dir in pythonDirs)
{
additions.Add(dir);
}
}
catch { /* Ignore if directory doesn't exist */ }
}
// User scripts
if (!string.IsNullOrEmpty(homeDir))
additions.Add(Path.Combine(homeDir, ".local", "bin"));
return additions.ToArray();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1aedc29caa5704c07b487d20a27e9334
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c11944bcfb9ec4576bab52874b7df584
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ea652131dcdaa44ca8cb35cd1191be3f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 94cb070dc5e15024da86150b27699ca0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,668 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Services;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Provides common utility methods for working with Unity asset paths.
/// </summary>
public static class AssetPathUtility
{
/// <summary>
/// Normalizes path separators to forward slashes without modifying the path structure.
/// Use this for non-asset paths (e.g., file system paths, relative directories).
/// </summary>
public static string NormalizeSeparators(string path)
{
if (string.IsNullOrEmpty(path))
return path;
return path.Replace('\\', '/');
}
/// <summary>
/// Normalizes a Unity asset path by ensuring forward slashes are used and that it is rooted under "Assets/".
/// Also protects against path traversal attacks using "../" sequences.
/// </summary>
public static string SanitizeAssetPath(string path)
{
if (string.IsNullOrEmpty(path))
{
return path;
}
path = NormalizeSeparators(path);
// Check for path traversal sequences
if (path.Contains(".."))
{
McpLog.Warn($"[AssetPathUtility] Path contains potential traversal sequence: '{path}'");
return null;
}
// Ensure path starts with Assets/
if (string.Equals(path, "Assets", StringComparison.OrdinalIgnoreCase))
{
return "Assets";
}
if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
return "Assets/" + path.TrimStart('/');
}
return path;
}
/// <summary>
/// Checks if a given asset path is valid and safe (no traversal, within Assets folder).
/// </summary>
/// <returns>True if the path is valid, false otherwise.</returns>
public static bool IsValidAssetPath(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
// Normalize for comparison
string normalized = NormalizeSeparators(path);
// Must start with Assets/
if (!normalized.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Must not contain traversal sequences
if (normalized.Contains(".."))
{
return false;
}
// Must not contain invalid path characters
char[] invalidChars = { ':', '*', '?', '"', '<', '>', '|' };
foreach (char c in invalidChars)
{
if (normalized.IndexOf(c) >= 0)
{
return false;
}
}
return true;
}
/// <summary>
/// Gets the MCP for Unity package root path.
/// Works for registry Package Manager, local Package Manager, and Asset Store installations.
/// </summary>
/// <returns>The package root path (virtual for PM, absolute for Asset Store), or null if not found</returns>
public static string GetMcpPackageRootPath()
{
try
{
// Try Package Manager first (registry and local installs)
var packageInfo = PackageInfo.FindForAssembly(typeof(AssetPathUtility).Assembly);
if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.assetPath))
{
return packageInfo.assetPath;
}
// Fallback to AssetDatabase for Asset Store installs (Assets/MCPForUnity)
string[] guids = AssetDatabase.FindAssets($"t:Script {nameof(AssetPathUtility)}");
if (guids.Length == 0)
{
McpLog.Warn("Could not find AssetPathUtility script in AssetDatabase");
return null;
}
string scriptPath = AssetDatabase.GUIDToAssetPath(guids[0]);
// Script is at: {packageRoot}/Editor/Helpers/AssetPathUtility.cs
// Extract {packageRoot}
int editorIndex = scriptPath.LastIndexOf("/Editor/", StringComparison.Ordinal);
if (editorIndex >= 0)
{
return scriptPath.Substring(0, editorIndex);
}
McpLog.Warn($"Could not determine package root from script path: {scriptPath}");
return null;
}
catch (Exception ex)
{
McpLog.Error($"Failed to get package root path: {ex.Message}");
return null;
}
}
/// <summary>
/// Reads and parses the package.json file for MCP for Unity.
/// Handles both Package Manager (registry/local) and Asset Store installations.
/// </summary>
/// <returns>JObject containing package.json data, or null if not found or parse failed</returns>
public static JObject GetPackageJson()
{
try
{
string packageRoot = GetMcpPackageRootPath();
if (string.IsNullOrEmpty(packageRoot))
{
return null;
}
string packageJsonPath = Path.Combine(packageRoot, "package.json");
// Convert virtual asset path to file system path
if (packageRoot.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase))
{
// Package Manager install - must use PackageInfo.resolvedPath
// Virtual paths like "Packages/..." don't work with File.Exists()
// Registry packages live in Library/PackageCache/package@version/
var packageInfo = PackageInfo.FindForAssembly(typeof(AssetPathUtility).Assembly);
if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.resolvedPath))
{
packageJsonPath = Path.Combine(packageInfo.resolvedPath, "package.json");
}
else
{
McpLog.Warn("Could not resolve Package Manager path for package.json");
return null;
}
}
else if (packageRoot.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
// Asset Store install - convert to absolute file system path
// Application.dataPath is the absolute path to the Assets folder
string relativePath = packageRoot.Substring("Assets/".Length);
packageJsonPath = Path.Combine(Application.dataPath, relativePath, "package.json");
}
if (!File.Exists(packageJsonPath))
{
McpLog.Warn($"package.json not found at: {packageJsonPath}");
return null;
}
string json = File.ReadAllText(packageJsonPath);
return JObject.Parse(json);
}
catch (Exception ex)
{
McpLog.Warn($"Failed to read or parse package.json: {ex.Message}");
return null;
}
}
/// <summary>
/// Gets the package source for the MCP server (used with uvx --from).
/// Checks for EditorPrefs override first (supports git URLs, file:// paths, etc.),
/// then falls back to PyPI package reference.
/// When the override is a local path, auto-corrects to the "Server" subdirectory
/// if the path doesn't contain pyproject.toml but Server/pyproject.toml exists.
/// </summary>
/// <returns>Package source string for uvx --from argument</returns>
public static string GetMcpServerPackageSource()
{
// Check for override first (supports git URLs, file:// paths, local paths)
string sourceOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
if (!string.IsNullOrEmpty(sourceOverride))
{
string resolved = ResolveLocalServerPath(sourceOverride);
// Persist the corrected path so future reads are consistent
if (resolved != sourceOverride)
{
EditorPrefs.SetString(EditorPrefKeys.GitUrlOverride, resolved);
McpLog.Info($"Auto-corrected server source override from '{sourceOverride}' to '{resolved}'");
}
return resolved;
}
// Default to PyPI package (avoids Windows long path issues with git clone)
string version = GetPackageVersion();
if (version == "unknown")
{
// Fall back to latest PyPI version so configs remain valid in test scenarios
return "mcpforunityserver";
}
// Package.json uses semver prerelease tags (e.g., 9.4.5-beta.1) that are not valid
// PEP 440 pins for uvx. Use the beta prerelease range instead of a pinned prerelease.
if (IsSemVerPreRelease(version))
{
return "mcpforunityserver>=0.0.0a0";
}
return $"mcpforunityserver=={version}";
}
/// <summary>
/// Validates and auto-corrects a local server source path to ensure it points to the
/// directory containing pyproject.toml. If the path points to a parent directory
/// (e.g. the repo root "unity-mcp") instead of the Python package directory ("Server"),
/// this checks for a "Server" subdirectory with pyproject.toml and returns that path.
/// Non-local paths (URLs, PyPI references) are returned unchanged.
/// </summary>
internal static string ResolveLocalServerPath(string path)
{
if (string.IsNullOrEmpty(path))
return path;
// Skip non-local paths (git URLs, PyPI package names, etc.)
if (path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("git+", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("ssh://", StringComparison.OrdinalIgnoreCase))
{
return path;
}
// If it looks like a PyPI package reference (no path separators), skip
if (!path.Contains('/') && !path.Contains('\\') && !path.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
{
return path;
}
// Strip file:// prefix for filesystem checks, preserve for return value
string checkPath = path;
string prefix = string.Empty;
if (checkPath.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
prefix = checkPath.Substring(0, 7); // preserve original casing
checkPath = checkPath.Substring(7);
}
// Already correct — pyproject.toml exists at this path
if (System.IO.File.Exists(System.IO.Path.Combine(checkPath, "pyproject.toml")))
{
return path;
}
// Check if "Server" subdirectory contains pyproject.toml
string serverSubDir = System.IO.Path.Combine(checkPath, "Server");
if (System.IO.File.Exists(System.IO.Path.Combine(serverSubDir, "pyproject.toml")))
{
return prefix + serverSubDir;
}
// Return as-is; uvx will report the error if the path is truly invalid
return path;
}
/// <summary>
/// Deprecated: Use GetMcpServerPackageSource() instead.
/// Kept for backwards compatibility.
/// </summary>
[System.Obsolete("Use GetMcpServerPackageSource() instead")]
public static string GetMcpServerGitUrl() => GetMcpServerPackageSource();
/// <summary>
/// Gets structured uvx command parts for different client configurations
/// </summary>
/// <returns>Tuple containing (uvxPath, fromUrl, packageName)</returns>
public static (string uvxPath, string fromUrl, string packageName) GetUvxCommandParts()
{
string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
string fromUrl = GetMcpServerPackageSource();
string packageName = "mcp-for-unity";
return (uvxPath, fromUrl, packageName);
}
/// <summary>
/// Builds the uvx package source arguments for the MCP server.
/// Handles prerelease package mode (prerelease from PyPI) vs stable mode (pinned version or override).
/// Centralizes the prerelease logic to avoid duplication between HTTP and stdio transports.
/// Priority: explicit fromUrl override > package-version-driven prerelease mode > stable pinned package.
/// NOTE: This overload reads from EditorPrefs/cache and MUST be called from the main thread.
/// For background threads, use the overload that accepts pre-captured parameters.
/// </summary>
/// <param name="quoteFromPath">Whether to quote the --from path (needed for command-line strings, not for arg lists)</param>
/// <returns>The package source arguments (e.g., "--prerelease explicit --from mcpforunityserver>=0.0.0a0")</returns>
public static string GetBetaServerFromArgs(bool quoteFromPath = false)
{
string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
string packageSource = GetMcpServerPackageSource();
return GetBetaServerFromArgs(gitUrlOverride, packageSource, quoteFromPath);
}
/// <summary>
/// Thread-safe overload that accepts pre-captured values.
/// Use this when calling from background threads.
/// </summary>
/// <param name="gitUrlOverride">Pre-captured value from EditorPrefs GitUrlOverride</param>
/// <param name="packageSource">Pre-captured value from GetMcpServerPackageSource()</param>
/// <param name="quoteFromPath">Whether to quote the --from path</param>
public static string GetBetaServerFromArgs(string gitUrlOverride, string packageSource, bool quoteFromPath = false)
{
// Explicit override (local path, git URL, etc.) always wins
if (!string.IsNullOrEmpty(gitUrlOverride))
{
string fromValue = quoteFromPath ? $"\"{gitUrlOverride}\"" : gitUrlOverride;
return $"--from {fromValue}";
}
bool usePrereleaseRange = string.Equals(packageSource, "mcpforunityserver>=0.0.0a0", StringComparison.OrdinalIgnoreCase);
// Prerelease package mode: use prerelease from PyPI.
if (usePrereleaseRange)
{
// Use --prerelease explicit with version specifier to only get prereleases of our package,
// not of dependencies (which can be broken on PyPI).
string fromValue = quoteFromPath ? "\"mcpforunityserver>=0.0.0a0\"" : "mcpforunityserver>=0.0.0a0";
return $"--prerelease explicit --from {fromValue}";
}
// Standard mode: use pinned version from package.json
if (!string.IsNullOrEmpty(packageSource))
{
string fromValue = quoteFromPath ? $"\"{packageSource}\"" : packageSource;
return $"--from {fromValue}";
}
return string.Empty;
}
/// <summary>
/// Builds the uvx package source arguments as a list (for JSON config builders).
/// Priority: explicit fromUrl override > package-version-driven prerelease mode > stable pinned package.
/// NOTE: This overload reads from EditorPrefs/cache and MUST be called from the main thread.
/// For background threads, use the overload that accepts pre-captured parameters.
/// </summary>
/// <returns>List of arguments to add to uvx command</returns>
public static System.Collections.Generic.IList<string> GetBetaServerFromArgsList()
{
string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
string packageSource = GetMcpServerPackageSource();
return GetBetaServerFromArgsList(gitUrlOverride, packageSource);
}
/// <summary>
/// Thread-safe overload that accepts pre-captured values.
/// Use this when calling from background threads.
/// </summary>
/// <param name="gitUrlOverride">Pre-captured value from EditorPrefs GitUrlOverride</param>
/// <param name="packageSource">Pre-captured value from GetMcpServerPackageSource()</param>
public static System.Collections.Generic.IList<string> GetBetaServerFromArgsList(string gitUrlOverride, string packageSource)
{
var args = new System.Collections.Generic.List<string>();
// Explicit override (local path, git URL, etc.) always wins
if (!string.IsNullOrEmpty(gitUrlOverride))
{
args.Add("--from");
args.Add(gitUrlOverride);
return args;
}
bool usePrereleaseRange = string.Equals(packageSource, "mcpforunityserver>=0.0.0a0", StringComparison.OrdinalIgnoreCase);
// Prerelease package mode: use prerelease from PyPI.
if (usePrereleaseRange)
{
args.Add("--prerelease");
args.Add("explicit");
args.Add("--from");
args.Add("mcpforunityserver>=0.0.0a0");
return args;
}
// Standard mode: use pinned version from package.json
if (!string.IsNullOrEmpty(packageSource))
{
args.Add("--from");
args.Add(packageSource);
}
return args;
}
/// <summary>
/// Determines whether uvx should use --no-cache --refresh flags.
/// Returns true if DevModeForceServerRefresh is enabled OR if the server URL is a local path.
/// Local paths (file:// or absolute) always need fresh builds to avoid stale uvx cache.
/// Note: --reinstall is not supported by uvx and will cause a warning.
/// </summary>
public static bool ShouldForceUvxRefresh()
{
bool devForceRefresh = false;
try { devForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); } catch { }
if (devForceRefresh)
return true;
// Auto-enable force refresh when using a local path override.
return IsLocalServerPath();
}
private static bool _offlineCacheResult;
private static double _offlineCacheTimestamp = -999;
private const double OfflineCacheTtlSeconds = 30.0;
/// <summary>
/// Determines whether uvx should use --offline mode for faster startup.
/// Runs a lightweight probe (uvx --offline ... mcp-for-unity --help) with a 3-second timeout
/// to check if the package is already cached. If cached, --offline skips the network
/// dependency check that can hang for 30+ seconds on poor connections.
/// Returns false if force refresh is enabled (new download needed).
/// The result is cached for 30 seconds to avoid redundant subprocess spawns.
/// Must be called on the main thread (reads EditorPrefs).
/// </summary>
public static bool ShouldUseUvxOffline()
{
if (ShouldForceUvxRefresh())
return false;
return GetCachedOfflineProbeResult();
}
private static bool GetCachedOfflineProbeResult()
{
double now = EditorApplication.timeSinceStartup;
if (now - _offlineCacheTimestamp < OfflineCacheTtlSeconds)
return _offlineCacheResult;
bool result = RunOfflineProbe();
_offlineCacheResult = result;
_offlineCacheTimestamp = now;
return result;
}
private static bool RunOfflineProbe()
{
try
{
string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
if (string.IsNullOrEmpty(uvxPath))
return false;
string fromArgs = GetBetaServerFromArgs(quoteFromPath: false);
string probeArgs = string.IsNullOrEmpty(fromArgs)
? "--offline mcp-for-unity --help"
: $"--offline {fromArgs} mcp-for-unity --help";
return ExecPath.TryRun(uvxPath, probeArgs, null, out _, out _, timeoutMs: 3000);
}
catch
{
return false;
}
}
/// <summary>
/// Returns the uvx dev-mode flags as a single string for command-line builders.
/// Returns "--no-cache --refresh " if force refresh is enabled,
/// "--offline " if the cache is warm, or string.Empty otherwise.
/// Must be called on the main thread (reads EditorPrefs).
/// </summary>
public static string GetUvxDevFlags()
{
bool forceRefresh = ShouldForceUvxRefresh();
return GetUvxDevFlags(forceRefresh, !forceRefresh && GetCachedOfflineProbeResult());
}
/// <summary>
/// Returns the uvx dev-mode flags from pre-captured bool values.
/// Use this overload when values were captured on the main thread for background use.
/// </summary>
public static string GetUvxDevFlags(bool forceRefresh, bool useOffline)
{
if (forceRefresh) return "--no-cache --refresh ";
if (useOffline) return "--offline ";
return string.Empty;
}
/// <summary>
/// Returns the uvx dev-mode flags as a list of individual arguments.
/// Suitable for callers that build argument lists (ConfigJsonBuilder, CodexConfigHelper).
/// Must be called on the main thread (reads EditorPrefs).
/// </summary>
public static IReadOnlyList<string> GetUvxDevFlagsList()
{
bool forceRefresh = ShouldForceUvxRefresh();
if (forceRefresh) return new[] { "--no-cache", "--refresh" };
if (GetCachedOfflineProbeResult()) return new[] { "--offline" };
return Array.Empty<string>();
}
/// <summary>
/// Returns true if the server URL is a local path (file:// or absolute path).
/// </summary>
public static bool IsLocalServerPath()
{
string fromUrl = GetMcpServerPackageSource();
if (string.IsNullOrEmpty(fromUrl))
return false;
// Check for file:// protocol or absolute local path
if (fromUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
return true;
try
{
return System.IO.Path.IsPathRooted(fromUrl);
}
catch (System.ArgumentException)
{
// fromUrl contains characters illegal in paths (e.g. a remote URL)
return false;
}
}
/// <summary>
/// Gets the local server path if GitUrlOverride points to a local directory.
/// Returns null if not using a local path.
/// </summary>
public static string GetLocalServerPath()
{
if (!IsLocalServerPath())
return null;
string fromUrl = GetMcpServerPackageSource();
if (fromUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
// Strip file:// prefix
fromUrl = fromUrl.Substring(7);
}
return fromUrl;
}
/// <summary>
/// Cleans stale Python build artifacts from the local server path.
/// This is necessary because Python's build system doesn't remove deleted files from build/,
/// and the auto-discovery mechanism will pick up old .py files causing ghost resources/tools.
/// </summary>
/// <returns>True if cleaning was performed, false if not applicable or failed.</returns>
public static bool CleanLocalServerBuildArtifacts()
{
string localPath = GetLocalServerPath();
if (string.IsNullOrEmpty(localPath))
return false;
// Clean the build/ directory which can contain stale .py files
string buildPath = System.IO.Path.Combine(localPath, "build");
if (System.IO.Directory.Exists(buildPath))
{
try
{
System.IO.Directory.Delete(buildPath, recursive: true);
McpLog.Info($"Cleaned stale build artifacts from: {buildPath}");
return true;
}
catch (Exception ex)
{
McpLog.Warn($"Failed to clean build artifacts: {ex.Message}");
return false;
}
}
return false;
}
/// <summary>
/// Gets the package version from package.json
/// </summary>
/// <returns>Version string, or "unknown" if not found</returns>
public static string GetPackageVersion()
{
try
{
var packageJson = GetPackageJson();
if (packageJson == null)
{
return "unknown";
}
string version = packageJson["version"]?.ToString();
return string.IsNullOrEmpty(version) ? "unknown" : version;
}
catch (Exception ex)
{
McpLog.Warn($"Failed to get package version: {ex.Message}");
return "unknown";
}
}
/// <summary>
/// Returns true if the installed package version is a prerelease (beta, alpha, rc, etc.).
/// Used to auto-enable beta server mode for beta package users.
/// </summary>
public static bool IsPreReleaseVersion()
{
try
{
string version = GetPackageVersion();
if (string.IsNullOrEmpty(version) || version == "unknown")
return false;
return IsSemVerPreRelease(version);
}
catch
{
return false;
}
}
private static bool IsSemVerPreRelease(string version)
{
if (string.IsNullOrEmpty(version))
return false;
// Common semver prerelease indicators:
// e.g., "9.3.0-beta.1", "9.3.0-alpha", "9.3.0-rc.2", "9.3.0-preview"
return version.Contains("-beta", StringComparison.OrdinalIgnoreCase) ||
version.Contains("-alpha", StringComparison.OrdinalIgnoreCase) ||
version.Contains("-rc", StringComparison.OrdinalIgnoreCase) ||
version.Contains("-preview", StringComparison.OrdinalIgnoreCase) ||
version.Contains("-pre", StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1d42f5b5ea5d4d43ad1a771e14bda2a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,316 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Services;
using MCPForUnity.External.Tommy;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Codex CLI specific configuration helpers. Handles TOML snippet
/// generation and lightweight parsing so Codex can join the auto-setup
/// flow alongside JSON-based clients.
/// </summary>
public static class CodexConfigHelper
{
private static void AddUvxModeFlags(TomlArray args)
{
if (args == null) return;
foreach (var flag in AssetPathUtility.GetUvxDevFlagsList())
args.Add(new TomlString { Value = flag });
}
public static string BuildCodexServerBlock(string uvPath)
{
var table = new TomlTable();
var mcpServers = new TomlTable();
var unityMCP = new TomlTable();
// Check transport preference
bool useHttpTransport = EditorPrefs.GetBool(MCPForUnity.Editor.Constants.EditorPrefKeys.UseHttpTransport, true);
if (useHttpTransport)
{
// HTTP mode: Use url field
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
unityMCP["url"] = new TomlString { Value = httpUrl };
// Enable Codex's Rust MCP client for HTTP/SSE transport
EnsureRmcpClientFeature(table);
}
else
{
// Stdio mode: Use command and args
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
unityMCP["command"] = uvxPath;
var args = new TomlArray();
AddUvxModeFlags(args);
// Use centralized helper for beta server / prerelease args
foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList())
{
args.Add(new TomlString { Value = arg });
}
args.Add(new TomlString { Value = packageName });
args.Add(new TomlString { Value = "--transport" });
args.Add(new TomlString { Value = "stdio" });
unityMCP["args"] = args;
// Add Windows-specific environment configuration for stdio mode
var platformService = MCPServiceLocator.Platform;
if (platformService.IsWindows())
{
var envTable = new TomlTable { IsInline = true };
envTable["SystemRoot"] = new TomlString { Value = platformService.GetSystemRoot() };
unityMCP["env"] = envTable;
}
// Allow extra time for uvx to download packages on first run
unityMCP["startup_timeout_sec"] = new TomlInteger { Value = 60 };
}
mcpServers["unityMCP"] = unityMCP;
table["mcp_servers"] = mcpServers;
using var writer = new StringWriter();
table.WriteTo(writer);
return writer.ToString();
}
public static string UpsertCodexServerBlock(string existingToml, string uvPath)
{
// Parse existing TOML or create new root table
var root = TryParseToml(existingToml) ?? new TomlTable();
bool useHttpTransport = EditorPrefs.GetBool(MCPForUnity.Editor.Constants.EditorPrefKeys.UseHttpTransport, true);
// Ensure mcp_servers table exists
if (!root.TryGetNode("mcp_servers", out var mcpServersNode) || !(mcpServersNode is TomlTable))
{
root["mcp_servers"] = new TomlTable();
}
var mcpServers = root["mcp_servers"] as TomlTable;
// Create or update unityMCP table
mcpServers["unityMCP"] = CreateUnityMcpTable(uvPath);
if (useHttpTransport)
{
EnsureRmcpClientFeature(root);
}
// Serialize back to TOML
using var writer = new StringWriter();
root.WriteTo(writer);
return writer.ToString();
}
public static bool TryParseCodexServer(string toml, out string command, out string[] args)
{
return TryParseCodexServer(toml, out command, out args, out _);
}
public static bool TryParseCodexServer(string toml, out string command, out string[] args, out string url)
{
command = null;
args = null;
url = null;
var root = TryParseToml(toml);
if (root == null) return false;
if (!TryGetTable(root, "mcp_servers", out var servers)
&& !TryGetTable(root, "mcpServers", out servers))
{
return false;
}
if (!TryGetTable(servers, "unityMCP", out var unity))
{
return false;
}
// Check for HTTP mode (url field)
url = GetTomlString(unity, "url");
if (!string.IsNullOrEmpty(url))
{
// HTTP mode detected - return true with url
return true;
}
// Check for stdio mode (command + args)
command = GetTomlString(unity, "command");
args = GetTomlStringArray(unity, "args");
return !string.IsNullOrEmpty(command) && args != null;
}
/// <summary>
/// Safely parses TOML string, returning null on failure
/// </summary>
private static TomlTable TryParseToml(string toml)
{
if (string.IsNullOrWhiteSpace(toml)) return null;
try
{
using var reader = new StringReader(toml);
return TOML.Parse(reader);
}
catch (TomlParseException)
{
return null;
}
catch (TomlSyntaxException)
{
return null;
}
catch (FormatException)
{
return null;
}
}
/// <summary>
/// Creates a TomlTable for the unityMCP server configuration
/// </summary>
/// <param name="uvPath">Path to uv executable (used as fallback if uvx is not available)</param>
private static TomlTable CreateUnityMcpTable(string uvPath)
{
var unityMCP = new TomlTable();
// Check transport preference
bool useHttpTransport = EditorPrefs.GetBool(MCPForUnity.Editor.Constants.EditorPrefKeys.UseHttpTransport, true);
if (useHttpTransport)
{
// HTTP mode: Use url field
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
unityMCP["url"] = new TomlString { Value = httpUrl };
}
else
{
// Stdio mode: Use command and args
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
unityMCP["command"] = new TomlString { Value = uvxPath };
var argsArray = new TomlArray();
AddUvxModeFlags(argsArray);
// Use centralized helper for beta server / prerelease args
foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList())
{
argsArray.Add(new TomlString { Value = arg });
}
argsArray.Add(new TomlString { Value = packageName });
argsArray.Add(new TomlString { Value = "--transport" });
argsArray.Add(new TomlString { Value = "stdio" });
unityMCP["args"] = argsArray;
// Add Windows-specific environment configuration for stdio mode
var platformService = MCPServiceLocator.Platform;
if (platformService.IsWindows())
{
var envTable = new TomlTable { IsInline = true };
envTable["SystemRoot"] = new TomlString { Value = platformService.GetSystemRoot() };
unityMCP["env"] = envTable;
}
// Allow extra time for uvx to download packages on first run
unityMCP["startup_timeout_sec"] = new TomlInteger { Value = 60 };
}
return unityMCP;
}
/// <summary>
/// Ensures the features table contains the rmcp_client flag for HTTP/SSE transport.
/// </summary>
private static void EnsureRmcpClientFeature(TomlTable root)
{
if (root == null) return;
if (!root.TryGetNode("features", out var featuresNode) || featuresNode is not TomlTable features)
{
features = new TomlTable();
root["features"] = features;
}
features["rmcp_client"] = new TomlBoolean { Value = true };
}
private static bool TryGetTable(TomlTable parent, string key, out TomlTable table)
{
table = null;
if (parent == null) return false;
if (parent.TryGetNode(key, out var node))
{
if (node is TomlTable tbl)
{
table = tbl;
return true;
}
if (node is TomlArray array)
{
var firstTable = array.Children.OfType<TomlTable>().FirstOrDefault();
if (firstTable != null)
{
table = firstTable;
return true;
}
}
}
return false;
}
private static string GetTomlString(TomlTable table, string key)
{
if (table != null && table.TryGetNode(key, out var node))
{
if (node is TomlString str) return str.Value;
if (node.HasValue) return node.ToString();
}
return null;
}
private static string[] GetTomlStringArray(TomlTable table, string key)
{
if (table == null) return null;
if (!table.TryGetNode(key, out var node)) return null;
if (node is TomlArray array)
{
List<string> values = new List<string>();
foreach (TomlNode element in array.Children)
{
if (element is TomlString str)
{
values.Add(str.Value);
}
else if (element.HasValue)
{
values.Add(element.ToString());
}
}
return values.Count > 0 ? values.ToArray() : Array.Empty<string>();
}
if (node is TomlString single)
{
return new[] { single.Value };
}
return null;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b3e68082ffc0b4cd39d3747673a4cc22
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,997 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.Events;
using MCPForUnity.Runtime.Helpers;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Low-level component operations extracted from ManageGameObject and ManageComponents.
/// Provides pure C# operations without JSON parsing or response formatting.
/// </summary>
public static class ComponentOps
{
/// <summary>
/// Adds a component to a GameObject with Undo support.
/// </summary>
/// <param name="target">The target GameObject</param>
/// <param name="componentType">The type of component to add</param>
/// <param name="error">Error message if operation fails</param>
/// <returns>The added component, or null if failed</returns>
public static Component AddComponent(GameObject target, Type componentType, out string error)
{
error = null;
if (target == null)
{
error = "Target GameObject is null.";
return null;
}
if (componentType == null || !typeof(Component).IsAssignableFrom(componentType))
{
error = $"Type '{componentType?.Name ?? "null"}' is not a valid Component type.";
return null;
}
// Prevent adding duplicate Transform
if (componentType == typeof(Transform))
{
error = "Cannot add another Transform component.";
return null;
}
// Check for 2D/3D physics conflicts
string conflictError = CheckPhysicsConflict(target, componentType);
if (conflictError != null)
{
error = conflictError;
return null;
}
// Produce a clearer error when this component already exists and cannot be duplicated.
Component existingComponent = target.GetComponent(componentType);
if (existingComponent != null && !AllowsMultiple(target, componentType))
{
error = $"Component '{componentType.Name}' already exists on '{target.name}' and this type does not allow multiple instances.";
return null;
}
try
{
Component newComponent = Undo.AddComponent(target, componentType);
if (newComponent == null)
{
if (target.GetComponent(componentType) != null && !AllowsMultiple(target, componentType))
{
error = $"Component '{componentType.Name}' already exists on '{target.name}' and this type does not allow multiple instances.";
}
else
{
error = $"Failed to add component '{componentType.Name}' to '{target.name}'. Unity may restrict this component on the current target.";
}
return null;
}
// Apply default values for specific component types
ApplyDefaultValues(newComponent);
return newComponent;
}
catch (Exception ex)
{
error = $"Error adding component '{componentType.Name}': {ex.Message}";
return null;
}
}
/// <summary>
/// Removes a component from a GameObject with Undo support.
/// </summary>
/// <param name="target">The target GameObject</param>
/// <param name="componentType">The type of component to remove</param>
/// <param name="error">Error message if operation fails</param>
/// <returns>True if component was removed successfully</returns>
public static bool RemoveComponent(GameObject target, Type componentType, out string error)
{
error = null;
if (target == null)
{
error = "Target GameObject is null.";
return false;
}
if (componentType == null)
{
error = "Component type is null.";
return false;
}
// Prevent removing Transform
if (componentType == typeof(Transform))
{
error = "Cannot remove Transform component.";
return false;
}
Component component = target.GetComponent(componentType);
if (component == null)
{
error = $"Component '{componentType.Name}' not found on '{target.name}'.";
return false;
}
try
{
Undo.DestroyObjectImmediate(component);
return true;
}
catch (Exception ex)
{
error = $"Error removing component '{componentType.Name}': {ex.Message}";
return false;
}
}
/// <summary>
/// Sets a property value on a component using reflection.
/// </summary>
/// <param name="component">The target component</param>
/// <param name="propertyName">The property or field name</param>
/// <param name="value">The value to set (JToken)</param>
/// <param name="error">Error message if operation fails</param>
/// <returns>True if property was set successfully</returns>
public static bool SetProperty(Component component, string propertyName, JToken value, out string error)
{
error = null;
if (component == null)
{
error = "Component is null.";
return false;
}
if (string.IsNullOrEmpty(propertyName))
{
error = "Property name is null or empty.";
return false;
}
Type type = component.GetType();
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
string normalizedName = ParamCoercion.NormalizePropertyName(propertyName);
// UnityEventBase-derived types must be set via SerializedProperty, not reflection.
// Reflection creates a disconnected object that Unity's serialization layer doesn't track,
// causing m_PersistentCalls to be empty when the scene is saved.
Type memberType = ResolveMemberType(type, propertyName, normalizedName);
if (memberType != null && typeof(UnityEventBase).IsAssignableFrom(memberType))
{
return SetViaSerializedProperty(component, propertyName, normalizedName, value, out error);
}
// Try reflection first (property, field, then non-public serialized field)
if (TrySetViaReflection(component, type, propertyName, normalizedName, flags, value, out error))
return true;
// Reflection failed — fall back to SerializedProperty which handles arrays,
// custom serialization (e.g. UdonSharp), and types reflection can't convert.
string reflectionError = error;
if (SetViaSerializedProperty(component, propertyName, normalizedName, value, out error))
return true;
// Both paths failed. If reflection found the member but couldn't convert,
// report that (more useful than the SerializedProperty error).
// If reflection didn't find it at all, report the SerializedProperty error.
if (reflectionError != null && !reflectionError.Contains("not found"))
error = reflectionError;
return false;
}
private static bool TrySetViaReflection(object component, Type type, string propertyName, string normalizedName, BindingFlags flags, JToken value, out string error)
{
error = null;
// Skip reflection for UnityEngine.Object types with JObject values
// so SerializedProperty can resolve guid/spriteName/fileID forms.
bool isJObjectValue = value != null && value.Type == JTokenType.Object;
// Try property first
PropertyInfo propInfo = type.GetProperty(propertyName, flags)
?? type.GetProperty(normalizedName, flags);
if (propInfo != null && propInfo.CanWrite)
{
if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(propInfo.PropertyType))
{
// Let SerializedProperty path handle complex object references.
return false;
}
try
{
object convertedValue = PropertyConversion.ConvertToType(value, propInfo.PropertyType);
if (convertedValue == null && value.Type != JTokenType.Null)
{
error = $"Failed to convert value for property '{propertyName}' to type '{propInfo.PropertyType.Name}'.";
return false;
}
propInfo.SetValue(component, convertedValue);
return true;
}
catch (Exception ex)
{
error = $"Failed to set property '{propertyName}': {ex.Message}";
return false;
}
}
// Try field
FieldInfo fieldInfo = type.GetField(propertyName, flags)
?? type.GetField(normalizedName, flags);
if (fieldInfo != null && !fieldInfo.IsInitOnly)
{
if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(fieldInfo.FieldType))
{
// Let SerializedProperty path handle complex object references.
return false;
}
try
{
object convertedValue = PropertyConversion.ConvertToType(value, fieldInfo.FieldType);
if (convertedValue == null && value.Type != JTokenType.Null)
{
error = $"Failed to convert value for field '{propertyName}' to type '{fieldInfo.FieldType.Name}'.";
return false;
}
fieldInfo.SetValue(component, convertedValue);
return true;
}
catch (Exception ex)
{
error = $"Failed to set field '{propertyName}': {ex.Message}";
return false;
}
}
// Try non-public serialized fields — traverse inheritance hierarchy
fieldInfo = FindSerializedFieldInHierarchy(type, propertyName)
?? FindSerializedFieldInHierarchy(type, normalizedName);
if (fieldInfo != null)
{
if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(fieldInfo.FieldType))
{
// Let SerializedProperty path handle complex object references.
return false;
}
try
{
object convertedValue = PropertyConversion.ConvertToType(value, fieldInfo.FieldType);
if (convertedValue == null && value.Type != JTokenType.Null)
{
error = $"Failed to convert value for serialized field '{propertyName}' to type '{fieldInfo.FieldType.Name}'.";
return false;
}
fieldInfo.SetValue(component, convertedValue);
return true;
}
catch (Exception ex)
{
error = $"Failed to set serialized field '{propertyName}': {ex.Message}";
return false;
}
}
error = $"Property or field '{propertyName}' not found on component '{type.Name}'.";
return false;
}
/// <summary>
/// Gets all public properties and fields from a component type.
/// </summary>
public static List<string> GetAccessibleMembers(Type componentType)
{
var members = new List<string>();
if (componentType == null) return members;
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
foreach (var prop in componentType.GetProperties(flags))
{
if (prop.CanWrite && prop.GetSetMethod() != null)
{
members.Add(prop.Name);
}
}
foreach (var field in componentType.GetFields(flags))
{
if (!field.IsInitOnly)
{
members.Add(field.Name);
}
}
// Include private [SerializeField] fields - traverse inheritance hierarchy
// Type.GetFields with NonPublic only returns fields declared directly on that type,
// so we need to walk up the chain to find inherited private serialized fields
var seenFieldNames = new HashSet<string>(members); // Avoid duplicates with public fields
Type currentType = componentType;
while (currentType != null && currentType != typeof(object))
{
foreach (var field in currentType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
if (field.GetCustomAttribute<SerializeField>() != null && !seenFieldNames.Contains(field.Name))
{
members.Add(field.Name);
seenFieldNames.Add(field.Name);
}
}
currentType = currentType.BaseType;
}
members.Sort();
return members;
}
// --- Private Helpers ---
/// <summary>
/// Searches for a non-public [SerializeField] field through the entire inheritance hierarchy.
/// Type.GetField() with NonPublic only returns fields declared directly on that type,
/// so this method walks up the chain to find inherited private serialized fields.
/// </summary>
internal static FieldInfo FindSerializedFieldInHierarchy(Type type, string fieldName)
{
if (type == null || string.IsNullOrEmpty(fieldName))
return null;
BindingFlags privateFlags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
Type currentType = type;
// Walk up the inheritance chain
while (currentType != null && currentType != typeof(object))
{
// Search for the field on this specific type (case-insensitive)
foreach (var field in currentType.GetFields(privateFlags))
{
if (string.Equals(field.Name, fieldName, StringComparison.OrdinalIgnoreCase) &&
field.GetCustomAttribute<SerializeField>() != null)
{
return field;
}
}
currentType = currentType.BaseType;
}
return null;
}
private static string CheckPhysicsConflict(GameObject target, Type componentType)
{
bool isAdding2DPhysics =
typeof(Rigidbody2D).IsAssignableFrom(componentType) ||
typeof(Collider2D).IsAssignableFrom(componentType);
bool isAdding3DPhysics =
typeof(Rigidbody).IsAssignableFrom(componentType) ||
typeof(Collider).IsAssignableFrom(componentType);
if (isAdding2DPhysics)
{
if (target.GetComponent<Rigidbody>() != null || target.GetComponent<Collider>() != null)
{
return $"Cannot add 2D physics component '{componentType.Name}' because the GameObject '{target.name}' already has a 3D Rigidbody or Collider.";
}
}
else if (isAdding3DPhysics)
{
if (target.GetComponent<Rigidbody2D>() != null || target.GetComponent<Collider2D>() != null)
{
return $"Cannot add 3D physics component '{componentType.Name}' because the GameObject '{target.name}' already has a 2D Rigidbody or Collider.";
}
}
return null;
}
private static void ApplyDefaultValues(Component component)
{
// Default newly added Lights to Directional
if (component is Light light)
{
light.type = LightType.Directional;
}
}
private static bool AllowsMultiple(GameObject target, Type componentType)
{
if (target == null || componentType == null)
{
return false;
}
if (Attribute.IsDefined(componentType, typeof(DisallowMultipleComponent), inherit: true))
{
return false;
}
return true;
}
// --- UnityEvent SerializedProperty support ---
private static Type ResolveMemberType(Type componentType, string propertyName, string normalizedName)
{
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
PropertyInfo propInfo = componentType.GetProperty(propertyName, flags)
?? componentType.GetProperty(normalizedName, flags);
if (propInfo != null)
return propInfo.PropertyType;
FieldInfo fieldInfo = componentType.GetField(propertyName, flags)
?? componentType.GetField(normalizedName, flags);
if (fieldInfo != null)
return fieldInfo.FieldType;
fieldInfo = FindSerializedFieldInHierarchy(componentType, propertyName)
?? FindSerializedFieldInHierarchy(componentType, normalizedName);
if (fieldInfo != null)
return fieldInfo.FieldType;
return null;
}
private static bool SetViaSerializedProperty(Component component, string propertyName, string normalizedName, JToken value, out string error)
{
error = null;
using var so = new SerializedObject(component);
SerializedProperty prop = so.FindProperty(propertyName)
?? so.FindProperty(normalizedName);
if (prop == null)
{
error = $"SerializedProperty '{propertyName}' not found on component '{component.GetType().Name}'.";
return false;
}
if (!SetSerializedPropertyRecursive(prop, value, out error, 0))
return false;
so.ApplyModifiedProperties();
// Readback verification for ObjectReference — these can silently fail
if (prop.propertyType == SerializedPropertyType.ObjectReference
&& value != null
&& !(value is JValue jv && jv.Type == JTokenType.Null))
{
so.Update();
var verifyProp = so.FindProperty(propertyName)
?? so.FindProperty(normalizedName);
if (verifyProp != null
&& verifyProp.propertyType == SerializedPropertyType.ObjectReference
&& verifyProp.objectReferenceValue == null)
{
error = $"Property '{propertyName}' was set but the object reference did not persist. " +
"Check that the referenced object exists and is the correct type.";
return false;
}
}
return true;
}
private static bool SetSerializedPropertyRecursive(SerializedProperty prop, JToken value, out string error, int depth)
{
error = null;
const int MaxDepth = 20;
if (depth > MaxDepth)
{
error = $"Maximum recursion depth ({MaxDepth}) exceeded.";
return false;
}
try
{
// Array + JArray
if (prop.isArray && prop.propertyType != SerializedPropertyType.String && value is JArray jArray)
{
prop.arraySize = jArray.Count;
prop.serializedObject.ApplyModifiedProperties();
prop.serializedObject.Update();
for (int i = 0; i < jArray.Count; i++)
{
var element = prop.GetArrayElementAtIndex(i);
if (!SetSerializedPropertyRecursive(element, jArray[i], out error, depth + 1))
return false;
}
return true;
}
// Generic (struct/class) + JObject
if (prop.propertyType == SerializedPropertyType.Generic && !prop.isArray && value is JObject jObj)
{
foreach (var kvp in jObj)
{
var child = FindPropertyRelativeFuzzy(prop, kvp.Key);
if (child == null)
{
error = $"Sub-property '{kvp.Key}' not found under '{prop.propertyPath}'.";
return false;
}
if (!SetSerializedPropertyRecursive(child, kvp.Value, out error, depth + 1))
return false;
}
return true;
}
// ObjectReference
if (prop.propertyType == SerializedPropertyType.ObjectReference)
return SetObjectReference(prop, value, out error);
// Leaf types
switch (prop.propertyType)
{
case SerializedPropertyType.Integer:
if (value == null || value.Type == JTokenType.Null
|| (value.Type != JTokenType.Integer && value.Type != JTokenType.Float
&& !long.TryParse(value.ToString(), out _)))
{
error = "Expected integer value.";
return false;
}
if (prop.type == "long")
prop.longValue = ParamCoercion.CoerceLong(value, 0);
else
prop.intValue = ParamCoercion.CoerceInt(value, 0);
return true;
case SerializedPropertyType.Boolean:
if (value == null || value.Type == JTokenType.Null)
{
error = "Expected boolean value.";
return false;
}
prop.boolValue = ParamCoercion.CoerceBool(value, false);
return true;
case SerializedPropertyType.Float:
float floatVal = ParamCoercion.CoerceFloat(value, float.NaN);
if (float.IsNaN(floatVal))
{
error = "Expected float value.";
return false;
}
prop.floatValue = floatVal;
return true;
case SerializedPropertyType.String:
prop.stringValue = value == null || value.Type == JTokenType.Null ? string.Empty : value.ToString();
return true;
case SerializedPropertyType.Enum:
return SetEnum(prop, value, out error);
default:
error = $"Unsupported SerializedPropertyType: {prop.propertyType} at '{prop.propertyPath}'.";
return false;
}
}
catch (Exception ex)
{
error = $"Error setting '{prop.propertyPath}': {ex.Message}";
return false;
}
}
internal static bool SetObjectReference(SerializedProperty prop, JToken value, out string error)
{
error = null;
if (value == null || value.Type == JTokenType.Null)
{
prop.objectReferenceValue = null;
return true;
}
if (value.Type == JTokenType.Integer)
{
int id = value.Value<int>();
var resolved = GameObjectLookup.ResolveInstanceID(id);
if (resolved == null)
{
error = $"No object found with instanceID {id}.";
return false;
}
return AssignObjectReference(prop, resolved, null, out error);
}
if (value is JObject jObj)
{
// Optional component type filter — e.g. {"instanceID": 123, "component": "Button"}
string componentFilter = jObj["component"]?.ToString();
var idToken = jObj["instanceID"];
if (idToken != null)
{
int id = ParamCoercion.CoerceInt(idToken, 0);
var resolved = GameObjectLookup.ResolveInstanceID(id);
if (resolved == null)
{
error = $"No object found with instanceID {id}.";
return false;
}
return AssignObjectReference(prop, resolved, componentFilter, out error);
}
var guidToken = jObj["guid"];
if (guidToken != null)
{
string path = AssetDatabase.GUIDToAssetPath(guidToken.ToString());
if (string.IsNullOrEmpty(path))
{
error = $"No asset found for GUID '{guidToken}'.";
return false;
}
var spriteNameToken = jObj["spriteName"];
if (spriteNameToken != null)
{
string spriteName = spriteNameToken.ToString();
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
var originalRef = prop.objectReferenceValue;
foreach (var asset in allAssets)
{
if (asset is Sprite sprite && sprite.name == spriteName)
{
prop.objectReferenceValue = sprite;
if (prop.objectReferenceValue != null)
return true;
// Unity rejected the type — restore and report
prop.objectReferenceValue = originalRef;
error = $"Sprite '{spriteName}' found but is not compatible with the property type.";
return false;
}
}
error = $"Sprite '{spriteName}' not found in atlas '{path}'.";
return false;
}
var fileIdToken = jObj["fileID"];
if (fileIdToken != null)
{
long targetFileId = fileIdToken.Value<long>();
if (targetFileId != 0)
{
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
var originalRef = prop.objectReferenceValue;
foreach (var asset in allAssets)
{
if (asset is Sprite sprite)
{
long spriteFileId = GetSpriteFileId(sprite);
if (spriteFileId == targetFileId)
{
prop.objectReferenceValue = sprite;
if (prop.objectReferenceValue != null)
return true;
prop.objectReferenceValue = originalRef;
error = $"Sprite with fileID '{targetFileId}' found but is not compatible with the property type.";
return false;
}
}
}
}
error = $"Sprite with fileID '{targetFileId}' not found in atlas '{path}'.";
return false;
}
var loaded = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
return AssignObjectReference(prop, loaded, componentFilter, out error);
}
var pathToken = jObj["path"];
if (pathToken != null)
{
string sanitized = AssetPathUtility.SanitizeAssetPath(pathToken.ToString());
var resolved = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(sanitized);
if (resolved == null)
{
error = $"No asset found at path '{pathToken}'.";
return false;
}
return AssignObjectReference(prop, resolved, componentFilter, out error);
}
var nameToken = jObj["name"];
if (nameToken != null)
{
return ResolveSceneObjectByName(prop, nameToken.ToString(), componentFilter, out error);
}
error = "Object reference must contain 'instanceID', 'guid', 'path', or 'name'.";
return false;
}
if (value.Type == JTokenType.String)
{
string strVal = value.ToString();
// Try as instanceID if the string is purely numeric
if (int.TryParse(strVal, out int parsedId))
{
var resolved = GameObjectLookup.ResolveInstanceID(parsedId);
if (resolved != null)
return AssignObjectReference(prop, resolved, null, out error);
// Not a valid instanceID — fall through to path/name resolution
}
if (strVal.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) || strVal.Contains("/"))
{
string sanitized = AssetPathUtility.SanitizeAssetPath(strVal);
var resolved = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(sanitized);
if (resolved == null)
{
error = $"No asset found at path '{strVal}'.";
return false;
}
return AssignObjectReference(prop, resolved, null, out error);
}
// Try as asset GUID (32-char hex string)
if (strVal.Length == 32 && IsHexString(strVal))
{
string assetPath = AssetDatabase.GUIDToAssetPath(strVal);
if (!string.IsNullOrEmpty(assetPath))
{
var resolved = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
if (resolved != null)
return AssignObjectReference(prop, resolved, null, out error);
}
}
// Fall back to scene hierarchy lookup by name.
return ResolveSceneObjectByName(prop, strVal, null, out error);
}
error = $"Unsupported object reference format: {value.Type}.";
return false;
}
/// <summary>
/// Assigns a resolved object to a SerializedProperty, with automatic component fallback.
/// If the resolved object is a GameObject but the property expects a Component type,
/// searches the GameObject's components for a compatible one.
/// Optionally filters by component type name (e.g. "Button", "Rigidbody").
/// </summary>
private static bool AssignObjectReference(SerializedProperty prop, UnityEngine.Object resolved, string componentFilter, out string error)
{
error = null;
if (resolved == null)
{
error = "Resolved object is null.";
return false;
}
// If a component filter is specified and the resolved object is a GameObject,
// find the specific component by type name.
if (!string.IsNullOrEmpty(componentFilter) && resolved is GameObject filterGo)
{
var components = filterGo.GetComponents<Component>();
foreach (var comp in components)
{
if (comp == null) continue;
if (string.Equals(comp.GetType().Name, componentFilter, StringComparison.OrdinalIgnoreCase) ||
string.Equals(comp.GetType().FullName, componentFilter, StringComparison.OrdinalIgnoreCase))
{
prop.objectReferenceValue = comp;
if (prop.objectReferenceValue != null)
return true;
}
}
error = $"Component '{componentFilter}' not found on GameObject '{filterGo.name}'.";
return false;
}
// Try direct assignment first
prop.objectReferenceValue = resolved;
if (prop.objectReferenceValue != null)
return true;
// Sub-asset fallback: e.g., Texture2D → Sprite
string subAssetPath = AssetDatabase.GetAssetPath(resolved);
if (!string.IsNullOrEmpty(subAssetPath))
{
var subAssets = AssetDatabase.LoadAllAssetsAtPath(subAssetPath);
UnityEngine.Object match = null;
int matchCount = 0;
foreach (var sub in subAssets)
{
if (sub == null || sub == resolved) continue;
prop.objectReferenceValue = sub;
if (prop.objectReferenceValue != null)
{
match = sub;
matchCount++;
if (matchCount > 1) break;
}
}
if (matchCount == 1)
{
prop.objectReferenceValue = match;
return true;
}
// Clean up: probing may have left the property dirty
prop.objectReferenceValue = null;
if (matchCount > 1)
{
error = $"Multiple compatible sub-assets found in '{subAssetPath}'. " +
"Use {\"guid\": \"...\", \"spriteName\": \"<name>\"} or " +
"{\"guid\": \"...\", \"fileID\": <id>} for precise selection.";
return false;
}
}
// If the resolved object is a GameObject but the property expects a Component,
// try each component on the GameObject until one is accepted.
if (resolved is GameObject go)
{
var components = go.GetComponents<Component>();
foreach (var comp in components)
{
if (comp == null) continue;
prop.objectReferenceValue = comp;
if (prop.objectReferenceValue != null)
return true;
}
error = $"GameObject '{go.name}' found but no compatible component for the property type.";
return false;
}
error = $"Object '{resolved.name}' (type: {resolved.GetType().Name}) is not compatible with the property type.";
return false;
}
/// <summary>
/// Resolves a scene GameObject by name and assigns it (or a component on it)
/// to a SerializedProperty. Uses GameObjectLookup for robust search
/// including inactive objects and prefab stage support.
/// </summary>
private static bool ResolveSceneObjectByName(SerializedProperty prop, string name, string componentFilter, out string error)
{
error = null;
if (string.IsNullOrWhiteSpace(name))
{
error = "Cannot resolve object reference from empty name.";
return false;
}
var ids = GameObjectLookup.SearchGameObjects(
GameObjectLookup.SearchMethod.ByName, name, includeInactive: true, maxResults: 1);
if (ids.Count == 0)
{
error = $"No GameObject named '{name}' found in scene.";
return false;
}
var go = GameObjectLookup.FindById(ids[0]);
if (go == null)
{
error = $"GameObject '{name}' found but could not be resolved.";
return false;
}
return AssignObjectReference(prop, go, componentFilter, out error);
}
/// <summary>
/// Finds a child SerializedProperty by name, falling back to underscore-insensitive matching.
/// The batch_execute transport can strip underscores from JSON keys
/// (e.g. m_PersistentCalls → mPersistentCalls), so we iterate immediate children
/// and compare with underscores removed.
/// </summary>
private static SerializedProperty FindPropertyRelativeFuzzy(SerializedProperty parent, string key)
{
var child = parent.FindPropertyRelative(key);
if (child != null) return child;
string normalizedKey = key.Replace("_", "").ToLowerInvariant();
var end = parent.GetEndProperty();
var iter = parent.Copy();
if (!iter.Next(true)) return null;
while (!SerializedProperty.EqualContents(iter, end))
{
if (iter.depth == parent.depth + 1)
{
string normalizedName = iter.name.Replace("_", "").ToLowerInvariant();
if (normalizedName == normalizedKey)
return parent.FindPropertyRelative(iter.name);
}
if (!iter.Next(false))
break;
}
return null;
}
private static bool SetEnum(SerializedProperty prop, JToken value, out string error)
{
error = null;
var names = prop.enumNames;
if (names == null || names.Length == 0)
{
error = "Enum has no names.";
return false;
}
if (value.Type == JTokenType.Integer)
{
int idx = value.Value<int>();
if (idx < 0 || idx >= names.Length)
{
error = $"Enum index out of range: {idx}.";
return false;
}
prop.enumValueIndex = idx;
return true;
}
string s = value.ToString();
for (int i = 0; i < names.Length; i++)
{
if (string.Equals(names[i], s, StringComparison.OrdinalIgnoreCase))
{
prop.enumValueIndex = i;
return true;
}
}
error = $"Unknown enum name '{s}'.";
return false;
}
private static long GetSpriteFileId(Sprite sprite)
{
if (sprite == null)
return 0;
try
{
var globalId = GlobalObjectId.GetGlobalObjectIdSlow(sprite);
return unchecked((long)globalId.targetObjectId);
}
catch (Exception ex)
{
McpLog.Warn($"Failed to get fileID for sprite '{sprite.name}' (instanceID={sprite.GetInstanceIDCompat()}): {ex.Message}");
return 0;
}
}
private static bool IsHexString(string str)
{
foreach (char c in str)
{
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 13dead161bc4540eeb771961df437779
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
public static class ConfigJsonBuilder
{
public static string BuildManualConfigJson(string uvPath, McpClient client)
{
var root = new JObject();
bool isVSCode = client?.IsVsCodeLayout == true;
JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");
var unity = new JObject();
PopulateUnityNode(unity, uvPath, client, isVSCode);
container["unityMCP"] = unity;
return root.ToString(Formatting.Indented);
}
public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, McpClient client)
{
if (root == null) root = new JObject();
bool isVSCode = client?.IsVsCodeLayout == true;
JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");
JObject unity = container["unityMCP"] as JObject ?? new JObject();
PopulateUnityNode(unity, uvPath, client, isVSCode);
container["unityMCP"] = unity;
return root;
}
/// <summary>
/// Centralized builder that applies all caveats consistently.
/// - Sets command/args with uvx and package version
/// - Ensures env exists
/// - Adds transport configuration (HTTP or stdio)
/// - Adds disabled:false for Windsurf/Kiro only when missing
/// </summary>
private static void PopulateUnityNode(JObject unity, string uvPath, McpClient client, bool isVSCode)
{
// Get transport preference (default to HTTP)
bool prefValue = EditorConfigurationCache.Instance.UseHttpTransport;
bool clientSupportsHttp = client?.SupportsHttpTransport != false;
bool useHttpTransport = clientSupportsHttp && prefValue;
bool isCline = client?.name == "Cline";
string httpProperty = string.IsNullOrEmpty(client?.HttpUrlProperty) ? "url" : client.HttpUrlProperty;
var urlPropsToRemove = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "url", "serverUrl" };
urlPropsToRemove.Remove(httpProperty);
if (useHttpTransport)
{
// HTTP mode: Use URL, no command
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
unity[httpProperty] = httpUrl;
foreach (var prop in urlPropsToRemove)
{
if (unity[prop] != null) unity.Remove(prop);
}
// Remove command/args if they exist from previous config
if (unity["command"] != null) unity.Remove("command");
if (unity["args"] != null) unity.Remove("args");
// Only include API key header for remote-hosted mode
if (HttpEndpointUtility.IsRemoteScope())
{
string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty);
if (!string.IsNullOrEmpty(apiKey))
{
var headers = new JObject { [AuthConstants.ApiKeyHeader] = apiKey };
unity["headers"] = headers;
}
else
{
if (unity["headers"] != null) unity.Remove("headers");
}
}
else
{
// Local HTTP doesn't use API keys; remove any stale headers
if (unity["headers"] != null) unity.Remove("headers");
}
// Cline expects streamableHttp for HTTP endpoints.
if (isCline)
{
unity["type"] = "streamableHttp";
}
else
{
// "type" is standard MCP protocol; include for all clients to avoid
// clients that default to SSE when they see a URL without a type field.
unity["type"] = "http";
}
}
else
{
// Stdio mode: Use uvx command
var (uvxPath, fromUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
var toolArgs = BuildUvxArgs(fromUrl, packageName);
unity["command"] = uvxPath;
unity["args"] = JArray.FromObject(toolArgs.ToArray());
// Remove url/serverUrl if they exist from previous config
if (unity["url"] != null) unity.Remove("url");
if (unity["serverUrl"] != null) unity.Remove("serverUrl");
// Include type for all clients — standard MCP protocol field.
unity["type"] = "stdio";
}
bool requiresEnv = client?.EnsureEnvObject == true;
bool stripEnv = client?.StripEnvWhenNotRequired == true;
if (requiresEnv)
{
if (unity["env"] == null)
{
unity["env"] = new JObject();
}
}
else if (stripEnv && unity["env"] != null)
{
unity.Remove("env");
}
if (client?.DefaultUnityFields != null)
{
foreach (var kvp in client.DefaultUnityFields)
{
if (unity[kvp.Key] == null)
{
unity[kvp.Key] = kvp.Value != null ? JToken.FromObject(kvp.Value) : JValue.CreateNull();
}
}
}
}
private static JObject EnsureObject(JObject parent, string name)
{
if (parent[name] is JObject o) return o;
var created = new JObject();
parent[name] = created;
return created;
}
private static IList<string> BuildUvxArgs(string fromUrl, string packageName)
{
// Dev mode: force a fresh install/resolution (avoids stale cached builds while iterating).
// `--no-cache` avoids reading from cache; `--refresh` ensures metadata is revalidated.
// Note: --reinstall is not supported by uvx and will cause a warning.
// Keep ordering consistent with other uvx builders: dev flags first, then --from <url>, then package name.
var args = new List<string>();
foreach (var flag in AssetPathUtility.GetUvxDevFlagsList())
args.Add(flag);
// Use centralized helper for beta server / prerelease args
foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList())
{
args.Add(arg);
}
args.Add(packageName);
args.Add("--transport");
args.Add("stdio");
return args;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5c07c3369f73943919d9e086a81d1dcc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More