Second commit

This commit is contained in:
ThomasCharester committed 2026-09-20 15:26:03 +03:00
commit 36840f3ad4
999 files changed
+61364

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2e90b79b320149d0a8e107ccecdfa7f5
timeCreated: 1710848891
+461
View File
@@ -0,0 +1,461 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
// This class based on BSP Map Tools for Unity by John Evans (evans3d512@gmail.com)
//
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace TinyGoose.Tremble
{
public static class BspParser
{
public static MapBsp Parse(string bspPath, float importScale, BspOptions options = null, MapGroupData[] foundGroups = null)
{
StreamReadIO reader = new(bspPath);
string signature = new (reader.ReadChars(4));
int version = reader.ReadInt32();
if (signature != "IBSP" || version != 46)
{
Debug.LogWarning($"BSP Importer: Unknown signature or version. Expected IBSP, 46. Got {signature}, {version}");
}
int entitiesOffset = reader.ReadInt32();
int entitiesLength = reader.ReadInt32(); // 16
int texturesOffset = reader.ReadInt32();
int texturesLength = reader.ReadInt32(); // 24
int planesOffset = reader.ReadInt32();
int planesLength = reader.ReadInt32();
int nodesOffset = reader.ReadInt32();
int nodesLength = reader.ReadInt32();
int leavesOffset = reader.ReadInt32();
int leavesLength = reader.ReadInt32();
int leafFacesOffset = reader.ReadInt32();
int leaffacesLength = reader.ReadInt32();
reader.Seek(64);
int modelsOffset = reader.ReadInt32();
int modelsLength = reader.ReadInt32();
reader.Seek(88); // Vertices
int verticesOffset = reader.ReadInt32();
int verticesLength = reader.ReadInt32();
int mVerticesOffset = reader.ReadInt32();
int mVerticesLength = reader.ReadInt32();
reader.Seek(112); // Faces
int facesOffset = reader.ReadInt32();
int facesLength = reader.ReadInt32();
int vertexCount = verticesLength / 44;
int mVertexCount = mVerticesLength / 4;
int faceCount = facesLength / 104;
int texCount = texturesLength / 72;
int modelCount = modelsLength / 40;
int planesCount = planesLength / 16;
int nodesCount = nodesLength / 36;
int leavesCount = leavesLength / 48;
int leafFacesCount = leaffacesLength / 4;
MapBsp newMapBsp = new() {
MapName = Path.GetFileNameWithoutExtension(bspPath),
ImportScale = importScale,
Options = options ?? new(),
TexInfos = new TexInfo[texCount],
Planes = new Plane[planesCount],
Nodes = new BspNode[nodesCount],
Leaves = new BspLeaf[leavesCount],
LeafFaces = new int[leafFacesCount],
Models = new BspModel[modelCount],
Vertices = new BspVertex[vertexCount],
Faces = new BspFace[faceCount],
MeshVertices = new int[mVertexCount],
Entities = new(),
UsedFaces = new bool[faceCount],
Groups = foundGroups,
};
reader.Seek(texturesOffset);
for (int i = 0; i < texCount; i++)
{
string tempName = reader.ReadString(64);
if (tempName.StartsWithInvariant("textures/"))
{
tempName = tempName.Substring("textures/".Length);
}
newMapBsp.TexInfos[i] = new()
{
Name = tempName,
Flags = reader.ReadInt32(),
Contents = reader.ReadInt32(),
};
}
reader.Seek(planesOffset);
for (int i = 0; i < planesCount; i++)
{
Vector3 planeNormal = reader.ReadVector3(convertToQ3: true);
newMapBsp.Planes[i] = new()
{
normal = planeNormal,
distance = reader.ReadSingle() * importScale,
};
}
reader.Seek(leavesOffset);
for (int i = 0; i < leavesCount; i++)
{
newMapBsp.Leaves[i] = new()
{
VisCluster = reader.ReadInt32(),
PortalArea = reader.ReadInt32(),
Min = reader.ReadVector3Int(),
Max = reader.ReadVector3Int(),
LeafFaceStartIdx = reader.ReadInt32(),
NumLeafFaces = reader.ReadInt32(),
LeafBrushStartIdx = reader.ReadInt32(),
NumLeafBrushes = reader.ReadInt32(),
};
}
reader.Seek(nodesOffset);
for (int i = 0; i < nodesCount; i++)
{
newMapBsp.Nodes[i] = new()
{
PlaneIndex = reader.ReadInt32(),
Child1 = reader.ReadInt32(),
Child2 = reader.ReadInt32(),
Min = reader.ReadVector3Int(),
Max = reader.ReadVector3Int(),
};
}
reader.Seek(leafFacesOffset);
for (int i = 0; i < leafFacesCount; i++)
{
newMapBsp.LeafFaces[i] = reader.ReadInt32();
}
reader.Seek(modelsOffset);
for (int i = 0; i < modelCount; i++)
{
newMapBsp.Models[i] = new()
{
Min = reader.ReadVector3(convertToQ3: true, scale: importScale),
Max = reader.ReadVector3(convertToQ3: true, scale: importScale),
StartFaceIdx = reader.ReadInt32(),
NumFaces = reader.ReadInt32(),
StartBrushIdx = reader.ReadInt32(),
NumBrushes = reader.ReadInt32(),
};
}
reader.Seek(verticesOffset);
for (int i = 0; i < vertexCount; i++)
{
newMapBsp.Vertices[i] = new()
{
Position = reader.ReadVector3(convertToQ3: true, scale: importScale),
TexCoord = reader.ReadUV(),
LightmapCoord = reader.ReadUV(),
Normal = Vector3.Normalize(reader.ReadVector3(convertToQ3: true)),
Color = reader.ReadColour(),
};
}
reader.Seek(facesOffset);
for (int i = 0; i < faceCount; i++)
{
newMapBsp.Faces[i] = new()
{
TexID = reader.ReadInt32(),
Effect = reader.ReadInt32(),
Type = reader.ReadInt32(),
VertexStartIdx = reader.ReadInt32(),
NumVertices = reader.ReadInt32(),
MeshVertexStartIdx = reader.ReadInt32(),
NumMeshVertices = reader.ReadInt32(),
LightmapIndex = reader.ReadInt32(),
LightmapStart = reader.ReadVector2Int(),
LightmapSize = reader.ReadVector2Int(),
LightmapOrigin = reader.ReadVector3Int(),
LightmapVector1 = reader.ReadVector3(),
LightmapVector2 = reader.ReadVector3(),
Normal = reader.ReadVector3(convertToQ3: true),
Size = reader.ReadVector2Int(),
};
}
reader.Seek(mVerticesOffset);
for (int i = 0; i < mVertexCount; i++)
{
newMapBsp.MeshVertices[i] = reader.ReadInt32();
}
// Read entity key/value pairs
reader.Seek(entitiesOffset);
List<string> kvPairs = new();
int entityNumber = 0;
int endOfEntities = entitiesOffset + entitiesLength - 1;
int charsToRead = endOfEntities - reader.Position;
string entities = new(reader.ReadChars(charsToRead));
TokenParser tokenParser = new(entities);
do
{
ReadOnlySpan<char> token = tokenParser.ReadToken();
if (token == null || token.Length == 0)
{
kvPairs.Add("");
continue;
}
switch (token[0])
{
case '{':
{
continue;
}
case '}':
{
Dictionary<string, string> entries = new();
for (int kvIdx = 0; kvIdx < kvPairs.Count; kvIdx += 2)
{
entries.Add(kvPairs[kvIdx], kvPairs[kvIdx+1]);
}
newMapBsp.Entities.Add(new() { SerialNumber = entityNumber++, Entries = entries, ImportScale = newMapBsp.ImportScale});
kvPairs.Clear();
continue;
}
default:
{
kvPairs.Add(token.ToString());
break;
}
}
} while (!tokenParser.IsAtEnd);
return newMapBsp;
}
private static float GetTriangleAreaSqr(in Vector3 v1, in Vector3 v2, in Vector3 v3)
{
// Sqr Magnitudes inlined!
float ax = v1.x - v2.x;
float ay = v1.y - v2.y;
float az = v1.z - v2.z;
float a = ax * ax + ay * ay + az * az;
float bx = v2.x - v3.x;
float by = v2.y - v3.y;
float bz = v2.z - v3.z;
float b = bx * bx + by * by + bz * bz;
float cx = v2.x - v3.x;
float cy = v2.y - v3.y;
float cz = v2.z - v3.z;
float c = cx * cx + cy * cy + cz * cz;
float s = (a + b + c) * 0.25f;
float surfAreaSqr = Mathf.Sqrt(s * (s - a) * (s - b) * (s - c));
return float.IsNaN(surfAreaSqr) ? 0f : surfAreaSqr;
}
public static void GetFaceList(MapBsp mapBsp, BspLeaf leaf, List<int> receiver)
{
int faceMin = mapBsp.Models[0].StartFaceIdx;
int faceMax = faceMin + mapBsp.Models[0].NumFaces;
for (int faceOffset = leaf.LeafFaceStartIdx; faceOffset < leaf.LeafFaceStartIdx + leaf.NumLeafFaces; faceOffset++)
{
int leafFaceIdx = mapBsp.LeafFaces[faceOffset];
if (leafFaceIdx < faceMin || leafFaceIdx > faceMax)
continue;
if (mapBsp.UsedFaces[leafFaceIdx])
continue;
receiver.Add(leafFaceIdx);
mapBsp.UsedFaces[leafFaceIdx] = true;
}
}
public static void GetFaceList(MapBsp mapBsp, BspNode node, List<int> receiver)
{
if (node.Child1 < 0)
{
GetFaceList(mapBsp, mapBsp.Leaves[-node.Child1 - 1], receiver);
}
else
{
GetFaceList(mapBsp, mapBsp.Nodes[node.Child1], receiver);
}
if (node.Child2 < 0)
{
GetFaceList(mapBsp, mapBsp.Leaves[-node.Child2 - 1], receiver);
}
else
{
GetFaceList(mapBsp, mapBsp.Nodes[node.Child2], receiver);
}
}
private static void GetSurfaceAreaSqr(MapBsp mapBsp, BspLeaf leaf, ref float receiver, HashSet<int> faceList)
{
int faceMin = mapBsp.Models[0].StartFaceIdx;
int faceMax = faceMin + mapBsp.Models[0].NumFaces;
for (int faceOffset = leaf.LeafFaceStartIdx; faceOffset < leaf.LeafFaceStartIdx + leaf.NumLeafFaces; faceOffset++)
{
if (mapBsp.LeafFaces[faceOffset] < faceMin || mapBsp.LeafFaces[faceOffset] > faceMax)
continue;
int leafFaceIdx = mapBsp.LeafFaces[faceOffset];
if (faceList.Contains(leafFaceIdx))
continue;
BspFace meshFace = mapBsp.Faces[leafFaceIdx];
for (int vertexOffset = meshFace.MeshVertexStartIdx; vertexOffset < meshFace.MeshVertexStartIdx + meshFace.NumMeshVertices; vertexOffset += 3)
{
receiver += GetTriangleAreaSqr
(
mapBsp.Vertices[mapBsp.MeshVertices[vertexOffset + 0] + meshFace.VertexStartIdx].Position,
mapBsp.Vertices[mapBsp.MeshVertices[vertexOffset + 1] + meshFace.VertexStartIdx].Position,
mapBsp.Vertices[mapBsp.MeshVertices[vertexOffset + 2] + meshFace.VertexStartIdx].Position
);
}
faceList.Add(leafFaceIdx);
}
}
public static void GetSurfaceAreaSqr(MapBsp mapBsp, BspNode node, ref float receiver, HashSet<int> faceList)
{
if (node.Child1 < 0)
{
GetSurfaceAreaSqr(mapBsp, mapBsp.Leaves[-node.Child1 - 1], ref receiver, faceList);
}
else
{
GetSurfaceAreaSqr(mapBsp, mapBsp.Nodes[node.Child1], ref receiver, faceList);
}
if (node.Child2 < 0)
{
GetSurfaceAreaSqr(mapBsp, mapBsp.Leaves[-node.Child2 - 1], ref receiver, faceList);
}
else
{
GetSurfaceAreaSqr(mapBsp, mapBsp.Nodes[node.Child2], ref receiver, faceList);
}
}
public static void DumpToFile(this MapBsp mapBsp, string file)
{
using FileStream fs = new(file, FileMode.Create, FileAccess.Write);
using StreamWriter sw = new(fs);
sw.WriteLine($"m_TexInfos[{mapBsp.TexInfos.Length}]:");
for (int i = 0; i < mapBsp.TexInfos.Length; i++)
{
sw.WriteLine($" [{i}]: {mapBsp.TexInfos[i].Contents} {mapBsp.TexInfos[i].Flags} {mapBsp.TexInfos[i].Name}");
}
sw.WriteLine();
sw.WriteLine($"bsp.Planes[{mapBsp.Planes.Length}]:");
for (int i = 0; i < mapBsp.Planes.Length; i++)
{
sw.WriteLine($" [{i}]: {mapBsp.Planes[i].normal} {mapBsp.Planes[i].distance}");
}
sw.WriteLine();
sw.WriteLine($"bsp.Nodes[{mapBsp.Nodes.Length}]:");
for (int i = 0; i < mapBsp.Nodes.Length; i++)
{
sw.WriteLine($" [{i}]: {mapBsp.Nodes[i].Child1} {mapBsp.Nodes[i].Child2} {mapBsp.Nodes[i].PlaneIndex} {mapBsp.Nodes[i].Min} {mapBsp.Nodes[i].Max}");
}
sw.WriteLine();
sw.WriteLine($"bsp.Leaves[{mapBsp.Leaves.Length}]:");
for (int i = 0; i < mapBsp.Leaves.Length; i++)
{
sw.WriteLine($" [{i}]: {mapBsp.Leaves[i].Min} {mapBsp.Leaves[i].Max} {mapBsp.Leaves[i].LeafBrushStartIdx} {mapBsp.Leaves[i].NumLeafBrushes} {mapBsp.Leaves[i].LeafFaceStartIdx} {mapBsp.Leaves[i].NumLeafFaces}");
}
sw.WriteLine();
sw.WriteLine($"bsp.LeafFaces[{mapBsp.LeafFaces.Length}]:");
for (int i = 0; i < mapBsp.LeafFaces.Length; i++)
{
sw.WriteLine($" [{i}]: {mapBsp.LeafFaces[i]}");
}
sw.WriteLine();
sw.WriteLine($"bsp.UsedFaces[{mapBsp.UsedFaces.Length}]:");
for (int i = 0; i < mapBsp.UsedFaces.Length; i++)
{
sw.WriteLine($" [{i}]: {mapBsp.UsedFaces[i]}");
}
sw.WriteLine();
sw.WriteLine($"bsp.Models[{mapBsp.Models.Length}]:");
for (int i = 0; i < mapBsp.Models.Length; i++)
{
sw.WriteLine($" [{i}]: {mapBsp.Models[i].Min} {mapBsp.Models[i].Max} {mapBsp.Models[i].StartBrushIdx} {mapBsp.Models[i].NumBrushes} {mapBsp.Models[i].StartFaceIdx} {mapBsp.Models[i].NumFaces}");
}
sw.WriteLine();
sw.WriteLine($"bsp.Vertices[{mapBsp.Vertices.Length}]:");
for (int i = 0; i < mapBsp.Vertices.Length; i++)
{
sw.WriteLine($" [{i}]: {mapBsp.Vertices[i].Position} {mapBsp.Vertices[i].Normal} {mapBsp.Vertices[i].Color} {mapBsp.Vertices[i].TexCoord} {mapBsp.Vertices[i].LightmapCoord}");
}
sw.WriteLine();
sw.WriteLine($"bsp.Faces[{mapBsp.Faces.Length}]:");
for (int i = 0; i < mapBsp.Faces.Length; i++)
{
sw.WriteLine($" [{i}]: {mapBsp.Faces[i].VertexStartIdx} {mapBsp.Faces[i].NumVertices} {mapBsp.Faces[i].MeshVertexStartIdx} {mapBsp.Faces[i].NumMeshVertices} {mapBsp.Faces[i].Type}");
}
sw.WriteLine();
sw.WriteLine($"bsp.MeshVertices[{mapBsp.MeshVertices.Length}]:");
for (int i = 0; i < mapBsp.MeshVertices.Length; i++)
{
sw.WriteLine($" [{i}]: {mapBsp.MeshVertices[i]}");
}
sw.WriteLine();
sw.WriteLine($"bsp.Entities[{mapBsp.Entities.Count}]:");
for (int i = 0; i < mapBsp.Entities.Count; i++)
{
sw.Write($" [{i}]: {mapBsp.Entities[i].GetClassname()} {{");
foreach (string key in mapBsp.Entities[i].Entries.Keys)
{
sw.Write($" {key}=\"{mapBsp.Entities[i].Entries[key]}\"");
}
sw.WriteLine(" }");
}
sw.WriteLine();
}
}
}
+18
View File
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 71af40a1b7d341a6a9a72e17150e1a52
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/BSP/BspParser.cs
uploadId: 939664
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 377682f99735490bb297fbe22cf1cd1a
timeCreated: 1710848878
+166
View File
@@ -0,0 +1,166 @@
//
// 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 UnityEngine;
namespace TinyGoose.Tremble
{
public enum FgdClassType
{
Base,
Point,
Brush,
}
public class FgdClass
{
public FgdClass(FgdClassType type, string name, string description)
{
Type = type;
Name = name;
Description = description;
}
public FgdClassType Type { get; }
public string Name { get; }
public string Description { get; set; }
public bool HasModel { get; init; }
public Color? Colour { get; init; }
public string Sprite { get; init; }
public Bounds? Box { get; init; }
private readonly List<string> m_BaseClasses = new();
private readonly List<FgdFieldBase> m_Fields = new();
private readonly List<FgdSpawnFlagField> m_SpawnFlags = new();
public void AddBaseClass(string baseClass) => m_BaseClasses.Add(baseClass);
public void AddBaseClassInterfaces(Type entityType)
{
foreach (Type iface in entityType.GetInterfaces())
{
if (!iface.GetInterfaces().Contains(typeof(ITrembleBaseClass)))
continue;
AddBaseClass(iface.Name.Substring(1)); // Remove 'I'
break;
}
}
public void AddField(FgdFieldBase fgdField)
{
if (fgdField is FgdSpawnFlagField flag)
{
m_SpawnFlags.Add(flag);
}
else
{
m_Fields.Add(fgdField);
}
}
public List<FgdFieldBase> AllFields => m_Fields;
public List<FgdSpawnFlagField> AllSpawnFlags => m_SpawnFlags;
public List<string> BaseClassNames => m_BaseClasses;
public bool HasField(string name) => m_Fields.Any(m => m.FieldName.EqualsInvariant(name, caseSensitive: true));
public bool HasSpawnFlag(int bit) => m_SpawnFlags.Any(sf => sf.Bit == bit);
public void AddComment(string comment)
{
m_Fields.Add(new FgdComment { Comment = comment });
}
public void WriteFgd(StreamWriter stream, float importScale)
{
string typeString = Type switch
{
FgdClassType.Base => "@BaseClass",
FgdClassType.Point => "@PointClass",
FgdClassType.Brush => "@SolidClass",
_ => throw new ArgumentOutOfRangeException()
};
stream.WriteLine(typeString);
if (m_BaseClasses.Count > 0)
{
string baseList = String.Join(", ", m_BaseClasses);
stream.WriteLine($"\tbase({baseList})");
}
if (HasModel)
{
stream.WriteLine($"\tmodel({{ path: \"models/{Name}.md3\", scale: scale }})");
}
if (Colour.HasValue)
{
stream.WriteLine($"\tcolor({Colour.Value.ToStringInvariant()})");
}
if (!Sprite.IsNullOrEmpty())
{
stream.WriteLine($"\tsprite(\"models/{Sprite}.png\")");
}
if (Box.HasValue)
{
Vector3 q3Min = Box.Value.min.UnityToQ3Vector(scale: importScale);
Vector3 q3Max = Box.Value.max.UnityToQ3Vector(scale: importScale);
// From xage on TB Discord: bboxes need to be symmetrical
Vector3 extents = new()
{
x = (Mathf.Abs(q3Min.x) + Mathf.Abs(q3Max.x)),
y = (Mathf.Abs(q3Min.y) + Mathf.Abs(q3Max.y)),
z = (Mathf.Abs(q3Min.z) + Mathf.Abs(q3Max.z))
};
stream.WriteLine($"\tsize({Mathf.RoundToInt(extents.x)} {Mathf.RoundToInt(extents.y)} {Mathf.RoundToInt(extents.z)})");
}
stream.Write($" = {Name} : \"{Description}\"");
// Write fields, and then spawnflags fields
if (m_Fields.Count == 0 && m_SpawnFlags.Count == 0)
{
stream.WriteLine(" []");
}
else
{
stream.WriteLine();
stream.WriteLine("[");
foreach (FgdFieldBase variable in m_Fields)
{
stream.Write("\t");
variable.Write(stream);
}
if (m_SpawnFlags.Count > 0)
{
stream.WriteLine("\tspawnflags(Flags) = ");
stream.WriteLine("\t[");
foreach (FgdSpawnFlagField flag in m_SpawnFlags.OrderBy(sf => sf.Bit))
{
stream.Write("\t\t");
flag.Write(stream);
}
stream.WriteLine("\t]");
}
stream.WriteLine("]");
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: e3de2d23101644c9a58c281bfcca7eae
timeCreated: 1709808579
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FgdClass.cs
uploadId: 939664
+89
View File
@@ -0,0 +1,89 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
namespace TinyGoose.Tremble
{
public abstract class FgdFieldBase
{
public abstract void Write(StreamWriter sw);
public abstract void WriteCSharpClass(StreamWriter sw, int indent);
public abstract void WriteCSharpInterface(StreamWriter sw, int indent);
public virtual string FieldName => null;
public string FriendlyTypeName
{
get
{
string friendlyTypeName = GetType().Name;
if (friendlyTypeName.StartsWithInvariant("Fgd", caseSensitive: true))
{
friendlyTypeName = friendlyTypeName.Remove(0, 3);
}
if (friendlyTypeName.EndsWithInvariant("Field", caseSensitive: true))
{
friendlyTypeName = friendlyTypeName.Remove(friendlyTypeName.Length - 5, 5);
}
return friendlyTypeName;
}
}
protected void WriteClassField(StreamWriter sw, int indent, string unityType, string name, string description, string defaultValue = null, string extraAttributes = null)
{
sw.WriteIndent(indent);
sw.Write("[");
{
if (!TrembleSyncSettings.Get().SyncSerializedFields)
{
sw.Write("Tremble, ");
}
sw.Write($"SerializeField");
if (!description.IsNullOrEmpty())
{
sw.Write($", Tooltip(\"{description}\")");
}
}
if (!extraAttributes.IsNullOrEmpty())
{
sw.Write(", ");
sw.Write(extraAttributes);
}
sw.Write("] ");
sw.Write($"private {unityType} m_{name.ToNamingConvention(NamingConvention.UpperCamelCase)}");
if (defaultValue == null)
{
sw.WriteLine(";");
}
else
{
sw.WriteLine($" = {defaultValue};");
}
}
protected void WriteInterfaceProperty(StreamWriter sw, int indent, string unityType, string name, string description, string attributes = null)
{
sw.WriteIndent(indent);
if (!attributes.IsNullOrEmpty())
{
sw.Write("[");
sw.Write(attributes);
sw.Write("] ");
}
sw.Write($"public {unityType} {name.ToNamingConvention(NamingConvention.UpperCamelCase)} => default;");
sw.WriteLine(description.IsNullOrEmpty() ? "" : $" // \"{description}\"");
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: f13869dde160409ca3a827a856d24d48
timeCreated: 1698227234
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FgdFieldBase.cs
uploadId: 939664
+86
View File
@@ -0,0 +1,86 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System.Globalization;
using UnityEngine;
namespace TinyGoose.Tremble
{
public static class FgdFieldFactory
{
public static FgdFieldBase CreateField(string typename, string name, string description, string defaultValue)
{
switch (typename.ToLowerInvariant())
{
case "float":
return new FgdFloatField
{
Name = name,
Description = description,
DefaultValue = defaultValue != null ? float.Parse(defaultValue, NumberStyles.Any, CultureInfo.InvariantCulture) : 0f
};
case "integer":
return new FgdIntegerField
{
Name = name,
Description = description,
DefaultValue = defaultValue != null ? int.Parse(defaultValue, NumberStyles.Any, CultureInfo.InvariantCulture) : 0
};
case "vector":
if (defaultValue == null || !defaultValue.TryParseQ3Vector(out Vector3 vector))
{
vector = Vector3.zero;
}
return new FgdVectorField
{
Name = name,
Description = description,
DefaultValue = vector
};
case "color":
if (!defaultValue.TryParseQ3Colour(out Color colour))
{
colour = Color.white;
}
return new FgdColourField
{
Name = name,
Description = description,
DefaultColour = colour
};
case "string":
return new FgdStringField
{
Name = name,
Description = description,
DefaultValue = defaultValue
};
case "target_source":
return new FgdTargetSourceField
{
Name = name,
Description = description,
DefaultValue = defaultValue
};
case "target_destination":
return new FgdTargetDestinationField
{
Name = name,
Description = description,
DefaultValue = defaultValue
};
}
return new FgdStringField
{
Name = name,
Description = description,
DefaultValue = defaultValue
};
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: b3806920a91d5469f8c9ef060771e83b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FgdFieldFactory.cs
uploadId: 939664
@@ -0,0 +1,130 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.Globalization;
namespace TinyGoose.Tremble
{
public static class FgdInferredFieldExtensions
{
public static void TryAddInferredField(this FgdClass fgdClass, string key, string value)
{
// ID property (or "targetname")
string identityProperty = TrembleSyncSettings.Get().IdentityPropertyName;
if (key.EqualsInvariant("targetname") || key.EqualsInvariant(identityProperty, caseSensitive: true))
{
return;
}
// "Target"
if (key.ContainsInvariant(FgdConsts.PROPERTY_TARGET))
{
fgdClass.AddField(new FgdTargetDestinationField
{
Name = key,
Description = $"{key.ToNamingConvention(NamingConvention.UpperCamelCase)} of this Entity"
});
return;
}
// "Origin" - skip
if (key.EqualsInvariant(FgdConsts.PROPERTY_ORIGIN))
{
return;
}
// "Scale" - skip
if (key.EqualsInvariant(FgdConsts.PROPERTY_SCALE))
{
return;
}
// "Parent" - skip
if (key.EqualsInvariant(FgdConsts.PROPERTY_PARENT))
{
return;
}
// Angles - skip
if (key.EqualsInvariant(FgdConsts.PROPERTY_ANGLES))
{
return;
}
// _tb_textures, group, etc - skip
if (key.StartsWithInvariant(TBConsts.TB_PREFIX))
{
return;
}
// Spawnflags
if (key.EqualsInvariant(FgdConsts.PROPERTY_SPAWNFLAGS))
{
if (!int.TryParse(value, out int flags))
return;
int bit = 0;
while (flags > 0)
{
if (!fgdClass.HasSpawnFlag(bit))
{
fgdClass.AddField(new FgdSpawnFlagField
{
Bit = bit,
Description = $"Unknown spawnflag bit #{bit}"
});
}
bit++;
flags >>= 1;
}
return;
}
// Anything else? Try to infer type.
if (IsValueVector(value))
{
fgdClass.AddField(new FgdVectorField
{
Name = key,
Description = $"Vector {key}",
});
}
else if (IsValueFloat(value))
{
fgdClass.AddField(new FgdFloatField
{
Name = key,
Description = $"Float {key}",
});
}
else
{
fgdClass.AddField(new FgdStringField
{
Name = key,
Description = $"String {key}",
});
}
}
private static bool IsValueFloat(string value) => value.TryParseFloat(out float _);
private static bool IsValueVector(string value)
{
if (value.IsNullOrEmpty())
return false;
string[] parts = value.Split(' ');
if (parts.Length != 3)
return false;
return IsValueFloat(parts[0]) && IsValueFloat(parts[1]) && IsValueFloat(parts[2]);
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: a207e07b3115419e90f7b5092c0ea67f
timeCreated: 1712223098
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FgdInferredFieldExtensions.cs
uploadId: 939664
+283
View File
@@ -0,0 +1,283 @@
//
// 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.Globalization;
using System.IO;
using System.Linq;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace TinyGoose.Tremble
{
public record EnumDefinition(string Name, string[] Values);
public static class FgdParser
{
public static void Import(string fgdFilePath)
{
string fgdName = Path.GetFileNameWithoutExtension(fgdFilePath).ToNamingConvention(NamingConvention.UpperCamelCase);
TokenParser tokenParser = new(File.ReadAllText(fgdFilePath));
List<FgdClass> baseClasses = new(16);
List<FgdClass> concreteClasses = new(64);
List<EnumDefinition> enums = new(64);
// Work out type of class and parse
while (!tokenParser.IsAtEnd)
{
string classType = tokenParser.ReadToken(eatAllBracketContent: true).ToString();
switch (classType.ToLowerInvariant())
{
case "@baseclass":
baseClasses.Add(ParseClass(FgdClassType.Base, tokenParser, fgdName, enums));
break;
case "@pointclass":
concreteClasses.Add(ParseClass(FgdClassType.Point, tokenParser, fgdName, enums));
break;
case "@solidclass":
concreteClasses.Add(ParseClass(FgdClassType.Brush, tokenParser, fgdName, enums));
break;
default:
continue;
}
}
string basePath = Path.GetDirectoryName(fgdFilePath);
string baseEnumPath = Path.Combine(basePath, fgdName, "Enums");
string baseEntitiesPath = Path.Combine(basePath, fgdName, "Entities");
DirectoryUtil.CreateAllDirectories(baseEnumPath);
DirectoryUtil.CreateAllDirectories(baseEntitiesPath);
// Write Enums first
foreach (EnumDefinition enumDef in enums)
{
string enumFile = Path.Combine(baseEnumPath, enumDef.Name + ".cs");
enumDef.WriteCSharp(enumFile, fgdName);
}
// ... then base classes ...
foreach (FgdClass baseClass in baseClasses)
{
string namespaceName = fgdName;
string folderPath = baseEntitiesPath;
baseClass.Name.Split('_', out string folderName, out string bareClassName);
if (!folderName.IsNullOrEmpty())
{
namespaceName = $"{fgdName.ToNamingConvention(NamingConvention.UpperCamelCase)}.{folderName.ToNamingConvention(NamingConvention.UpperCamelCase)}";
folderPath = Path.Combine(folderPath, folderName);
}
DirectoryUtil.CreateAllDirectories(folderPath);
string classFile = Path.Combine(folderPath, "I" + bareClassName.ToNamingConvention(NamingConvention.UpperCamelCase) + ".cs");
baseClass.WriteCSharp(classFile, baseClasses, namespaceName);
}
// ... finally concrete classes
foreach (FgdClass concreteClass in concreteClasses)
{
string namespaceName = fgdName;
string folderPath = baseEntitiesPath;
concreteClass.Name.Split('_', out string folderName, out string bareClassName);
if (!folderName.IsNullOrEmpty())
{
namespaceName = $"{fgdName.ToNamingConvention(NamingConvention.UpperCamelCase)}.{folderName.ToNamingConvention(NamingConvention.UpperCamelCase)}";
folderPath = Path.Combine(folderPath, folderName);
}
DirectoryUtil.CreateAllDirectories(folderPath);
string classFile = Path.Combine(folderPath, bareClassName.ToNamingConvention(NamingConvention.UpperCamelCase) + ".cs");
concreteClass.WriteCSharp(classFile, baseClasses, namespaceName);
}
// Refresh code, in editor
#if UNITY_EDITOR
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
#endif
}
private static FgdClass ParseClass(FgdClassType type, TokenParser tokenParser, string fgdNamespace, List<EnumDefinition> outEnumDefinitions)
{
Dictionary<string, string> metadata = new();
while (!tokenParser.PeekToken("=", eatAllBracketContent: true))
{
string metadataString = tokenParser.ReadToken(eatAllBracketContent: true).ToString(); // e.g. base(asfasf, asdgs) or model({f: "fasf"})
metadataString.Split('(', out string key, out string value);
metadata[key.ToLowerInvariant()] = value[..^1];
}
tokenParser.MatchToken("=", eatAllBracketContent: true);
ReadOnlySpan<char> classname = tokenParser.ReadToken();
string classDescription = null;
classname.Split('_', out string _, out string unityClassname);
unityClassname = unityClassname.ToNamingConvention(NamingConvention.UpperCamelCase);
if (tokenParser.PeekToken(":"))
{
tokenParser.ReadToken();
classDescription = tokenParser.ReadToken().ToString();
}
FgdClass newClass = new(type, classname.ToString(), classDescription ?? $"{unityClassname} Class");
// Add base classes
if (metadata.TryGetValue("base", out string bases))
{
foreach (string baseClass in bases.Split(","))
{
newClass.AddBaseClass(baseClass.Trim());
}
}
//TODO(jwf): what about model, colour, size, etc?
if (!tokenParser.PeekToken("[]"))
{
tokenParser.MatchToken("["); // Eat '['
while (!tokenParser.IsAtEnd && !tokenParser.PeekToken("]"))
{
// Get name and type
ReadOnlySpan<char> typeString = tokenParser.ReadToken(eatAllBracketContent: true); // e.g. thing(Integer)
typeString.Split('(', out string varName, out string typename);
typename = typename[..^1];
string description = null;
string defaultValue = null;
// Has description?
if (tokenParser.PeekToken(":"))
{
tokenParser.MatchToken(":");
description = tokenParser.ReadToken().ToString();
}
// Has default value?
if (tokenParser.PeekToken(":"))
{
tokenParser.MatchToken(":");
defaultValue = tokenParser.ReadToken().ToString();
}
// Has multiple selections?
if (tokenParser.PeekToken("="))
{
tokenParser.MatchToken("=");
tokenParser.MatchToken("[");
if (typename.EqualsInvariant("choices"))
{
// Collect values (not necessarily in order, not necessarily sequential)
Dictionary<int, string> values = new(64);
int counter = 0;
while (!tokenParser.PeekToken("]"))
{
if (!int.TryParse(tokenParser.ReadToken(supportCommentsAndQuotes: false), NumberStyles.Any, CultureInfo.InvariantCulture, out int enumValue))
{
enumValue = counter++;
}
tokenParser.MatchToken(":");
string enumValueName = NamingConventionUtil.FromHuman(tokenParser.ReadToken());
values[enumValue] = enumValueName;
}
tokenParser.MatchToken("]");
int maxEnumValue = values.Keys.Max();
string[] valueStrings = new string[maxEnumValue+1];
for (int i = 0; i <= maxEnumValue; i++)
{
valueStrings[i] = values.TryGetValue(i, out string valueName) ? valueName : $"Value_{i}";
}
string enumName = $"{unityClassname.ToNamingConvention(NamingConvention.UpperCamelCase)}{varName.ToNamingConvention(NamingConvention.UpperCamelCase)}";
outEnumDefinitions.Add(new
(
Name: enumName,
Values: valueStrings
));
newClass.AddField(new FgdEnumField
{
Name = varName,
Description = description,
EnumClassName = $"{fgdNamespace}.{enumName}",
DefaultValue = 0,
Values = valueStrings,
});
}
else if (typename.EqualsInvariant("flags"))
{
//TODO(jwf): this assumes all "flags" are "spawnflags" - we should support other flags
while (!tokenParser.PeekToken("]"))
{
int flagBit = (int)Math.Log(int.Parse(tokenParser.ReadToken(), NumberStyles.Any, CultureInfo.InvariantCulture), 2);
tokenParser.MatchToken(":");
ReadOnlySpan<char> flagName = tokenParser.ReadToken();
int flagDefaultValue = 0;
if (tokenParser.PeekToken(":"))
{
tokenParser.MatchToken(":");
flagDefaultValue = int.Parse(tokenParser.ReadToken(), NumberStyles.Any, CultureInfo.InvariantCulture);
}
newClass.AddField(new FgdSpawnFlagField
{
Bit = flagBit,
Description = flagName.ToString(),
DefaultValue = flagDefaultValue == 1
});
}
}
else
{
Debug.LogWarning("Unexpected options list... skipping...");
while (!tokenParser.PeekToken("]"))
{
tokenParser.ReadToken();
}
}
}
else
{
FgdFieldBase field = FgdFieldFactory.CreateField(typename, varName, description, defaultValue);
if (field == null)
{
Debug.LogWarning($"Could not parse field '{varName}' of type '{typename}'");
continue;
}
newClass.AddField(field);
}
}
}
else
{
// Eat "[]"
tokenParser.MatchToken("[]");
}
return newClass;
}
}
}
+18
View File
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: af5ff8df18459483090b8292fe668523
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FgdParser.cs
uploadId: 939664
+239
View File
@@ -0,0 +1,239 @@
//
// 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<FgdClass> m_OtherClasses = new();
private readonly List<FgdClass> 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<FgdClass> BaseClasses => m_BaseClasses;
public IReadOnlyList<FgdClass> 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<FgdClass>.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<FgdClass>.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);
}
}
}
+10
View File
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 8ae2428dfb70468ab002a264f8bc17de
timeCreated: 1698227021
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FgdWriter.cs
uploadId: 939664
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 80fdd27eed8a4e8cbca74e05b26cfc12
timeCreated: 1712223157
@@ -0,0 +1,44 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
using UnityEngine;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdAnglesField : FgdFieldBase
{
public string Name { get; init; }
public string Description { get; init; }
public Vector3 DefaultValue { get; init; }
public override string FieldName => Name;
public override void Write(StreamWriter sw)
{
if (Mathf.Approximately(DefaultValue.x, DefaultValue.y) && Mathf.Approximately(DefaultValue.y, DefaultValue.z))
{
sw.WriteLine($"{Name}(float) : \"{Description.Replace("\"", "'")}\" : \"{DefaultValue.x.ToStringInvariant()}\"");
}
else
{
sw.WriteLine($"{Name}(float) : \"{Description.Replace("\"", "'")}\" : \"{DefaultValue.ToStringInvariant()}\"");
}
}
public override void WriteCSharpClass(StreamWriter sw, int indent)
{
string defaultValueString = DefaultValue == Vector3.zero
? "Vector3.zero"
: $"new Vector3({DefaultValue.x.ToStringInvariant()}, {DefaultValue.y.ToStringInvariant()}, {DefaultValue.z.ToStringInvariant()})";
WriteClassField(sw, indent, nameof(Vector3), Name, Description, defaultValueString);
}
public override void WriteCSharpInterface(StreamWriter sw, int indent)
=> WriteInterfaceProperty(sw, indent, nameof(Vector3), Name, Description);
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 67ec51e0586d4dedaad0c34cef140416
timeCreated: 1712223260
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdAnglesField.cs
uploadId: 939664
@@ -0,0 +1,39 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdBooleanField : FgdFieldBase
{
public string Name { get; init; }
public string Description { get; init; }
public bool DefaultValue { get; init; }
public override string FieldName => Name;
public override void Write(StreamWriter sw)
{
string description = Description != null ? Description.Replace("\"", "'") : Name;
sw.WriteLine($"{Name}(choices) : \"{description}\" : {(DefaultValue ? "1" : "0")} = ");
sw.WriteLine("\t[");
{
sw.WriteLine("\t\t0 : \"False\"");
sw.WriteLine("\t\t1 : \"True\"");
}
sw.WriteLine("\t]");
}
public override void WriteCSharpClass(StreamWriter sw, int indent)
=> WriteClassField(sw, indent, "bool", Name, Description, DefaultValue ? "true" : null);
public override void WriteCSharpInterface(StreamWriter sw, int indent)
=> WriteInterfaceProperty(sw, indent, "bool", Name, Description);
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 31b88cf082164f41b2a7d78f67919483
timeCreated: 1712223319
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdBooleanField.cs
uploadId: 939664
@@ -0,0 +1,48 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdChoicesIntField : FgdFieldBase
{
public string Name { get; init; }
public string Description { get; init; }
public string[] Choices { get; init; }
public int DefaultValue { get; init; }
public override string FieldName => Name;
public override void Write(StreamWriter sw)
{
if (DefaultValue == 0)
{
sw.WriteLine($"{Name}(choices) : \"{Description.Replace("\"", "'")}\" = ");
}
else
{
sw.WriteLine($"{Name}(choices) : \"{Description.Replace("\"", "'")}\" : {DefaultValue} = ");
}
sw.WriteLine("\t[");
{
for (int i = 0; i < Choices.Length; i++)
{
sw.WriteLine($"\t\t{i} : \"{Choices[i]}\"");
}
}
sw.WriteLine("\t]");
}
public override void WriteCSharpClass(StreamWriter sw, int indent)
=> WriteClassField(sw, indent, "int", Name, Description, DefaultValue == 0 ? null : DefaultValue.ToString());
public override void WriteCSharpInterface(StreamWriter sw, int indent)
=> WriteInterfaceProperty(sw, indent, "int", Name, Description);
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 8b1808df0f9246d981380501211318d8
timeCreated: 1712223378
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdChoicesIntField.cs
uploadId: 939664
@@ -0,0 +1,48 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdChoicesStringField : FgdFieldBase
{
public string Name { get; init; }
public string Description { get; init; }
public string[] Choices { get; init; }
public string DefaultValue { get; init; }
public override string FieldName => Name;
public override void Write(StreamWriter sw)
{
sw.WriteLine($"{Name}(choices) : \"{Description.Replace("\"", "'")}\" : \"{DefaultValue ?? Choices[0]}\" = ");
sw.WriteLine("\t[");
{
foreach (string choice in Choices)
{
if (choice.IsNullOrEmpty())
{
sw.WriteLine("\t\t\"\" : \"(none)\"");
}
else
{
sw.WriteLine($"\t\t\"{choice}\" : \"{choice.ToNamingConvention(NamingConvention.HumanFriendly)}\"");
}
}
}
sw.WriteLine("\t]");
}
//TODO(jwf): support default values?
public override void WriteCSharpClass(StreamWriter sw, int indent)
=> WriteClassField(sw, indent, "string", Name, Description);
public override void WriteCSharpInterface(StreamWriter sw, int indent)
=> WriteInterfaceProperty(sw, indent, "string", Name, Description);
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 6c59811822b3441d835a848021e6ef17
timeCreated: 1712223387
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdChoicesStringField.cs
uploadId: 939664
@@ -0,0 +1,39 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.Globalization;
using System.IO;
using UnityEngine;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdColourField : FgdFieldBase
{
public string Name { get; init; }
public string Description { get; init; }
public Color DefaultColour { get; init; }
public override string FieldName => Name;
public override void Write(StreamWriter sw)
{
sw.WriteLine($"{Name}(color) : \"{Description.Replace("\"", "'")}\" : \"{DefaultColour.ToStringInvariant()}\"");
}
public override void WriteCSharpClass(StreamWriter sw, int indent)
{
string defaultValueString = DefaultColour == Color.white
? "Color.white"
: $"new Color({DefaultColour.r.ToStringInvariant()}, {DefaultColour.g.ToStringInvariant()}, {DefaultColour.b.ToStringInvariant()})";
WriteClassField(sw, indent, nameof(Color), Name, Description, DefaultColour == Color.white ? null : defaultValueString);
}
public override void WriteCSharpInterface(StreamWriter sw, int indent)
=> WriteInterfaceProperty(sw, indent, nameof(Color), Name, Description);
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: d14b410b2bd14d4e8a9a0b9e4cc5d984
timeCreated: 1712223272
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdColourField.cs
uploadId: 939664
@@ -0,0 +1,33 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdComment : FgdFieldBase
{
public string Comment { get; init; }
public override void Write(StreamWriter sw)
{
sw.WriteLine($"// {Comment}");
}
public override void WriteCSharpClass(StreamWriter sw, int indent)
{
sw.WriteIndent(indent);
sw.WriteLine($"// {Comment}");
}
public override void WriteCSharpInterface(StreamWriter sw, int indent)
{
sw.WriteIndent(indent);
sw.WriteLine($"// {Comment}");
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: a46440a8a9254de1aad67d79e4cb1509
timeCreated: 1712223182
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdComment.cs
uploadId: 939664
@@ -0,0 +1,124 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
using System.Linq;
using UnityEngine;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdEnumField : FgdFieldBase
{
public string Name { get; init; }
public string Description { get; init; }
public string EnumClassName { get; init; }
public int DefaultValue { get; init; }
public string[] Values { get; set; }
public override string FieldName => Name;
private string[] EnumValuesOrCached
{
get
{
if (Values == null || Values.Length == 0)
{
if (EnumClassName.ContainsInvariant("+"))
{
EnumClassName.Split('+', out string className, out string enumName);
Debug.LogError($"Couldn't use {enumName} enum. Enums embedded in classes are "
+ $"not supported by Tremble :(. Move your '{enumName}' enum "
+ $"outside of {className} to use it!");
return Array.Empty<string>();
}
Type enumType = TypeDatabase.GetType(EnumClassName);
string[] names = Enum.GetNames(enumType);
int[] values = Enum.GetValues(enumType).Cast<int>().ToArray();
int maxValue = values.Max();
Values = new string[maxValue + 1];
for (int i = 0; i <= maxValue; i++)
{
int nameIdx = Array.FindIndex(values, v => v == i);
if (nameIdx == -1)
{
Values[i] = "_";
}
else
{
Values[i] = names[nameIdx];
}
}
}
return Values;
}
}
public override void Write(StreamWriter sw)
{
TrembleSyncSettings syncSettings = TrembleSyncSettings.Get();
if (syncSettings.SyncEnumsAsStringValue)
{
string[] values = EnumValuesOrCached;
string defaultString = values.Length > DefaultValue && !values[DefaultValue].EqualsInvariant("_") ? values[DefaultValue] : "(none)";
sw.WriteLine($"{Name}(choices) : \"{Description.Replace("\"", "'")}\" : \"{defaultString}\" = ");
}
else
{
if (DefaultValue == 0)
{
sw.WriteLine($"{Name}(choices) : \"{Description.Replace("\"", "'")}\"");
}
else
{
sw.WriteLine($"{Name}(choices) : \"{Description.Replace("\"", "'")}\" : {DefaultValue} = ");
}
}
sw.WriteLine("\t[");
{
for (int i = 0; i < EnumValuesOrCached.Length; i++)
{
string value = EnumValuesOrCached[i];
if (value.EqualsInvariant("_"))
continue;
if (syncSettings.SyncEnumsAsStringValue)
{
sw.WriteLine($"\t\t\"{value}\" : \"{value}\"");
}
else
{
sw.WriteLine($"\t\t{i} : \"{value}\"");
}
}
}
sw.WriteLine("\t]");
}
public override void WriteCSharpClass(StreamWriter sw, int indent)
{
string enumTypeName = EnumClassName;
int lastDot = enumTypeName.LastIndexOf('.');
if (lastDot != -1)
{
enumTypeName = enumTypeName.Substring(lastDot + 1);
}
string defaultValueString = $"{EnumClassName}.{EnumValuesOrCached[DefaultValue]}";
WriteClassField(sw, indent, enumTypeName, Name, Description, defaultValueString);
}
public override void WriteCSharpInterface(StreamWriter sw, int indent)
=> WriteInterfaceProperty(sw, indent, EnumClassName, Name, Description);
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 7cb83d47f4a14179baba19b9197b90ea
timeCreated: 1712223369
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdEnumField.cs
uploadId: 939664
@@ -0,0 +1,97 @@
//
// 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 UnityEngine;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdFlagsField : FgdFieldBase
{
public string Name { get; init; }
public string Description { get; init; }
public string EnumClassName { get; init; }
public string[] EnumNames { get; set; }
public override string FieldName => Name;
private string[] EnumNamesOrCached
{
get
{
if (EnumNames == null || EnumNames.Length == 0)
{
if (EnumClassName.ContainsInvariant("+"))
{
EnumClassName.Split('+', out string className, out string enumName);
Debug.LogError($"Couldn't use {enumName} enum. Enums embedded in classes are "
+ $"not supported by Tremble :(. Move your '{enumName}' enum "
+ $"outside of {className} to use it!");
return Array.Empty<string>();
}
Type enumType = TypeDatabase.GetType(EnumClassName);
string[] names = Enum.GetNames(enumType);
int[] values = Enum.GetValues(enumType).Cast<int>().ToArray();
int maxValue = values.Max();
EnumNames = new string[maxValue + 1];
for (int i = 0; i <= maxValue; i++)
{
int nameIdx = Array.FindIndex(values, v => v == i);
if (nameIdx == -1)
{
EnumNames[i] = "_";
}
else
{
EnumNames[i] = names[nameIdx];
}
}
}
return EnumNames;
}
}
public override void Write(StreamWriter sw)
{
sw.WriteLine($"{Name}(flags) = ");
sw.WriteLine("\t[");
{
for (int i = 0; i < EnumNamesOrCached.Length; i++)
{
string name = EnumNamesOrCached[i];
if (name.EqualsInvariant("_"))
continue;
sw.WriteLine($"\t\t{i} : \"{name}\" : 0");
}
}
sw.WriteLine("\t]");
}
public override void WriteCSharpClass(StreamWriter sw, int indent)
{
string enumTypeName = EnumClassName;
int lastDot = enumTypeName.LastIndexOf('.');
if (lastDot != -1)
{
enumTypeName = enumTypeName.Substring(lastDot + 1);
}
WriteClassField(sw, indent, enumTypeName, Name, Description, "default");
}
public override void WriteCSharpInterface(StreamWriter sw, int indent)
=> WriteInterfaceProperty(sw, indent, EnumClassName, Name, Description, "Flags");
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 86f42cfbe8b54eeeaf7d8102766a3fba
timeCreated: 1774358034
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdFlagsField.cs
uploadId: 939664
@@ -0,0 +1,38 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdFloatField : FgdFieldBase
{
public string Name { get; init; }
public string Description { get; init; }
public float DefaultValue { get; init; }
public override string FieldName => Name;
public override void Write(StreamWriter sw)
{
if (DefaultValue == 0f)
{
sw.WriteLine($"{Name}(float) : \"{Description.Replace("\"", "'")}\"");
}
else
{
sw.WriteLine($"{Name}(float) : \"{Description.Replace("\"", "'")}\" : \"{DefaultValue.ToStringInvariant()}\"");
}
}
public override void WriteCSharpClass(StreamWriter sw, int indent)
=> WriteClassField(sw, indent, "float", Name, Description, DefaultValue == 0f ? null : DefaultValue.ToStringInvariant() + "f");
public override void WriteCSharpInterface(StreamWriter sw, int indent)
=> WriteInterfaceProperty(sw, indent, "float", Name, Description);
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 56cba705d7a240a8b2b05ae6edb8ddeb
timeCreated: 1712223287
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdFloatField.cs
uploadId: 939664
@@ -0,0 +1,38 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdIntegerField : FgdFieldBase
{
public string Name { get; init; }
public string Description { get; init; }
public int DefaultValue { get; init; }
public override string FieldName => Name;
public override void Write(StreamWriter sw)
{
if (DefaultValue == 0)
{
sw.WriteLine($"{Name}(integer) : \"{Description.Replace("\"", "'")}\"");
}
else
{
sw.WriteLine($"{Name}(integer) : \"{Description.Replace("\"", "'")}\" : {DefaultValue}");
}
}
public override void WriteCSharpClass(StreamWriter sw, int indent)
=> WriteClassField(sw, indent, "int", Name, Description, DefaultValue == 0 ? null : DefaultValue.ToString());
public override void WriteCSharpInterface(StreamWriter sw, int indent)
=> WriteInterfaceProperty(sw, indent, "int", Name, Description);
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 96bcc84614cf430590935b78d98f0ca0
timeCreated: 1712223311
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdIntegerField.cs
uploadId: 939664
@@ -0,0 +1,33 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdSpawnFlagField : FgdFieldBase
{
public int Bit { get; init; }
public string Description { get; init; }
public bool DefaultValue { get; init; }
public override string FieldName => NamingConventionUtil.FromHuman(Description);
public override void Write(StreamWriter sw)
{
int bitDefaultValue = 1 << Bit;
sw.WriteLine($"{bitDefaultValue} : \"{Description.Replace("\"", "'")}\" : {(DefaultValue ? "1" : "0")}");
}
public override void WriteCSharpClass(StreamWriter sw, int indent)
=> WriteClassField(sw, indent, "bool", FieldName, Description, DefaultValue ? "true" : null, extraAttributes: $"SpawnFlags({Bit})");
public override void WriteCSharpInterface(StreamWriter sw, int indent)
=> WriteInterfaceProperty(sw, indent, "bool", FieldName, Description, attributes: $"SpawnFlags({Bit})");
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: d5d3344597c842eb8c287ffb9e4e6854
timeCreated: 1712223402
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdSpawnFlagField.cs
uploadId: 939664
@@ -0,0 +1,38 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdStringField : FgdFieldBase
{
public string Name { get; init; }
public string Description { get; init; }
public string DefaultValue { get; init; }
public override string FieldName => Name;
public override void Write(StreamWriter sw)
{
if (DefaultValue.IsNullOrEmpty())
{
sw.WriteLine($"{Name}(string) : \"{Description.Replace("\"", "'")}\"");
}
else
{
sw.WriteLine($"{Name}(string) : \"{Description.Replace("\"", "'")}\" : \"{DefaultValue}\"");
}
}
public override void WriteCSharpClass(StreamWriter sw, int indent)
=> WriteClassField(sw, indent, "string", Name, Description, DefaultValue.IsNullOrEmpty() ? null : $"\"{DefaultValue}\"");
public override void WriteCSharpInterface(StreamWriter sw, int indent)
=> WriteInterfaceProperty(sw, indent, "string", Name, Description);
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: b475d7d5cead4105af48ebd7252e50db
timeCreated: 1712223334
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdStringField.cs
uploadId: 939664
@@ -0,0 +1,40 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
using UnityEngine;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdTargetDestinationField : FgdFieldBase
{
public string Name { get; init; }
public string Description { get; init; }
public string DefaultValue { get; init; }
public override string FieldName => Name;
public override void Write(StreamWriter sw)
{
if (DefaultValue.IsNullOrEmpty())
{
sw.WriteLine($"{Name}(target_destination) : \"{Description.Replace("\"", "'")}\"");
}
else
{
sw.WriteLine($"{Name}(target_destination) : \"{Description.Replace("\"", "'")}\" : \"{DefaultValue}\"");
}
}
//TODO(jwf): support default values?
public override void WriteCSharpClass(StreamWriter sw, int indent)
=> WriteClassField(sw, indent, nameof(Transform), Name, Description);
public override void WriteCSharpInterface(StreamWriter sw, int indent)
=> WriteInterfaceProperty(sw, indent, nameof(Transform), Name, Description);
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: ce6a69e950e34780a3a25026a94798b1
timeCreated: 1712223349
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdTargetDestinationField.cs
uploadId: 939664
@@ -0,0 +1,38 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdTargetSourceField : FgdFieldBase
{
public string Name { get; init; }
public string Description { get; init; }
public string DefaultValue { get; init; }
public override string FieldName => Name;
public override void Write(StreamWriter sw)
{
if (DefaultValue.IsNullOrEmpty())
{
sw.WriteLine($"{Name}(target_source) : \"{Description.Replace("\"", "'")}\"");
}
else
{
sw.WriteLine($"{Name}(target_source) : \"{Description.Replace("\"", "'")}\" : \"{DefaultValue}\"");
}
}
public override void WriteCSharpClass(StreamWriter sw, int indent)
=> WriteClassField(sw, indent, "string", Name, Description, DefaultValue.IsNullOrEmpty() ? null : $"\"{DefaultValue}\"");
public override void WriteCSharpInterface(StreamWriter sw, int indent)
=> WriteInterfaceProperty(sw, indent, "string", Name, Description);
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: ef716bdc4952478b80d299a922219b1a
timeCreated: 1712223341
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdTargetSourceField.cs
uploadId: 939664
@@ -0,0 +1,53 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
using UnityEngine;
namespace TinyGoose.Tremble
{
[Serializable]
public class FgdVectorField : FgdFieldBase
{
public string Name { get; init; }
public string Description { get; init; }
public Vector3 DefaultValue { get; init; }
public bool ConvertToQ3 { get; init; }
public float ImportScale { get; init; }
public override string FieldName => Name;
public override void Write(StreamWriter sw)
{
Vector3 value = DefaultValue;
if (ConvertToQ3)
{
value = new Vector3(-value.x, -value.z, value.y) / ImportScale;
}
if (Mathf.Approximately(value.x, value.y) && Mathf.Approximately(value.y, value.z))
{
sw.WriteLine($"{Name}(float) : \"{Description.Replace("\"", "'")}\" : \"{value.x.ToStringInvariant()}\"");
}
else
{
sw.WriteLine($"{Name}(float) : \"{Description.Replace("\"", "'")}\" : \"{value.ToStringInvariant()}\"");
}
}
public override void WriteCSharpClass(StreamWriter sw, int indent)
{
string defaultValueString = DefaultValue == Vector3.zero
? "Vector3.zero"
: $"new Vector3({DefaultValue.x.ToStringInvariant()}, {DefaultValue.y.ToStringInvariant()}, {DefaultValue.z.ToStringInvariant()})";
WriteClassField(sw, indent, nameof(Vector3), Name, Description, defaultValueString);
}
public override void WriteCSharpInterface(StreamWriter sw, int indent)
=> WriteInterfaceProperty(sw, indent, nameof(Vector3), Name, Description);
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 078725a3929747ceb3f93ad3e68e36d5
timeCreated: 1712223216
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/FieldTypes/FgdVectorField.cs
uploadId: 939664
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f8e7b72eacf0406e90949f0230906787
timeCreated: 1712323557
@@ -0,0 +1,330 @@
//
// 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<FgdClass> 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<FgdClass> 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<string> writtenBaseClasses = new(64);
List<string> 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<string> writtenBaseClasses, List<string> writtenFields, List<FgdClass> 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("}");
}
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: ae5ecd0fb11d4baf988975b57deb3f53
timeCreated: 1712132457
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/Writers/EntityClassWriter.cs
uploadId: 939664
@@ -0,0 +1,69 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System.IO;
namespace TinyGoose.Tremble
{
public static class EntityEnumWriter
{
public static void WriteCSharp(this EnumDefinition enumDefinition, string file, string namespaceOrNull)
{
using FileStream classFile = new(file, FileMode.Create, FileAccess.Write);
using StreamWriter writer = new(classFile);
writer.WriteLine($"// Enum '{enumDefinition.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("{");
}
if (hasNamespace)
{
writer.Write("\t");
}
writer.WriteLine($"public enum {enumDefinition.Name}");
if (hasNamespace)
{
writer.Write("\t");
}
writer.WriteLine("{");
for (int i = 0; i < enumDefinition.Values.Length; i++)
{
string value = enumDefinition.Values[i];
writer.Write("\t");
if (hasNamespace)
{
writer.Write("\t");
}
writer.WriteLine($"{value} = {i},");
}
if (hasNamespace)
{
writer.Write("\t");
}
writer.WriteLine("}");
if (hasNamespace)
{
writer.WriteLine("}");
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: a0ba86bd49f1a4af5b7b8522b5e470de
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/FGD/Writers/EntityEnumWriter.cs
uploadId: 939664
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 17bd3f5d03144731a1a86bfdc8852b41
timeCreated: 1711272095
+294
View File
@@ -0,0 +1,294 @@
//
// 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.Text;
using UnityEngine;
namespace TinyGoose.Tremble.Editor
{
public class MapBrushFace
{
// public Vector3 Position { get; init; }
// public Vector3 Normal { get; init; }
public Vector3 P1 { get; set; }
public Vector3 P2 { get; set; }
public Vector3 P3 { get; set; }
public string TextureName { get; set; }
public float[] TexU { get; init; }
public float[] TexV { get; init; }
public float Rotation { get; init; }
public Vector2 Scale { get; init; }
}
public class MapBrush
{
public List<MapBrushFace> Faces { get; init; }
}
public class MapEntity
{
public Dictionary<string, string> Entries { get; init; }
public List<MapBrush> Brushes { get; init; }
public string Classname => Entries.GetValueOrDefault("classname", null);
}
public class Map
{
public Dictionary<string, string> Metadata { get; init; }
public List<MapEntity> Entities { get; init; }
public void WriteTo(string path)
{
using FileStream fs = new(path, FileMode.Create, FileAccess.Write);
using StreamWriter sw = new(fs);
sw.WriteLine($"// Game: {TrembleConsts.GAME_NAME}");
sw.WriteLine("// Format: Quake3 (Valve)");
for (int entityIdx = 0; entityIdx < Entities.Count; entityIdx++)
{
sw.WriteLine($"// entity {entityIdx}");
sw.WriteLine("{");
foreach ((string key, string value) in Entities[entityIdx].Entries)
{
sw.WriteLine($"\"{key}\" \"{value}\"");
}
if (Entities[entityIdx].Brushes != null)
{
for (int brushIdx = 0; brushIdx < Entities[entityIdx].Brushes.Count; brushIdx++)
{
MapBrush brush = Entities[entityIdx].Brushes[brushIdx];
sw.WriteLine($"// brush {brushIdx}");
sw.WriteLine("{");
foreach (MapBrushFace face in brush.Faces)
{
//Vector3[] points = new Vector3[3];
// Vector3 arb = Vector3.Normalize(face.Normal + new Vector3(2f, 3f, 1f));
// points[0] = face.Position;
// points[1] = face.Position + Vector3.Cross(face.Normal, arb);
// points[2] = face.Position + Vector3.Cross(points[1], face.Normal);
// foreach (Vector3 p in points)
// {
// sw.Write($"( {p.x} {p.y} {p.z} ) ");
// }
sw.Write($"( {face.P1.ToStringInvariant()} ) ");
sw.Write($"( {face.P2.ToStringInvariant()} ) ");
sw.Write($"( {face.P3.ToStringInvariant()} ) ");
sw.Write(face.TextureName);
sw.Write(" [ ");
foreach (float f in face.TexU)
{
sw.Write(f.ToStringInvariant(5));
sw.Write(" ");
}
sw.Write("] [ ");
foreach (float f in face.TexV)
{
sw.Write(f.ToStringInvariant(5));
sw.Write(" ");
}
sw.WriteLine($"] {face.Rotation.ToStringInvariant()} {face.Scale.ToStringInvariant()}");
}
sw.WriteLine("}");
}
}
sw.WriteLine("}");
}
}
}
public static class MapParser
{
private const string TOKEN_START = "{";
private const string TOKEN_END = "}";
public static Map Parse(string mapPath)
{
List<MapEntity> entities = new(512);
Dictionary<string, string> metadata = new(16);
// Quicker than using streams (wtf?)
string mapContents = File.ReadAllText(mapPath).Replace("\r", "");
// Read metadata
string remaining = mapContents;
while (remaining.StartsWithInvariant("//") && remaining.ContainsInvariant(":"))
{
remaining.Split('\n', out string thisLine, out remaining);
thisLine.Substring(2).Split(':', out string key, out string value);
if (key == null || value == null)
break;
metadata[key] = value;
}
// Parse map
TokenParser parser = new(mapContents);
while (!parser.IsAtEnd)
{
entities.Add(parser.ReadEntity());
}
return new() { Metadata = metadata, Entities = entities };
}
public static Dictionary<string, string> ParseMetadataOnly(string mapPath)
{
Dictionary<string, string> metadata = new(16);
// Quicker than using streams (wtf?)
using FileStream file = new(mapPath, FileMode.Open, FileAccess.Read);
using StreamReader sr = new(file, Encoding.ASCII);
// Read metadata
while (!sr.EndOfStream)
{
string line = sr.ReadLine();
if (line == null)
break;
if (!line.StartsWithInvariant("//") || !line.ContainsInvariant(":"))
break;
line.Substring(2).Split(':', out string key, out string value);
if (key == null || value == null)
break;
metadata[key] = value;
}
return metadata;
}
private static MapEntity ReadEntity(this TokenParser parser)
{
if (!parser.MatchToken(TOKEN_START))
return new();
Dictionary<string, string> entries = new(64);
while (!parser.IsAtEnd)
{
ReadOnlySpan<char> token = parser.PeekToken();
if (token.EqualsInvariant(TOKEN_START, caseSensitive: true))
{
// Found brushes, parse them
List<MapBrush> brushes = ParseBrushes(parser);
parser.MatchToken(TOKEN_END); // Consume end brace of entity
return new() { Brushes = brushes, Entries = entries };
}
else if (token.EqualsInvariant(TOKEN_END, caseSensitive: true))
{
// Found end - with no brushes
parser.MatchToken(TOKEN_END); // Consume end brace of entity
return new() { Brushes = new(), Entries = entries };
}
else
{
// Still parsing entries
string key = parser.ReadToken().ToString();
string value = parser.ReadToken().ToString();
entries.Add(key, value);
}
}
return new();
}
private static List<MapBrush> ParseBrushes(in TokenParser parser)
{
List<MapBrush> brushes = new(64);
while (true)
{
brushes.Add(parser.ReadBrush());
if (parser.PeekToken(TOKEN_END))
break;
}
return brushes;
}
private static MapBrush ReadBrush(this TokenParser parser)
{
parser.MatchToken(TOKEN_START); // Consume open brace
List<MapBrushFace> faces = new(64);
while (!parser.IsAtEnd)
{
ReadOnlySpan<char> token = parser.PeekToken();
if (token.EqualsInvariant(TOKEN_END))
{
// End of brush faces
break;
}
faces.Add(parser.ReadBrushFace());
}
parser.MatchToken(TOKEN_END); // Consume close brace
return new() { Faces = faces };
}
private static MapBrushFace ReadBrushFace(this TokenParser parser)
{
Vector3 v0 = parser.ReadVector3(false);
Vector3 v1 = parser.ReadVector3(false);
Vector3 v2 = parser.ReadVector3(false);
ReadOnlySpan<char> textureName = parser.ReadToken(disableNumLock: true);
float[] texU, texV;
if (parser.PeekToken("["))
{
// Valve format
texU = parser.ReadMatrix(4);
texV = parser.ReadMatrix(4);
}
else
{
// Standard format
Vector3 u = Vector3.Normalize(v0 - v1);
Vector3 v = Vector3.Normalize(v2 - v1);
texU = new [] { u.x, u.y, u.z, parser.ReadFloat() };
texV = new [] { v.x, v.y, v.z, parser.ReadFloat() };
}
//Vector3 planeNormal = Vector3.Normalize(Vector3.Cross(v1 - v0, v2 - v0));
float rotation = parser.ReadFloat();
Vector2 scale = new() { x = parser.ReadFloat(), y = parser.ReadFloat() };
return new()
{
// Position = v0,
// Normal = planeNormal,
P1 = v0,
P2 = v1,
P3 = v2,
TextureName = textureName.ToString(),
TexU = texU,
TexV = texV,
Rotation = rotation,
Scale = scale,
};
}
}
}
+10
View File
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: aecd40d2f27c43f1b907b5479abb07f8
timeCreated: 1711272189
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/MAP/MapParser.cs
uploadId: 939664
+246
View File
@@ -0,0 +1,246 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.IO;
using UnityEngine;
namespace TinyGoose.Tremble
{
// -----------------------------------------------------------------------------------------------------------------------------
// Writing
// -----------------------------------------------------------------------------------------------------------------------------
// For writing to the MD3 stream, we bypass BinaryWriter because it's MUCH slower.
// I think it's because the BinaryWriter does Endianness and Encoding related things, which we
// don't need for our limited use-case of ASCII-encoded MD3 models.
public class StreamWriteIO : IDisposable
{
private const int BUFFER_SIZE_KB = 160; // 160kB buffer
public StreamWriteIO(Stream stream)
{
m_Stream = stream;
}
public void Dispose()
{
Flush();
}
private readonly Stream m_Stream;
private readonly byte[] m_Buffer = new byte[BUFFER_SIZE_KB * 1024];
private int m_BufPtr;
private readonly float[] m_Floats = new float[1];
private readonly byte[] m_Bytes = new byte[4];
public void WriteInt32(int value)
{
WriteByte((byte)(value & 0xFF));
WriteByte((byte)((value >> 8) & 0xFF));
WriteByte((byte)((value >> 16) & 0xFF));
WriteByte((byte)((value >> 24) & 0xFF));
}
public void WriteFloat(float value)
{
// Block copy the float value's bytes into preallocated buffer
m_Floats[0] = value;
Buffer.BlockCopy(m_Floats, 0, m_Bytes, 0, 4);
// Write out
WriteByte(m_Bytes[0]);
WriteByte(m_Bytes[1]);
WriteByte(m_Bytes[2]);
WriteByte(m_Bytes[3]);
}
public void WriteInt16(short value)
{
WriteByte((byte)(value & 0xFF));
WriteByte((byte)((value >> 8) & 0xFF));
}
public void WriteNormalAsLatLong(Vector3 normal)
{
const float BYTE_TO_DEG = (255.0f / 360.0f) * Mathf.Rad2Deg;
Vector3 q3Normal = normal.UnityToQ3Vector(scale: 1f);
if (q3Normal is { x: 0, y: 0 })
{
WriteByte(q3Normal.z > 0 ? (byte)0x00 : (byte)0x80); // U
WriteByte(0x00); // V
}
else
{
WriteByte((byte)(Mathf.Acos(q3Normal.z) * BYTE_TO_DEG)); // U
WriteByte((byte)(Mathf.Atan2(q3Normal.y, q3Normal.x) * BYTE_TO_DEG)); // V
}
}
public void WriteBytes(byte[] bytes)
{
foreach (byte b in bytes)
{
WriteByte(b);
}
}
public void WriteBytes(byte[] buffer, int count)
{
for (int b = 0; b < count; b++)
{
WriteByte(buffer[b]);
}
}
public void WriteString(string value)
{
foreach (char c in value)
{
WriteByte((byte)c);
}
}
public void WriteString(string value, int length)
{
for (int i = 0; i < length; i++)
{
if (i < value.Length)
{
WriteByte((byte)value[i]);
}
else
{
WriteByte(0x00);
}
}
}
private void WriteByte(byte b)
{
if (m_BufPtr >= m_Buffer.Length)
{
Flush();
}
m_Buffer[m_BufPtr++] = b;
}
private void Flush()
{
if (m_BufPtr == 0)
return;
m_Stream.Write(m_Buffer, 0, m_BufPtr);
m_BufPtr = 0;
}
}
// -----------------------------------------------------------------------------------------------------------------------------
// Reading
// -----------------------------------------------------------------------------------------------------------------------------
public class StreamReadIO
{
private readonly float[] m_Floats = new float[1];
private readonly byte[] m_Bytes = new byte[8];
private readonly byte[] m_StreamContents;
private int m_Offset;
public StreamReadIO(string path)
{
m_StreamContents = File.ReadAllBytes(path);
}
public void Seek(int offset)
{
m_Offset = offset;
}
public int Position => m_Offset;
public byte ReadByte()
{
return m_StreamContents[m_Offset++];
}
public void SkipBytes(int count)
{
m_Offset += count;
}
public short ReadInt16()
{
m_Bytes[0] = ReadByte();
m_Bytes[1] = ReadByte();
return (short)(m_Bytes[0] | m_Bytes[1] << 8);
}
public int ReadInt32()
{
m_Bytes[0] = ReadByte();
m_Bytes[1] = ReadByte();
m_Bytes[2] = ReadByte();
m_Bytes[3] = ReadByte();
return (int)(m_Bytes[0] | m_Bytes[1] << 8 | m_Bytes[2] << 16 | m_Bytes[3] << 24);
}
public float ReadSingle()
{
// Block copy the bytes into a float, using a preallocated buffer
m_Bytes[0] = ReadByte();
m_Bytes[1] = ReadByte();
m_Bytes[2] = ReadByte();
m_Bytes[3] = ReadByte();
Buffer.BlockCopy(m_Bytes, 0, m_Floats, 0, 4);
// Return
return m_Floats[0];
}
public char[] ReadChars(int numChars)
{
char[] result = new char[numChars];
for (int i = 0; i < numChars; i++)
{
result[i] = (char)ReadByte();
}
return result;
}
public byte[] ReadBytes(int numBytes)
{
byte[] result = new byte[numBytes];
Buffer.BlockCopy(m_StreamContents, m_Offset, result, 0, numBytes);
m_Offset += numBytes;
return result;
}
public string ReadString(int numChars)
{
string result = new(ReadChars(numChars));
int nulCharacter = result.IndexOf('\0');
return nulCharacter > 0 ? result.Substring(0, nulCharacter) : result;
}
public Vector3 ReadVector3(bool convertToQ3 = false, float scale = 1f)
{
Vector3 result = new(ReadSingle() * scale, ReadSingle() * scale, ReadSingle() * scale);
return convertToQ3 ? result.Q3ToUnityVector() : result;
}
public Vector2 ReadUV()
=> new(ReadSingle() + 0.5f, 1f - ReadSingle() + 0.5f); // Flip Y and offset by 0.5, 0.5
public Color32 ReadColour()
=> new(ReadByte(), ReadByte(), ReadByte(), ReadByte());
public Vector3Int ReadVector3Int() => new(ReadInt32(), ReadInt32(), ReadInt32());
public Vector2Int ReadVector2Int() => new(ReadInt32(), ReadInt32());
}
}
+10
View File
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 06ba2a69ad404fb2a03db07dd4ce5ccf
timeCreated: 1710420269
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/StreamIO.cs
uploadId: 939664
+251
View File
@@ -0,0 +1,251 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.Globalization;
using System.IO;
using UnityEngine;
namespace TinyGoose.Tremble
{
public class TokenParser
{
private readonly string m_String;
private int m_Index;
public TokenParser(string inString)
{
m_String = inString.Replace("\r", ""); // Remove Windows newlines
m_Index = 0;
}
public bool IsAtEnd
{
get
{
// Fast-forward to next non-whitespace character, if we can
int index = m_Index;
while (index < m_String.Length)
{
if (m_String[index] is '\n' or '\t')
{
index++;
continue;
}
break;
}
return (index >= m_String.Length);
}
}
#if UNITY_EDITOR
public string DEBUG_CurrentBuffer
{
get
{
const int WINDOW_SIZE = 20;
string before = m_String.Substring(Math.Max(0, m_Index - WINDOW_SIZE), WINDOW_SIZE);
string after = m_String.Substring(m_Index + 1, Math.Min(m_String.Length - m_Index - 1, WINDOW_SIZE));
return $"{before} -->{PeekToken().ToString()}<-- {after}";
}
}
#endif
private char Take() => !IsAtEnd ? m_String[m_Index++] : '\n';
private char Peek() => !IsAtEnd ? m_String[m_Index] : '\n';
private void Skip() => m_Index++;
private void FastForwardTo(char target)
{
while (!IsAtEnd && m_String[m_Index] != target)
{
Skip();
}
}
public ReadOnlySpan<char> ReadToken(bool supportCommentsAndQuotes = true, bool eatAllBracketContent = false, bool disableNumLock = false)
{
if (IsAtEnd)
return "\n";
// Skip newlines and leading whitespace
while (Peek() is '\n' or '\t' or ' ')
{
Take();
}
bool quoteLock = false;
if (supportCommentsAndQuotes)
{
// Handle comment lines:
// If it's a /, check for // and if found, skip to end of line
while (Peek() == '/')
{
int commentStartIdx = m_Index;
Skip();
if (Peek() != '/')
{
// No second /? Weird, roll back and continue as normal
m_Index = commentStartIdx;
break;
}
// Fast-forwarding to EOL...
FastForwardTo('\n');
Skip();
// Skip newlines if found
while (Peek() == '\n')
{
Take();
}
}
// Turn on "quote lock" and skip the quote if the token starts with a quote
quoteLock = Peek() == '"';
if (quoteLock)
{
Skip();
}
}
int startIdx = m_Index;
int numChars = 0;
bool numLock = false;
int inBrackets = 0; // keep track of whether inside brackets
while (m_Index < m_String.Length)
{
char nextChar = Take();
numChars++;
// Turn on numlock when we see a number outside brackets/quotes
if (!disableNumLock && IsCharNumberFast(nextChar) && inBrackets == 0 && !quoteLock)
{
numLock = true;
}
// In numlock, but that wasn't a number or "number punctuation"!
if (numLock && !IsCharNumberFast(nextChar) && nextChar is not '.' and not ',' and not 'e' and not 'E' and not '-' and not '\'')
{
if (!IsAtEnd)
{
m_Index--; // rewind if not at end!
}
break;
}
// End of line - that's it!
if (inBrackets == 0 && nextChar == '\n')
break;
if (eatAllBracketContent && nextChar == '(') { inBrackets++; }
if (eatAllBracketContent && nextChar == ')') { inBrackets--; }
// Not in quote-lock - found space
if (inBrackets == 0 && !quoteLock && nextChar == ' ')
break;
// We're in quote-lock and we hit the end-quote
if (nextChar == '"' && quoteLock)
break;
}
// Return string slice
return (numChars == 0) ? "" : m_String.AsSpan(startIdx, numChars - 1);
}
private bool IsCharNumberFast(char c) => c is >= '0' and <= '9';
public ReadOnlySpan<char> PeekToken(bool supportCommentsAndQuotes = true, bool eatAllBracketContent = false, bool disableNumLock = false)
{
int index = m_Index;
try
{
return ReadToken(supportCommentsAndQuotes, eatAllBracketContent, disableNumLock);
}
finally
{
m_Index = index;
}
}
public bool PeekToken(string expected, bool supportCommentsAndQuotes = true, bool eatAllBracketContent = false, bool disableNumLock = false)
{
return PeekToken(supportCommentsAndQuotes, eatAllBracketContent, disableNumLock).Equals(expected.AsSpan(), StringComparison.InvariantCultureIgnoreCase);
}
public bool MatchToken(string expected, bool supportCommentsAndQuotes = true, bool eatAllBracketContent = false, bool disableNumLock = false)
{
ReadOnlySpan<char> token = ReadToken(supportCommentsAndQuotes, eatAllBracketContent, disableNumLock);
bool tokenMatches;
if (expected.Length == 1)
{
tokenMatches = (token[0] == expected[0]);
}
else
{
tokenMatches = token.Equals(expected.AsSpan(), StringComparison.InvariantCulture);
}
if (tokenMatches)
return true;
throw new InvalidDataException($"Expected a '{expected}', got a '{token.ToString()}'");
}
public Vector3 ReadVector3(bool convertToUnity = true)
{
MatchToken("(");
Vector3 vec = new()
{
x = ReadFloat(),
y = ReadFloat(),
z = ReadFloat()
};
MatchToken(")");
float scale = TrembleSyncSettings.Get().ImportScale;
return convertToUnity ? vec.Q3ToUnityVector(scale) : vec;
}
public float ReadFloat()
{
ReadOnlySpan<char> floatValue = ReadToken(supportCommentsAndQuotes: false);
if (!floatValue.TryParseFloat(out float val))
{
Debug.Log($"Not a float '{floatValue.ToString()}'!");
}
return val;
}
public ulong ReadUlong() => ulong.Parse(ReadToken(supportCommentsAndQuotes: false), NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat);
public float[] ReadMatrix(int length)
{
float[] matrix = new float[length];
MatchToken("[");
for (int tok = 0; tok < length; tok++)
{
matrix[tok] = ReadFloat();
}
MatchToken("]");
return matrix;
}
}
}
+10
View File
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 58dbe14b462d43a8a9f6ccb02d2ebf86
timeCreated: 1702378526
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/TokenParser.cs
uploadId: 939664
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d3a206c04d254db180f8492831596dad
timeCreated: 1722843889
+304
View File
@@ -0,0 +1,304 @@
//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System.Collections.Generic;
using UnityEngine;
namespace TinyGoose.Tremble
{
public class WadPalette
{
private Color32[] m_Colours;
public ref Color32 Get(int pos) => ref m_Colours[pos];
private static WadPalette s_QuakePalette;
public static WadPalette QuakePalette
{
get
{
if (s_QuakePalette == null)
{
s_QuakePalette = new()
{
m_Colours = new Color32[]
{
new(0x00, 0x00, 0x00, 0xFF),
new(0x0F, 0x0F, 0x0F, 0xFF),
new(0x1F, 0x1F, 0x1F, 0xFF),
new(0x2F, 0x2F, 0x2F, 0xFF),
new(0x3F, 0x3F, 0x3F, 0xFF),
new(0x4B, 0x4B, 0x4B, 0xFF),
new(0x5B, 0x5B, 0x5B, 0xFF),
new(0x6B, 0x6B, 0x6B, 0xFF),
new(0x7B, 0x7B, 0x7B, 0xFF),
new(0x8B, 0x8B, 0x8B, 0xFF),
new(0x9B, 0x9B, 0x9B, 0xFF),
new(0xAB, 0xAB, 0xAB, 0xFF),
new(0xBB, 0xBB, 0xBB, 0xFF),
new(0xCB, 0xCB, 0xCB, 0xFF),
new(0xDB, 0xDB, 0xDB, 0xFF),
new(0xEB, 0xEB, 0xEB, 0xFF),
new(0x0F, 0x0B, 0x07, 0xFF),
new(0x17, 0x0F, 0x0B, 0xFF),
new(0x1F, 0x17, 0x0B, 0xFF),
new(0x27, 0x1B, 0x0F, 0xFF),
new(0x2F, 0x23, 0x13, 0xFF),
new(0x37, 0x2B, 0x17, 0xFF),
new(0x3F, 0x2F, 0x17, 0xFF),
new(0x4B, 0x37, 0x1B, 0xFF),
new(0x53, 0x3B, 0x1B, 0xFF),
new(0x5B, 0x43, 0x1F, 0xFF),
new(0x63, 0x4B, 0x1F, 0xFF),
new(0x6B, 0x53, 0x1F, 0xFF),
new(0x73, 0x57, 0x1F, 0xFF),
new(0x7B, 0x5F, 0x23, 0xFF),
new(0x83, 0x67, 0x23, 0xFF),
new(0x8F, 0x6F, 0x23, 0xFF),
new(0x0B, 0x0B, 0x0F, 0xFF),
new(0x13, 0x13, 0x1B, 0xFF),
new(0x1B, 0x1B, 0x27, 0xFF),
new(0x27, 0x27, 0x33, 0xFF),
new(0x2F, 0x2F, 0x3F, 0xFF),
new(0x37, 0x37, 0x4B, 0xFF),
new(0x3F, 0x3F, 0x57, 0xFF),
new(0x47, 0x47, 0x67, 0xFF),
new(0x4F, 0x4F, 0x73, 0xFF),
new(0x5B, 0x5B, 0x7F, 0xFF),
new(0x63, 0x63, 0x8B, 0xFF),
new(0x6B, 0x6B, 0x97, 0xFF),
new(0x73, 0x73, 0xA3, 0xFF),
new(0x7B, 0x7B, 0xAF, 0xFF),
new(0x83, 0x83, 0xBB, 0xFF),
new(0x8B, 0x8B, 0xCB, 0xFF),
new(0x00, 0x00, 0x00, 0xFF),
new(0x07, 0x07, 0x00, 0xFF),
new(0x0B, 0x0B, 0x00, 0xFF),
new(0x13, 0x13, 0x00, 0xFF),
new(0x1B, 0x1B, 0x00, 0xFF),
new(0x23, 0x23, 0x00, 0xFF),
new(0x2B, 0x2B, 0x07, 0xFF),
new(0x2F, 0x2F, 0x07, 0xFF),
new(0x37, 0x37, 0x07, 0xFF),
new(0x3F, 0x3F, 0x07, 0xFF),
new(0x47, 0x47, 0x07, 0xFF),
new(0x4B, 0x4B, 0x0B, 0xFF),
new(0x53, 0x53, 0x0B, 0xFF),
new(0x5B, 0x5B, 0x0B, 0xFF),
new(0x63, 0x63, 0x0B, 0xFF),
new(0x6B, 0x6B, 0x0F, 0xFF),
new(0x07, 0x00, 0x00, 0xFF),
new(0x0F, 0x00, 0x00, 0xFF),
new(0x17, 0x00, 0x00, 0xFF),
new(0x1F, 0x00, 0x00, 0xFF),
new(0x27, 0x00, 0x00, 0xFF),
new(0x2F, 0x00, 0x00, 0xFF),
new(0x37, 0x00, 0x00, 0xFF),
new(0x3F, 0x00, 0x00, 0xFF),
new(0x47, 0x00, 0x00, 0xFF),
new(0x4F, 0x00, 0x00, 0xFF),
new(0x57, 0x00, 0x00, 0xFF),
new(0x5F, 0x00, 0x00, 0xFF),
new(0x67, 0x00, 0x00, 0xFF),
new(0x6F, 0x00, 0x00, 0xFF),
new(0x77, 0x00, 0x00, 0xFF),
new(0x7F, 0x00, 0x00, 0xFF),
new(0x13, 0x13, 0x00, 0xFF),
new(0x1B, 0x1B, 0x00, 0xFF),
new(0x23, 0x23, 0x00, 0xFF),
new(0x2F, 0x2B, 0x00, 0xFF),
new(0x37, 0x2F, 0x00, 0xFF),
new(0x43, 0x37, 0x00, 0xFF),
new(0x4B, 0x3B, 0x07, 0xFF),
new(0x57, 0x43, 0x07, 0xFF),
new(0x5F, 0x47, 0x07, 0xFF),
new(0x6B, 0x4B, 0x0B, 0xFF),
new(0x77, 0x53, 0x0F, 0xFF),
new(0x83, 0x57, 0x13, 0xFF),
new(0x8B, 0x5B, 0x13, 0xFF),
new(0x97, 0x5F, 0x1B, 0xFF),
new(0xA3, 0x63, 0x1F, 0xFF),
new(0xAF, 0x67, 0x23, 0xFF),
new(0x23, 0x13, 0x07, 0xFF),
new(0x2F, 0x17, 0x0B, 0xFF),
new(0x3B, 0x1F, 0x0F, 0xFF),
new(0x4B, 0x23, 0x13, 0xFF),
new(0x57, 0x2B, 0x17, 0xFF),
new(0x63, 0x2F, 0x1F, 0xFF),
new(0x73, 0x37, 0x23, 0xFF),
new(0x7F, 0x3B, 0x2B, 0xFF),
new(0x8F, 0x43, 0x33, 0xFF),
new(0x9F, 0x4F, 0x33, 0xFF),
new(0xAF, 0x63, 0x2F, 0xFF),
new(0xBF, 0x77, 0x2F, 0xFF),
new(0xCF, 0x8F, 0x2B, 0xFF),
new(0xDF, 0xAB, 0x27, 0xFF),
new(0xEF, 0xCB, 0x1F, 0xFF),
new(0xFF, 0xF3, 0x1B, 0xFF),
new(0x0B, 0x07, 0x00, 0xFF),
new(0x1B, 0x13, 0x00, 0xFF),
new(0x2B, 0x23, 0x0F, 0xFF),
new(0x37, 0x2B, 0x13, 0xFF),
new(0x47, 0x33, 0x1B, 0xFF),
new(0x53, 0x37, 0x23, 0xFF),
new(0x63, 0x3F, 0x2B, 0xFF),
new(0x6F, 0x47, 0x33, 0xFF),
new(0x7F, 0x53, 0x3F, 0xFF),
new(0x8B, 0x5F, 0x47, 0xFF),
new(0x9B, 0x6B, 0x53, 0xFF),
new(0xA7, 0x7B, 0x5F, 0xFF),
new(0xB7, 0x87, 0x6B, 0xFF),
new(0xC3, 0x93, 0x7B, 0xFF),
new(0xD3, 0xA3, 0x8B, 0xFF),
new(0xE3, 0xB3, 0x97, 0xFF),
new(0xAB, 0x8B, 0xA3, 0xFF),
new(0x9F, 0x7F, 0x97, 0xFF),
new(0x93, 0x73, 0x87, 0xFF),
new(0x8B, 0x67, 0x7B, 0xFF),
new(0x7F, 0x5B, 0x6F, 0xFF),
new(0x77, 0x53, 0x63, 0xFF),
new(0x6B, 0x4B, 0x57, 0xFF),
new(0x5F, 0x3F, 0x4B, 0xFF),
new(0x57, 0x37, 0x43, 0xFF),
new(0x4B, 0x2F, 0x37, 0xFF),
new(0x43, 0x27, 0x2F, 0xFF),
new(0x37, 0x1F, 0x23, 0xFF),
new(0x2B, 0x17, 0x1B, 0xFF),
new(0x23, 0x13, 0x13, 0xFF),
new(0x17, 0x0B, 0x0B, 0xFF),
new(0x0F, 0x07, 0x07, 0xFF),
new(0xBB, 0x73, 0x9F, 0xFF),
new(0xAF, 0x6B, 0x8F, 0xFF),
new(0xA3, 0x5F, 0x83, 0xFF),
new(0x97, 0x57, 0x77, 0xFF),
new(0x8B, 0x4F, 0x6B, 0xFF),
new(0x7F, 0x4B, 0x5F, 0xFF),
new(0x73, 0x43, 0x53, 0xFF),
new(0x6B, 0x3B, 0x4B, 0xFF),
new(0x5F, 0x33, 0x3F, 0xFF),
new(0x53, 0x2B, 0x37, 0xFF),
new(0x47, 0x23, 0x2B, 0xFF),
new(0x3B, 0x1F, 0x23, 0xFF),
new(0x2F, 0x17, 0x1B, 0xFF),
new(0x23, 0x13, 0x13, 0xFF),
new(0x17, 0x0B, 0x0B, 0xFF),
new(0x0F, 0x07, 0x07, 0xFF),
new(0xDB, 0xC3, 0xBB, 0xFF),
new(0xCB, 0xB3, 0xA7, 0xFF),
new(0xBF, 0xA3, 0x9B, 0xFF),
new(0xAF, 0x97, 0x8B, 0xFF),
new(0xA3, 0x87, 0x7B, 0xFF),
new(0x97, 0x7B, 0x6F, 0xFF),
new(0x87, 0x6F, 0x5F, 0xFF),
new(0x7B, 0x63, 0x53, 0xFF),
new(0x6B, 0x57, 0x47, 0xFF),
new(0x5F, 0x4B, 0x3B, 0xFF),
new(0x53, 0x3F, 0x33, 0xFF),
new(0x43, 0x33, 0x27, 0xFF),
new(0x37, 0x2B, 0x1F, 0xFF),
new(0x27, 0x1F, 0x17, 0xFF),
new(0x1B, 0x13, 0x0F, 0xFF),
new(0x0F, 0x0B, 0x07, 0xFF),
new(0x6F, 0x83, 0x7B, 0xFF),
new(0x67, 0x7B, 0x6F, 0xFF),
new(0x5F, 0x73, 0x67, 0xFF),
new(0x57, 0x6B, 0x5F, 0xFF),
new(0x4F, 0x63, 0x57, 0xFF),
new(0x47, 0x5B, 0x4F, 0xFF),
new(0x3F, 0x53, 0x47, 0xFF),
new(0x37, 0x4B, 0x3F, 0xFF),
new(0x2F, 0x43, 0x37, 0xFF),
new(0x2B, 0x3B, 0x2F, 0xFF),
new(0x23, 0x33, 0x27, 0xFF),
new(0x1F, 0x2B, 0x1F, 0xFF),
new(0x17, 0x23, 0x17, 0xFF),
new(0x0F, 0x1B, 0x13, 0xFF),
new(0x0B, 0x13, 0x0B, 0xFF),
new(0x07, 0x0B, 0x07, 0xFF),
new(0xFF, 0xF3, 0x1B, 0xFF),
new(0xEF, 0xDF, 0x17, 0xFF),
new(0xDB, 0xCB, 0x13, 0xFF),
new(0xCB, 0xB7, 0x0F, 0xFF),
new(0xBB, 0xA7, 0x0F, 0xFF),
new(0xAB, 0x97, 0x0B, 0xFF),
new(0x9B, 0x83, 0x07, 0xFF),
new(0x8B, 0x73, 0x07, 0xFF),
new(0x7B, 0x63, 0x07, 0xFF),
new(0x6B, 0x53, 0x00, 0xFF),
new(0x5B, 0x47, 0x00, 0xFF),
new(0x4B, 0x37, 0x00, 0xFF),
new(0x3B, 0x2B, 0x00, 0xFF),
new(0x2B, 0x1F, 0x00, 0xFF),
new(0x1B, 0x0F, 0x00, 0xFF),
new(0x0B, 0x07, 0x00, 0xFF),
new(0x00, 0x00, 0xFF, 0xFF),
new(0x0B, 0x0B, 0xEF, 0xFF),
new(0x13, 0x13, 0xDF, 0xFF),
new(0x1B, 0x1B, 0xCF, 0xFF),
new(0x23, 0x23, 0xBF, 0xFF),
new(0x2B, 0x2B, 0xAF, 0xFF),
new(0x2F, 0x2F, 0x9F, 0xFF),
new(0x2F, 0x2F, 0x8F, 0xFF),
new(0x2F, 0x2F, 0x7F, 0xFF),
new(0x2F, 0x2F, 0x6F, 0xFF),
new(0x2F, 0x2F, 0x5F, 0xFF),
new(0x2B, 0x2B, 0x4F, 0xFF),
new(0x23, 0x23, 0x3F, 0xFF),
new(0x1B, 0x1B, 0x2F, 0xFF),
new(0x13, 0x13, 0x1F, 0xFF),
new(0x0B, 0x0B, 0x0F, 0xFF),
new(0x2B, 0x00, 0x00, 0xFF),
new(0x3B, 0x00, 0x00, 0xFF),
new(0x4B, 0x07, 0x00, 0xFF),
new(0x5F, 0x07, 0x00, 0xFF),
new(0x6F, 0x0F, 0x00, 0xFF),
new(0x7F, 0x17, 0x07, 0xFF),
new(0x93, 0x1F, 0x07, 0xFF),
new(0xA3, 0x27, 0x0B, 0xFF),
new(0xB7, 0x33, 0x0F, 0xFF),
new(0xC3, 0x4B, 0x1B, 0xFF),
new(0xCF, 0x63, 0x2B, 0xFF),
new(0xDB, 0x7F, 0x3B, 0xFF),
new(0xE3, 0x97, 0x4F, 0xFF),
new(0xE7, 0xAB, 0x5F, 0xFF),
new(0xEF, 0xBF, 0x77, 0xFF),
new(0xF7, 0xD3, 0x8B, 0xFF),
new(0xA7, 0x7B, 0x3B, 0xFF),
new(0xB7, 0x9B, 0x37, 0xFF),
new(0xC7, 0xC3, 0x37, 0xFF),
new(0xE7, 0xE3, 0x57, 0xFF),
new(0x7F, 0xBF, 0xFF, 0xFF),
new(0xAB, 0xE7, 0xFF, 0xFF),
new(0xD7, 0xFF, 0xFF, 0xFF),
new(0x67, 0x00, 0x00, 0xFF),
new(0x8B, 0x00, 0x00, 0xFF),
new(0xB3, 0x00, 0x00, 0xFF),
new(0xD7, 0x00, 0x00, 0xFF),
new(0xFF, 0x00, 0x00, 0xFF),
new(0xFF, 0xF3, 0x93, 0xFF),
new(0xFF, 0xF7, 0xC7, 0xFF),
new(0xFF, 0xFF, 0xFF, 0xFF),
new(0x9F, 0x5B, 0x53, 0xFF)
}
};
}
return s_QuakePalette;
}
}
}
public readonly struct WadEntry
{
public int Address { get; init; }
public int Size { get; init; }
public char Type { get; init; }
public string Name { get; init; }
}
public record WadFile
{
public List<WadEntry> Entries { get; init; }
}
}
+10
View File
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 81577f4367354726bf894c329ef36a6c
timeCreated: 1722844184
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/WAD/WadFile.cs
uploadId: 939664
+102
View File
@@ -0,0 +1,102 @@
//
// 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 UnityEngine;
using UnityEngine.Experimental.Rendering;
namespace TinyGoose.Tremble
{
public class WadParser
{
private readonly StreamReadIO m_Stream;
public WadParser(string filename)
{
m_Stream = new(filename);
}
public void WriteTextureToFile(in WadEntry entry, string outFile)
{
m_Stream.Seek(entry.Address);
string name = m_Stream.ReadString(16);
int width = m_Stream.ReadInt32();
int height = m_Stream.ReadInt32();
m_Stream.SkipBytes(16);
byte[] palettedTextureData = m_Stream.ReadBytes(width * height);
byte[] rgbTextureData = new byte[palettedTextureData.Length * 3];
// Copy paletted to RGB, flipping as we go
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
int readOffset = y * width + x;
int invY = height - y - 1;
int writeOffset = (invY * width + x) * 3;
ref Color32 colour = ref WadPalette.QuakePalette.Get(palettedTextureData[readOffset]);
rgbTextureData[writeOffset + 0] = colour.r;
rgbTextureData[writeOffset + 1] = colour.g;
rgbTextureData[writeOffset + 2] = colour.b;
}
}
byte[] pngBytes = ImageConversion.EncodeArrayToPNG(rgbTextureData, GraphicsFormat.R8G8B8_UNorm, (uint)width, (uint)height);
File.WriteAllBytes(outFile, pngBytes);
}
public WadFile Parse()
{
m_Stream.Seek(0);
string magic = new(m_Stream.ReadChars(4));
if (!magic.EqualsInvariant("wad2"))
{
Debug.LogError($"File was not a WAD2 :(");
return default;
}
int numEntries = m_Stream.ReadInt32();
int dirOffset = m_Stream.ReadInt32();
m_Stream.Seek(dirOffset);
List<WadEntry> entries = new(numEntries);
for (int entryIdx = 0; entryIdx < numEntries; entryIdx++)
{
int entryAddress = m_Stream.ReadInt32();
int entrySize = m_Stream.ReadInt32();
m_Stream.SkipBytes(4);
char type = (char)m_Stream.ReadByte();
m_Stream.SkipBytes(3);
string name = new(m_Stream.ReadString(16));
// Skip anything that's not a texture
if (type != 'D')
continue;
entries.Add(new()
{
Address = entryAddress,
Size = entrySize,
Type = type,
Name = name.Replace('*', '_') // * is not a valid name for an asset!
});
}
return new()
{
Entries = entries
};
}
}
}
+10
View File
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: f8ecbbe775a9445a9f8f2148c4a06be2
timeCreated: 1722843896
AssetOrigin:
serializedVersion: 1
productId: 277805
packageName: Tremble - Map Importer
packageVersion: 1.11.1
assetPath: Assets/Tremble/IO/WAD/WadParser.cs
uploadId: 939664