using System.Collections.Generic; using System.IO; using System.Text; using SabreTools.Data.Models.BSP; using SabreTools.IO.Extensions; using SabreTools.Numerics.Extensions; using static SabreTools.Data.Models.BSP.Constants; #pragma warning disable IDE0017 // Simplify object initialization #pragma warning disable IDE0060 // Remove unused parameter namespace SabreTools.Serialization.Readers { public class BSP : BaseBinaryReader { /// public override BspFile? Deserialize(Stream? data) { // If the data is invalid if (data is null || !data.CanRead) return null; try { // Cache the current offset long initialOffset = data.Position; // Create a new Half-Life Level to fill var file = new BspFile(); #region Header // Try to parse the header var header = ParseBspHeader(data); if (header.Version < 29 || header.Version > 30) return null; // Set the level header file.Header = header; #endregion #region Lumps for (int l = 0; l < BSP_HEADER_LUMPS; l++) { // Get the next lump entry var lumpEntry = header.Lumps![l]; if (lumpEntry is null) continue; if (lumpEntry.Offset == 0 || lumpEntry.Length == 0) continue; // Seek to the lump offset data.SeekIfPossible(initialOffset + lumpEntry.Offset, SeekOrigin.Begin); // Read according to the lump type switch ((BspLumpType)l) { case BspLumpType.LUMP_ENTITIES: file.Entities = ParseEntitiesLump(data, lumpEntry.Offset, lumpEntry.Length); break; case BspLumpType.LUMP_PLANES: file.PlanesLump = ParsePlanesLump(data, lumpEntry.Offset, lumpEntry.Length); break; case BspLumpType.LUMP_TEXTURES: file.TextureLump = ParseTextureLump(data, lumpEntry.Offset, lumpEntry.Length); break; case BspLumpType.LUMP_VERTICES: file.VerticesLump = ParseVerticesLump(data, lumpEntry.Offset, lumpEntry.Length); break; case BspLumpType.LUMP_VISIBILITY: var visiblityLump = ParseVisibilityLump(data, lumpEntry.Offset, lumpEntry.Length); if (visiblityLump is not null) file.VisibilityLump = visiblityLump; break; case BspLumpType.LUMP_NODES: file.NodesLump = ParseNodesLump(data, lumpEntry.Offset, lumpEntry.Length); break; case BspLumpType.LUMP_TEXINFO: file.TexinfoLump = ParseTexinfoLump(data, lumpEntry.Offset, lumpEntry.Length); break; case BspLumpType.LUMP_FACES: file.FacesLump = ParseFacesLump(data, lumpEntry.Offset, lumpEntry.Length); break; case BspLumpType.LUMP_LIGHTING: file.LightmapLump = ParseLightmapLump(data, lumpEntry.Offset, lumpEntry.Length); break; case BspLumpType.LUMP_CLIPNODES: file.ClipnodesLump = ParseClipnodesLump(data, lumpEntry.Offset, lumpEntry.Length); break; case BspLumpType.LUMP_LEAVES: file.LeavesLump = ParseLeavesLump(data, lumpEntry.Offset, lumpEntry.Length); break; case BspLumpType.LUMP_MARKSURFACES: file.MarksurfacesLump = ParseMarksurfacesLump(data, lumpEntry.Offset, lumpEntry.Length); break; case BspLumpType.LUMP_EDGES: file.EdgesLump = ParseEdgesLump(data, lumpEntry.Offset, lumpEntry.Length); break; case BspLumpType.LUMP_SURFEDGES: file.SurfedgesLump = ParseSurfedgesLump(data, lumpEntry.Offset, lumpEntry.Length); break; case BspLumpType.LUMP_MODELS: file.ModelsLump = ParseModelsLump(data, lumpEntry.Offset, lumpEntry.Length); break; default: // Unsupported BspLumpType value, ignore break; } } #endregion return file; } catch { // Ignore the actual error return null; } } /// /// Parse a Stream into BspFace /// /// Stream to parse /// Filled BspFace on success, null on error public static BspFace ParseBspFace(Stream data) { var obj = new BspFace(); obj.PlaneIndex = data.ReadUInt16LittleEndian(); obj.PlaneSideCount = data.ReadUInt16LittleEndian(); obj.FirstEdgeIndex = data.ReadUInt32LittleEndian(); obj.NumberOfEdges = data.ReadUInt16LittleEndian(); obj.TextureInfoIndex = data.ReadUInt16LittleEndian(); obj.LightingStyles = data.ReadBytes(4); obj.LightmapOffset = data.ReadInt32LittleEndian(); return obj; } /// /// Parse a Stream into BspHeader /// /// Stream to parse /// Filled BspHeader on success, null on error public static BspHeader ParseBspHeader(Stream data) { var obj = new BspHeader(); obj.Version = data.ReadInt32LittleEndian(); obj.Lumps = new BspLumpEntry[BSP_HEADER_LUMPS]; for (int i = 0; i < BSP_HEADER_LUMPS; i++) { obj.Lumps[i] = ParseBspLumpEntry(data); } return obj; } /// /// Parse a Stream into BspLeaf /// /// Stream to parse /// Filled BspLeaf on success, null on error public static BspLeaf ParseBspLeaf(Stream data) { var obj = new BspLeaf(); obj.Contents = (BspContents)data.ReadInt32LittleEndian(); obj.VisOffset = data.ReadInt32LittleEndian(); obj.Mins = new short[3]; for (int i = 0; i < 3; i++) { obj.Mins[i] = data.ReadInt16LittleEndian(); } obj.Maxs = new short[3]; for (int i = 0; i < 3; i++) { obj.Maxs[i] = data.ReadInt16LittleEndian(); } obj.FirstMarkSurfaceIndex = data.ReadUInt16LittleEndian(); obj.MarkSurfacesCount = data.ReadUInt16LittleEndian(); obj.AmbientLevels = data.ReadBytes(4); return obj; } /// /// Parse a Stream into BspLumpEntry /// /// Stream to parse /// Filled BspLumpEntry on success, null on error public static BspLumpEntry ParseBspLumpEntry(Stream data) { var obj = new BspLumpEntry(); obj.Offset = data.ReadInt32LittleEndian(); obj.Length = data.ReadInt32LittleEndian(); return obj; } /// /// Parse a Stream into BspModel /// /// Stream to parse /// Filled BspModel on success, null on error public static BspModel ParseBspModel(Stream data) { var obj = new BspModel(); obj.Mins = ParseVector3D(data); obj.Maxs = ParseVector3D(data); obj.OriginVector = ParseVector3D(data); obj.HeadnodesIndex = new int[MAX_MAP_HULLS]; for (int i = 0; i < MAX_MAP_HULLS; i++) { obj.HeadnodesIndex[i] = data.ReadInt32LittleEndian(); } obj.VisLeafsCount = data.ReadInt32LittleEndian(); obj.FirstFaceIndex = data.ReadInt32LittleEndian(); obj.FacesCount = data.ReadInt32LittleEndian(); return obj; } /// /// Parse a Stream into BspNode /// /// Stream to parse /// Filled BspNode on success, null on error public static BspNode ParseBspNode(Stream data) { var obj = new BspNode(); obj.PlaneIndex = data.ReadUInt32LittleEndian(); obj.Children = new ushort[2]; for (int i = 0; i < 2; i++) { obj.Children[i] = data.ReadUInt16LittleEndian(); } obj.Mins = new ushort[3]; for (int i = 0; i < 3; i++) { obj.Mins[i] = data.ReadUInt16LittleEndian(); } obj.Maxs = new ushort[3]; for (int i = 0; i < 3; i++) { obj.Maxs[i] = data.ReadUInt16LittleEndian(); } obj.FirstFace = data.ReadUInt16LittleEndian(); obj.FaceCount = data.ReadUInt16LittleEndian(); return obj; } /// /// Parse a Stream into BspTexinfo /// /// Stream to parse /// Filled BspTexinfo on success, null on error public static BspTexinfo ParseBspTexinfo(Stream data) { var obj = new BspTexinfo(); obj.SVector = ParseVector3D(data); obj.TextureSShift = data.ReadSingle(); obj.TVector = ParseVector3D(data); obj.TextureTShift = data.ReadSingle(); obj.MiptexIndex = data.ReadUInt32LittleEndian(); obj.Flags = (TextureFlag)data.ReadUInt32LittleEndian(); return obj; } /// /// Parse a Stream into Clipnode /// /// Stream to parse /// Filled Clipnode on success, null on error public static Clipnode ParseClipnode(Stream data) { var obj = new Clipnode(); obj.PlaneIndex = data.ReadInt32LittleEndian(); obj.ChildrenIndices = new short[2]; for (int i = 0; i < 2; i++) { obj.ChildrenIndices[i] = data.ReadInt16LittleEndian(); } return obj; } /// /// Parse a Stream into Edge /// /// Stream to parse /// Filled Edge on success, null on error public static Edge ParseEdge(Stream data) { var obj = new Edge(); obj.VertexIndices = new ushort[2]; for (int i = 0; i < 2; i++) { obj.VertexIndices[i] = data.ReadUInt16LittleEndian(); } return obj; } /// /// Parse a Stream into MipTexture /// /// Stream to parse /// Filled MipTexture on success, null on error public static MipTexture ParseMipTexture(Stream data) { var obj = new MipTexture(); byte[] name = data.ReadBytes(MAXTEXTURENAME); obj.Name = Encoding.ASCII.GetString(name).TrimEnd('\0'); obj.Width = data.ReadUInt32LittleEndian(); obj.Height = data.ReadUInt32LittleEndian(); obj.Offsets = new uint[MIPLEVELS]; for (int i = 0; i < MIPLEVELS; i++) { obj.Offsets[i] = data.ReadUInt32LittleEndian(); } return obj; } /// /// Parse a Stream into Plane /// /// Stream to parse /// Filled Plane on success, null on error public static Plane ParsePlane(Stream data) { var obj = new Plane(); obj.NormalVector = ParseVector3D(data); obj.Distance = data.ReadSingle(); obj.PlaneType = (PlaneType)data.ReadInt32LittleEndian(); return obj; } /// /// Parse a Stream into a TextureHeader /// /// Stream to parse /// Filled TextureHeader on success, null on error public static TextureHeader ParseTextureHeader(Stream data) { var obj = new TextureHeader(); obj.MipTextureCount = data.ReadUInt32LittleEndian(); obj.Offsets = new int[obj.MipTextureCount]; for (int i = 0; i < obj.Offsets.Length; i++) { obj.Offsets[i] = data.ReadInt16LittleEndian(); } return obj; } /// /// Parse a Stream into Vector3D /// /// Stream to parse /// Filled Vector3D on success, null on error public static Vector3D ParseVector3D(Stream data) { var obj = new Vector3D(); obj.X = data.ReadSingle(); obj.Y = data.ReadSingle(); obj.Z = data.ReadSingle(); return obj; } /// /// Parse a Stream into LUMP_ENTITIES /// /// Stream to parse /// Filled LUMP_ENTITIES on success, null on error private static EntitiesLump ParseEntitiesLump(Stream data, int offset, int length) { var entities = new List(); // Read the entire lump as text byte[] lumpData = data.ReadBytes(length); string lumpText = Encoding.ASCII.GetString(lumpData); // Break the text by ending curly braces string[] lumpSections = lumpText.Split('}'); // Loop through all sections for (int i = 0; i < lumpSections.Length; i++) { // Prepare an attributes list var attributes = new List>(); // Split the section by newlines string section = lumpSections[i].Trim('{', '}'); string[] lines = section.Split('\n'); // Convert each line into a key-value pair and add for (int j = 0; j < lines.Length; j++) { // TODO: Split lines and add } // Create a new entity and add var entity = new Entity { Attributes = attributes }; entities.Add(entity); } return new EntitiesLump { Entities = [.. entities] }; } /// /// Parse a Stream into LUMP_PLANES /// /// Stream to parse /// Filled LUMP_PLANES on success, null on error private static PlanesLump ParsePlanesLump(Stream data, int offset, int length) { var planes = new List(); while (data.Position < offset + length) { var plane = ParsePlane(data); planes.Add(plane); } return new PlanesLump { Planes = [.. planes] }; } /// /// Parse a Stream into LUMP_TEXTURES /// /// Stream to parse /// Filled LUMP_TEXTURES on success, null on error private static TextureLump ParseTextureLump(Stream data, int offset, int length) { var lump = new TextureLump(); lump.Header = ParseTextureHeader(data); var textures = new List(); while (data.Position < offset + length) { var texture = ParseMipTexture(data); textures.Add(texture); } lump.Textures = [.. textures]; return lump; } /// /// Parse a Stream into LUMP_VERTICES /// /// Stream to parse /// Filled LUMP_VERTICES on success, null on error private static VerticesLump ParseVerticesLump(Stream data, int offset, int length) { var vertices = new List(); while (data.Position < offset + length) { var vertex = ParseVector3D(data); vertices.Add(vertex); } return new VerticesLump { Vertices = [.. vertices] }; } /// /// Parse a Stream into LUMP_VISIBILITY /// /// Stream to parse /// Filled LUMP_VISIBILITY on success, null on error private static VisibilityLump? ParseVisibilityLump(Stream data, int offset, int length) { var lump = new VisibilityLump(); lump.NumClusters = data.ReadInt32LittleEndian(); // BSP29 has an incompatible value here int bytesNeeded = lump.NumClusters * 8; if (bytesNeeded > length) return null; lump.ByteOffsets = new int[lump.NumClusters][]; for (int i = 0; i < lump.NumClusters; i++) { lump.ByteOffsets[i] = new int[2]; for (int j = 0; j < 2; j++) { lump.ByteOffsets[i][j] = data.ReadInt32LittleEndian(); } } return lump; } /// /// Parse a Stream into LUMP_NODES /// /// Stream to parse /// Filled LUMP_NODES on success, null on error private static BspNodesLump ParseNodesLump(Stream data, int offset, int length) { var nodes = new List(); while (data.Position < offset + length) { var node = ParseBspNode(data); nodes.Add(node); } return new BspNodesLump { Nodes = [.. nodes] }; } /// /// Parse a Stream into LUMP_TEXINFO /// /// Stream to parse /// Filled LUMP_TEXINFO on success, null on error private static BspTexinfoLump ParseTexinfoLump(Stream data, int offset, int length) { var texinfos = new List(); while (data.Position < offset + length) { var texinfo = ParseBspTexinfo(data); texinfos.Add(texinfo); } return new BspTexinfoLump { Texinfos = [.. texinfos] }; } /// /// Parse a Stream into LUMP_FACES /// /// Stream to parse /// Filled LUMP_FACES on success, null on error private static BspFacesLump ParseFacesLump(Stream data, int offset, int length) { var faces = new List(); while (data.Position < offset + length) { var face = ParseBspFace(data); faces.Add(face); } return new BspFacesLump { Faces = [.. faces] }; } /// /// Parse a Stream into LUMP_LIGHTING /// /// Stream to parse /// Filled LUMP_LIGHTING on success, null on error private static LightmapLump ParseLightmapLump(Stream data, int offset, int length) { var lump = new LightmapLump(); lump.Lightmap = new byte[length / 3][]; for (int i = 0; i < length / 3; i++) { lump.Lightmap[i] = data.ReadBytes(3); } return lump; } /// /// Parse a Stream into LUMP_CLIPNODES /// /// Stream to parse /// Filled LUMP_CLIPNODES on success, null on error private static ClipnodesLump ParseClipnodesLump(Stream data, int offset, int length) { var clipnodes = new List(); while (data.Position < offset + length) { var clipnode = ParseClipnode(data); clipnodes.Add(clipnode); } return new ClipnodesLump { Clipnodes = [.. clipnodes] }; } /// /// Parse a Stream into LUMP_LEAVES /// /// Stream to parse /// Filled LUMP_LEAVES on success, null on error private static BspLeavesLump ParseLeavesLump(Stream data, int offset, int length) { var leaves = new List(); while (data.Position < offset + length) { var leaf = ParseBspLeaf(data); leaves.Add(leaf); } return new BspLeavesLump { Leaves = [.. leaves] }; } /// /// Parse a Stream into LUMP_MARKSURFACES /// /// Stream to parse /// Filled LUMP_MARKSURFACES on success, null on error private static MarksurfacesLump ParseMarksurfacesLump(Stream data, int offset, int length) { var marksurfaces = new List(); while (data.Position < offset + length) { marksurfaces.Add(data.ReadUInt16LittleEndian()); } return new MarksurfacesLump { Marksurfaces = [.. marksurfaces] }; } /// /// Parse a Stream into LUMP_EDGES /// /// Stream to parse /// Filled LUMP_EDGES on success, null on error private static EdgesLump ParseEdgesLump(Stream data, int offset, int length) { var edges = new List(); while (data.Position < offset + length) { var edge = ParseEdge(data); edges.Add(edge); } return new EdgesLump { Edges = [.. edges] }; } /// /// Parse a Stream into LUMP_SURFEDGES /// /// Stream to parse /// Filled LUMP_SURFEDGES on success, null on error private static SurfedgesLump ParseSurfedgesLump(Stream data, int offset, int length) { var surfedges = new List(); while (data.Position < offset + length) { surfedges.Add(data.ReadInt32LittleEndian()); } return new SurfedgesLump { Surfedges = [.. surfedges] }; } /// /// Parse a Stream into LUMP_MODELS /// /// Stream to parse /// Filled LUMP_MODELS on success, null on error private static BspModelsLump ParseModelsLump(Stream data, int offset, int length) { var models = new List(); while (data.Position < offset + length) { var model = ParseBspModel(data); models.Add(model); } return new BspModelsLump { Models = [.. models] }; } } }