using System; using System.IO; using JetBrains.Annotations; using LLM.Editor.Core; using LLM.Editor.Helper; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; namespace LLM.Editor.Commands { public enum AssetType { Folder, Text, Script, Material, AssemblyDefinition, Prefab } [Serializable] public class CreateAssetParams { public AssetType assetType; public string assetName; public string logicalName; // A unique name for the LLM to reference this object later public string path; // Relative to Assets folder public string content; public string sourceAssetGuid; // Optional, for creating prefabs from models public string sourceGameObjectIdentifier; // Optional, for creating prefabs from scene objects public string shaderName; // Optional, for creating materials } [UsedImplicitly] public class CreateAssetCommand : ICommand { private readonly CreateAssetParams _params; public CreateAssetCommand(string jsonParams) { _params = jsonParams?.FromJson(); } public CommandOutcome Execute(Data.CommandContext context) { if (_params == null || string.IsNullOrEmpty(_params.assetName)) { context.ErrorMessage = "Invalid parameters: assetName is required."; return CommandOutcome.Error; } var relativePath = _params.path?.Replace("Assets/", string.Empty) ?? ""; var fullPath = Path.Combine(Application.dataPath, relativePath, _params.assetName); var assetPathForDb = $"Assets/{Path.Combine(relativePath, _params.assetName)}"; var dirName = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(dirName) && !Directory.Exists(dirName)) { Directory.CreateDirectory(dirName); } try { switch (_params.assetType) { case AssetType.Folder: Directory.CreateDirectory(fullPath); assetPathForDb += "/"; break; case AssetType.Text: File.WriteAllText(fullPath, _params.content ?? ""); break; case AssetType.Script: assetPathForDb += ".cs"; fullPath += ".cs"; File.WriteAllText(fullPath, _params.content ?? "using UnityEngine;\n\npublic class NewScript : MonoBehaviour\n{\n}\n"); break; case AssetType.Material: assetPathForDb += ".mat"; var shader = string.IsNullOrEmpty(_params.shaderName) ? Shader.Find("Standard") : Shader.Find(_params.shaderName); if (shader == null) { Debug.LogWarning($"[CreateAssetCommand] Shader '{_params.shaderName}' not found. Falling back to Standard."); shader = Shader.Find("Standard"); } var material = new Material(shader); AssetDatabase.CreateAsset(material, assetPathForDb); break; case AssetType.AssemblyDefinition: assetPathForDb += ".asmdef"; var asmdefContent = _params.content ?? $"{{\n \"name\": \"{_params.assetName}\"\n}}"; File.WriteAllText(assetPathForDb, asmdefContent); break; case AssetType.Prefab: assetPathForDb += ".prefab"; CreatePrefab(context, assetPathForDb); break; default: context.ErrorMessage = $"Unsupported asset type: {_params.assetType}"; return CommandOutcome.Error; } AssetDatabase.Refresh(); // Folders don't have GUIDs in the same way assets do, so we handle them differently. // Their path is their identifier. if (_params.assetType == AssetType.Folder) { var logicalNameForFolder = !string.IsNullOrEmpty(_params.logicalName) ? _params.logicalName : $"{_params.assetName}_{Guid.NewGuid():N}"; context.IdentifierMap[logicalNameForFolder] = assetPathForDb; SessionManager.SaveIdentifierMap(context.IdentifierMap); context.CurrentSubject = logicalNameForFolder; Debug.Log($"[CreateAssetCommand] Successfully created folder '{_params.assetName}' and mapped it to logical name '{logicalNameForFolder}'."); return CommandOutcome.Success; } // For all other assets, we require a valid GUID. var assetGuid = AssetDatabase.AssetPathToGUID(assetPathForDb); if (string.IsNullOrEmpty(assetGuid)) { AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); assetGuid = AssetDatabase.AssetPathToGUID(assetPathForDb); if (string.IsNullOrEmpty(assetGuid)) { throw new Exception($"Could not get GUID for newly created asset at path: {assetPathForDb}"); } } var logicalName = !string.IsNullOrEmpty(_params.logicalName) ? _params.logicalName : $"{_params.assetName}_{Guid.NewGuid():N}"; context.IdentifierMap[logicalName] = assetGuid; SessionManager.SaveIdentifierMap(context.IdentifierMap); context.CurrentSubject = logicalName; Debug.Log($"[CreateAssetCommand] Successfully created asset '{_params.assetName}' and mapped it to logical name '{logicalName}'."); return CommandOutcome.Success; } catch (Exception e) { context.ErrorMessage = $"Failed to create asset '{_params.assetName}'. Exception: {e.Message}"; Debug.LogException(e); return CommandOutcome.Error; } } private void CreatePrefab(Data.CommandContext context, string finalPrefabPath) { GameObject sourceObject = null; if (!string.IsNullOrEmpty(_params.sourceGameObjectIdentifier)) { var resolvedObject = CommandUtility.ResolveIdentifier(context, _params.sourceGameObjectIdentifier); if (resolvedObject is GameObject go) { sourceObject = go; } else { throw new Exception($"Could not resolve scene object with logical name: '{_params.sourceGameObjectIdentifier}'"); } } else if (!string.IsNullOrEmpty(_params.sourceAssetGuid)) { var sourcePath = AssetDatabase.GUIDToAssetPath(_params.sourceAssetGuid); sourceObject = AssetDatabase.LoadAssetAtPath(sourcePath); if (sourceObject == null) { throw new Exception($"Could not find source asset with GUID: {_params.sourceAssetGuid}"); } } else { sourceObject = new GameObject(_params.assetName); // Save the new empty object as a prefab, then immediately destroy the instance. PrefabUtility.SaveAsPrefabAsset(sourceObject, finalPrefabPath); Object.DestroyImmediate(sourceObject); return; } // For existing scene objects or assets, save as a prefab. PrefabUtility.SaveAsPrefabAssetAndConnect(sourceObject, finalPrefabPath, InteractionMode.UserAction); } } }