1+ using System ;
2+ using System . IO ;
3+ using System . Linq ;
4+ using System . Text . RegularExpressions ;
5+ using Newtonsoft . Json . Linq ;
6+ using UnityEditor ;
7+ using UnityEngine ;
8+ using UnityMcpBridge . Editor . Helpers ;
9+
10+ namespace UnityMcpBridge . Editor . Tools
11+ {
12+ /// <summary>
13+ /// Handles CRUD operations for shader files within the Unity project.
14+ /// </summary>
15+ public static class ManageShader
16+ {
17+ /// <summary>
18+ /// Main handler for shader management actions.
19+ /// </summary>
20+ public static object HandleCommand ( JObject @params )
21+ {
22+ // Extract parameters
23+ string action = @params [ "action" ] ? . ToString ( ) . ToLower ( ) ;
24+ string name = @params [ "name" ] ? . ToString ( ) ;
25+ string path = @params [ "path" ] ? . ToString ( ) ; // Relative to Assets/
26+ string contents = null ;
27+
28+ // Check if we have base64 encoded contents
29+ bool contentsEncoded = @params [ "contentsEncoded" ] ? . ToObject < bool > ( ) ?? false ;
30+ if ( contentsEncoded && @params [ "encodedContents" ] != null )
31+ {
32+ try
33+ {
34+ contents = DecodeBase64 ( @params [ "encodedContents" ] . ToString ( ) ) ;
35+ }
36+ catch ( Exception e )
37+ {
38+ return Response . Error ( $ "Failed to decode shader contents: { e . Message } ") ;
39+ }
40+ }
41+ else
42+ {
43+ contents = @params [ "contents" ] ? . ToString ( ) ;
44+ }
45+
46+ // Validate required parameters
47+ if ( string . IsNullOrEmpty ( action ) )
48+ {
49+ return Response . Error ( "Action parameter is required." ) ;
50+ }
51+ if ( string . IsNullOrEmpty ( name ) )
52+ {
53+ return Response . Error ( "Name parameter is required." ) ;
54+ }
55+ // Basic name validation (alphanumeric, underscores, cannot start with number)
56+ if ( ! Regex . IsMatch ( name , @"^[a-zA-Z_][a-zA-Z0-9_]*$" ) )
57+ {
58+ return Response . Error (
59+ $ "Invalid shader name: '{ name } '. Use only letters, numbers, underscores, and don't start with a number."
60+ ) ;
61+ }
62+
63+ // Ensure path is relative to Assets/, removing any leading "Assets/"
64+ // Set default directory to "Shaders" if path is not provided
65+ string relativeDir = path ?? "Shaders" ; // Default to "Shaders" if path is null
66+ if ( ! string . IsNullOrEmpty ( relativeDir ) )
67+ {
68+ relativeDir = relativeDir . Replace ( '\\ ' , '/' ) . Trim ( '/' ) ;
69+ if ( relativeDir . StartsWith ( "Assets/" , StringComparison . OrdinalIgnoreCase ) )
70+ {
71+ relativeDir = relativeDir . Substring ( "Assets/" . Length ) . TrimStart ( '/' ) ;
72+ }
73+ }
74+ // Handle empty string case explicitly after processing
75+ if ( string . IsNullOrEmpty ( relativeDir ) )
76+ {
77+ relativeDir = "Shaders" ; // Ensure default if path was provided as "" or only "/" or "Assets/"
78+ }
79+
80+ // Construct paths
81+ string shaderFileName = $ "{ name } .shader";
82+ string fullPathDir = Path . Combine ( Application . dataPath , relativeDir ) ;
83+ string fullPath = Path . Combine ( fullPathDir , shaderFileName ) ;
84+ string relativePath = Path . Combine ( "Assets" , relativeDir , shaderFileName )
85+ . Replace ( '\\ ' , '/' ) ; // Ensure "Assets/" prefix and forward slashes
86+
87+ // Ensure the target directory exists for create/update
88+ if ( action == "create" || action == "update" )
89+ {
90+ try
91+ {
92+ if ( ! Directory . Exists ( fullPathDir ) )
93+ {
94+ Directory . CreateDirectory ( fullPathDir ) ;
95+ // Refresh AssetDatabase to recognize new folders
96+ AssetDatabase . Refresh ( ) ;
97+ }
98+ }
99+ catch ( Exception e )
100+ {
101+ return Response . Error (
102+ $ "Could not create directory '{ fullPathDir } ': { e . Message } "
103+ ) ;
104+ }
105+ }
106+
107+ // Route to specific action handlers
108+ switch ( action )
109+ {
110+ case "create" :
111+ return CreateShader ( fullPath , relativePath , name , contents ) ;
112+ case "read" :
113+ return ReadShader ( fullPath , relativePath ) ;
114+ case "update" :
115+ return UpdateShader ( fullPath , relativePath , name , contents ) ;
116+ case "delete" :
117+ return DeleteShader ( fullPath , relativePath ) ;
118+ default :
119+ return Response . Error (
120+ $ "Unknown action: '{ action } '. Valid actions are: create, read, update, delete."
121+ ) ;
122+ }
123+ }
124+
125+ /// <summary>
126+ /// Decode base64 string to normal text
127+ /// </summary>
128+ private static string DecodeBase64 ( string encoded )
129+ {
130+ byte [ ] data = Convert . FromBase64String ( encoded ) ;
131+ return System . Text . Encoding . UTF8 . GetString ( data ) ;
132+ }
133+
134+ /// <summary>
135+ /// Encode text to base64 string
136+ /// </summary>
137+ private static string EncodeBase64 ( string text )
138+ {
139+ byte [ ] data = System . Text . Encoding . UTF8 . GetBytes ( text ) ;
140+ return Convert . ToBase64String ( data ) ;
141+ }
142+
143+ private static object CreateShader (
144+ string fullPath ,
145+ string relativePath ,
146+ string name ,
147+ string contents
148+ )
149+ {
150+ // Check if shader already exists
151+ if ( File . Exists ( fullPath ) )
152+ {
153+ return Response . Error (
154+ $ "Shader already exists at '{ relativePath } '. Use 'update' action to modify."
155+ ) ;
156+ }
157+
158+ // Add validation for shader name conflicts in Unity
159+ if ( Shader . Find ( name ) != null )
160+ {
161+ return Response . Error (
162+ $ "A shader with name '{ name } ' already exists in the project. Choose a different name."
163+ ) ;
164+ }
165+
166+ // Generate default content if none provided
167+ if ( string . IsNullOrEmpty ( contents ) )
168+ {
169+ contents = GenerateDefaultShaderContent ( name ) ;
170+ }
171+
172+ try
173+ {
174+ File . WriteAllText ( fullPath , contents ) ;
175+ AssetDatabase . ImportAsset ( relativePath ) ;
176+ AssetDatabase . Refresh ( ) ; // Ensure Unity recognizes the new shader
177+ return Response . Success (
178+ $ "Shader '{ name } .shader' created successfully at '{ relativePath } '.",
179+ new { path = relativePath }
180+ ) ;
181+ }
182+ catch ( Exception e )
183+ {
184+ return Response . Error ( $ "Failed to create shader '{ relativePath } ': { e . Message } ") ;
185+ }
186+ }
187+
188+ private static object ReadShader ( string fullPath , string relativePath )
189+ {
190+ if ( ! File . Exists ( fullPath ) )
191+ {
192+ return Response . Error ( $ "Shader not found at '{ relativePath } '.") ;
193+ }
194+
195+ try
196+ {
197+ string contents = File . ReadAllText ( fullPath ) ;
198+
199+ // Return both normal and encoded contents for larger files
200+ //TODO: Consider a threshold for large files
201+ bool isLarge = contents . Length > 10000 ; // If content is large, include encoded version
202+ var responseData = new
203+ {
204+ path = relativePath ,
205+ contents = contents ,
206+ // For large files, also include base64-encoded version
207+ encodedContents = isLarge ? EncodeBase64 ( contents ) : null ,
208+ contentsEncoded = isLarge ,
209+ } ;
210+
211+ return Response . Success (
212+ $ "Shader '{ Path . GetFileName ( relativePath ) } ' read successfully.",
213+ responseData
214+ ) ;
215+ }
216+ catch ( Exception e )
217+ {
218+ return Response . Error ( $ "Failed to read shader '{ relativePath } ': { e . Message } ") ;
219+ }
220+ }
221+
222+ private static object UpdateShader (
223+ string fullPath ,
224+ string relativePath ,
225+ string name ,
226+ string contents
227+ )
228+ {
229+ if ( ! File . Exists ( fullPath ) )
230+ {
231+ return Response . Error (
232+ $ "Shader not found at '{ relativePath } '. Use 'create' action to add a new shader."
233+ ) ;
234+ }
235+ if ( string . IsNullOrEmpty ( contents ) )
236+ {
237+ return Response . Error ( "Content is required for the 'update' action." ) ;
238+ }
239+
240+ try
241+ {
242+ File . WriteAllText ( fullPath , contents ) ;
243+ AssetDatabase . ImportAsset ( relativePath ) ;
244+ AssetDatabase . Refresh ( ) ;
245+ return Response . Success (
246+ $ "Shader '{ Path . GetFileName ( relativePath ) } ' updated successfully.",
247+ new { path = relativePath }
248+ ) ;
249+ }
250+ catch ( Exception e )
251+ {
252+ return Response . Error ( $ "Failed to update shader '{ relativePath } ': { e . Message } ") ;
253+ }
254+ }
255+
256+ private static object DeleteShader ( string fullPath , string relativePath )
257+ {
258+ if ( ! File . Exists ( fullPath ) )
259+ {
260+ return Response . Error ( $ "Shader not found at '{ relativePath } '.") ;
261+ }
262+
263+ try
264+ {
265+ // Delete the asset through Unity's AssetDatabase first
266+ bool success = AssetDatabase . DeleteAsset ( relativePath ) ;
267+ if ( ! success )
268+ {
269+ return Response . Error ( $ "Failed to delete shader through Unity's AssetDatabase: '{ relativePath } '") ;
270+ }
271+
272+ // If the file still exists (rare case), try direct deletion
273+ if ( File . Exists ( fullPath ) )
274+ {
275+ File . Delete ( fullPath ) ;
276+ }
277+
278+ return Response . Success ( $ "Shader '{ Path . GetFileName ( relativePath ) } ' deleted successfully.") ;
279+ }
280+ catch ( Exception e )
281+ {
282+ return Response . Error ( $ "Failed to delete shader '{ relativePath } ': { e . Message } ") ;
283+ }
284+ }
285+
286+ //This is a CGProgram template
287+ //TODO: making a HLSL template as well?
288+ private static string GenerateDefaultShaderContent ( string name )
289+ {
290+ return @"Shader """ + name + @"""
291+ {
292+ Properties
293+ {
294+ _MainTex (""Texture"", 2D) = ""white"" {}
295+ }
296+ SubShader
297+ {
298+ Tags { ""RenderType""=""Opaque"" }
299+ LOD 100
300+
301+ Pass
302+ {
303+ CGPROGRAM
304+ #pragma vertex vert
305+ #pragma fragment frag
306+ #include ""UnityCG.cginc""
307+
308+ struct appdata
309+ {
310+ float4 vertex : POSITION;
311+ float2 uv : TEXCOORD0;
312+ };
313+
314+ struct v2f
315+ {
316+ float2 uv : TEXCOORD0;
317+ float4 vertex : SV_POSITION;
318+ };
319+
320+ sampler2D _MainTex;
321+ float4 _MainTex_ST;
322+
323+ v2f vert (appdata v)
324+ {
325+ v2f o;
326+ o.vertex = UnityObjectToClipPos(v.vertex);
327+ o.uv = TRANSFORM_TEX(v.uv, _MainTex);
328+ return o;
329+ }
330+
331+ fixed4 frag (v2f i) : SV_Target
332+ {
333+ fixed4 col = tex2D(_MainTex, i.uv);
334+ return col;
335+ }
336+ ENDCG
337+ }
338+ }
339+ }" ;
340+ }
341+ }
342+ }
0 commit comments