Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 716f55c138 |
File diff suppressed because one or more lines are too long
@ -297,7 +297,7 @@ MonoBehaviour:
|
|||||||
m_NumColorKeys: 3
|
m_NumColorKeys: 3
|
||||||
m_NumAlphaKeys: 3
|
m_NumAlphaKeys: 3
|
||||||
overrideTexture: 1
|
overrideTexture: 1
|
||||||
texture: {fileID: 0}
|
texture: {fileID: 2800000, guid: 375a132719b8f114aadcfda6bf62f8b1, type: 3}
|
||||||
--- !u!114 &2117915465640148810
|
--- !u!114 &2117915465640148810
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
@ -8,6 +8,14 @@ PrefabInstance:
|
|||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_TransformParent: {fileID: 0}
|
m_TransformParent: {fileID: 0}
|
||||||
m_Modifications:
|
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}
|
- target: {fileID: 1088233528469808677, guid: 6331eaa4049f7fc4a8107649fc474455, type: 3}
|
||||||
propertyPath: m_Color.b
|
propertyPath: m_Color.b
|
||||||
value: 1
|
value: 1
|
||||||
|
|||||||
@ -37,7 +37,7 @@ TextureImporter:
|
|||||||
maxTextureSize: 2048
|
maxTextureSize: 2048
|
||||||
textureSettings:
|
textureSettings:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
filterMode: 1
|
filterMode: 0
|
||||||
aniso: 1
|
aniso: 1
|
||||||
mipBias: 0
|
mipBias: 0
|
||||||
wrapU: 1
|
wrapU: 1
|
||||||
|
|||||||
@ -500,6 +500,11 @@ Transform:
|
|||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children:
|
m_Children:
|
||||||
- {fileID: 1818583591}
|
- {fileID: 1818583591}
|
||||||
|
- {fileID: 874137942}
|
||||||
|
- {fileID: 440292227}
|
||||||
|
- {fileID: 1064847901}
|
||||||
|
- {fileID: 1038585760}
|
||||||
|
- {fileID: 1917762239}
|
||||||
m_Father: {fileID: 144963232}
|
m_Father: {fileID: 144963232}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &9184405
|
--- !u!1 &9184405
|
||||||
@ -138694,6 +138699,105 @@ SpriteRenderer:
|
|||||||
m_SpriteTileMode: 0
|
m_SpriteTileMode: 0
|
||||||
m_WasSpriteAssigned: 1
|
m_WasSpriteAssigned: 1
|
||||||
m_SpriteSortPoint: 0
|
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
|
--- !u!1 &440338873
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@ -164920,6 +165024,103 @@ SpriteRenderer:
|
|||||||
m_SpriteTileMode: 0
|
m_SpriteTileMode: 0
|
||||||
m_WasSpriteAssigned: 1
|
m_WasSpriteAssigned: 1
|
||||||
m_SpriteSortPoint: 0
|
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
|
--- !u!4 &874646962 stripped
|
||||||
Transform:
|
Transform:
|
||||||
m_CorrespondingSourceObject: {fileID: 5491244471745524865, guid: 1c7455363049dbc4996e240f0bcdceff, type: 3}
|
m_CorrespondingSourceObject: {fileID: 5491244471745524865, guid: 1c7455363049dbc4996e240f0bcdceff, type: 3}
|
||||||
@ -173328,6 +173529,103 @@ SpriteRenderer:
|
|||||||
m_SpriteTileMode: 0
|
m_SpriteTileMode: 0
|
||||||
m_WasSpriteAssigned: 1
|
m_WasSpriteAssigned: 1
|
||||||
m_SpriteSortPoint: 0
|
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
|
--- !u!1 &1040802952
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@ -174566,6 +174864,105 @@ SpriteRenderer:
|
|||||||
m_SpriteTileMode: 0
|
m_SpriteTileMode: 0
|
||||||
m_WasSpriteAssigned: 1
|
m_WasSpriteAssigned: 1
|
||||||
m_SpriteSortPoint: 0
|
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
|
--- !u!1 &1067041237
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@ -504034,6 +504431,7 @@ MonoBehaviour:
|
|||||||
weight: 0
|
weight: 0
|
||||||
- prefab: {fileID: 5620532696342616547, guid: 64b2e8b5daaf53048a7ae0709db68282, type: 3}
|
- prefab: {fileID: 5620532696342616547, guid: 64b2e8b5daaf53048a7ae0709db68282, type: 3}
|
||||||
weight: 0
|
weight: 0
|
||||||
|
spawnerNameFilter: Enemy
|
||||||
--- !u!61 &1818583593
|
--- !u!61 &1818583593
|
||||||
BoxCollider2D:
|
BoxCollider2D:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@ -509171,6 +509569,103 @@ PrefabInstance:
|
|||||||
m_AddedGameObjects: []
|
m_AddedGameObjects: []
|
||||||
m_AddedComponents: []
|
m_AddedComponents: []
|
||||||
m_SourcePrefab: {fileID: 100100000, guid: e5783e81b8b0fd648974f0ec7ebde930, type: 3}
|
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
|
--- !u!1 &1918398179
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
226
Assets/Scripts/Behaviour/PlayerSkinOverride.cs
Normal file
226
Assets/Scripts/Behaviour/PlayerSkinOverride.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/Behaviour/PlayerSkinOverride.cs.meta
Normal file
2
Assets/Scripts/Behaviour/PlayerSkinOverride.cs.meta
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3784dd4dcbda97c44a85ea59fdee2076
|
||||||
@ -90,9 +90,6 @@
|
|||||||
{
|
{
|
||||||
"m_Id": "0d74e8c23ef1428eaf8c38a4c033c563"
|
"m_Id": "0d74e8c23ef1428eaf8c38a4c033c563"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"m_Id": "ea6c916c3dd64bcaa527955c3fbb4f62"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"m_Id": "79f96f582c954fcc9e238541746f9671"
|
"m_Id": "79f96f582c954fcc9e238541746f9671"
|
||||||
},
|
},
|
||||||
@ -137,6 +134,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"m_Id": "b1f7ddf2f27e4298b62af4614df310c0"
|
"m_Id": "b1f7ddf2f27e4298b62af4614df310c0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"m_Id": "73ce726c69084867adf2c184625cead4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"m_Id": "55f2c3eb7ea742a5a0959c2495bd8554"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"m_GroupDatas": [
|
"m_GroupDatas": [
|
||||||
@ -149,6 +152,20 @@
|
|||||||
],
|
],
|
||||||
"m_StickyNoteDatas": [],
|
"m_StickyNoteDatas": [],
|
||||||
"m_Edges": [
|
"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_OutputSlot": {
|
||||||
"m_Node": {
|
"m_Node": {
|
||||||
@ -163,34 +180,6 @@
|
|||||||
"m_SlotId": 1
|
"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_OutputSlot": {
|
||||||
"m_Node": {
|
"m_Node": {
|
||||||
@ -303,6 +292,20 @@
|
|||||||
"m_SlotId": 0
|
"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_OutputSlot": {
|
||||||
"m_Node": {
|
"m_Node": {
|
||||||
@ -317,6 +320,20 @@
|
|||||||
"m_SlotId": 0
|
"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_OutputSlot": {
|
||||||
"m_Node": {
|
"m_Node": {
|
||||||
@ -443,6 +460,20 @@
|
|||||||
"m_SlotId": 1
|
"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_OutputSlot": {
|
||||||
"m_Node": {
|
"m_Node": {
|
||||||
@ -835,8 +866,8 @@
|
|||||||
"m_ObjectId": "06f798f4e4d445d0a65b4f9f8db515b7",
|
"m_ObjectId": "06f798f4e4d445d0a65b4f9f8db515b7",
|
||||||
"m_Title": "Texture",
|
"m_Title": "Texture",
|
||||||
"m_Position": {
|
"m_Position": {
|
||||||
"x": -2312.999755859375,
|
"x": -2313.0,
|
||||||
"y": -619.9999389648438
|
"y": -620.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -911,10 +942,10 @@
|
|||||||
"m_Expanded": true,
|
"m_Expanded": true,
|
||||||
"m_Position": {
|
"m_Position": {
|
||||||
"serializedVersion": "2",
|
"serializedVersion": "2",
|
||||||
"x": -1011.0,
|
"x": -896.9999389648438,
|
||||||
"y": 40.99998092651367,
|
"y": 46.99999237060547,
|
||||||
"width": 207.9998779296875,
|
"width": 207.99993896484376,
|
||||||
"height": 302.0
|
"height": 301.9999694824219
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"m_Slots": [
|
"m_Slots": [
|
||||||
@ -1311,6 +1342,31 @@
|
|||||||
"m_LiteralMode": false
|
"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_SGVersion": 0,
|
||||||
"m_Type": "UnityEditor.ShaderGraph.DynamicValueMaterialSlot",
|
"m_Type": "UnityEditor.ShaderGraph.DynamicValueMaterialSlot",
|
||||||
@ -1375,6 +1431,31 @@
|
|||||||
"m_LiteralMode": false
|
"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_SGVersion": 0,
|
||||||
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
|
"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_SGVersion": 0,
|
||||||
"m_Type": "UnityEditor.ShaderGraph.BlockNode",
|
"m_Type": "UnityEditor.ShaderGraph.BlockNode",
|
||||||
@ -1985,31 +2109,6 @@
|
|||||||
"m_Channel": 0
|
"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_SGVersion": 0,
|
||||||
"m_Type": "UnityEditor.ShaderGraph.UVMaterialSlot",
|
"m_Type": "UnityEditor.ShaderGraph.UVMaterialSlot",
|
||||||
@ -2133,31 +2232,6 @@
|
|||||||
"m_Labels": []
|
"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_SGVersion": 0,
|
||||||
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
|
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
|
||||||
@ -2239,8 +2313,8 @@
|
|||||||
"m_ObjectId": "69f637288cb54756bdd28eab6e8ffd25",
|
"m_ObjectId": "69f637288cb54756bdd28eab6e8ffd25",
|
||||||
"m_Title": "Noise",
|
"m_Title": "Noise",
|
||||||
"m_Position": {
|
"m_Position": {
|
||||||
"x": -2807.999755859375,
|
"x": -2808.0,
|
||||||
"y": 231.99996948242188
|
"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_SGVersion": 0,
|
||||||
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
|
"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_SGVersion": 0,
|
||||||
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
|
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
|
||||||
@ -3356,6 +3494,31 @@
|
|||||||
"m_LiteralMode": false
|
"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_SGVersion": 0,
|
||||||
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
|
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
|
||||||
@ -3894,10 +4057,10 @@
|
|||||||
"m_Expanded": true,
|
"m_Expanded": true,
|
||||||
"m_Position": {
|
"m_Position": {
|
||||||
"serializedVersion": "2",
|
"serializedVersion": "2",
|
||||||
"x": -305.99981689453127,
|
"x": 100.00003051757813,
|
||||||
"y": 209.00001525878907,
|
"y": 144.9999542236328,
|
||||||
"width": 119.99990844726563,
|
"width": 119.99989318847656,
|
||||||
"height": 149.0000457763672
|
"height": 149.0000762939453
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"m_Slots": [
|
"m_Slots": [
|
||||||
@ -3995,10 +4158,10 @@
|
|||||||
"m_Expanded": true,
|
"m_Expanded": true,
|
||||||
"m_Position": {
|
"m_Position": {
|
||||||
"serializedVersion": "2",
|
"serializedVersion": "2",
|
||||||
"x": -536.9998779296875,
|
"x": -214.00006103515626,
|
||||||
"y": 56.00006103515625,
|
"y": 9.000008583068848,
|
||||||
"width": 207.99993896484376,
|
"width": 208.00003051757813,
|
||||||
"height": 302.0
|
"height": 301.9999694824219
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"m_Slots": [
|
"m_Slots": [
|
||||||
@ -4227,31 +4390,6 @@
|
|||||||
"m_LiteralMode": false
|
"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_SGVersion": 0,
|
||||||
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
|
"m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot",
|
||||||
@ -4724,45 +4862,6 @@
|
|||||||
"m_Labels": []
|
"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_SGVersion": 0,
|
||||||
"m_Type": "UnityEditor.ShaderGraph.DynamicVectorMaterialSlot",
|
"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_SGVersion": 1,
|
||||||
"m_Type": "UnityEditor.ShaderGraph.NoiseNode",
|
"m_Type": "UnityEditor.ShaderGraph.NoiseNode",
|
||||||
@ -5024,6 +5148,31 @@
|
|||||||
"m_Labels": []
|
"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_SGVersion": 0,
|
||||||
"m_Type": "UnityEditor.ShaderGraph.Vector2MaterialSlot",
|
"m_Type": "UnityEditor.ShaderGraph.Vector2MaterialSlot",
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -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}
|
|
||||||
@ -1,6 +1,8 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
|
<Project Path="MCPForUnity.Runtime.csproj" />
|
||||||
<Project Path="VFXGraph.OutputEventHandlers.csproj" />
|
<Project Path="VFXGraph.OutputEventHandlers.csproj" />
|
||||||
<Project Path="Assembly-CSharp.csproj" />
|
<Project Path="Assembly-CSharp.csproj" />
|
||||||
|
<Project Path="MCPForUnity.Editor.csproj" />
|
||||||
<Project Path="Assembly-CSharp-Editor.csproj" />
|
<Project Path="Assembly-CSharp-Editor.csproj" />
|
||||||
<Project Path="VFXGraph.OutputEventHandlers.Editor.csproj" />
|
<Project Path="VFXGraph.OutputEventHandlers.Editor.csproj" />
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
8
Packages/com.coplaydev.unity-mcp/Editor.meta
Normal file
8
Packages/com.coplaydev.unity-mcp/Editor.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 31e7fac5858840340a75cc6df0ad3d9e
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
3
Packages/com.coplaydev.unity-mcp/Editor/AssemblyInfo.cs
Normal file
3
Packages/com.coplaydev.unity-mcp/Editor/AssemblyInfo.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")]
|
||||||
11
Packages/com.coplaydev.unity-mcp/Editor/AssemblyInfo.cs.meta
Normal file
11
Packages/com.coplaydev.unity-mcp/Editor/AssemblyInfo.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: be61633e00d934610ac1ff8192ffbe3d
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/com.coplaydev.unity-mcp/Editor/Clients.meta
Normal file
8
Packages/com.coplaydev.unity-mcp/Editor/Clients.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c9d47f01d06964ee7843765d1bd71205
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 59ff83375c2c74c8385c4a22549778dd
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 331b33961513042e3945d0a1d06615b5
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6de06c6bb0399154d840a1e4c84be869
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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'"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d0d22681fc594475db1c189f2d9abdf7
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d5e5d87c9db57495f842dc366f1ebd65
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6b8abf0951c7413d9ff97a053b0adf2d
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 923728a98c8c74cfaa6e9203c408f34e
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c7037ef8b168e49f79247cb31c3be75a
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 14a4b9a7f749248d496466c2a3a53e56
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b708eda314746481fb8f4a1fb0652b03
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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'"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c5e9bbb45e552453ab5cb557a22d43e7
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3286d62ffe5644f5ea60488fd7e6513d
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e9b73ff071a6043dda1f2ec7d682ef71
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 19f29c88761345158fc766d24e7c18f3
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 489f99ffb7e6743e88e3203552c8b37b
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 46891bcdb00e468cbd04afbfb8f3095e
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2511b0d05271d486bb61f8cc9fd11363
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b3ab39e22ae0948ab94beae307f9902e
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bcc7ead475a4d4ea2978151c217757b8
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2c4a1b0d3b34489cbf0f8c40c49c4f3b
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b528971e189f141d38db577f155bd222
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8d408fd7733cb4a1eb80f785307db2ff
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4ce08555f995e4e848a826c63f18cb35
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/com.coplaydev.unity-mcp/Editor/Constants.meta
Normal file
8
Packages/com.coplaydev.unity-mcp/Editor/Constants.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f7e009cbf3e74f6c987331c2b438ec59
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 96844bc39e9a94cf18b18f8127f3854f
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7317786cfb9304b0db20ca73a774b9fa
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c15ed2426f43860479f1b8a99a343d16
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 221a4d6e595be6897a5b17b77aedd4d0
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4a6d2236d370b4f1db4d0e3d3ce0dcac
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4c0f2e87395b4c6c9df8c21b6d0fae13
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f6df82faa423f4e9ebb13a3dcee8ba19
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ddeeeca2f876f4083a84417404175199
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bdbaced669d14798a4ceeebfbff2b22c
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 67d73d0e8caef4e60942f4419c6b76bf
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b682b492eb80d4ed6834b76f72c9f0f3
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c6f602b0a8ca848859197f9a949a7a5d
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 44d715aedea2b8b41bf914433bbb2c49
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1aedc29caa5704c07b487d20a27e9334
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/com.coplaydev.unity-mcp/Editor/External.meta
Normal file
8
Packages/com.coplaydev.unity-mcp/Editor/External.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c11944bcfb9ec4576bab52874b7df584
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
2138
Packages/com.coplaydev.unity-mcp/Editor/External/Tommy.cs
vendored
Normal file
2138
Packages/com.coplaydev.unity-mcp/Editor/External/Tommy.cs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
11
Packages/com.coplaydev.unity-mcp/Editor/External/Tommy.cs.meta
vendored
Normal file
11
Packages/com.coplaydev.unity-mcp/Editor/External/Tommy.cs.meta
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ea652131dcdaa44ca8cb35cd1191be3f
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/com.coplaydev.unity-mcp/Editor/Helpers.meta
Normal file
8
Packages/com.coplaydev.unity-mcp/Editor/Helpers.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 94cb070dc5e15024da86150b27699ca0
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1d42f5b5ea5d4d43ad1a771e14bda2a0
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b3e68082ffc0b4cd39d3747673a4cc22
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
997
Packages/com.coplaydev.unity-mcp/Editor/Helpers/ComponentOps.cs
Normal file
997
Packages/com.coplaydev.unity-mcp/Editor/Helpers/ComponentOps.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 13dead161bc4540eeb771961df437779
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
Loading…
x
Reference in New Issue
Block a user