592 lines
18 KiB
C#
592 lines
18 KiB
C#
//
|
|
// This file is part of the Tremble package by Tiny Goose.
|
|
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
|
|
//
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace TinyGoose.Tremble.Editor
|
|
{
|
|
[ManualPage("Entities/Prefab Entity Builder", showInTree: false)]
|
|
public class PrefabBuilderPage : BuilderPageBase<PrefabBuilderPage.SampleData>
|
|
{
|
|
private static readonly Dictionary<Type, string> s_ScriptFileLookup = new();
|
|
|
|
protected override bool HasInitialQuestions => true;
|
|
|
|
private bool m_MonoBehaviourListDirty = false;
|
|
private readonly List<Type> m_CandidateTypes = new();
|
|
|
|
private readonly TrembleFieldConverterCollection m_FieldConverterCollection = new();
|
|
|
|
private Type m_UnityScriptType;
|
|
private string m_NewUnityScriptName;
|
|
private Action m_ResolutionAction;
|
|
|
|
private bool m_IsOnlyAddingSpawnablePrefab = false;
|
|
|
|
private bool m_IsAlreadyTrembleSpawnable = false;
|
|
private bool m_IsAlreadyPrefabEntity = false;
|
|
private bool m_IsMap = false;
|
|
|
|
private bool IsNewScript => !m_NewUnityScriptName.IsNullOrEmpty();
|
|
private string ScriptName => m_UnityScriptType?.Name ?? m_NewUnityScriptName;
|
|
|
|
public class SampleData : ScriptableObject
|
|
{
|
|
[SerializeField] internal GameObject m_BasePrefab;
|
|
[SerializeField] internal bool m_WantsData;
|
|
|
|
[SerializeField] internal string m_Category = "";
|
|
[SerializeField] internal GameObject[] m_ExcludedPrefabs = Array.Empty<GameObject>();
|
|
[SerializeField] internal GameObject[] m_IncludedPrefabs = Array.Empty<GameObject>();
|
|
[SerializeField] internal bool m_OnlyVariants = false;
|
|
}
|
|
|
|
protected override void OnInitialQuestionsGUI()
|
|
{
|
|
Text("Please select a prefab that you want to use in Tremble!");
|
|
|
|
m_MonoBehaviourListDirty = RenderProperty(nameof(SampleData.m_BasePrefab),
|
|
title: "Target Prefab",
|
|
description: "The prefab to use");
|
|
|
|
RenderProperty(nameof(SampleData.m_WantsData),
|
|
title: "I want to add data",
|
|
description: "Whether to allow setting fields on the prefab from TrenchBroom. e.g. "+
|
|
"variables such as Health, Colour, etc?");
|
|
|
|
if (Sample.m_BasePrefab)
|
|
{
|
|
if (m_MonoBehaviourListDirty)
|
|
{
|
|
Type[] monoBehaviours = Sample.m_BasePrefab
|
|
.GetComponents<MonoBehaviour>()
|
|
.Select(m => m.GetType())
|
|
.ToArray();
|
|
|
|
m_CandidateTypes.Clear();
|
|
m_CandidateTypes.AddRange(monoBehaviours
|
|
.Where(t => t != typeof(TrembleSpawnablePrefab)));
|
|
|
|
m_IsAlreadyTrembleSpawnable = monoBehaviours.Contains(typeof(TrembleSpawnablePrefab));
|
|
m_IsMap = monoBehaviours.Contains(typeof(MapDocument));
|
|
m_IsAlreadyPrefabEntity = !m_IsAlreadyTrembleSpawnable && monoBehaviours.Any(t => t.HasCustomAttribute<PrefabEntityAttribute>());
|
|
}
|
|
|
|
if (m_IsMap)
|
|
{
|
|
Text("Ha! No, that's a map prefab. We better not.");
|
|
}
|
|
else if (m_IsAlreadyPrefabEntity)
|
|
{
|
|
Text("Well, that's already spawnable in TrenchBroom. Try another prefab!");
|
|
}
|
|
else if (m_CandidateTypes.Count == 0)
|
|
{
|
|
if (m_IsAlreadyTrembleSpawnable)
|
|
{
|
|
Callout("Note, this prefab already has a TrembleSpawnablePrefab component",
|
|
"allowing it to be spawned from TrenchBroom - but without field support.",
|
|
"Converting this to a PrefabEntity will remove the TrembleSpawnablePrefab component!");
|
|
}
|
|
|
|
if (m_IsAlreadyTrembleSpawnable)
|
|
{
|
|
if (Sample.m_WantsData)
|
|
{
|
|
Resolution("Create a new MonoBehaviour to use - this allows",
|
|
"you to add fields which can be set in TrenchBroom",
|
|
"- in order to be able to set data (i.e. Health, Type, etc).");
|
|
ActionBar_SingleAction("Create New MonoBehaviour", () =>
|
|
{
|
|
m_NewUnityScriptName = GetScriptNameFromPrefab(Sample.m_BasePrefab);
|
|
|
|
if (Sample.m_BasePrefab.TryGetComponent(out TrembleSpawnablePrefab tsp))
|
|
{
|
|
Sample.m_Category = tsp.Category;
|
|
Sample.m_OnlyVariants = tsp.OnlyVariants;
|
|
}
|
|
|
|
SetResolution(Resolve_CreateNewClassAndAdd);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
Text("Well, that's already spawnable in TrenchBroom. Tick the box above",
|
|
"to add data using a [PrefabEntity].");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Sample.m_WantsData)
|
|
{
|
|
Resolution("Create a new MonoBehaviour to use - this allows",
|
|
"you to add fields which can be set in TrenchBroom",
|
|
"- in order to be able to set data (i.e. Health, Type, etc).");
|
|
ActionBar_SingleAction("Create New MonoBehaviour", () =>
|
|
{
|
|
m_NewUnityScriptName = GetScriptNameFromPrefab(Sample.m_BasePrefab);
|
|
|
|
if (Sample.m_BasePrefab.TryGetComponent(out TrembleSpawnablePrefab tsp))
|
|
{
|
|
Sample.m_Category = tsp.Category;
|
|
Sample.m_OnlyVariants = tsp.OnlyVariants;
|
|
}
|
|
|
|
SetResolution(Resolve_CreateNewClassAndAdd);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
Resolution("Add a 'TrembleSpawnablePrefab' component - this",
|
|
"only allows you to spawn this prefab without being able to",
|
|
"set any data on it.");
|
|
|
|
ActionBar_SingleAction("Add TrembleSpawnablePrefab component", () =>
|
|
{
|
|
m_IsOnlyAddingSpawnablePrefab = true;
|
|
SetResolution(Resolve_AddTrembleSpawnablePrefab);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
else if (m_IsAlreadyTrembleSpawnable)
|
|
{
|
|
// Already spawnable, but no candidates for [PrefabEntity] - make a new code class
|
|
if (Sample.m_WantsData)
|
|
{
|
|
Resolution("Upgrade to a [PrefabEntity] - this allows",
|
|
"you to add fields which can be set in TrenchBroom",
|
|
"- in order to be able to set data (i.e. Health, Type, etc).");
|
|
|
|
foreach (Type type in m_CandidateTypes)
|
|
{
|
|
Bullet(type.Name);
|
|
ActionBar_SingleAction($"Add [PrefabEntity] to {type.Name}", () =>
|
|
{
|
|
m_UnityScriptType = type;
|
|
|
|
if (Sample.m_BasePrefab.TryGetComponent(out TrembleSpawnablePrefab tsp))
|
|
{
|
|
Sample.m_Category = tsp.Category;
|
|
Sample.m_OnlyVariants = tsp.OnlyVariants;
|
|
}
|
|
|
|
SetResolution(Resolve_AddPrefabEntityAttribute);
|
|
});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Text("This is already a TrembleSpawnablePrefab. Unless you want to add data,",
|
|
"you don't need to do anything!");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Sample.m_WantsData)
|
|
{
|
|
if (m_CandidateTypes.Count == 1)
|
|
{
|
|
Type type = m_CandidateTypes[0];
|
|
|
|
Resolution($"You can add a [PrefabEntity] attribute to {type.Name} on this",
|
|
"prefab. This will enable setting the following fields:");
|
|
|
|
Indent(() =>
|
|
{
|
|
foreach (string fieldName in GetFieldNames(type))
|
|
{
|
|
Bullet(fieldName);
|
|
}
|
|
});
|
|
|
|
ActionBar_SingleAction($"Add [PrefabEntity]", () =>
|
|
{
|
|
m_UnityScriptType = type;
|
|
SetResolution(Resolve_AddPrefabEntityAttribute);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
Resolution("This prefab has MonoBehaviours which can be set to be a",
|
|
"[PrefabEntity]. Select which one to add this attribute to.");
|
|
|
|
foreach (Type type in m_CandidateTypes)
|
|
{
|
|
string[] fieldNames = GetFieldNames(type);
|
|
|
|
string fields = (fieldNames is { Length: > 0 }) ? String.Join(", ", fieldNames) : "no fields";
|
|
Bullet($"{type.Name} ({fields})");
|
|
|
|
ActionBar_SingleAction($"Add [PrefabEntity] to {type.Name}", () =>
|
|
{
|
|
m_UnityScriptType = type;
|
|
SetResolution(Resolve_AddPrefabEntityAttribute);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Resolution("Simply add a TrembleSpawnablePrefab component to it, which will",
|
|
"allow you to spawn the prefab in TrenchBroom, but not allow you to set any",
|
|
"data on it.");
|
|
ActionBar_SingleAction("Add TrembleSpawnablePrefab component", () =>
|
|
{
|
|
m_IsOnlyAddingSpawnablePrefab = true;
|
|
SetResolution(Resolve_AddTrembleSpawnablePrefab);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private string[] GetFieldNames(Type type)
|
|
{
|
|
List<Type> soTypes= new();
|
|
FgdClass dummyClass = new(FgdClassType.Point, type.Name, type.Name);
|
|
TrembleSync.AddExposedFieldsToEntity(type, dummyClass, m_FieldConverterCollection, soTypes);
|
|
|
|
// Gather spawnflags
|
|
NamingConvention spawnFlagNaming = TrembleSyncSettings.Get().SpawnFlagNamingConvention;
|
|
|
|
IEnumerable<string> spawnFlagNames = dummyClass.AllSpawnFlags
|
|
.Select(f =>
|
|
{
|
|
string name = f.FieldName.ToNamingConvention(spawnFlagNaming);
|
|
if (spawnFlagNaming == NamingConvention.HumanFriendly)
|
|
{
|
|
name += "?";
|
|
}
|
|
|
|
return $"{name} (Boolean)";
|
|
});
|
|
|
|
return dummyClass.AllFields
|
|
.Select(f => $"{f.FieldName} ({f.FriendlyTypeName})")
|
|
.Concat(spawnFlagNames)
|
|
.ToArray();
|
|
}
|
|
|
|
protected override void OnPropertiesGUI()
|
|
{
|
|
H2("Optional Extras");
|
|
|
|
RenderProperty(nameof(SampleData.m_Category),
|
|
title: "Category Name",
|
|
description: "(optional) The category/group name to use in TrenchBroom. Leave this blank to use the default.");
|
|
|
|
Foldout("(optional) Include/Exclude specific Prefabs?", () =>
|
|
{
|
|
if (!m_IsOnlyAddingSpawnablePrefab)
|
|
{
|
|
Text("By default, any other prefabs containing your MonoBehaviour will be exported.");
|
|
Text("Instead, you can choose to explicitly whitelist prefab(s) here.");
|
|
Text("Or, you can choose to blacklist certain prefab(s) here.");
|
|
|
|
RenderProperty(nameof(SampleData.m_IncludedPrefabs),
|
|
title: "Included Prefabs",
|
|
description: $"A whitelist of the ONLY prefab(s) to include, even though others may include your {ScriptName} MonoBehaviour?");
|
|
|
|
RenderProperty(nameof(SampleData.m_ExcludedPrefabs),
|
|
title: "Excluded Prefabs",
|
|
description: $"A blacklist of any prefab(s) to ignore, even though they include your {ScriptName} MonoBehaviour?");
|
|
}
|
|
|
|
RenderProperty(nameof(SampleData.m_OnlyVariants),
|
|
title: "Only Prefab Variants?",
|
|
description: "Whether to only export VARIANTS of the prefab(s) found?");
|
|
});
|
|
}
|
|
|
|
protected override void OnPostPropertiesGUI()
|
|
{
|
|
string catName = Sample.m_Category.IsNullOrEmpty() ? FgdConsts.PREFAB_PREFIX : Sample.m_Category;
|
|
//string entName = Sample.m_UnityName.ToNamingConvention(SyncSettings.TypeNamingConvention);
|
|
string fullEntName = $"{catName}_{Sample.m_BasePrefab.name}";
|
|
|
|
if (m_IsOnlyAddingSpawnablePrefab)
|
|
{
|
|
H2("Tremble Spawnable Prefab Details");
|
|
PropertyDescription("Prefab Name", labelWidth: 250f, Sample.m_BasePrefab.name);
|
|
PropertyDescription("Exported TrenchBroom Name", labelWidth: 250f, fullEntName);
|
|
}
|
|
else
|
|
{
|
|
H2("C# Code");
|
|
Text($"This is the code that will be used for {Sample.m_BasePrefab.name} if you click Apply below. You don't need to copy",
|
|
"it manually (unless you want to!)");
|
|
CodeWithHeader($"C# Script: {ScriptName}.cs (for TrenchBroom Prefab entity '{fullEntName}')", GenerateFullCodeSnippet());
|
|
}
|
|
|
|
Space();
|
|
|
|
GUI.color = Color.green;
|
|
LargeButton("Apply & Finish!", Resolve);
|
|
GUI.color = Color.white;
|
|
}
|
|
|
|
private void Resolution(params string[] resolution)
|
|
{
|
|
HorizontalLine();
|
|
|
|
H1("Action to take");
|
|
Text(resolution);
|
|
}
|
|
|
|
private string GetAttributeLine()
|
|
{
|
|
List<string> attributeParts = new(16);
|
|
|
|
if (!Sample.m_Category.IsNullOrEmpty())
|
|
{
|
|
attributeParts.Add($"category: \"{Sample.m_Category}\"");
|
|
}
|
|
|
|
if (TryFormatPrefabNames("excludePrefab", Sample.m_ExcludedPrefabs, out string excludePart))
|
|
{
|
|
attributeParts.Add(excludePart);
|
|
}
|
|
|
|
if (TryFormatPrefabNames("includePrefab", Sample.m_IncludedPrefabs, out string includePart))
|
|
{
|
|
attributeParts.Add(includePart);
|
|
}
|
|
|
|
if (Sample.m_OnlyVariants)
|
|
{
|
|
attributeParts.Add("onlyVariants: true");
|
|
}
|
|
|
|
string attrName = FormatAttributeName(typeof(PrefabEntityAttribute));
|
|
|
|
return attributeParts.Count == 0
|
|
? $"[{attrName}]"
|
|
: $"[{attrName}({String.Join(", ", attributeParts)})]";
|
|
}
|
|
|
|
private string GenerateFullCodeSnippet()
|
|
{
|
|
return
|
|
"using UnityEngine;\n" +
|
|
$"using {nameof(TinyGoose)}.{nameof(Tremble)};\n" +
|
|
"\n" +
|
|
GetAttributeLine() + "\n" +
|
|
$"public class {ScriptName} : MonoBehaviour\n" +
|
|
"{\n" +
|
|
(IsNewScript ? $" // Your new {ScriptName} class contents ;)" : $" // (your existing {ScriptName} code)") + "\n" +
|
|
"}";
|
|
}
|
|
|
|
private bool TryFormatPrefabNames(string propertyName, GameObject[] prefabs, out string formatted)
|
|
{
|
|
GameObject[] filteredPrefabs = prefabs.Where(p => p).ToArray();
|
|
|
|
switch (filteredPrefabs.Length)
|
|
{
|
|
case 1:
|
|
formatted = $"{propertyName}: \"{filteredPrefabs[0].name}\"";
|
|
return true;
|
|
case > 1:
|
|
{
|
|
string prefabList = String.Join("\", \"", filteredPrefabs.Select(p => p.name));
|
|
formatted = $"{propertyName}s: \"{prefabList}\"";
|
|
return true;
|
|
}
|
|
}
|
|
|
|
formatted = default;
|
|
return false;
|
|
}
|
|
|
|
private void SetResolution(Action resolutionAction)
|
|
{
|
|
m_ResolutionAction = resolutionAction;
|
|
MarkInitialQuestionsDone();
|
|
}
|
|
|
|
private void Resolve()
|
|
{
|
|
m_ResolutionAction.Invoke();
|
|
GoToPage(typeof(PrefabEntitiesPage));
|
|
}
|
|
|
|
private void Resolve_AddTrembleSpawnablePrefab()
|
|
{
|
|
Sample.m_BasePrefab.AddComponent<TrembleSpawnablePrefab>().Modify(so =>
|
|
{
|
|
so.FindBackedProperty(nameof(TrembleSpawnablePrefab.OnlyVariants)).boolValue = Sample.m_OnlyVariants;
|
|
so.FindBackedProperty(nameof(TrembleSpawnablePrefab.Category)).stringValue = Sample.m_Category;
|
|
});
|
|
|
|
EditorUtility.SetDirty(Sample.m_BasePrefab);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
EditorApplication.delayCall += TrembleEditorAPI.SyncToTrenchBroom;
|
|
|
|
ShowCompleteDialog(
|
|
title: "TrembleSpawnablePrefab component added!",
|
|
message: $"{Sample.m_BasePrefab.name} now has a Tremble Spawnable Prefab component and should be available from TrenchBroom.");
|
|
}
|
|
|
|
private void Resolve_CreateNewClassAndAdd()
|
|
{
|
|
// Write code and compile
|
|
string assetsFolder = Path.Combine(Directory.GetCurrentDirectory(), "Assets");
|
|
|
|
string scriptFilePath = EditorUtility.SaveFilePanel($"Choose where to save {ScriptName}", assetsFolder, $"{ScriptName}.cs", "cs");
|
|
if (scriptFilePath.IsNullOrEmpty())
|
|
return;
|
|
|
|
File.WriteAllText(scriptFilePath, GenerateFullCodeSnippet());
|
|
|
|
EditorCompileUtil.CompileThen(typeof(PrefabBuilderPage), nameof(OnPostCompile_AddNewScript),
|
|
ScriptName, AssetDatabase.GetAssetPath(Sample.m_BasePrefab));
|
|
}
|
|
|
|
private void Resolve_AddPrefabEntityAttribute()
|
|
{
|
|
string scriptFileName = GetScriptFilename(m_UnityScriptType);
|
|
if (scriptFileName.IsNullOrEmpty())
|
|
{
|
|
EditorUtility.DisplayDialog(
|
|
title: "Uh-oh!",
|
|
message: $"Tremble could not find the source file for {ScriptName}.",
|
|
"Oh no!");
|
|
return;
|
|
}
|
|
|
|
bool prefabEntityWasAdded = false;
|
|
bool usingWasAdded = false;
|
|
|
|
string[] lines = File.ReadAllLines(scriptFileName);
|
|
List<string> outLines = new(lines.Length+2);
|
|
foreach (string line in lines)
|
|
{
|
|
if (!usingWasAdded && !line.ContainsInvariant("using", caseSensitive: true))
|
|
{
|
|
outLines.Add("using TinyGoose.Tremble;");
|
|
usingWasAdded = true;
|
|
}
|
|
|
|
if (line.ContainsInvariant($"class {ScriptName}", caseSensitive: true))
|
|
{
|
|
prefabEntityWasAdded = true;
|
|
outLines.Add($"\t{GetAttributeLine()}");
|
|
}
|
|
|
|
outLines.Add(line);
|
|
}
|
|
|
|
if (!prefabEntityWasAdded)
|
|
{
|
|
EditorUtility.DisplayDialog(
|
|
title: "Uh-oh!",
|
|
message: $"Tremble could not find the class {ScriptName} in the source file {Path.GetFileName(scriptFileName)}.",
|
|
"Oh no!");
|
|
return;
|
|
}
|
|
|
|
File.WriteAllLines(scriptFileName, outLines);
|
|
|
|
EditorCompileUtil.CompileThen(typeof(PrefabBuilderPage), nameof(OnPostCompile_AddAttribute), ScriptName);
|
|
}
|
|
|
|
private static string GetScriptNameFromPrefab(GameObject prefab)
|
|
{
|
|
string newName = prefab.name;
|
|
if (newName.StartsWithInvariant("P_", caseSensitive: true))
|
|
return newName.Substring(2);
|
|
|
|
StringBuilder finalName = new(newName.Length);
|
|
foreach (char c in newName)
|
|
{
|
|
if (c is '-')
|
|
{
|
|
// Swap characters
|
|
finalName.Append('_');
|
|
}
|
|
else if (c is ' ')
|
|
{
|
|
// Omit characters
|
|
}
|
|
else
|
|
{
|
|
// Allowed characters
|
|
finalName.Append(c);
|
|
}
|
|
}
|
|
|
|
return finalName.ToString();
|
|
}
|
|
|
|
private static string GetScriptFilename(Type monoBehaviourType)
|
|
{
|
|
if (s_ScriptFileLookup.TryGetValue(monoBehaviourType, out string existingPath))
|
|
return existingPath;
|
|
|
|
foreach (string path in AssetDatabase.GetAllAssetPaths())
|
|
{
|
|
// Check if the file name matches exactly (without the full path)
|
|
if (Path.GetFileNameWithoutExtension(path).EqualsInvariant(monoBehaviourType.Name))
|
|
{
|
|
s_ScriptFileLookup[monoBehaviourType] = path;
|
|
return path;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static void ShowCompleteDialog(string title, string message)
|
|
{
|
|
if (TrenchBroomUtil.IsTrenchBroomRunning)
|
|
{
|
|
message += "\n\n Note: Press F6 inside TrenchBroom to refresh Entity Definitions for it to appear!";
|
|
}
|
|
|
|
EditorUtility.DisplayDialog(title, message, "Great!");
|
|
}
|
|
|
|
private static void OnPostCompile_AddAttribute(string scriptName)
|
|
{
|
|
TrembleEditorAPI.SyncToTrenchBroom();
|
|
|
|
ShowCompleteDialog(
|
|
title: "[PrefabEntity] attribute added!",
|
|
message: $"{scriptName} now has a [PrefabEntity] attribute and should be available from TrenchBroom.");
|
|
}
|
|
|
|
private static void OnPostCompile_AddNewScript(string scriptName, string prefabPath)
|
|
{
|
|
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
|
|
|
Type newComponentType = Unsupported.GetTypeFromFullName(scriptName);
|
|
prefab.AddComponent(newComponentType);
|
|
|
|
foreach (TrembleSpawnablePrefab tsp in prefab.GetComponents<TrembleSpawnablePrefab>())
|
|
{
|
|
Component.DestroyImmediate(tsp, true);
|
|
}
|
|
|
|
EditorUtility.SetDirty(prefab);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
// Now sync
|
|
EditorApplication.delayCall += TrembleEditorAPI.SyncToTrenchBroom;
|
|
|
|
// And tell the user!
|
|
ShowCompleteDialog(
|
|
title: "New Prefab Entity Created!",
|
|
message: $"{scriptName}.cs was created and attached to {prefab.name}, and should be available from TrenchBroom.");
|
|
}
|
|
}
|
|
} |