// // 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; using System.Collections.Generic; using System.IO; using UnityEngine; namespace TinyGoose.Tremble { public class FgdWriter { // ----------------------------------------------------------------------------------------------------------------------------- // State // ----------------------------------------------------------------------------------------------------------------------------- private FgdClass m_WorldSpawn; private readonly List m_OtherClasses = new(); private readonly List m_BaseClasses = new(); private readonly string m_VersionString; private readonly float m_ImportScale; public FgdWriter(string versionString, TrembleSyncSettings settings) { m_VersionString = versionString; m_ImportScale = settings.ImportScale; // Add base entities AddMapBaseEntity(); AddMapPointBaseEntity(); AddMapPrefabBaseEntity(); } public FgdClass WorldspawnClass { get => m_WorldSpawn; set => m_WorldSpawn = value; } public IReadOnlyList BaseClasses => m_BaseClasses; public IReadOnlyList Classes => m_OtherClasses; public void AddClass(FgdClass entityClass) { if (entityClass.Name.Length > 31) { Debug.LogError($"Class '{entityClass.Name}' is too long - it cannot be longer than 31 characters." + " Add a custom [PrefabEntity], [BrushEntity], or [PointEntity] attribute to this MonoBehaviour and set a shorter name!"); } if (entityClass.Type == FgdClassType.Base) { int insertionIdx = m_BaseClasses.BinarySearch(entityClass, Comparer.Create((c1, c2) => String.Compare(c1.Name, c2.Name, StringComparison.Ordinal))); if (insertionIdx < 0) { m_BaseClasses.Add(entityClass); } else { m_BaseClasses.Insert(insertionIdx, entityClass); } } else { int insertionIdx = m_OtherClasses.BinarySearch(entityClass, Comparer.Create((c1, c2) => String.Compare(c1.Name, c2.Name, StringComparison.Ordinal))); if (insertionIdx < 0) { m_OtherClasses.Add(entityClass); } else { m_OtherClasses.Insert(insertionIdx, entityClass); } } } public void WriteFgd(StreamWriter stream, bool humanReadable = true) { if (humanReadable) { stream.WriteLine("// TrenchBroom game definition file (.fgd)"); stream.WriteLine($"// Generated by Tremble for Unity v{m_VersionString}"); stream.WriteLine($"// For: {Application.productName}. (c) {DateTime.UtcNow.Year} {Application.companyName}"); stream.WriteLine(); stream.WriteLine(); stream.WriteLine("// ----------------------------------------------------------------------------------------------------"); stream.WriteLine("//"); stream.WriteLine("// Base Classes"); stream.WriteLine("//"); stream.WriteLine("// ----------------------------------------------------------------------------------------------------"); } // Base entities foreach (FgdClass entity in m_BaseClasses) { if (humanReadable) { stream.WriteLine("// ----------------------------------------------------------------------------------------------------"); stream.WriteLine($"// Tremble Base Class '{entity.Name}'"); stream.WriteLine($"// {entity.Description}"); stream.WriteLine("// ----------------------------------------------------------------------------------------------------"); } entity.WriteFgd(stream, m_ImportScale); if (humanReadable) { stream.WriteLine(); stream.WriteLine(); } } if (m_WorldSpawn != null) { if (humanReadable) { stream.WriteLine(); stream.WriteLine(); stream.WriteLine(); stream.WriteLine("// ----------------------------------------------------------------------------------------------------"); stream.WriteLine("//"); stream.WriteLine("// worldspawn - World Root Properties"); stream.WriteLine("//"); stream.WriteLine("// ----------------------------------------------------------------------------------------------------"); } m_WorldSpawn.WriteFgd(stream, m_ImportScale); if (humanReadable) { stream.WriteLine(); stream.WriteLine(); } } if (humanReadable) { stream.WriteLine(); stream.WriteLine(); stream.WriteLine(); stream.WriteLine("// ----------------------------------------------------------------------------------------------------"); stream.WriteLine("//"); stream.WriteLine("// Game Entities"); stream.WriteLine("//"); stream.WriteLine("// ----------------------------------------------------------------------------------------------------"); stream.WriteLine(); } foreach (FgdClass entity in m_OtherClasses) { if (humanReadable) { string type = entity.Type switch { FgdClassType.Base => nameof(ITrembleBaseClass), FgdClassType.Point => "Point Entity", FgdClassType.Brush => "Brush Entity", _ => throw new ArgumentOutOfRangeException() }; stream.WriteLine("// ----------------------------------------------------------------------------------------------------"); stream.WriteLine($"// {type}"); stream.WriteLine($"// {entity.Description}"); stream.WriteLine("// ----------------------------------------------------------------------------------------------------"); } entity.WriteFgd(stream, m_ImportScale); } } // ----------------------------------------------------------------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------------------------------------------------------------- private void AddMapBaseEntity() { FgdClass mapBase = new(FgdClassType.Base, FgdConsts.CLASS_MAP_BASE, "The base class for all map entities"); mapBase.Description = "Base class for common map properties"; mapBase.AddField(new FgdTargetSourceField { Name = TrembleSyncSettings.Get().IdentityPropertyName, Description = "The ID/Name of this entity", DefaultValue = "" }); mapBase.AddField(new FgdAnglesField { Name = FgdConsts.PROPERTY_ANGLES, Description = "Rotation Angles", DefaultValue = Vector3.zero }); mapBase.AddField(new FgdVectorField { Name = FgdConsts.PROPERTY_ORIGIN, Description = "Spawn position of this entity", DefaultValue = Vector3.zero, ConvertToQ3 = true, ImportScale = m_ImportScale }); AddClass(mapBase); } private void AddMapPointBaseEntity() { FgdClass mapBase = new(FgdClassType.Base, FgdConsts.CLASS_MAP_POINT_BASE, "The base class for all Point entities"); mapBase.Description = "Base class for common Point properties"; mapBase.AddField(new FgdTargetDestinationField { Name = FgdConsts.PROPERTY_PARENT, Description = "Parent Transform", DefaultValue = null }); AddClass(mapBase); } private void AddMapPrefabBaseEntity() { FgdClass mapBase = new(FgdClassType.Base, FgdConsts.CLASS_MAP_PREFAB_BASE, "The base class for all Prefab entities"); mapBase.Description = "Base class for common Prefab properties"; mapBase.AddBaseClass(FgdConsts.CLASS_MAP_POINT_BASE); mapBase.AddField(new FgdVectorField { Name = FgdConsts.PROPERTY_SCALE, Description = "Prefab Scale", DefaultValue = Vector3.one }); AddClass(mapBase); } } }