// // 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; namespace TinyGoose.Tremble { public static class EntityClassWriter { public static void WriteCSharp(this FgdClass fgdClass, string file, List allBaseClasses = null, string namespaceName = null) { if (fgdClass.Type == FgdClassType.Base) { WriteCSharpInterface(fgdClass, file, namespaceName); } else { WriteCSharpClass(fgdClass, file, allBaseClasses, namespaceName); } } private static void WriteCSharpClass(this FgdClass fgdClass, string file, List allBaseClasses = null, string namespaceName = null) { using FileStream classFile = new(file, FileMode.Create, FileAccess.Write); using StreamWriter writer = new(classFile); writer.WriteLine($"// Class '{fgdClass.Name}'"); writer.WriteLine($"// Generated by Tremble v{TrembleConsts.VERSION_STRING}"); writer.WriteLine(); writer.WriteLine("using UnityEngine;"); writer.WriteLine("using TinyGoose.Tremble;"); writer.WriteLine(); writer.WriteLine("// Disable 'Field is assigned but its value is never used' warning"); writer.WriteLine("#pragma warning disable CS0414"); writer.WriteLine(); bool hasNamespace = !namespaceName.IsNullOrEmpty(); if (hasNamespace) { writer.WriteLine($"namespace {namespaceName}"); writer.WriteLine("{"); } if (!fgdClass.Description.IsNullOrEmpty()) { writer.WriteLine($"\t// \"{fgdClass.Description}\""); } { fgdClass.Name.Split('_', out string category, out string classname); if (fgdClass.Type == FgdClassType.Brush) { if (hasNamespace) { writer.Write("\t"); } writer.Write("[BrushEntity("); } else { if (hasNamespace) { writer.Write("\t"); } writer.Write("[PointEntity("); } writer.Write($"\"{classname}\""); if (category.IsNullOrEmpty()) { writer.Write($", category: \"\" /* empty category - gross, but necessary */"); } else { writer.Write($", category: \"{category}\""); } if (fgdClass.Type == FgdClassType.Brush && fgdClass.Name.ContainsInvariant("trigger")) { writer.Write($", type: BrushType.Trigger"); } writer.WriteLine(")]"); string csharpClassname = classname.ToNamingConvention(NamingConvention.UpperCamelCase); if (!category.IsNullOrEmpty() && !category.EqualsInvariant("misc") && !category.EqualsInvariant("func")) { csharpClassname += category.ToNamingConvention(NamingConvention.UpperCamelCase); } if (csharpClassname.EqualsInvariant("light")) { // Hack: you can't call a MonoBehaviour "Light" in Unity, but Quake for example // has an entity called "light" - let's call it LightEntity csharpClassname = "LightEntity"; } if (hasNamespace) { writer.Write("\t"); } writer.Write($"public class {csharpClassname} : MonoBehaviour"); foreach (string baseClass in fgdClass.BaseClassNames) { // Not the Tremble ones! if (baseClass.EqualsInvariant(FgdConsts.CLASS_MAP_BASE, caseSensitive: true) || baseClass.EqualsInvariant(FgdConsts.CLASS_MAP_POINT_BASE, caseSensitive: true)) continue; writer.Write($", I{baseClass}"); } writer.WriteLine(); if (hasNamespace) { writer.Write("\t"); } writer.WriteLine("{"); if (fgdClass.AllFields.Count > 0) { writer.WriteIndent(hasNamespace ? 2 : 1); writer.WriteLine("// Fields:"); foreach (FgdFieldBase field in fgdClass.AllFields) { field.WriteCSharpClass(writer, hasNamespace ? 2 : 1); } writer.WriteLine(); } if (fgdClass.AllSpawnFlags.Count > 0) { writer.WriteIndent(hasNamespace ? 2 : 1); writer.WriteLine("// Spawnflags: "); foreach (FgdSpawnFlagField spawnFlagField in fgdClass.AllSpawnFlags) { spawnFlagField.WriteCSharpClass(writer, hasNamespace ? 2 : 1); } writer.WriteLine(); } // Write base class fields if (allBaseClasses != null) { List writtenBaseClasses = new(64); List writtenFields = new(64); foreach (string baseClassName in fgdClass.BaseClassNames) { AddBaseClassFields(fgdClass, writer, hasNamespace, baseClassName, writtenBaseClasses, writtenFields, allBaseClasses); } } if (hasNamespace) { writer.Write("\t"); } writer.WriteLine("}"); } if (hasNamespace) { writer.WriteLine("}"); } writer.WriteLine(); writer.WriteLine("#pragma warning restore CS0414"); } private static void AddBaseClassFields(FgdClass thisClass, StreamWriter writer, bool hasNamespace, string baseClassName, List writtenBaseClasses, List writtenFields, List allBaseClasses) { // Find the base class and add! FgdClass baseClass = allBaseClasses.FirstOrDefault(bc => bc.Name.EqualsInvariant(baseClassName, caseSensitive: true)); if (baseClass == null) return; if (baseClass.AllFields.Count + baseClass.AllSpawnFlags.Count == 0) return; if (writtenBaseClasses.Contains(baseClassName)) return; writtenBaseClasses.Add(baseClassName); foreach (string baseBaseClassName in baseClass.BaseClassNames) { AddBaseClassFields(thisClass, writer, hasNamespace, baseBaseClassName, writtenBaseClasses, writtenFields, allBaseClasses); } writer.WriteLine(); writer.WriteIndent(hasNamespace ? 2 : 1); writer.WriteLine($"// From Base Class '{baseClassName}':"); foreach (FgdFieldBase field in baseClass.AllFields) { if (writtenFields.Contains(field.FieldName)) { writer.WriteIndent(hasNamespace ? 2 : 1); writer.WriteLine($"// Skipped '{field.FieldName}' - already defined by a base class!"); continue; } writtenFields.Add(field.FieldName); bool overridden = thisClass.AllFields.Any(m => m.FieldName.EqualsInvariant(field.FieldName, caseSensitive: true)); if (overridden) { writer.WriteLine("/* (overriden by subclass)"); } field.WriteCSharpClass(writer, hasNamespace ? 2 : 1); if (overridden) { writer.WriteLine("(overriden by subclass) */"); } } foreach (FgdSpawnFlagField spawnFlag in baseClass.AllSpawnFlags) { bool overridden = thisClass.AllSpawnFlags.Any(m => m.FieldName.EqualsInvariant(spawnFlag.FieldName, caseSensitive: true)); if (overridden) { writer.WriteLine("/* (overriden by subclass)"); } spawnFlag.WriteCSharpClass(writer, hasNamespace ? 2 : 1); if (overridden) { writer.WriteLine("(overriden by subclass) */"); } } } private static void WriteCSharpInterface(this FgdClass fgdClass, string file, string namespaceOrNull) { using FileStream classFile = new(file, FileMode.Create, FileAccess.Write); using StreamWriter writer = new(classFile); writer.WriteLine($"// Interface 'I{fgdClass.Name}'"); writer.WriteLine($"// Generated by Tremble v{TrembleConsts.VERSION_STRING}"); writer.WriteLine(); writer.WriteLine("using UnityEngine;"); writer.WriteLine("using TinyGoose.Tremble;"); writer.WriteLine(); bool hasNamespace = !namespaceOrNull.IsNullOrEmpty(); if (hasNamespace) { writer.WriteLine($"namespace {namespaceOrNull}"); writer.WriteLine("{"); } { fgdClass.Name.Split('_', out string _, out string classname); string csharpClassname = classname.ToNamingConvention(NamingConvention.UpperCamelCase); if (hasNamespace) { writer.Write("\t"); } writer.WriteLine($"public interface I{csharpClassname} : ITrembleBaseClass"); if (hasNamespace) { writer.Write("\t"); } writer.WriteLine("{"); if (fgdClass.AllFields.Count > 0) { if (hasNamespace) { writer.Write("\t"); } writer.WriteLine("\t// Fields:"); foreach (FgdFieldBase field in fgdClass.AllFields) { field.WriteCSharpInterface(writer, hasNamespace ? 2 : 1); } } if (fgdClass.AllSpawnFlags.Count > 0) { if (hasNamespace) { writer.Write("\t"); } writer.WriteLine("\t// Spawnflags: "); foreach (FgdSpawnFlagField spawnFlagField in fgdClass.AllSpawnFlags) { spawnFlagField.WriteCSharpInterface(writer, hasNamespace ? 2 : 1); } } if (hasNamespace) { writer.Write("\t"); } writer.WriteLine("}"); } if (hasNamespace) { writer.WriteLine("}"); } } } }