diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..505de9c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,332 @@
+HLView.Graphics/Shaders/*.exe
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+**/Properties/launchSettings.json
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8c47ed4
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Daniel Walder
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Readme.md b/Readme.md
new file mode 100644
index 0000000..8318977
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,51 @@
+# Half-Life Formats Library
+
+These C# libraries parse the formats you'll find in your Half-Life install directory. These are low-level libraries intended for developers. Some additional formats are included to include other related formats, such as those found in the Source and Quake engines.
+
+## Install using NuGet
+
+Coming soon™
+
+## Currently Supported Formats
+
+- Sledge.Formats - Small formats, or formats that are shared
+ - Valve
+ - **Liblist** - The format used for the liblist.gam file.
+ - **SerialisedObject** - The format used for many Valve config files used in Steam HL, Source games, and Steam. Some file types that use this format are VMT, VMF, RES, VDF, gameinfo.txt, and many text files found in Source game directories.
+- Sledge.Formats.Map - Map source files used by level editors
+ - Formats
+ - **HammerVmfFormat** - The format used by Valve Hammer Editor 4 for map source files.
+ - **QuakeMapFormat** - The format used by most Quake engines for map source files.
+ - Supports the formats used in Quake 1 (idTech2) and Half-Life 1 .map files.
+ - idTech3 and idTech4 .map files are not currently supported.
+ - **WorldcraftRmfFormat** - The format used by Valve Hammer Editor 3 for map source files.
+ - Only RMF version 2.2 (Worldcraft 3.3 and up) is currently supported.
+- Sledge.Formats.Bsp - Compiled map files used by the engine
+ - **BspFile** - A format used by Quake based engines for compiled maps.
+ - Currently supports Quake 1 (v29), Quake 2 (IBSP v38), and Half-Life 1 (v30) bsp formats.
+ - Not currently supported: BSP2 (DarkPlaces engine), Quake 3 (IBSP v46), Source (VBSP v17-21)
+ - Currently, visibility data is not parsed, it is kept as a binary blob.
+ - Editing of lightmap data is currently not well supported and must be done manually.
+ - The library does no checking to ensure that the indexes and offsets are correct. Possibly a higher-level library could wrap around this format to provide developers with a more flexible BSP creation experience.
+
+## Unsupported formats (may be added in the future)
+
+- **FGD**, used for entity definitions in Worldcraft and Valve Hammer Editor.
+- **JMF**, used for Jackhammer/JACK editor for map source files.
+- **WAD**, used for textures in Quake and Half-Life.
+ - **WAD2**, used in Quake for textures.
+ - **WAD3**, used in Half-Life for textures.
+- **VTF**, used in Source for texture data.
+- **PAK**, used in Quake and non-Steam Half-Life.
+- **VPK**, used in post-SteamPipe Source games.
+- **MDL**, used for models in many Quake-based games.
+ - **MDL v10** (IDSQ/IDST), used in Half-Life, which adds skeletal animation.
+ - **MDL v44-49** (IDSQ/IDST), used in Source, along with VTX, VVD, ANI, and PHY files. This is a very complex format, so it's not high priority.
+
+## Unsupported formats (probably won't be added)
+
+- **GCF**, used by pre-SteamPipe Steam Half-Life - this format is no longer in use, so it's not really useful to create a library for it.
+- **PK3**, used in Quake 3 games - this format is just a zip file with a different extension. Other libraries (including .NET itself) already have good support for zip files.
+- **WAD1**, used in the Doom engine - this is a bit too far out of scope for this project, which is focused mostly on Half-Life 1.
+- **MDL v6** (IDPO), **MD2** and **MD3**, used for models in non-Valve Quake engines - MDL formats are extremely complex and not very well documented, so these are considered out of scope for this project for now.
+- Anything introduced in Doom 3, Source 2 or newer engines.
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Blob.cs b/Sledge.Formats.Bsp/Blob.cs
new file mode 100644
index 0000000..85ef581
--- /dev/null
+++ b/Sledge.Formats.Bsp/Blob.cs
@@ -0,0 +1,14 @@
+namespace Sledge.Formats.Bsp
+{
+ ///
+ /// An unprocessed lump
+ ///
+ public struct Blob
+ {
+ public int Index;
+ public int Offset;
+ public int Length;
+ public uint Version;
+ public string Ident;
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/BspFile.cs b/Sledge.Formats.Bsp/BspFile.cs
new file mode 100644
index 0000000..7dbda9a
--- /dev/null
+++ b/Sledge.Formats.Bsp/BspFile.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using Sledge.Formats.Bsp.Lumps;
+using Sledge.Formats.Bsp.Readers;
+using Sledge.Formats.Bsp.Writers;
+
+namespace Sledge.Formats.Bsp
+{
+ public class BspFile
+ {
+ public Version Version { get; set; }
+ public List Blobs { get; set; }
+ public List Lumps { get; set; }
+
+ public BspFile(Stream stream)
+ {
+ using (var br = new BinaryReader(stream, Encoding.ASCII, true))
+ {
+ // Read the version number
+ // More recent formats have a "magic" number when different engines forked
+ var magic = (Magic) br.ReadUInt32();
+ switch (magic)
+ {
+ case Magic.Ibsp:
+ case Magic.Vbsp:
+ Version = (Version) ((br.ReadUInt32() << 32) + magic);
+ break;
+ default:
+ Version = (Version) magic;
+ break;
+ }
+
+ // Initialise the reader
+ var reader = _readers.First(x => x.SupportedVersion == Version);
+
+ reader.StartHeader(this, br);
+
+ // Read the blobs
+ Blobs = new List();
+ for (var i = 0; i < reader.NumLumps; i++)
+ {
+ var blob = reader.ReadBlob(br);
+ blob.Index = i;
+ Blobs.Add(blob);
+ }
+
+ reader.EndHeader(this, br);
+
+ Lumps = new List();
+ foreach (var blob in Blobs)
+ {
+ var lump = reader.GetLump(blob);
+ if (lump == null) continue;
+
+ var pos = br.BaseStream.Position;
+ br.BaseStream.Seek(blob.Offset, SeekOrigin.Begin);
+
+ lump.Read(br, blob, Version);
+ Lumps.Add(lump);
+
+ br.BaseStream.Seek(pos, SeekOrigin.Begin);
+ }
+
+ foreach (var lump in Lumps)
+ {
+ lump.PostReadProcess(this);
+ }
+ }
+ }
+
+ public void WriteToStream(Stream s, Version version)
+ {
+ var writer = _writers.First(x => x.SupportedVersion == version);
+
+ foreach (var lump in Lumps)
+ {
+ lump.PreWriteProcess(this, version);
+ }
+
+ using (var bw = new BinaryWriter(s, Encoding.ASCII, true))
+ {
+ writer.SeekToFirstLump(bw);
+ var lumps = writer.GetLumps(this)
+ .Select((x, i) => new Blob
+ {
+ Offset = (int) bw.BaseStream.Position,
+ Length = x.Write(bw, version),
+ Index = i
+ })
+ .ToList();
+ bw.Seek(0, SeekOrigin.Begin);
+ writer.WriteHeader(this, lumps, bw);
+ }
+ }
+
+ public T GetLump() where T : ILump
+ {
+ return (T) Lumps.FirstOrDefault(x => x is T);
+ }
+
+ private readonly IBspReader[] _readers =
+ {
+ new GoldsourceBspReader(),
+ new Quake1BspReader(),
+ new Quake2BspReader(),
+ };
+
+ private readonly IBspWriter[] _writers =
+ {
+ new GoldsourceBspWriter(),
+ new Quake1BspWriter(),
+ };
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/AreaPortals.cs b/Sledge.Formats.Bsp/Lumps/AreaPortals.cs
new file mode 100644
index 0000000..bf874de
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/AreaPortals.cs
@@ -0,0 +1,115 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using Sledge.Formats.Bsp.Objects;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class AreaPortals : ILump, IList
+ {
+ private readonly IList _areaPortals;
+
+ public AreaPortals()
+ {
+ _areaPortals = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ while (br.BaseStream.Position < blob.Offset + blob.Length)
+ {
+ var ap = new AreaPortal
+ {
+ PortalNum = br.ReadInt32(),
+ OtherArea = br.ReadInt32()
+ };
+ _areaPortals.Add(ap);
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ var pos = bw.BaseStream.Position;
+ foreach (var ap in _areaPortals)
+ {
+ bw.Write((int) ap.PortalNum);
+ bw.Write((int) ap.OtherArea);
+ }
+ return (int)(bw.BaseStream.Position - pos);
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _areaPortals.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _areaPortals).GetEnumerator();
+ }
+
+ public void Add(AreaPortal item)
+ {
+ _areaPortals.Add(item);
+ }
+
+ public void Clear()
+ {
+ _areaPortals.Clear();
+ }
+
+ public bool Contains(AreaPortal item)
+ {
+ return _areaPortals.Contains(item);
+ }
+
+ public void CopyTo(AreaPortal[] array, int arrayIndex)
+ {
+ _areaPortals.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(AreaPortal item)
+ {
+ return _areaPortals.Remove(item);
+ }
+
+ public int Count => _areaPortals.Count;
+
+ public bool IsReadOnly => _areaPortals.IsReadOnly;
+
+ public int IndexOf(AreaPortal item)
+ {
+ return _areaPortals.IndexOf(item);
+ }
+
+ public void Insert(int index, AreaPortal item)
+ {
+ _areaPortals.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _areaPortals.RemoveAt(index);
+ }
+
+ public AreaPortal this[int index]
+ {
+ get => _areaPortals[index];
+ set => _areaPortals[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Areas.cs b/Sledge.Formats.Bsp/Lumps/Areas.cs
new file mode 100644
index 0000000..960c01d
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Areas.cs
@@ -0,0 +1,115 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using Sledge.Formats.Bsp.Objects;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Areas : ILump, IList
+ {
+ private readonly IList _areas;
+
+ public Areas()
+ {
+ _areas = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ while (br.BaseStream.Position < blob.Offset + blob.Length)
+ {
+ var area = new Area
+ {
+ NumAreaPortals = br.ReadInt32(),
+ FirstAreaPortal = br.ReadInt32()
+ };
+ _areas.Add(area);
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ var pos = bw.BaseStream.Position;
+ foreach (var area in _areas)
+ {
+ bw.Write((int) area.NumAreaPortals);
+ bw.Write((int) area.FirstAreaPortal);
+ }
+ return (int)(bw.BaseStream.Position - pos);
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _areas.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _areas).GetEnumerator();
+ }
+
+ public void Add(Area item)
+ {
+ _areas.Add(item);
+ }
+
+ public void Clear()
+ {
+ _areas.Clear();
+ }
+
+ public bool Contains(Area item)
+ {
+ return _areas.Contains(item);
+ }
+
+ public void CopyTo(Area[] array, int arrayIndex)
+ {
+ _areas.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(Area item)
+ {
+ return _areas.Remove(item);
+ }
+
+ public int Count => _areas.Count;
+
+ public bool IsReadOnly => _areas.IsReadOnly;
+
+ public int IndexOf(Area item)
+ {
+ return _areas.IndexOf(item);
+ }
+
+ public void Insert(int index, Area item)
+ {
+ _areas.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _areas.RemoveAt(index);
+ }
+
+ public Area this[int index]
+ {
+ get => _areas[index];
+ set => _areas[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/BrushSides.cs b/Sledge.Formats.Bsp/Lumps/BrushSides.cs
new file mode 100644
index 0000000..8416ee6
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/BrushSides.cs
@@ -0,0 +1,115 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using Sledge.Formats.Bsp.Objects;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class BrushSides : ILump, IList
+ {
+ private readonly IList _brushSides;
+
+ public BrushSides()
+ {
+ _brushSides = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ while (br.BaseStream.Position < blob.Offset + blob.Length)
+ {
+ var bs = new BrushSide
+ {
+ Plane = br.ReadUInt16(),
+ Texinfo = br.ReadInt16()
+ };
+ _brushSides.Add(bs);
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ var pos = bw.BaseStream.Position;
+ foreach (var bs in _brushSides)
+ {
+ bw.Write((ushort) bs.Plane);
+ bw.Write((short) bs.Texinfo);
+ }
+ return (int)(bw.BaseStream.Position - pos);
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _brushSides.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _brushSides).GetEnumerator();
+ }
+
+ public void Add(BrushSide item)
+ {
+ _brushSides.Add(item);
+ }
+
+ public void Clear()
+ {
+ _brushSides.Clear();
+ }
+
+ public bool Contains(BrushSide item)
+ {
+ return _brushSides.Contains(item);
+ }
+
+ public void CopyTo(BrushSide[] array, int arrayIndex)
+ {
+ _brushSides.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(BrushSide item)
+ {
+ return _brushSides.Remove(item);
+ }
+
+ public int Count => _brushSides.Count;
+
+ public bool IsReadOnly => _brushSides.IsReadOnly;
+
+ public int IndexOf(BrushSide item)
+ {
+ return _brushSides.IndexOf(item);
+ }
+
+ public void Insert(int index, BrushSide item)
+ {
+ _brushSides.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _brushSides.RemoveAt(index);
+ }
+
+ public BrushSide this[int index]
+ {
+ get => _brushSides[index];
+ set => _brushSides[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Brushes.cs b/Sledge.Formats.Bsp/Lumps/Brushes.cs
new file mode 100644
index 0000000..3661431
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Brushes.cs
@@ -0,0 +1,117 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using Sledge.Formats.Bsp.Objects;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Brushes : ILump, IList
+ {
+ private readonly IList _brushes;
+
+ public Brushes()
+ {
+ _brushes = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ while (br.BaseStream.Position < blob.Offset + blob.Length)
+ {
+ var brush = new Brush
+ {
+ FirstSide = br.ReadInt32(),
+ NumSides = br.ReadInt32(),
+ Contents = br.ReadInt32()
+ };
+ _brushes.Add(brush);
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ var pos = bw.BaseStream.Position;
+ foreach (var brush in _brushes)
+ {
+ bw.Write((int) brush.FirstSide);
+ bw.Write((int) brush.NumSides);
+ bw.Write((int) brush.Contents);
+ }
+ return (int)(bw.BaseStream.Position - pos);
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _brushes.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _brushes).GetEnumerator();
+ }
+
+ public void Add(Brush item)
+ {
+ _brushes.Add(item);
+ }
+
+ public void Clear()
+ {
+ _brushes.Clear();
+ }
+
+ public bool Contains(Brush item)
+ {
+ return _brushes.Contains(item);
+ }
+
+ public void CopyTo(Brush[] array, int arrayIndex)
+ {
+ _brushes.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(Brush item)
+ {
+ return _brushes.Remove(item);
+ }
+
+ public int Count => _brushes.Count;
+
+ public bool IsReadOnly => _brushes.IsReadOnly;
+
+ public int IndexOf(Brush item)
+ {
+ return _brushes.IndexOf(item);
+ }
+
+ public void Insert(int index, Brush item)
+ {
+ _brushes.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _brushes.RemoveAt(index);
+ }
+
+ public Brush this[int index]
+ {
+ get => _brushes[index];
+ set => _brushes[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Clipnodes.cs b/Sledge.Formats.Bsp/Lumps/Clipnodes.cs
new file mode 100644
index 0000000..3d90f46
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Clipnodes.cs
@@ -0,0 +1,116 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using Sledge.Formats.Bsp.Objects;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Clipnodes : ILump, IList
+ {
+ private readonly IList _clipnodes;
+
+ public Clipnodes()
+ {
+ _clipnodes = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ while (br.BaseStream.Position < blob.Offset + blob.Length)
+ {
+ var clip = new Clipnode
+ {
+ Plane = br.ReadUInt32(),
+ Children = new[] { br.ReadInt16(), br.ReadInt16() }
+ };
+ _clipnodes.Add(clip);
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ var pos = bw.BaseStream.Position;
+ foreach (var node in _clipnodes)
+ {
+ bw.Write((uint) node.Plane);
+ bw.Write((short) node.Children[0]);
+ bw.Write((short) node.Children[1]);
+ }
+ return (int)(bw.BaseStream.Position - pos);
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _clipnodes.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _clipnodes).GetEnumerator();
+ }
+
+ public void Add(Clipnode item)
+ {
+ _clipnodes.Add(item);
+ }
+
+ public void Clear()
+ {
+ _clipnodes.Clear();
+ }
+
+ public bool Contains(Clipnode item)
+ {
+ return _clipnodes.Contains(item);
+ }
+
+ public void CopyTo(Clipnode[] array, int arrayIndex)
+ {
+ _clipnodes.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(Clipnode item)
+ {
+ return _clipnodes.Remove(item);
+ }
+
+ public int Count => _clipnodes.Count;
+
+ public bool IsReadOnly => _clipnodes.IsReadOnly;
+
+ public int IndexOf(Clipnode item)
+ {
+ return _clipnodes.IndexOf(item);
+ }
+
+ public void Insert(int index, Clipnode item)
+ {
+ _clipnodes.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _clipnodes.RemoveAt(index);
+ }
+
+ public Clipnode this[int index]
+ {
+ get => _clipnodes[index];
+ set => _clipnodes[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Edges.cs b/Sledge.Formats.Bsp/Lumps/Edges.cs
new file mode 100644
index 0000000..4fc4dc2
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Edges.cs
@@ -0,0 +1,114 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using Sledge.Formats.Bsp.Objects;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Edges : ILump, IList
+ {
+ private readonly IList _edges;
+
+ public Edges()
+ {
+ _edges = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ while (br.BaseStream.Position < blob.Offset + blob.Length)
+ {
+ var edge = new Edge
+ {
+ Start = br.ReadUInt16(),
+ End = br.ReadUInt16()
+ };
+ _edges.Add(edge);
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ foreach (var edge in _edges)
+ {
+ bw.Write((ushort) edge.Start);
+ bw.Write((ushort) edge.End);
+ }
+ return sizeof(ushort) * 2 * _edges.Count;
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _edges.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _edges).GetEnumerator();
+ }
+
+ public void Add(Edge item)
+ {
+ _edges.Add(item);
+ }
+
+ public void Clear()
+ {
+ _edges.Clear();
+ }
+
+ public bool Contains(Edge item)
+ {
+ return _edges.Contains(item);
+ }
+
+ public void CopyTo(Edge[] array, int arrayIndex)
+ {
+ _edges.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(Edge item)
+ {
+ return _edges.Remove(item);
+ }
+
+ public int Count => _edges.Count;
+
+ public bool IsReadOnly => _edges.IsReadOnly;
+
+ public int IndexOf(Edge item)
+ {
+ return _edges.IndexOf(item);
+ }
+
+ public void Insert(int index, Edge item)
+ {
+ _edges.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _edges.RemoveAt(index);
+ }
+
+ public Edge this[int index]
+ {
+ get => _edges[index];
+ set => _edges[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Entities.cs b/Sledge.Formats.Bsp/Lumps/Entities.cs
new file mode 100644
index 0000000..856a37c
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Entities.cs
@@ -0,0 +1,242 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using Sledge.Formats.Bsp.Objects;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Entities : ILump, IList
+ {
+ private readonly IList _entities;
+
+ public Entities()
+ {
+ _entities = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ var text = Encoding.ASCII.GetString(br.ReadBytes(blob.Length));
+
+ // Remove comments
+ var cleaned = new StringBuilder();
+ foreach (var line in text.Split('\n'))
+ {
+ var l = line;
+ var idx = l.IndexOf("//", StringComparison.Ordinal);
+ if (idx >= 0) l = l.Substring(0, idx);
+ l = l.Trim();
+ cleaned.Append(l).Append('\n');
+ }
+
+ var data = cleaned.ToString();
+
+ Entity cur = null;
+ int i;
+ string key = null;
+ for (i = 0; i < data.Length; i++)
+ {
+ var token = GetToken();
+ if (token == "{")
+ {
+ // Start of new entity
+ cur = new Entity();
+ _entities.Add(cur);
+ key = null;
+ }
+ else if (token == "}")
+ {
+ // End of entity
+ cur = null;
+ key = null;
+ }
+ else if (cur != null && key != null)
+ {
+ // KeyValue value
+ SetKeyValue(cur, key, token);
+ key = null;
+ }
+ else if (cur != null)
+ {
+ // KeyValue key
+ key = token;
+ }
+ else if (token == null)
+ {
+ // End of file
+ break;
+ }
+ else
+ {
+ // Invalid
+ }
+ }
+
+ string GetToken()
+ {
+ if (!ScanToNonWhitespace()) return null;
+
+ if (data[i] == '{' || data[i] == '}')
+ {
+ // Start/end entity
+ return data[i].ToString();
+ }
+
+ if (data[i] == '"')
+ {
+ // Quoted string, find end quote
+ var idx = data.IndexOf('"', i + 1);
+ if (idx < 0) return null;
+ var tok = data.Substring(i + 1, idx - i - 1);
+ i = idx + 1;
+ return tok;
+ }
+
+ if (data[i] > 32)
+ {
+ // Not whitespace
+ var s = "";
+ while (data[i] > 32)
+ {
+ s += data[i++];
+ }
+ return s;
+ }
+
+ return null;
+ }
+
+ bool ScanToNonWhitespace()
+ {
+ while (i < data.Length)
+ {
+ if (data[i] == ' ' || data[i] == '\n') i++;
+ else return true;
+ }
+
+ return false;
+ }
+
+ void SetKeyValue(Entity e, string k, string v)
+ {
+ e.KeyValues[k] = v;
+ switch (k)
+ {
+ case "classname":
+ e.ClassName = v;
+ break;
+ case "model":
+ if (int.TryParse(v.Substring(1), out var m)) e.Model = m;
+ break;
+ }
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ var pos = bw.BaseStream.Position;
+ var sb = new StringBuilder();
+ foreach (var entity in _entities)
+ {
+ sb.Append("{\n");
+
+ if (entity.Model > 0)
+ {
+ sb.Append($"\"model\" \"*{entity.Model}\"\n");
+ }
+
+ foreach (var kv in entity.KeyValues.Where(x => x.Key?.Length > 0 && x.Value?.Length > 0))
+ {
+ sb.Append($"\"{kv.Key}\" \"{kv.Value}\"\n");
+ }
+
+ if (entity.ClassName?.Length > 0)
+ {
+ sb.Append($"\"classname\" \"{entity.ClassName}\"\n");
+ }
+
+ sb.Append("}\n");
+ }
+ bw.Write(Encoding.ASCII.GetBytes(sb.ToString()));
+ return (int)(bw.BaseStream.Position - pos);
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _entities.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _entities).GetEnumerator();
+ }
+
+ public void Add(Entity item)
+ {
+ _entities.Add(item);
+ }
+
+ public void Clear()
+ {
+ _entities.Clear();
+ }
+
+ public bool Contains(Entity item)
+ {
+ return _entities.Contains(item);
+ }
+
+ public void CopyTo(Entity[] array, int arrayIndex)
+ {
+ _entities.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(Entity item)
+ {
+ return _entities.Remove(item);
+ }
+
+ public int Count => _entities.Count;
+
+ public bool IsReadOnly => _entities.IsReadOnly;
+
+ public int IndexOf(Entity item)
+ {
+ return _entities.IndexOf(item);
+ }
+
+ public void Insert(int index, Entity item)
+ {
+ _entities.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _entities.RemoveAt(index);
+ }
+
+ public Entity this[int index]
+ {
+ get => _entities[index];
+ set => _entities[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Faces.cs b/Sledge.Formats.Bsp/Lumps/Faces.cs
new file mode 100644
index 0000000..4f28ddd
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Faces.cs
@@ -0,0 +1,125 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using Sledge.Formats.Bsp.Objects;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Faces : ILump, IList
+ {
+ private readonly IList _faces;
+
+ public Faces()
+ {
+ _faces = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ while (br.BaseStream.Position < blob.Offset + blob.Length)
+ {
+ var face = new Face
+ {
+ Plane = br.ReadUInt16(),
+ Side = br.ReadUInt16(),
+ FirstEdge = br.ReadInt32(),
+ NumEdges = br.ReadUInt16(),
+ TextureInfo = br.ReadUInt16(),
+ Styles = br.ReadBytes(Face.MaxLightmaps),
+ LightmapOffset = br.ReadInt32()
+ };
+ _faces.Add(face);
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ var pos = bw.BaseStream.Position;
+ foreach (var face in _faces)
+ {
+ bw.Write((ushort) face.Plane);
+ bw.Write((ushort) face.Side);
+ bw.Write((int) face.FirstEdge);
+ bw.Write((ushort) face.NumEdges);
+ bw.Write((ushort) face.TextureInfo);
+ bw.Write((byte[]) face.Styles);
+ bw.Write((int) face.LightmapOffset);
+ }
+ return (int)(bw.BaseStream.Position - pos);
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _faces.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _faces).GetEnumerator();
+ }
+
+ public void Add(Face item)
+ {
+ _faces.Add(item);
+ }
+
+ public void Clear()
+ {
+ _faces.Clear();
+ }
+
+ public bool Contains(Face item)
+ {
+ return _faces.Contains(item);
+ }
+
+ public void CopyTo(Face[] array, int arrayIndex)
+ {
+ _faces.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(Face item)
+ {
+ return _faces.Remove(item);
+ }
+
+ public int Count => _faces.Count;
+
+ public bool IsReadOnly => _faces.IsReadOnly;
+
+ public int IndexOf(Face item)
+ {
+ return _faces.IndexOf(item);
+ }
+
+ public void Insert(int index, Face item)
+ {
+ _faces.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _faces.RemoveAt(index);
+ }
+
+ public Face this[int index]
+ {
+ get => _faces[index];
+ set => _faces[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/ILump.cs b/Sledge.Formats.Bsp/Lumps/ILump.cs
new file mode 100644
index 0000000..9182080
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/ILump.cs
@@ -0,0 +1,13 @@
+using System.IO;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public interface ILump
+ {
+ void Read(BinaryReader br, Blob blob, Version version);
+ void PostReadProcess(BspFile bsp);
+
+ void PreWriteProcess(BspFile bsp, Version version);
+ int Write(BinaryWriter bw, Version version);
+ }
+}
diff --git a/Sledge.Formats.Bsp/Lumps/LeafBrushes.cs b/Sledge.Formats.Bsp/Lumps/LeafBrushes.cs
new file mode 100644
index 0000000..a90699a
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/LeafBrushes.cs
@@ -0,0 +1,108 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class LeafBrushes : ILump, IList
+ {
+ private readonly IList _leafBrushes;
+
+ public LeafBrushes()
+ {
+ _leafBrushes = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ var num = blob.Length / sizeof(ushort);
+ for (var i = 0; i < num; i++)
+ {
+ _leafBrushes.Add(br.ReadUInt16());
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ foreach (var lb in _leafBrushes)
+ {
+ bw.Write((ushort) lb);
+ }
+ return sizeof(ushort) * _leafBrushes.Count;
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _leafBrushes.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _leafBrushes).GetEnumerator();
+ }
+
+ public void Add(ushort item)
+ {
+ _leafBrushes.Add(item);
+ }
+
+ public void Clear()
+ {
+ _leafBrushes.Clear();
+ }
+
+ public bool Contains(ushort item)
+ {
+ return _leafBrushes.Contains(item);
+ }
+
+ public void CopyTo(ushort[] array, int arrayIndex)
+ {
+ _leafBrushes.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(ushort item)
+ {
+ return _leafBrushes.Remove(item);
+ }
+
+ public int Count => _leafBrushes.Count;
+
+ public bool IsReadOnly => _leafBrushes.IsReadOnly;
+
+ public int IndexOf(ushort item)
+ {
+ return _leafBrushes.IndexOf(item);
+ }
+
+ public void Insert(int index, ushort item)
+ {
+ _leafBrushes.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _leafBrushes.RemoveAt(index);
+ }
+
+ public ushort this[int index]
+ {
+ get => _leafBrushes[index];
+ set => _leafBrushes[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/LeafFaces.cs b/Sledge.Formats.Bsp/Lumps/LeafFaces.cs
new file mode 100644
index 0000000..b5a1994
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/LeafFaces.cs
@@ -0,0 +1,108 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class LeafFaces : ILump, IList
+ {
+ private readonly IList _leafFaces;
+
+ public LeafFaces()
+ {
+ _leafFaces = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ var num = blob.Length / sizeof(ushort);
+ for (var i = 0; i < num; i++)
+ {
+ _leafFaces.Add(br.ReadUInt16());
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ foreach (var se in _leafFaces)
+ {
+ bw.Write((ushort) se);
+ }
+ return sizeof(ushort) * _leafFaces.Count;
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _leafFaces.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _leafFaces).GetEnumerator();
+ }
+
+ public void Add(ushort item)
+ {
+ _leafFaces.Add(item);
+ }
+
+ public void Clear()
+ {
+ _leafFaces.Clear();
+ }
+
+ public bool Contains(ushort item)
+ {
+ return _leafFaces.Contains(item);
+ }
+
+ public void CopyTo(ushort[] array, int arrayIndex)
+ {
+ _leafFaces.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(ushort item)
+ {
+ return _leafFaces.Remove(item);
+ }
+
+ public int Count => _leafFaces.Count;
+
+ public bool IsReadOnly => _leafFaces.IsReadOnly;
+
+ public int IndexOf(ushort item)
+ {
+ return _leafFaces.IndexOf(item);
+ }
+
+ public void Insert(int index, ushort item)
+ {
+ _leafFaces.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _leafFaces.RemoveAt(index);
+ }
+
+ public ushort this[int index]
+ {
+ get => _leafFaces[index];
+ set => _leafFaces[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Leaves.cs b/Sledge.Formats.Bsp/Lumps/Leaves.cs
new file mode 100644
index 0000000..ea6739a
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Leaves.cs
@@ -0,0 +1,179 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using Sledge.Formats.Bsp.Objects;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Leaves : ILump, IList
+ {
+ private readonly IList _leaves;
+
+ public Leaves()
+ {
+ _leaves = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ while (br.BaseStream.Position < blob.Offset + blob.Length)
+ {
+ var leaf = new Leaf
+ {
+ Contents = (Contents)br.ReadInt32(),
+ AmbientLevels = new byte[Leaf.MaxNumAmbientLevels]
+ };
+
+ switch (version)
+ {
+ case Version.Goldsource:
+ case Version.Quake1:
+ leaf.VisOffset = br.ReadInt32();
+ break;
+ case Version.Quake2:
+ leaf.Cluster = br.ReadInt16();
+ leaf.Area = br.ReadInt16();
+ break;
+ }
+
+ leaf.Mins = new[] { br.ReadInt16(), br.ReadInt16(), br.ReadInt16() };
+ leaf.Maxs = new[] { br.ReadInt16(), br.ReadInt16(), br.ReadInt16() };
+
+ leaf.FirstLeafFace = br.ReadUInt16();
+ leaf.NumLeafFaces = br.ReadUInt16();
+
+ switch (version)
+ {
+ case Version.Goldsource:
+ case Version.Quake1:
+ leaf.AmbientLevels = br.ReadBytes(Leaf.MaxNumAmbientLevels);
+ break;
+ case Version.Quake2:
+ leaf.FirstLeafBrush = br.ReadUInt16();
+ leaf.NumLeafBrushes = br.ReadUInt16();
+ break;
+ }
+
+ _leaves.Add(leaf);
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ var pos = bw.BaseStream.Position;
+ foreach (var leaf in _leaves)
+ {
+ bw.Write((int) leaf.Contents);
+ switch (version)
+ {
+ case Version.Goldsource:
+ case Version.Quake1:
+ bw.Write((int) leaf.VisOffset);
+ break;
+ case Version.Quake2:
+ bw.Write((short) leaf.Cluster);
+ bw.Write((short) leaf.Area);
+ break;
+ }
+
+ bw.Write((short) leaf.Mins[0]);
+ bw.Write((short) leaf.Mins[1]);
+ bw.Write((short) leaf.Mins[2]);
+
+ bw.Write((short) leaf.Maxs[0]);
+ bw.Write((short) leaf.Maxs[1]);
+ bw.Write((short) leaf.Maxs[2]);
+
+ bw.Write((ushort) leaf.FirstLeafFace);
+ bw.Write((ushort) leaf.NumLeafFaces);
+
+ switch (version)
+ {
+ case Version.Goldsource:
+ case Version.Quake1:
+ bw.Write((byte[]) leaf.AmbientLevels);
+ break;
+ case Version.Quake2:
+ bw.Write((ushort)leaf.FirstLeafBrush);
+ bw.Write((ushort)leaf.NumLeafBrushes);
+ break;
+ }
+ }
+ return (int) (bw.BaseStream.Position - pos);
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _leaves.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _leaves).GetEnumerator();
+ }
+
+ public void Add(Leaf item)
+ {
+ _leaves.Add(item);
+ }
+
+ public void Clear()
+ {
+ _leaves.Clear();
+ }
+
+ public bool Contains(Leaf item)
+ {
+ return _leaves.Contains(item);
+ }
+
+ public void CopyTo(Leaf[] array, int arrayIndex)
+ {
+ _leaves.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(Leaf item)
+ {
+ return _leaves.Remove(item);
+ }
+
+ public int Count => _leaves.Count;
+
+ public bool IsReadOnly => _leaves.IsReadOnly;
+
+ public int IndexOf(Leaf item)
+ {
+ return _leaves.IndexOf(item);
+ }
+
+ public void Insert(int index, Leaf item)
+ {
+ _leaves.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _leaves.RemoveAt(index);
+ }
+
+ public Leaf this[int index]
+ {
+ get => _leaves[index];
+ set => _leaves[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Lightmaps.cs b/Sledge.Formats.Bsp/Lumps/Lightmaps.cs
new file mode 100644
index 0000000..5e334e5
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Lightmaps.cs
@@ -0,0 +1,162 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using Sledge.Formats.Bsp.Objects;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Lightmaps : ILump, IList
+ {
+ private readonly IList _lightmaps;
+ private byte[] _lightmapData;
+
+ public Lightmaps()
+ {
+ _lightmaps = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ _lightmapData = br.ReadBytes(blob.Length);
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+ var textureInfos = bsp.GetLump();
+ var planes = bsp.GetLump();
+ var surfEdges = bsp.GetLump();
+ var edges = bsp.GetLump();
+ var vertices = bsp.GetLump();
+ var faces = bsp.GetLump()
+ .Where(x => x.Styles.Length > 0 && x.Styles[0] != byte.MaxValue) // Indicates a fullbright face, no offset
+ .Where(x => x.LightmapOffset >= 0 && x.LightmapOffset < _lightmapData.Length) // Invalid offset
+ .ToList();
+
+ var offsetDict = new Dictionary();
+ foreach (var face in faces)
+ {
+ if (offsetDict.ContainsKey(face.LightmapOffset)) continue;
+
+ var ti = textureInfos[face.TextureInfo];
+ var pl = planes[face.Plane];
+
+ var uvs = new List();
+ for (var i = 0; i < face.NumEdges; i++)
+ {
+ var ei = surfEdges[face.FirstEdge + i];
+ var edge = edges[Math.Abs(ei)];
+ var point = vertices[ei > 0 ? edge.Start : edge.End];
+
+ var sn = new Vector3(ti.S.X, ti.S.Y, ti.S.Z);
+ var u = Vector3.Dot(point, sn) + ti.S.W;
+
+ var tn = new Vector3(ti.T.X, ti.T.Y, ti.T.Z);
+ var v = Vector3.Dot(point, tn) + ti.T.W;
+
+ uvs.Add(new Vector2(u, v));
+ }
+
+ var minu = uvs.Min(x => x.X);
+ var maxu = uvs.Max(x => x.X);
+ var minv = uvs.Min(x => x.Y);
+ var maxv = uvs.Max(x => x.Y);
+
+ var width = (int) Math.Ceiling(maxu / 16) - (int)Math.Floor(minu / 16) + 1;
+ var height = (int) Math.Ceiling(maxv / 16) - (int)Math.Floor(minv / 16) + 1;
+ var bpp = bsp.Version == Version.Quake1 ? 1 : 3;
+
+ var data = new byte[bpp * width * height];
+ Array.Copy(_lightmapData, face.LightmapOffset, data, 0, data.Length);
+
+ var map = new Lightmap
+ {
+ Offset = face.LightmapOffset,
+ Width = width,
+ Height = height,
+ BitsPerPixel = bpp,
+ Data = data
+ };
+ _lightmaps.Add(map);
+ offsetDict.Add(map.Offset, map);
+ }
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+ // throw new NotImplementedException("Lightmap data must be pre-processed");
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ bw.Write(_lightmapData);
+ return _lightmapData.Length;
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _lightmaps.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _lightmaps).GetEnumerator();
+ }
+
+ public void Add(Lightmap item)
+ {
+ _lightmaps.Add(item);
+ }
+
+ public void Clear()
+ {
+ _lightmaps.Clear();
+ }
+
+ public bool Contains(Lightmap item)
+ {
+ return _lightmaps.Contains(item);
+ }
+
+ public void CopyTo(Lightmap[] array, int arrayIndex)
+ {
+ _lightmaps.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(Lightmap item)
+ {
+ return _lightmaps.Remove(item);
+ }
+
+ public int Count => _lightmaps.Count;
+
+ public bool IsReadOnly => _lightmaps.IsReadOnly;
+
+ public int IndexOf(Lightmap item)
+ {
+ return _lightmaps.IndexOf(item);
+ }
+
+ public void Insert(int index, Lightmap item)
+ {
+ _lightmaps.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _lightmaps.RemoveAt(index);
+ }
+
+ public Lightmap this[int index]
+ {
+ get => _lightmaps[index];
+ set => _lightmaps[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Models.cs b/Sledge.Formats.Bsp/Lumps/Models.cs
new file mode 100644
index 0000000..8e24d30
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Models.cs
@@ -0,0 +1,147 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Numerics;
+using Sledge.Formats.Bsp.Objects;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Models : ILump, IList
+ {
+ private readonly IList _models;
+
+ public Models()
+ {
+ _models = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ while (br.BaseStream.Position < blob.Offset + blob.Length)
+ {
+ var model = new Model
+ {
+ Mins = new Vector3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()),
+ Maxs = new Vector3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()),
+ Origin = new Vector3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()),
+ };
+ switch (version)
+ {
+ case Version.Goldsource:
+ case Version.Quake1:
+ model.HeadNodes = new[] { br.ReadInt32(), br.ReadInt32(), br.ReadInt32(), br.ReadInt32() };
+ model.VisLeaves = br.ReadInt32();
+ break;
+ case Version.Quake2:
+ model.HeadNodes = new[] { br.ReadInt32(), -1, -1, -1 };
+ break;
+ }
+ model.FirstFace = br.ReadInt32();
+ model.NumFaces = br.ReadInt32();
+ _models.Add(model);
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ var pos = bw.BaseStream.Position;
+ foreach (var model in _models)
+ {
+ bw.WriteVector3(model.Mins);
+ bw.WriteVector3(model.Maxs);
+ bw.WriteVector3(model.Origin);
+ switch (version)
+ {
+ case Version.Goldsource:
+ case Version.Quake1:
+ bw.Write((int) model.HeadNodes[0]);
+ bw.Write((int) model.HeadNodes[1]);
+ bw.Write((int) model.HeadNodes[2]);
+ bw.Write((int) model.HeadNodes[3]);
+ bw.Write((int) model.VisLeaves);
+ break;
+ case Version.Quake2:
+ bw.Write((int) model.HeadNodes[0]);
+ break;
+ }
+ bw.Write((int) model.FirstFace);
+ bw.Write((int) model.NumFaces);
+ }
+ return (int) (bw.BaseStream.Position - pos);
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _models.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _models).GetEnumerator();
+ }
+
+ public void Add(Model item)
+ {
+ _models.Add(item);
+ }
+
+ public void Clear()
+ {
+ _models.Clear();
+ }
+
+ public bool Contains(Model item)
+ {
+ return _models.Contains(item);
+ }
+
+ public void CopyTo(Model[] array, int arrayIndex)
+ {
+ _models.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(Model item)
+ {
+ return _models.Remove(item);
+ }
+
+ public int Count => _models.Count;
+
+ public bool IsReadOnly => _models.IsReadOnly;
+
+ public int IndexOf(Model item)
+ {
+ return _models.IndexOf(item);
+ }
+
+ public void Insert(int index, Model item)
+ {
+ _models.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _models.RemoveAt(index);
+ }
+
+ public Model this[int index]
+ {
+ get => _models[index];
+ set => _models[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Nodes.cs b/Sledge.Formats.Bsp/Lumps/Nodes.cs
new file mode 100644
index 0000000..a1f0bc3
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Nodes.cs
@@ -0,0 +1,154 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using Sledge.Formats.Bsp.Objects;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Nodes : ILump, IList
+ {
+ private readonly IList _nodes;
+
+ public Nodes()
+ {
+ _nodes = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ while (br.BaseStream.Position < blob.Offset + blob.Length)
+ {
+ var node = new Node
+ {
+ Plane = br.ReadUInt32(),
+ Children = new int[2],
+ Mins = new short[3],
+ Maxs = new short[3]
+ };
+ switch (version)
+ {
+ case Version.Quake1:
+ case Version.Goldsource:
+ for (var i = 0; i < 2; i++) node.Children[i] = br.ReadInt16();
+ break;
+ case Version.Quake2:
+ for (var i = 0; i < 2; i++) node.Children[i] = br.ReadInt32();
+ break;
+ }
+ for (var i = 0; i < 3; i++) node.Mins[i] = br.ReadInt16();
+ for (var i = 0; i < 3; i++) node.Maxs[i] = br.ReadInt16();
+ node.FirstFace = br.ReadUInt16();
+ node.NumFaces = br.ReadUInt16();
+ _nodes.Add(node);
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ var pos = bw.BaseStream.Position;
+ foreach (var node in _nodes)
+ {
+ bw.Write((uint) node.Plane);
+
+ switch (version)
+ {
+ case Version.Quake1:
+ case Version.Goldsource:
+ bw.Write((short) node.Children[0]);
+ bw.Write((short) node.Children[1]);
+ break;
+ case Version.Quake2:
+ bw.Write((int) node.Children[0]);
+ bw.Write((int) node.Children[1]);
+ break;
+ }
+
+ bw.Write((short) node.Mins[0]);
+ bw.Write((short) node.Mins[1]);
+ bw.Write((short) node.Mins[2]);
+
+ bw.Write((short) node.Maxs[0]);
+ bw.Write((short) node.Maxs[1]);
+ bw.Write((short) node.Maxs[2]);
+
+ bw.Write((ushort) node.FirstFace);
+ bw.Write((ushort) node.NumFaces);
+ }
+ return (int)(bw.BaseStream.Position - pos);
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _nodes.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _nodes).GetEnumerator();
+ }
+
+ public void Add(Node item)
+ {
+ _nodes.Add(item);
+ }
+
+ public void Clear()
+ {
+ _nodes.Clear();
+ }
+
+ public bool Contains(Node item)
+ {
+ return _nodes.Contains(item);
+ }
+
+ public void CopyTo(Node[] array, int arrayIndex)
+ {
+ _nodes.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(Node item)
+ {
+ return _nodes.Remove(item);
+ }
+
+ public int Count => _nodes.Count;
+
+ public bool IsReadOnly => _nodes.IsReadOnly;
+
+ public int IndexOf(Node item)
+ {
+ return _nodes.IndexOf(item);
+ }
+
+ public void Insert(int index, Node item)
+ {
+ _nodes.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _nodes.RemoveAt(index);
+ }
+
+ public Node this[int index]
+ {
+ get => _nodes[index];
+ set => _nodes[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Planes.cs b/Sledge.Formats.Bsp/Lumps/Planes.cs
new file mode 100644
index 0000000..e5d8464
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Planes.cs
@@ -0,0 +1,118 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Numerics;
+using Sledge.Formats.Bsp.Objects;
+using Plane = Sledge.Formats.Bsp.Objects.Plane;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Planes : ILump, IList
+ {
+ private readonly IList _planes;
+
+ public Planes()
+ {
+ _planes = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ while (br.BaseStream.Position < blob.Offset + blob.Length)
+ {
+ _planes.Add(new Plane
+ {
+ Normal = new Vector3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()),
+ Distance = br.ReadSingle(),
+ Type = (PlaneType) br.ReadInt32()
+ });
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ var pos = bw.BaseStream.Position;
+ foreach (var plane in _planes)
+ {
+ bw.WriteVector3(plane.Normal);
+ bw.Write((float) plane.Distance);
+ bw.Write((int) plane.Type);
+ }
+ return (int)(bw.BaseStream.Position - pos);
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _planes.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)_planes).GetEnumerator();
+ }
+
+ public void Add(Plane item)
+ {
+ _planes.Add(item);
+ }
+
+ public void Clear()
+ {
+ _planes.Clear();
+ }
+
+ public bool Contains(Plane item)
+ {
+ return _planes.Contains(item);
+ }
+
+ public void CopyTo(Plane[] array, int arrayIndex)
+ {
+ _planes.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(Plane item)
+ {
+ return _planes.Remove(item);
+ }
+
+ public int Count => _planes.Count;
+
+ public bool IsReadOnly => _planes.IsReadOnly;
+
+ public int IndexOf(Plane item)
+ {
+ return _planes.IndexOf(item);
+ }
+
+ public void Insert(int index, Plane item)
+ {
+ _planes.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _planes.RemoveAt(index);
+ }
+
+ public Plane this[int index]
+ {
+ get => _planes[index];
+ set => _planes[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Pop.cs b/Sledge.Formats.Bsp/Lumps/Pop.cs
new file mode 100644
index 0000000..278a1ae
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Pop.cs
@@ -0,0 +1,35 @@
+using System.IO;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Pop : ILump
+ {
+ public byte[] Data { get; set; }
+
+ public Pop()
+ {
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ // Unused ...?
+ Data = br.ReadBytes(blob.Length);
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ bw.Write((byte[]) Data);
+ return Data.Length;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Surfedges.cs b/Sledge.Formats.Bsp/Lumps/Surfedges.cs
new file mode 100644
index 0000000..d9fb93e
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Surfedges.cs
@@ -0,0 +1,108 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Surfedges : ILump, IList
+ {
+ private readonly IList _surfaceEdges;
+
+ public Surfedges()
+ {
+ _surfaceEdges = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ var num = blob.Length / sizeof(int);
+ for (var i = 0; i < num; i++)
+ {
+ _surfaceEdges.Add(br.ReadInt32());
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ foreach (var se in _surfaceEdges)
+ {
+ bw.Write((int) se);
+ }
+ return sizeof(int) * _surfaceEdges.Count;
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _surfaceEdges.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _surfaceEdges).GetEnumerator();
+ }
+
+ public void Add(int item)
+ {
+ _surfaceEdges.Add(item);
+ }
+
+ public void Clear()
+ {
+ _surfaceEdges.Clear();
+ }
+
+ public bool Contains(int item)
+ {
+ return _surfaceEdges.Contains(item);
+ }
+
+ public void CopyTo(int[] array, int arrayIndex)
+ {
+ _surfaceEdges.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(int item)
+ {
+ return _surfaceEdges.Remove(item);
+ }
+
+ public int Count => _surfaceEdges.Count;
+
+ public bool IsReadOnly => _surfaceEdges.IsReadOnly;
+
+ public int IndexOf(int item)
+ {
+ return _surfaceEdges.IndexOf(item);
+ }
+
+ public void Insert(int index, int item)
+ {
+ _surfaceEdges.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _surfaceEdges.RemoveAt(index);
+ }
+
+ public int this[int index]
+ {
+ get => _surfaceEdges[index];
+ set => _surfaceEdges[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Texinfo.cs b/Sledge.Formats.Bsp/Lumps/Texinfo.cs
new file mode 100644
index 0000000..bd74dc2
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Texinfo.cs
@@ -0,0 +1,160 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Numerics;
+using System.Text;
+using Sledge.Formats.Bsp.Objects;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Texinfo : ILump, IList
+ {
+ private readonly IList _textureInfos;
+
+ public Texinfo()
+ {
+ _textureInfos = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ while (br.BaseStream.Position < blob.Offset + blob.Length)
+ {
+ var info = new TextureInfo
+ {
+ S = new Vector4(br.ReadSingle(), br.ReadSingle(), br.ReadSingle(), br.ReadSingle()),
+ T = new Vector4(br.ReadSingle(), br.ReadSingle(), br.ReadSingle(), br.ReadSingle()),
+ };
+ switch (version)
+ {
+ case Version.Goldsource:
+ case Version.Quake1:
+ info.MipTexture = br.ReadInt32();
+ break;
+ }
+ info.Flags = br.ReadInt32();
+ switch (version)
+ {
+ case Version.Quake2:
+ info.Value = br.ReadInt32();
+ info.TextureName = br.ReadFixedLengthString(Encoding.ASCII, 32);
+ info.NextTextureInfo = br.ReadInt32();
+ break;
+ }
+ _textureInfos.Add(info);
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ var pos = bw.BaseStream.Position;
+ foreach (var ti in _textureInfos)
+ {
+ bw.Write((float) ti.S.X);
+ bw.Write((float) ti.S.Y);
+ bw.Write((float) ti.S.Z);
+ bw.Write((float) ti.S.W);
+
+ bw.Write((float) ti.T.X);
+ bw.Write((float) ti.T.Y);
+ bw.Write((float) ti.T.Z);
+ bw.Write((float) ti.T.W);
+
+ switch (version)
+ {
+ case Version.Goldsource:
+ case Version.Quake1:
+ bw.Write((int) ti.MipTexture);
+ break;
+ }
+
+
+ bw.Write((int) ti.Flags);
+
+ switch (version)
+ {
+ case Version.Quake2:
+ bw.Write((int) ti.Value);
+ bw.WriteFixedLengthString(Encoding.ASCII, 32, ti.TextureName);
+ bw.Write((int) ti.NextTextureInfo);
+ break;
+ }
+ }
+ return (int)(bw.BaseStream.Position - pos);
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _textureInfos.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _textureInfos).GetEnumerator();
+ }
+
+ public void Add(TextureInfo item)
+ {
+ _textureInfos.Add(item);
+ }
+
+ public void Clear()
+ {
+ _textureInfos.Clear();
+ }
+
+ public bool Contains(TextureInfo item)
+ {
+ return _textureInfos.Contains(item);
+ }
+
+ public void CopyTo(TextureInfo[] array, int arrayIndex)
+ {
+ _textureInfos.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(TextureInfo item)
+ {
+ return _textureInfos.Remove(item);
+ }
+
+ public int Count => _textureInfos.Count;
+
+ public bool IsReadOnly => _textureInfos.IsReadOnly;
+
+ public int IndexOf(TextureInfo item)
+ {
+ return _textureInfos.IndexOf(item);
+ }
+
+ public void Insert(int index, TextureInfo item)
+ {
+ _textureInfos.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _textureInfos.RemoveAt(index);
+ }
+
+ public TextureInfo this[int index]
+ {
+ get => _textureInfos[index];
+ set => _textureInfos[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Textures.cs b/Sledge.Formats.Bsp/Lumps/Textures.cs
new file mode 100644
index 0000000..eff31e7
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Textures.cs
@@ -0,0 +1,128 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using Sledge.Formats.Bsp.Objects;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Textures : ILump, IList
+ {
+ private readonly IList _textures;
+
+ public Textures()
+ {
+ _textures = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ var numTextures = br.ReadUInt32();
+ var offsets = new int[numTextures];
+ for (var i = 0; i < numTextures; i++) offsets[i] = br.ReadInt32();
+ foreach (var offset in offsets)
+ {
+ br.BaseStream.Seek(blob.Offset + offset, SeekOrigin.Begin);
+ var tex = Texture.ReadMipTexture(br, version);
+ _textures.Add(tex);
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ var pos = bw.BaseStream.Position;
+
+ bw.Write((uint) _textures.Count);
+ bw.Seek(sizeof(int) * _textures.Count, SeekOrigin.Current);
+
+ var offsets = new int[_textures.Count];
+ for (var i = 0; i < _textures.Count; i++)
+ {
+ var tex = _textures[i];
+ offsets[i] = (int) (bw.BaseStream.Position - pos);
+ Texture.WriteMipTexture(bw, version, tex);
+ }
+
+ var pos2 = bw.BaseStream.Position;
+ bw.BaseStream.Seek(pos + sizeof(uint), SeekOrigin.Begin);
+ foreach (var offset in offsets) bw.Write((int) offset);
+ bw.BaseStream.Seek(pos2, SeekOrigin.Begin);
+
+ return (int)(bw.BaseStream.Position - pos);
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _textures.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _textures).GetEnumerator();
+ }
+
+ public void Add(Texture item)
+ {
+ _textures.Add(item);
+ }
+
+ public void Clear()
+ {
+ _textures.Clear();
+ }
+
+ public bool Contains(Texture item)
+ {
+ return _textures.Contains(item);
+ }
+
+ public void CopyTo(Texture[] array, int arrayIndex)
+ {
+ _textures.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(Texture item)
+ {
+ return _textures.Remove(item);
+ }
+
+ public int Count => _textures.Count;
+
+ public bool IsReadOnly => _textures.IsReadOnly;
+
+ public int IndexOf(Texture item)
+ {
+ return _textures.IndexOf(item);
+ }
+
+ public void Insert(int index, Texture item)
+ {
+ _textures.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _textures.RemoveAt(index);
+ }
+
+ public Texture this[int index]
+ {
+ get => _textures[index];
+ set => _textures[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Vertices.cs b/Sledge.Formats.Bsp/Lumps/Vertices.cs
new file mode 100644
index 0000000..eb358d7
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Vertices.cs
@@ -0,0 +1,108 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Numerics;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Vertices : ILump, IList
+ {
+ private readonly IList _vertices;
+
+ public Vertices()
+ {
+ _vertices = new List();
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ while (br.BaseStream.Position < blob.Offset + blob.Length)
+ {
+ _vertices.Add(new Vector3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()));
+ }
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ foreach (var se in _vertices)
+ {
+ bw.WriteVector3(se);
+ }
+ return sizeof(float) * 3 * _vertices.Count;
+ }
+
+ #region IList
+
+ public IEnumerator GetEnumerator()
+ {
+ return _vertices.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable) _vertices).GetEnumerator();
+ }
+
+ public void Add(Vector3 item)
+ {
+ _vertices.Add(item);
+ }
+
+ public void Clear()
+ {
+ _vertices.Clear();
+ }
+
+ public bool Contains(Vector3 item)
+ {
+ return _vertices.Contains(item);
+ }
+
+ public void CopyTo(Vector3[] array, int arrayIndex)
+ {
+ _vertices.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(Vector3 item)
+ {
+ return _vertices.Remove(item);
+ }
+
+ public int Count => _vertices.Count;
+
+ public bool IsReadOnly => _vertices.IsReadOnly;
+
+ public int IndexOf(Vector3 item)
+ {
+ return _vertices.IndexOf(item);
+ }
+
+ public void Insert(int index, Vector3 item)
+ {
+ _vertices.Insert(index, item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _vertices.RemoveAt(index);
+ }
+
+ public Vector3 this[int index]
+ {
+ get => _vertices[index];
+ set => _vertices[index] = value;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Lumps/Visibility.cs b/Sledge.Formats.Bsp/Lumps/Visibility.cs
new file mode 100644
index 0000000..bda52bf
--- /dev/null
+++ b/Sledge.Formats.Bsp/Lumps/Visibility.cs
@@ -0,0 +1,36 @@
+using System;
+using System.IO;
+
+namespace Sledge.Formats.Bsp.Lumps
+{
+ public class Visibility : ILump
+ {
+ public byte[] VisData { get; private set; }
+
+ public Visibility()
+ {
+
+ }
+
+ public void Read(BinaryReader br, Blob blob, Version version)
+ {
+ VisData = br.ReadBytes(blob.Length);
+ }
+
+ public void PostReadProcess(BspFile bsp)
+ {
+ //throw new NotImplementedException("Visibility data must be post-processed");
+ }
+
+ public void PreWriteProcess(BspFile bsp, Version version)
+ {
+ // throw new NotImplementedException("Visibility data must be pre-processed");
+ }
+
+ public int Write(BinaryWriter bw, Version version)
+ {
+ bw.Write(VisData);
+ return VisData.Length;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Magic.cs b/Sledge.Formats.Bsp/Magic.cs
new file mode 100644
index 0000000..6de645b
--- /dev/null
+++ b/Sledge.Formats.Bsp/Magic.cs
@@ -0,0 +1,8 @@
+namespace Sledge.Formats.Bsp
+{
+ internal enum Magic : uint
+ {
+ Vbsp = ('P' << 24) + ('S' << 16) + ('B' << 8) + 'V',
+ Ibsp = ('P' << 24) + ('S' << 16) + ('B' << 8) + 'I',
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/Area.cs b/Sledge.Formats.Bsp/Objects/Area.cs
new file mode 100644
index 0000000..0c3438a
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/Area.cs
@@ -0,0 +1,8 @@
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class Area
+ {
+ public int NumAreaPortals { get; set; }
+ public int FirstAreaPortal { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/AreaPortal.cs b/Sledge.Formats.Bsp/Objects/AreaPortal.cs
new file mode 100644
index 0000000..77633fb
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/AreaPortal.cs
@@ -0,0 +1,8 @@
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class AreaPortal
+ {
+ public int PortalNum { get; set; }
+ public int OtherArea { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/Brush.cs b/Sledge.Formats.Bsp/Objects/Brush.cs
new file mode 100644
index 0000000..3f5e306
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/Brush.cs
@@ -0,0 +1,9 @@
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class Brush
+ {
+ public int FirstSide { get; set; }
+ public int NumSides { get; set; }
+ public int Contents { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/BrushSide.cs b/Sledge.Formats.Bsp/Objects/BrushSide.cs
new file mode 100644
index 0000000..9a5d706
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/BrushSide.cs
@@ -0,0 +1,8 @@
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class BrushSide
+ {
+ public ushort Plane { get; set; }
+ public short Texinfo { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/Clipnode.cs b/Sledge.Formats.Bsp/Objects/Clipnode.cs
new file mode 100644
index 0000000..be097fe
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/Clipnode.cs
@@ -0,0 +1,8 @@
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class Clipnode
+ {
+ public uint Plane { get; set; }
+ public short[] Children { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/Contents.cs b/Sledge.Formats.Bsp/Objects/Contents.cs
new file mode 100644
index 0000000..9a8847c
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/Contents.cs
@@ -0,0 +1,21 @@
+namespace Sledge.Formats.Bsp.Objects
+{
+ public enum Contents : int
+ {
+ Empty = -1,
+ Solid = -2,
+ Water = -3,
+ Slime = -4,
+ Lava = -5,
+ Sky = -6,
+ Origin = -7,
+ Clip = -8,
+ Current0 = -9,
+ Current90 = -10,
+ Current180 = -11,
+ Current270 = -12,
+ CurrentUp = -13,
+ CurrentDown = -14,
+ Translucent = -15
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/Edge.cs b/Sledge.Formats.Bsp/Objects/Edge.cs
new file mode 100644
index 0000000..3eb0b39
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/Edge.cs
@@ -0,0 +1,8 @@
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class Edge
+ {
+ public ushort Start { get; set; }
+ public ushort End { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/Entity.cs b/Sledge.Formats.Bsp/Objects/Entity.cs
new file mode 100644
index 0000000..09df61e
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/Entity.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class Entity
+ {
+ public int Model { get; set; }
+ public string ClassName { get; set; }
+ public Dictionary KeyValues { get; set; }
+
+ public Entity()
+ {
+ KeyValues = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
+ }
+
+ public Vector3 GetVector3(string name, Vector3 defaultValue)
+ {
+ if (!KeyValues.ContainsKey(name)) return defaultValue;
+
+ var val = KeyValues[name];
+ var spl = val.Split(' ');
+ if (spl.Length != 3) return defaultValue;
+
+ if (!float.TryParse(spl[0], out var x)) return defaultValue;
+ if (!float.TryParse(spl[1], out var y)) return defaultValue;
+ if (!float.TryParse(spl[2], out var z)) return defaultValue;
+
+ return new Vector3(x, y, z);
+ }
+
+ public T Get(string name, T defaultValue)
+ {
+ if (!KeyValues.ContainsKey(name)) return defaultValue;
+ var val = KeyValues[name];
+ try
+ {
+ return (T) Convert.ChangeType(val, typeof(T));
+ }
+ catch
+ {
+ return defaultValue;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/Face.cs b/Sledge.Formats.Bsp/Objects/Face.cs
new file mode 100644
index 0000000..a40cf68
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/Face.cs
@@ -0,0 +1,15 @@
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class Face
+ {
+ public const int MaxLightmaps = 4;
+
+ public ushort Plane { get; set; }
+ public ushort Side { get; set; }
+ public int FirstEdge { get; set; }
+ public ushort NumEdges { get; set; }
+ public ushort TextureInfo { get; set; }
+ public byte[] Styles { get; set; }
+ public int LightmapOffset { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/Leaf.cs b/Sledge.Formats.Bsp/Objects/Leaf.cs
new file mode 100644
index 0000000..48199e4
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/Leaf.cs
@@ -0,0 +1,24 @@
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class Leaf
+ {
+ public const int MaxNumAmbientLevels = 4;
+
+ public Contents Contents { get; set; }
+
+ public int VisOffset { get; set; }
+ public short Cluster { get; set; }
+ public short Area { get; set; }
+
+ public short[] Mins { get; set; }
+ public short[] Maxs { get; set; }
+
+ public ushort FirstLeafFace { get; set; }
+ public ushort NumLeafFaces { get; set; }
+
+ public ushort FirstLeafBrush { get; set; }
+ public ushort NumLeafBrushes { get; set; }
+
+ public byte[] AmbientLevels { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/Lightmap.cs b/Sledge.Formats.Bsp/Objects/Lightmap.cs
new file mode 100644
index 0000000..1a831f1
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/Lightmap.cs
@@ -0,0 +1,11 @@
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class Lightmap
+ {
+ public int Offset { get; set; }
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public int BitsPerPixel { get; set; }
+ public byte[] Data { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/Model.cs b/Sledge.Formats.Bsp/Objects/Model.cs
new file mode 100644
index 0000000..92f4870
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/Model.cs
@@ -0,0 +1,15 @@
+using System.Numerics;
+
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class Model
+ {
+ public Vector3 Mins { get; set; }
+ public Vector3 Maxs { get; set; }
+ public Vector3 Origin { get; set; }
+ public int[] HeadNodes { get; set; }
+ public int VisLeaves { get; set; }
+ public int FirstFace { get; set; }
+ public int NumFaces { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/Node.cs b/Sledge.Formats.Bsp/Objects/Node.cs
new file mode 100644
index 0000000..057687b
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/Node.cs
@@ -0,0 +1,12 @@
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class Node
+ {
+ public uint Plane { get; set; }
+ public int[] Children { get; set; }
+ public short[] Mins { get; set; }
+ public short[] Maxs { get; set; }
+ public ushort FirstFace { get; set; }
+ public ushort NumFaces { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/Plane.cs b/Sledge.Formats.Bsp/Objects/Plane.cs
new file mode 100644
index 0000000..d6943e8
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/Plane.cs
@@ -0,0 +1,11 @@
+using System.Numerics;
+
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class Plane
+ {
+ public Vector3 Normal { get; set; }
+ public float Distance { get; set; }
+ public PlaneType Type { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/PlaneType.cs b/Sledge.Formats.Bsp/Objects/PlaneType.cs
new file mode 100644
index 0000000..f737720
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/PlaneType.cs
@@ -0,0 +1,12 @@
+namespace Sledge.Formats.Bsp.Objects
+{
+ public enum PlaneType : int
+ {
+ X = 0,
+ Y = 1,
+ Z = 2,
+ AnyX = 3,
+ AnyY = 4,
+ AnyZ = 5,
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/Texture.cs b/Sledge.Formats.Bsp/Objects/Texture.cs
new file mode 100644
index 0000000..df3419f
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/Texture.cs
@@ -0,0 +1,205 @@
+using System;
+using System.IO;
+using System.Text;
+
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class Texture
+ {
+ public string Name { get; set; }
+ public uint Width { get; set; }
+ public uint Height { get; set; }
+ public int NumMips { get; set; }
+ public byte[][] MipData { get; set; }
+ public byte[] Palette { get; set; }
+
+ const int NameLength = 16;
+
+ public static Texture ReadMipTexture(BinaryReader br, Version version)
+ {
+ var position = br.BaseStream.Position;
+
+ var texture = new Texture();
+
+ var name = br.ReadChars(NameLength);
+ var len = Array.IndexOf(name, '\0');
+ texture.Name = new string(name, 0, len < 0 ? name.Length : len);
+
+ texture.Width = br.ReadUInt32();
+ texture.Height = br.ReadUInt32();
+ var offsets = new[] { br.ReadUInt32(), br.ReadUInt32(), br.ReadUInt32(), br.ReadUInt32() };
+
+ if (offsets[0] == 0)
+ {
+ texture.NumMips = 0;
+ texture.MipData = new byte[0][];
+ texture.Palette = version == Version.Quake1 ? QuakePalette : new byte[0];
+ return texture;
+ }
+
+ texture.NumMips = 4;
+ texture.MipData = new byte[4][];
+
+ int w = (int)texture.Width, h = (int)texture.Height;
+ for (var i = 0; i < 4; i++)
+ {
+ br.BaseStream.Seek(position + offsets[i], SeekOrigin.Begin);
+ texture.MipData[i] = br.ReadBytes(w * h);
+ w /= 2;
+ h /= 2;
+ }
+
+ switch (version)
+ {
+ case Version.Quake1:
+ texture.Palette = QuakePalette;
+ break;
+ case Version.Goldsource:
+ var paletteSize = br.ReadUInt16();
+ texture.Palette = br.ReadBytes(paletteSize * 3);
+ break;
+ }
+
+ return texture;
+ }
+
+ public static void WriteMipTexture(BinaryWriter bw, Version version, Texture texture)
+ {
+ bw.WriteFixedLengthString(Encoding.ASCII, NameLength, texture.Name);
+ bw.Write((uint) texture.Width);
+ bw.Write((uint) texture.Height);
+
+ if (texture.NumMips == 0)
+ {
+ bw.Write((uint) 0);
+ bw.Write((uint) 0);
+ bw.Write((uint) 0);
+ bw.Write((uint) 0);
+ bw.Write((ushort) 0);
+ return;
+ }
+
+ uint currentOffset = NameLength + sizeof(uint) * 2 + sizeof(uint) * 4;
+
+ for (var i = 0; i < 4; i++)
+ {
+ bw.Write((uint) currentOffset);
+ currentOffset += (uint) texture.MipData[i].Length;
+ }
+
+ for (var i = 0; i < 4; i++)
+ {
+ bw.Write((byte[]) texture.MipData[i]);
+ }
+
+ switch (version)
+ {
+ case Version.Goldsource:
+ bw.Write((ushort) (texture.Palette.Length / 3));
+ bw.Write((byte[]) texture.Palette);
+ break;
+ }
+ }
+
+ // The Quake palette is in the public domain
+ private static readonly byte[] QuakePalette =
+ {
+ 0x00, 0x00, 0x00, 0x0F, 0x0F, 0x0F, 0x1F, 0x1F,
+ 0x1F, 0x2F, 0x2F, 0x2F, 0x3F, 0x3F, 0x3F, 0x4B,
+ 0x4B, 0x4B, 0x5B, 0x5B, 0x5B, 0x6B, 0x6B, 0x6B,
+ 0x7B, 0x7B, 0x7B, 0x8B, 0x8B, 0x8B, 0x9B, 0x9B,
+ 0x9B, 0xAB, 0xAB, 0xAB, 0xBB, 0xBB, 0xBB, 0xCB,
+ 0xCB, 0xCB, 0xDB, 0xDB, 0xDB, 0xEB, 0xEB, 0xEB,
+ 0x0F, 0x0B, 0x07, 0x17, 0x0F, 0x0B, 0x1F, 0x17,
+ 0x0B, 0x27, 0x1B, 0x0F, 0x2F, 0x23, 0x13, 0x37,
+ 0x2B, 0x17, 0x3F, 0x2F, 0x17, 0x4B, 0x37, 0x1B,
+ 0x53, 0x3B, 0x1B, 0x5B, 0x43, 0x1F, 0x63, 0x4B,
+ 0x1F, 0x6B, 0x53, 0x1F, 0x73, 0x57, 0x1F, 0x7B,
+ 0x5F, 0x23, 0x83, 0x67, 0x23, 0x8F, 0x6F, 0x23,
+ 0x0B, 0x0B, 0x0F, 0x13, 0x13, 0x1B, 0x1B, 0x1B,
+ 0x27, 0x27, 0x27, 0x33, 0x2F, 0x2F, 0x3F, 0x37,
+ 0x37, 0x4B, 0x3F, 0x3F, 0x57, 0x47, 0x47, 0x67,
+ 0x4F, 0x4F, 0x73, 0x5B, 0x5B, 0x7F, 0x63, 0x63,
+ 0x8B, 0x6B, 0x6B, 0x97, 0x73, 0x73, 0xA3, 0x7B,
+ 0x7B, 0xAF, 0x83, 0x83, 0xBB, 0x8B, 0x8B, 0xCB,
+ 0x00, 0x00, 0x00, 0x07, 0x07, 0x00, 0x0B, 0x0B,
+ 0x00, 0x13, 0x13, 0x00, 0x1B, 0x1B, 0x00, 0x23,
+ 0x23, 0x00, 0x2B, 0x2B, 0x07, 0x2F, 0x2F, 0x07,
+ 0x37, 0x37, 0x07, 0x3F, 0x3F, 0x07, 0x47, 0x47,
+ 0x07, 0x4B, 0x4B, 0x0B, 0x53, 0x53, 0x0B, 0x5B,
+ 0x5B, 0x0B, 0x63, 0x63, 0x0B, 0x6B, 0x6B, 0x0F,
+ 0x07, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x17, 0x00,
+ 0x00, 0x1F, 0x00, 0x00, 0x27, 0x00, 0x00, 0x2F,
+ 0x00, 0x00, 0x37, 0x00, 0x00, 0x3F, 0x00, 0x00,
+ 0x47, 0x00, 0x00, 0x4F, 0x00, 0x00, 0x57, 0x00,
+ 0x00, 0x5F, 0x00, 0x00, 0x67, 0x00, 0x00, 0x6F,
+ 0x00, 0x00, 0x77, 0x00, 0x00, 0x7F, 0x00, 0x00,
+ 0x13, 0x13, 0x00, 0x1B, 0x1B, 0x00, 0x23, 0x23,
+ 0x00, 0x2F, 0x2B, 0x00, 0x37, 0x2F, 0x00, 0x43,
+ 0x37, 0x00, 0x4B, 0x3B, 0x07, 0x57, 0x43, 0x07,
+ 0x5F, 0x47, 0x07, 0x6B, 0x4B, 0x0B, 0x77, 0x53,
+ 0x0F, 0x83, 0x57, 0x13, 0x8B, 0x5B, 0x13, 0x97,
+ 0x5F, 0x1B, 0xA3, 0x63, 0x1F, 0xAF, 0x67, 0x23,
+ 0x23, 0x13, 0x07, 0x2F, 0x17, 0x0B, 0x3B, 0x1F,
+ 0x0F, 0x4B, 0x23, 0x13, 0x57, 0x2B, 0x17, 0x63,
+ 0x2F, 0x1F, 0x73, 0x37, 0x23, 0x7F, 0x3B, 0x2B,
+ 0x8F, 0x43, 0x33, 0x9F, 0x4F, 0x33, 0xAF, 0x63,
+ 0x2F, 0xBF, 0x77, 0x2F, 0xCF, 0x8F, 0x2B, 0xDF,
+ 0xAB, 0x27, 0xEF, 0xCB, 0x1F, 0xFF, 0xF3, 0x1B,
+ 0x0B, 0x07, 0x00, 0x1B, 0x13, 0x00, 0x2B, 0x23,
+ 0x0F, 0x37, 0x2B, 0x13, 0x47, 0x33, 0x1B, 0x53,
+ 0x37, 0x23, 0x63, 0x3F, 0x2B, 0x6F, 0x47, 0x33,
+ 0x7F, 0x53, 0x3F, 0x8B, 0x5F, 0x47, 0x9B, 0x6B,
+ 0x53, 0xA7, 0x7B, 0x5F, 0xB7, 0x87, 0x6B, 0xC3,
+ 0x93, 0x7B, 0xD3, 0xA3, 0x8B, 0xE3, 0xB3, 0x97,
+ 0xAB, 0x8B, 0xA3, 0x9F, 0x7F, 0x97, 0x93, 0x73,
+ 0x87, 0x8B, 0x67, 0x7B, 0x7F, 0x5B, 0x6F, 0x77,
+ 0x53, 0x63, 0x6B, 0x4B, 0x57, 0x5F, 0x3F, 0x4B,
+ 0x57, 0x37, 0x43, 0x4B, 0x2F, 0x37, 0x43, 0x27,
+ 0x2F, 0x37, 0x1F, 0x23, 0x2B, 0x17, 0x1B, 0x23,
+ 0x13, 0x13, 0x17, 0x0B, 0x0B, 0x0F, 0x07, 0x07,
+ 0xBB, 0x73, 0x9F, 0xAF, 0x6B, 0x8F, 0xA3, 0x5F,
+ 0x83, 0x97, 0x57, 0x77, 0x8B, 0x4F, 0x6B, 0x7F,
+ 0x4B, 0x5F, 0x73, 0x43, 0x53, 0x6B, 0x3B, 0x4B,
+ 0x5F, 0x33, 0x3F, 0x53, 0x2B, 0x37, 0x47, 0x23,
+ 0x2B, 0x3B, 0x1F, 0x23, 0x2F, 0x17, 0x1B, 0x23,
+ 0x13, 0x13, 0x17, 0x0B, 0x0B, 0x0F, 0x07, 0x07,
+ 0xDB, 0xC3, 0xBB, 0xCB, 0xB3, 0xA7, 0xBF, 0xA3,
+ 0x9B, 0xAF, 0x97, 0x8B, 0xA3, 0x87, 0x7B, 0x97,
+ 0x7B, 0x6F, 0x87, 0x6F, 0x5F, 0x7B, 0x63, 0x53,
+ 0x6B, 0x57, 0x47, 0x5F, 0x4B, 0x3B, 0x53, 0x3F,
+ 0x33, 0x43, 0x33, 0x27, 0x37, 0x2B, 0x1F, 0x27,
+ 0x1F, 0x17, 0x1B, 0x13, 0x0F, 0x0F, 0x0B, 0x07,
+ 0x6F, 0x83, 0x7B, 0x67, 0x7B, 0x6F, 0x5F, 0x73,
+ 0x67, 0x57, 0x6B, 0x5F, 0x4F, 0x63, 0x57, 0x47,
+ 0x5B, 0x4F, 0x3F, 0x53, 0x47, 0x37, 0x4B, 0x3F,
+ 0x2F, 0x43, 0x37, 0x2B, 0x3B, 0x2F, 0x23, 0x33,
+ 0x27, 0x1F, 0x2B, 0x1F, 0x17, 0x23, 0x17, 0x0F,
+ 0x1B, 0x13, 0x0B, 0x13, 0x0B, 0x07, 0x0B, 0x07,
+ 0xFF, 0xF3, 0x1B, 0xEF, 0xDF, 0x17, 0xDB, 0xCB,
+ 0x13, 0xCB, 0xB7, 0x0F, 0xBB, 0xA7, 0x0F, 0xAB,
+ 0x97, 0x0B, 0x9B, 0x83, 0x07, 0x8B, 0x73, 0x07,
+ 0x7B, 0x63, 0x07, 0x6B, 0x53, 0x00, 0x5B, 0x47,
+ 0x00, 0x4B, 0x37, 0x00, 0x3B, 0x2B, 0x00, 0x2B,
+ 0x1F, 0x00, 0x1B, 0x0F, 0x00, 0x0B, 0x07, 0x00,
+ 0x00, 0x00, 0xFF, 0x0B, 0x0B, 0xEF, 0x13, 0x13,
+ 0xDF, 0x1B, 0x1B, 0xCF, 0x23, 0x23, 0xBF, 0x2B,
+ 0x2B, 0xAF, 0x2F, 0x2F, 0x9F, 0x2F, 0x2F, 0x8F,
+ 0x2F, 0x2F, 0x7F, 0x2F, 0x2F, 0x6F, 0x2F, 0x2F,
+ 0x5F, 0x2B, 0x2B, 0x4F, 0x23, 0x23, 0x3F, 0x1B,
+ 0x1B, 0x2F, 0x13, 0x13, 0x1F, 0x0B, 0x0B, 0x0F,
+ 0x2B, 0x00, 0x00, 0x3B, 0x00, 0x00, 0x4B, 0x07,
+ 0x00, 0x5F, 0x07, 0x00, 0x6F, 0x0F, 0x00, 0x7F,
+ 0x17, 0x07, 0x93, 0x1F, 0x07, 0xA3, 0x27, 0x0B,
+ 0xB7, 0x33, 0x0F, 0xC3, 0x4B, 0x1B, 0xCF, 0x63,
+ 0x2B, 0xDB, 0x7F, 0x3B, 0xE3, 0x97, 0x4F, 0xE7,
+ 0xAB, 0x5F, 0xEF, 0xBF, 0x77, 0xF7, 0xD3, 0x8B,
+ 0xA7, 0x7B, 0x3B, 0xB7, 0x9B, 0x37, 0xC7, 0xC3,
+ 0x37, 0xE7, 0xE3, 0x57, 0x7F, 0xBF, 0xFF, 0xAB,
+ 0xE7, 0xFF, 0xD7, 0xFF, 0xFF, 0x67, 0x00, 0x00,
+ 0x8B, 0x00, 0x00, 0xB3, 0x00, 0x00, 0xD7, 0x00,
+ 0x00, 0xFF, 0x00, 0x00, 0xFF, 0xF3, 0x93, 0xFF,
+ 0xF7, 0xC7, 0xFF, 0xFF, 0xFF, 0x9F, 0x5B, 0x53
+ };
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Objects/TextureInfo.cs b/Sledge.Formats.Bsp/Objects/TextureInfo.cs
new file mode 100644
index 0000000..6b1f4d1
--- /dev/null
+++ b/Sledge.Formats.Bsp/Objects/TextureInfo.cs
@@ -0,0 +1,15 @@
+using System.Numerics;
+
+namespace Sledge.Formats.Bsp.Objects
+{
+ public class TextureInfo
+ {
+ public Vector4 S { get; set; }
+ public Vector4 T { get; set; }
+ public int MipTexture { get; set; }
+ public int Flags { get; set; }
+ public int Value { get; set; }
+ public string TextureName { get; set; }
+ public int NextTextureInfo { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Readers/GoldsourceBspReader.cs b/Sledge.Formats.Bsp/Readers/GoldsourceBspReader.cs
new file mode 100644
index 0000000..1c14b3d
--- /dev/null
+++ b/Sledge.Formats.Bsp/Readers/GoldsourceBspReader.cs
@@ -0,0 +1,91 @@
+using System;
+using System.IO;
+using Sledge.Formats.Bsp.Lumps;
+
+namespace Sledge.Formats.Bsp.Readers
+{
+ public class GoldsourceBspReader : IBspReader
+ {
+ public Version SupportedVersion => Version.Goldsource;
+ public int NumLumps => (int) Lump.NumLumps;
+
+ public void StartHeader(BspFile file, BinaryReader br)
+ {
+ //
+ }
+
+ public Blob ReadBlob(BinaryReader br)
+ {
+ return new Blob
+ {
+ Offset = br.ReadInt32(),
+ Length = br.ReadInt32()
+ };
+ }
+
+ public void EndHeader(BspFile file, BinaryReader br)
+ {
+ //
+ }
+
+ public ILump GetLump(Blob blob)
+ {
+ switch ((Lump) blob.Index)
+ {
+ case Lump.Entities:
+ return new Entities();
+ case Lump.Planes:
+ return new Planes();
+ case Lump.Textures:
+ return new Textures();
+ case Lump.Vertices:
+ return new Vertices();
+ case Lump.Visibility:
+ return new Visibility();
+ case Lump.Nodes:
+ return new Nodes();
+ case Lump.Texinfo:
+ return new Texinfo();
+ case Lump.Faces:
+ return new Faces();
+ case Lump.Lighting:
+ return new Lightmaps();
+ case Lump.Clipnodes:
+ return new Clipnodes();
+ case Lump.Leaves:
+ return new Leaves();
+ case Lump.Marksurfaces:
+ return new LeafFaces();
+ case Lump.Edges:
+ return new Edges();
+ case Lump.Surfedges:
+ return new Surfedges();
+ case Lump.Models:
+ return new Models();
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ private enum Lump : int
+ {
+ Entities = 0,
+ Planes = 1,
+ Textures = 2,
+ Vertices = 3,
+ Visibility = 4,
+ Nodes = 5,
+ Texinfo = 6,
+ Faces = 7,
+ Lighting = 8,
+ Clipnodes = 9,
+ Leaves = 10,
+ Marksurfaces = 11,
+ Edges = 12,
+ Surfedges = 13,
+ Models = 14,
+
+ NumLumps = 15
+ }
+ }
+}
diff --git a/Sledge.Formats.Bsp/Readers/IBspReader.cs b/Sledge.Formats.Bsp/Readers/IBspReader.cs
new file mode 100644
index 0000000..1a8df65
--- /dev/null
+++ b/Sledge.Formats.Bsp/Readers/IBspReader.cs
@@ -0,0 +1,17 @@
+using System.IO;
+using Sledge.Formats.Bsp.Lumps;
+
+namespace Sledge.Formats.Bsp.Readers
+{
+ public interface IBspReader
+ {
+ Version SupportedVersion { get; }
+ int NumLumps { get; }
+
+ void StartHeader(BspFile file, BinaryReader br);
+ Blob ReadBlob(BinaryReader br);
+ void EndHeader(BspFile file, BinaryReader br);
+
+ ILump GetLump(Blob blob);
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Readers/Quake1BspReader.cs b/Sledge.Formats.Bsp/Readers/Quake1BspReader.cs
new file mode 100644
index 0000000..374f0ef
--- /dev/null
+++ b/Sledge.Formats.Bsp/Readers/Quake1BspReader.cs
@@ -0,0 +1,91 @@
+using System;
+using System.IO;
+using Sledge.Formats.Bsp.Lumps;
+
+namespace Sledge.Formats.Bsp.Readers
+{
+ public class Quake1BspReader : IBspReader
+ {
+ public Version SupportedVersion => Version.Quake1;
+ public int NumLumps => (int) Lump.NumLumps;
+
+ public void StartHeader(BspFile file, BinaryReader br)
+ {
+ //
+ }
+
+ public Blob ReadBlob(BinaryReader br)
+ {
+ return new Blob
+ {
+ Offset = br.ReadInt32(),
+ Length = br.ReadInt32()
+ };
+ }
+
+ public void EndHeader(BspFile file, BinaryReader br)
+ {
+ //
+ }
+
+ public ILump GetLump(Blob blob)
+ {
+ switch ((Lump) blob.Index)
+ {
+ case Lump.Entities:
+ return new Entities();
+ case Lump.Planes:
+ return new Planes();
+ case Lump.Textures:
+ return new Textures();
+ case Lump.Vertices:
+ return new Vertices();
+ case Lump.Visibility:
+ return new Visibility();
+ case Lump.Nodes:
+ return new Nodes();
+ case Lump.Texinfo:
+ return new Texinfo();
+ case Lump.Faces:
+ return new Faces();
+ case Lump.Lighting:
+ return new Lightmaps();
+ case Lump.Clipnodes:
+ return new Clipnodes();
+ case Lump.Leaves:
+ return new Leaves();
+ case Lump.Marksurfaces:
+ return new LeafFaces();
+ case Lump.Edges:
+ return new Edges();
+ case Lump.Surfedges:
+ return new Surfedges();
+ case Lump.Models:
+ return new Models();
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ private enum Lump : int
+ {
+ Entities = 0,
+ Planes = 1,
+ Textures = 2,
+ Vertices = 3,
+ Visibility = 4,
+ Nodes = 5,
+ Texinfo = 6,
+ Faces = 7,
+ Lighting = 8,
+ Clipnodes = 9,
+ Leaves = 10,
+ Marksurfaces = 11,
+ Edges = 12,
+ Surfedges = 13,
+ Models = 14,
+
+ NumLumps = 15
+ }
+ }
+}
diff --git a/Sledge.Formats.Bsp/Readers/Quake2BspReader.cs b/Sledge.Formats.Bsp/Readers/Quake2BspReader.cs
new file mode 100644
index 0000000..c0aef3e
--- /dev/null
+++ b/Sledge.Formats.Bsp/Readers/Quake2BspReader.cs
@@ -0,0 +1,103 @@
+using System;
+using System.IO;
+using Sledge.Formats.Bsp.Lumps;
+
+namespace Sledge.Formats.Bsp.Readers
+{
+ public class Quake2BspReader : IBspReader
+ {
+ public Version SupportedVersion => Version.Quake2;
+ public int NumLumps => (int) Lump.NumLumps;
+
+ public void StartHeader(BspFile file, BinaryReader br)
+ {
+ //
+ }
+
+ public Blob ReadBlob(BinaryReader br)
+ {
+ return new Blob
+ {
+ Offset = br.ReadInt32(),
+ Length = br.ReadInt32()
+ };
+ }
+
+ public void EndHeader(BspFile file, BinaryReader br)
+ {
+ //
+ }
+
+ public ILump GetLump(Blob blob)
+ {
+ switch ((Lump) blob.Index)
+ {
+ case Lump.Entities:
+ return new Entities();
+ case Lump.Planes:
+ return new Planes();
+ case Lump.Vertices:
+ return new Vertices();
+ case Lump.Visibility:
+ return new Visibility();
+ case Lump.Nodes:
+ return new Nodes();
+ case Lump.Texinfo:
+ return new Texinfo();
+ case Lump.Faces:
+ return new Faces();
+ case Lump.Lighting:
+ return new Lightmaps();
+ case Lump.Leaves:
+ return new Leaves();
+ case Lump.LeafFaces:
+ return new LeafFaces();
+ case Lump.LeafBrushes:
+ return new LeafBrushes();
+ case Lump.Edges:
+ return new Edges();
+ case Lump.Surfedges:
+ return new Surfedges();
+ case Lump.Models:
+ return new Models();
+ case Lump.Brushes:
+ return new Brushes();
+ case Lump.BrushSides:
+ return new BrushSides();
+ case Lump.Pop:
+ return new Pop();
+ case Lump.Areas:
+ return new Areas();
+ case Lump.AreaPortals:
+ return new AreaPortals();
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ private enum Lump : int
+ {
+ Entities = 0,
+ Planes = 1,
+ Vertices = 2,
+ Visibility = 3,
+ Nodes = 4,
+ Texinfo = 5,
+ Faces = 6,
+ Lighting = 7,
+ Leaves = 8,
+ LeafFaces = 9,
+ LeafBrushes = 10,
+ Edges = 11,
+ Surfedges = 12,
+ Models = 13,
+ Brushes = 14,
+ BrushSides = 15,
+ Pop = 16,
+ Areas = 17,
+ AreaPortals = 18,
+
+ NumLumps = 19
+ }
+ }
+}
diff --git a/Sledge.Formats.Bsp/Sledge.Formats.Bsp.csproj b/Sledge.Formats.Bsp/Sledge.Formats.Bsp.csproj
new file mode 100644
index 0000000..7cbcf46
--- /dev/null
+++ b/Sledge.Formats.Bsp/Sledge.Formats.Bsp.csproj
@@ -0,0 +1,11 @@
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
diff --git a/Sledge.Formats.Bsp/Version.cs b/Sledge.Formats.Bsp/Version.cs
new file mode 100644
index 0000000..a2e008e
--- /dev/null
+++ b/Sledge.Formats.Bsp/Version.cs
@@ -0,0 +1,22 @@
+namespace Sledge.Formats.Bsp
+{
+ public enum Version : ulong
+ {
+ // No magic
+ Quake1 = 29,
+ Goldsource = 30,
+ Bsp2 = ('2' << 24) + ('P' << 16) + ('S' << 8) + 'B',
+ Bsp2Rmqe = ('P' << 24) + ('S' << 16) + ('B' << 8) + '2',
+
+ // IBSP
+ Quake2 = (Magic.Ibsp << 32) + 38L,
+ Quake3 = (Magic.Ibsp << 32) + 46L,
+
+ // VBSP
+ Source17 = (17L << 32) + Magic.Vbsp,
+ Source18 = (18L << 32) + Magic.Vbsp,
+ Source2004 = (19L << 32) + Magic.Vbsp,
+ Source2007 = (20L << 32) + Magic.Vbsp,
+ Source2013 = (21L << 32) + Magic.Vbsp,
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Bsp/Writers/GoldsourceBspWriter.cs b/Sledge.Formats.Bsp/Writers/GoldsourceBspWriter.cs
new file mode 100644
index 0000000..aa3e4d1
--- /dev/null
+++ b/Sledge.Formats.Bsp/Writers/GoldsourceBspWriter.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Sledge.Formats.Bsp.Lumps;
+
+namespace Sledge.Formats.Bsp.Writers
+{
+ public class GoldsourceBspWriter : IBspWriter
+ {
+ public Version SupportedVersion => Version.Goldsource;
+
+ public void SeekToFirstLump(BinaryWriter bw)
+ {
+ const int headerSize = 4 + 15 * (4 + 4);
+ bw.Seek(headerSize, SeekOrigin.Current);
+ }
+
+ public void WriteHeader(BspFile file, IEnumerable blobs, BinaryWriter bw)
+ {
+ bw.Write((int) Version.Goldsource);
+ foreach (var blob in blobs)
+ {
+ bw.Write(blob.Offset);
+ bw.Write(blob.Length);
+ }
+ }
+
+ public IEnumerable GetLumps(BspFile bsp)
+ {
+ var types = new[]
+ {
+ typeof(Entities),
+ typeof(Planes),
+ typeof(Textures),
+ typeof(Vertices),
+ typeof(Visibility),
+ typeof(Nodes),
+ typeof(Texinfo),
+ typeof(Faces),
+ typeof(Lightmaps),
+ typeof(Clipnodes),
+ typeof(Leaves),
+ typeof(LeafFaces),
+ typeof(Edges),
+ typeof(Surfedges),
+ typeof(Models),
+ };
+
+ return types
+ .Select(x => bsp.Lumps.FirstOrDefault(l => l.GetType() == x) ?? Activator.CreateInstance(x))
+ .OfType();
+ }
+ }
+}
diff --git a/Sledge.Formats.Bsp/Writers/IBspWriter.cs b/Sledge.Formats.Bsp/Writers/IBspWriter.cs
new file mode 100644
index 0000000..a043a79
--- /dev/null
+++ b/Sledge.Formats.Bsp/Writers/IBspWriter.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Sledge.Formats.Bsp.Lumps;
+
+namespace Sledge.Formats.Bsp.Writers
+{
+ public interface IBspWriter
+ {
+ Version SupportedVersion { get; }
+
+ void SeekToFirstLump(BinaryWriter bw);
+ void WriteHeader(BspFile file, IEnumerable blobs, BinaryWriter bw);
+
+ IEnumerable GetLumps(BspFile bsp);
+ }
+}
diff --git a/Sledge.Formats.Bsp/Writers/Quake1BspWriter.cs b/Sledge.Formats.Bsp/Writers/Quake1BspWriter.cs
new file mode 100644
index 0000000..40b8b12
--- /dev/null
+++ b/Sledge.Formats.Bsp/Writers/Quake1BspWriter.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Sledge.Formats.Bsp.Lumps;
+
+namespace Sledge.Formats.Bsp.Writers
+{
+ public class Quake1BspWriter : IBspWriter
+ {
+ public Version SupportedVersion => Version.Quake1;
+
+ public void SeekToFirstLump(BinaryWriter bw)
+ {
+ const int headerSize = 4 + 15 * (4 + 4);
+ bw.Seek(headerSize, SeekOrigin.Current);
+ }
+
+ public void WriteHeader(BspFile file, IEnumerable blobs, BinaryWriter bw)
+ {
+ bw.Write((int) Version.Goldsource);
+ foreach (var blob in blobs)
+ {
+ bw.Write(blob.Offset);
+ bw.Write(blob.Length);
+ }
+ }
+
+ public IEnumerable GetLumps(BspFile bsp)
+ {
+ var types = new[]
+ {
+ typeof(Entities),
+ typeof(Planes),
+ typeof(Textures),
+ typeof(Vertices),
+ typeof(Visibility),
+ typeof(Nodes),
+ typeof(Texinfo),
+ typeof(Faces),
+ typeof(Lightmaps),
+ typeof(Clipnodes),
+ typeof(Leaves),
+ typeof(LeafFaces),
+ typeof(Edges),
+ typeof(Surfedges),
+ typeof(Models),
+ };
+
+ return types
+ .Select(x => bsp.Lumps.FirstOrDefault(l => l.GetType() == x) ?? Activator.CreateInstance(x))
+ .OfType();
+ }
+ }
+}
diff --git a/Sledge.Formats.Map.Tests/Formats/TestHammerFormat.cs b/Sledge.Formats.Map.Tests/Formats/TestHammerFormat.cs
new file mode 100644
index 0000000..7e4311d
--- /dev/null
+++ b/Sledge.Formats.Map.Tests/Formats/TestHammerFormat.cs
@@ -0,0 +1,31 @@
+using System;
+using System.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Sledge.Formats.Map.Formats;
+
+namespace Sledge.Formats.Map.Tests.Formats
+{
+ [TestClass]
+ public class TestHammerFormat
+ {
+ [TestMethod]
+ public void TestVmfFormatLoading()
+ {
+ var format = new HammerVmfFormat();
+ foreach (var file in Directory.GetFiles(@"D:\Downloads\formats\vmf"))
+ {
+ using (var r = File.OpenRead(file))
+ {
+ try
+ {
+ format.Read(r);
+ }
+ catch (Exception ex)
+ {
+ Assert.Fail($"Unable to read file: {Path.GetFileName(file)}. {ex.Message}");
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map.Tests/Formats/TestQuakeFormat.cs b/Sledge.Formats.Map.Tests/Formats/TestQuakeFormat.cs
new file mode 100644
index 0000000..83117c0
--- /dev/null
+++ b/Sledge.Formats.Map.Tests/Formats/TestQuakeFormat.cs
@@ -0,0 +1,31 @@
+using System;
+using System.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Sledge.Formats.Map.Formats;
+
+namespace Sledge.Formats.Map.Tests.Formats
+{
+ [TestClass]
+ public class TestQuakeFormat
+ {
+ [TestMethod]
+ public void TestMapFormatLoading()
+ {
+ var format = new QuakeMapFormat();
+ foreach (var file in Directory.GetFiles(@"D:\Downloads\formats\map"))
+ {
+ using (var r = File.OpenRead(file))
+ {
+ try
+ {
+ format.Read(r);
+ }
+ catch (Exception ex)
+ {
+ Assert.Fail($"Unable to read file: {Path.GetFileName(file)}. {ex.Message}");
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map.Tests/Formats/TestWorldcraftFormat.cs b/Sledge.Formats.Map.Tests/Formats/TestWorldcraftFormat.cs
new file mode 100644
index 0000000..0f24780
--- /dev/null
+++ b/Sledge.Formats.Map.Tests/Formats/TestWorldcraftFormat.cs
@@ -0,0 +1,31 @@
+using System;
+using System.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Sledge.Formats.Map.Formats;
+
+namespace Sledge.Formats.Map.Tests.Formats
+{
+ [TestClass]
+ public class TestWorldcraftFormat
+ {
+ [TestMethod]
+ public void TestRmfFormatLoading()
+ {
+ var format = new WorldcraftRmfFormat();
+ foreach (var file in Directory.GetFiles(@"D:\Downloads\formats\rmf"))
+ {
+ using (var r = File.OpenRead(file))
+ {
+ try
+ {
+ format.Read(r);
+ }
+ catch (Exception ex)
+ {
+ Assert.Fail($"Unable to read file: {Path.GetFileName(file)}. {ex.Message}");
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map.Tests/Sledge.Formats.Map.Tests.csproj b/Sledge.Formats.Map.Tests/Sledge.Formats.Map.Tests.csproj
new file mode 100644
index 0000000..0abf930
--- /dev/null
+++ b/Sledge.Formats.Map.Tests/Sledge.Formats.Map.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netcoreapp2.2
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sledge.Formats.Map/Formats/HammerVmfFormat.cs b/Sledge.Formats.Map/Formats/HammerVmfFormat.cs
new file mode 100644
index 0000000..c41bfcd
--- /dev/null
+++ b/Sledge.Formats.Map/Formats/HammerVmfFormat.cs
@@ -0,0 +1,293 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using Sledge.Formats.Map.Formats.VmfObjects;
+using Sledge.Formats.Map.Objects;
+using Sledge.Formats.Valve;
+
+namespace Sledge.Formats.Map.Formats
+{
+ public class HammerVmfFormat : IMapFormat
+ {
+ public string Name => "Hammer VMF";
+ public string Description => "The .vmf file format used by Valve Hammer Editor 4.";
+ public string ApplicationName => "Hammer";
+ public string Extension => "vmf";
+ public string[] AdditionalExtensions => new[] { "vmx" };
+ public string[] SupportedStyleHints => new string[0];
+
+ private readonly SerialisedObjectFormatter _formatter;
+
+ public HammerVmfFormat()
+ {
+ _formatter = new SerialisedObjectFormatter();
+ }
+
+ public MapFile Read(Stream stream)
+ {
+ var map = new MapFile();
+
+ var objs = new List();
+ foreach (var so in _formatter.Deserialize(stream))
+ {
+ switch (so.Name?.ToLower())
+ {
+ case "visgroups":
+ LoadVisgroups(map, so);
+ break;
+ case "cameras":
+ LoadCameras(map, so);
+ break;
+ case "world":
+ case "entity":
+ objs.Add(so);
+ break;
+ default:
+ map.AdditionalObjects.Add(so);
+ break;
+ }
+ }
+ LoadWorld(map, objs);
+
+ return map;
+ }
+
+ #region Read
+
+ private void LoadWorld(MapFile map, List objects)
+ {
+ var vos = objects.Select(VmfObject.Deserialise).Where(vo => vo != null).ToList();
+ var world = vos.OfType().FirstOrDefault() ?? new VmfWorld(new SerialisedObject("world"));
+
+ // A map of loaded object -> vmf id
+ var mapToSource = new Dictionary();
+ world.Editor.Apply(map.Worldspawn);
+ mapToSource.Add(map.Worldspawn, world.ID);
+
+ map.Worldspawn.ClassName = world.ClassName;
+ map.Worldspawn.SpawnFlags = world.SpawnFlags;
+ foreach (var wp in world.Properties) map.Worldspawn.Properties[wp.Key] = wp.Value;
+
+ var tree = new List();
+
+ foreach (var vo in vos)
+ {
+ if (vo.Editor.ParentID == 0) vo.Editor.ParentID = world.ID;
+
+ // Flatten the tree (nested hiddens -> no more hiddens)
+ // (Flat tree includes self as well)
+ var flat = vo.Flatten().ToList();
+
+ // Set the default parent id for all the child objects
+ foreach (var child in flat)
+ {
+ if (child.Editor.ParentID == 0) child.Editor.ParentID = vo.ID;
+ }
+
+ // Add the objects to the tree
+ tree.AddRange(flat);
+ }
+
+ world.Editor.ParentID = 0;
+ tree.Remove(world);
+
+ // All objects should have proper ids by now, get rid of anything with parentid 0 just in case
+ var grouped = tree.GroupBy(x => x.Editor.ParentID).ToDictionary(x => x.Key, x => x.ToList());
+
+ // Step through each level of the tree and add them to their parent branches
+ var leaves = new List { map.Worldspawn };
+
+ // Use a iteration limit of 1000. If the tree's that deep, I don't want to load your map anyway...
+ for (var i = 0; i < 1000 && leaves.Any(); i++) // i.e. while (leaves.Any())
+ {
+ var newLeaves = new List();
+ foreach (var leaf in leaves)
+ {
+ var sourceId = mapToSource[leaf];
+ if (!grouped.ContainsKey(sourceId)) continue;
+
+ var items = grouped[sourceId];
+
+ // Create objects from items
+ foreach (var item in items)
+ {
+ var mapObject = item.ToMapObject();
+ mapToSource.Add(mapObject, item.ID);
+ leaf.Children.Add(mapObject);
+ newLeaves.Add(mapObject);
+ }
+ }
+ leaves = newLeaves;
+ }
+
+ // Now we should have a nice neat hierarchy of objects
+ }
+
+ private void LoadCameras(MapFile map, SerialisedObject so)
+ {
+ var activeCam = so.Get("activecamera", 0);
+
+ var cams = so.Children.Where(x => string.Equals(x.Name, "camera", StringComparison.InvariantCultureIgnoreCase)).ToList();
+ for (var i = 0; i < cams.Count; i++)
+ {
+ var cm = cams[i];
+ map.Cameras.Add(new Camera
+ {
+ EyePosition = cm.Get("position", Vector3.Zero),
+ LookPosition = cm.Get("look", Vector3.UnitX),
+ IsActive = activeCam == i
+ });
+ }
+ }
+
+ private void LoadVisgroups(MapFile map, SerialisedObject so)
+ {
+ var vis = new Visgroup();
+ LoadVisgroupsRecursive(so, vis);
+ map.Visgroups.AddRange(vis.Children);
+ }
+
+ private void LoadVisgroupsRecursive(SerialisedObject so, Visgroup parent)
+ {
+ foreach (var vg in so.Children.Where(x => string.Equals(x.Name, "visgroup", StringComparison.InvariantCultureIgnoreCase)))
+ {
+ var v = new Visgroup
+ {
+ Name = vg.Get("name", ""),
+ ID = vg.Get("visgroupid", -1),
+ Color = vg.GetColor("color"),
+ Visible = true
+ };
+ LoadVisgroupsRecursive(vg, v);
+ parent.Children.Add(v);
+ }
+ }
+
+ #endregion
+
+ public void Write(Stream stream, MapFile map, string styleHint)
+ {
+ var list = new List();
+
+ list.AddRange(map.AdditionalObjects);
+
+ var visObj = new SerialisedObject("visgroups");
+ SaveVisgroups(map.Visgroups, visObj);
+ list.Add(visObj);
+
+ SaveWorld(map, list);
+ SaveCameras(map, list);
+
+ _formatter.Serialize(stream, list);
+ }
+
+ #region Write
+
+ private static string FormatVector3(Vector3 c)
+ {
+ return $"{FormatDecimal(c.X)} {FormatDecimal(c.Y)} {FormatDecimal(c.Z)}";
+ }
+
+ private static string FormatDecimal(float d)
+ {
+ return d.ToString("0.00####", CultureInfo.InvariantCulture);
+ }
+
+ private void SaveVisgroups(IEnumerable visgroups, SerialisedObject parent)
+ {
+ foreach (var visgroup in visgroups)
+ {
+ var vgo = new SerialisedObject("visgroup");
+ vgo.Set("visgroupid", visgroup.ID);
+ vgo.SetColor("color", visgroup.Color);
+ SaveVisgroups(visgroup.Children, vgo);
+ parent.Children.Add(vgo);
+ }
+ }
+
+ private void SaveWorld(MapFile map, List list)
+ {
+ // call the avengers
+
+ var id = 1;
+ var idMap = map.Worldspawn.FindAll().ToDictionary(x => x, x => id++);
+
+ // Get the world, groups, and non-entity solids
+ var vmfWorld = new VmfWorld(map.Worldspawn);
+ var worldObj = vmfWorld.ToSerialisedObject();
+ SerialiseWorldspawnChildren(map.Worldspawn, worldObj, idMap, 0, map.Worldspawn.Children);
+ list.Add(worldObj);
+
+ // Entities are separate from the world
+ var entities = map.Worldspawn.FindAll().OfType().Where(x => x != map.Worldspawn).Select(x => SerialiseEntity(x, idMap)).ToList();
+ list.AddRange(entities);
+ }
+
+ private void SerialiseWorldspawnChildren(Worldspawn worldspawn, SerialisedObject worldObj, Dictionary idMap, int groupId, List list)
+ {
+ foreach (var c in list)
+ {
+ var cid = idMap[c];
+ switch (c)
+ {
+ case Entity _:
+ // Ignore everything underneath an entity
+ break;
+ case Group g:
+ var sg = new VmfGroup(g, cid);
+ if (groupId != 0) sg.Editor.GroupID = groupId;
+ worldObj.Children.Add(sg.ToSerialisedObject());
+ SerialiseWorldspawnChildren(worldspawn, worldObj, idMap, cid, g.Children);
+ break;
+ case Solid s:
+ var ss = new VmfSolid(s, cid);
+ if (groupId != 0) ss.Editor.GroupID = groupId;
+ worldObj.Children.Add(ss.ToSerialisedObject());
+ break;
+ }
+ }
+ }
+
+ private SerialisedObject SerialiseEntity(MapObject obj, Dictionary idMap)
+ {
+ var self = VmfObject.Serialise(obj, idMap[obj]);
+ if (self == null) return null;
+
+ var so = self.ToSerialisedObject();
+
+ foreach (var solid in obj.FindAll().OfType())
+ {
+ var s = VmfObject.Serialise(solid, idMap[obj]);
+ if (s != null) so.Children.Add(s.ToSerialisedObject());
+ }
+
+ return so;
+ }
+
+ private void SaveCameras(MapFile map, List list)
+ {
+ var cams = map.Cameras;
+
+ var so = new SerialisedObject("cameras");
+ so.Set("activecamera", -1);
+
+ for (var i = 0; i < cams.Count; i++)
+ {
+ var camera = cams[i];
+ if (camera.IsActive) so.Set("activecamera", i);
+
+ var vgo = new SerialisedObject("camera");
+ vgo.Set("position", $"[{FormatVector3(camera.EyePosition)}]");
+ vgo.Set("look", $"[{FormatVector3(camera.LookPosition)}]");
+ so.Children.Add(vgo);
+ }
+
+ list.Add(so);
+ }
+
+ #endregion
+ }
+}
diff --git a/Sledge.Formats.Map/Formats/IMapFormat.cs b/Sledge.Formats.Map/Formats/IMapFormat.cs
new file mode 100644
index 0000000..7611012
--- /dev/null
+++ b/Sledge.Formats.Map/Formats/IMapFormat.cs
@@ -0,0 +1,55 @@
+using System.IO;
+
+namespace Sledge.Formats.Map.Formats
+{
+ ///
+ /// A map format class
+ ///
+ public interface IMapFormat
+ {
+ ///
+ /// A short English name for the format.
+ ///
+ string Name { get; }
+
+ ///
+ /// A brief description of the format.
+ ///
+ string Description { get; }
+
+ ///
+ /// The name of the primary (or earliest) application to use this format.
+ ///
+ string ApplicationName { get; }
+
+ ///
+ /// The most common extension (without leading dot) for this format.
+ ///
+ string Extension { get; }
+
+ ///
+ /// Common additional extensions (without leading dot) this format can be found in.
+ ///
+ string[] AdditionalExtensions { get; }
+
+ ///
+ /// A list of style hints supported by this format.
+ ///
+ string[] SupportedStyleHints { get; }
+
+ ///
+ /// Read a map from a stream. This method will not close or dispose the stream.
+ ///
+ /// Seekable stream
+ /// Loaded map
+ Objects.MapFile Read(Stream stream);
+
+ ///
+ /// Write a map to a stream. This method will not close or dispose the stream.
+ ///
+ /// Stream to write to
+ /// The map to save
+ /// The style hint to apply
+ void Write(Stream stream, Objects.MapFile map, string styleHint);
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Formats/MapFormatExtensions.cs b/Sledge.Formats.Map/Formats/MapFormatExtensions.cs
new file mode 100644
index 0000000..187a8df
--- /dev/null
+++ b/Sledge.Formats.Map/Formats/MapFormatExtensions.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Sledge.Formats.Map.Objects;
+
+namespace Sledge.Formats.Map.Formats
+{
+ public static class MapFormatExtensions
+ {
+ public static MapFile ReadFromFile(this IMapFormat mapFormat, string fileName)
+ {
+ using (var fo = File.OpenRead(fileName))
+ {
+ return mapFormat.Read(fo);
+ }
+ }
+ }
+}
diff --git a/Sledge.Formats.Map/Formats/QuakeMapFormat.cs b/Sledge.Formats.Map/Formats/QuakeMapFormat.cs
new file mode 100644
index 0000000..a2bf9eb
--- /dev/null
+++ b/Sledge.Formats.Map/Formats/QuakeMapFormat.cs
@@ -0,0 +1,437 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using Sledge.Formats.Map.Objects;
+
+namespace Sledge.Formats.Map.Formats
+{
+ /* Quake format
+ * {
+ * "classname" "worldspawn"
+ * "key" "value"
+ * "spawnflags" "0"
+ * {
+ * // idTech2:
+ * ( x y z ) ( x y z ) ( x y z ) texturename xshift yshift rotation xscale yscale
+ * // idTech3:
+ * ( x y z ) ( x y z ) ( x y z ) shadername xshift yshift rotation xscale yscale contentflags surfaceflags value
+ * // Worldcraft:
+ * ( x y z ) ( x y z ) ( x y z ) texturename [ ux uy uz xshift ] [ vx vy vz yshift ] rotation xscale yscale
+ * }
+ * }
+ * {
+ * "spawnflags" "0"
+ * "classname" "entityname"
+ * "key" "value"
+ * }
+ * {
+ * "spawnflags" "0"
+ * "classname" "entityname"
+ * "key" "value"
+ * {
+ * ( x y z ) ( x y z ) ( x y z ) texturename xoff yoff rot xscale yscale
+ * }
+ * }
+ * {
+ * patchDef2 // idTech3 ONLY
+ * {
+ * shadername
+ * ( width height 0 0 0 )
+ * (
+ * ( ( x y z u v ) ... ( x y z u v ) )
+ * )
+ * }
+ * }
+ * }
+ * {
+ * brushDef // idTech3 ONLY
+ * {
+ * ( x y z ) ( x y z ) ( x y z ) ( ( ux uy uz ) ( vx vy vz ) ) shadername contentflags surfaceflags value
+ * }
+ * }
+ * {
+ * brushDef3 // idTech4 ONLY
+ * {
+ * ?
+ * }
+ * }
+ * {
+ * patchDef3 // idTech4 ONLY
+ * {
+ * ?
+ * }
+ * }
+ */
+ public class QuakeMapFormat : IMapFormat
+ {
+ public string Name => "Quake Map";
+ public string Description => "The .map file format used for most Quake editors.";
+ public string ApplicationName => "Radiant";
+ public string Extension => "map";
+ public string[] AdditionalExtensions => new[] { "max" };
+ public string[] SupportedStyleHints => new[] { "idTech2", "idTech3", "idTech4", "Worldcraft" };
+
+ public MapFile Read(Stream stream)
+ {
+ var map = new MapFile();
+ using (var rdr = new StreamReader(stream, Encoding.ASCII, true, 1024, true))
+ {
+ ReadEntities(rdr, map);
+ }
+ return map;
+ }
+
+ #region Read
+
+ private static string CleanLine(string line)
+ {
+ if (line == null) return null;
+ var ret = line;
+ if (ret.Contains("//")) ret = ret.Substring(0, ret.IndexOf("//", StringComparison.Ordinal)); // Comments
+ return ret.Trim();
+ }
+
+ private static void ReadEntities(StreamReader rdr, MapFile map)
+ {
+ string line;
+ while ((line = CleanLine(rdr.ReadLine())) != null)
+ {
+ if (string.IsNullOrWhiteSpace(line)) continue;
+ if (line == "{") ReadEntity(rdr, map);
+ }
+ }
+
+ private static void ReadEntity(StreamReader rdr, MapFile map)
+ {
+ var e = new Entity();
+
+ string line;
+ while ((line = CleanLine(rdr.ReadLine())) != null)
+ {
+ if (string.IsNullOrWhiteSpace(line)) continue;
+ if (line[0] == '"')
+ {
+ ReadProperty(e, line);
+ }
+ else if (line[0] == '{')
+ {
+ var s = ReadSolid(rdr);
+ if (s != null) e.Children.Add(s);
+ }
+ else if (line[0] == '}')
+ {
+ break;
+ }
+ }
+
+ if (e.ClassName == "worldspawn")
+ {
+ map.Worldspawn.SpawnFlags = e.SpawnFlags;
+ foreach (var p in e.Properties) map.Worldspawn.Properties[p.Key] = p.Value;
+ map.Worldspawn.Children.AddRange(e.Children);
+ }
+ else
+ {
+ map.Worldspawn.Children.Add(e);
+ }
+ }
+
+ private static void ReadProperty(Entity ent, string line)
+ {
+ // Quake id1 map sources use tabs between keys and values
+ var split = line.Split(' ', '\t');
+ var key = split[0].Trim('"');
+
+ var val = string.Join(" ", split.Skip(1)).Trim('"');
+
+ if (key == "classname")
+ {
+ ent.ClassName = val;
+ }
+ else if (key == "spawnflags")
+ {
+ ent.SpawnFlags = int.Parse(val);
+ }
+ else
+ {
+ ent.Properties[key] = val;
+ }
+ }
+
+ private static Solid ReadSolid(StreamReader rdr)
+ {
+ var s = new Solid();
+
+ string line;
+ while ((line = CleanLine(rdr.ReadLine())) != null)
+ {
+ if (string.IsNullOrWhiteSpace(line)) continue;
+
+ switch (line)
+ {
+ case "}":
+ s.ComputeVertices();
+ return s;
+ case "patchDef2":
+ case "brushDef":
+ Util.Assert(false, "idTech3 format maps are currently not supported.");
+ break;
+ case "patchDef3":
+ case "brushDef3":
+ Util.Assert(false, "idTech4 format maps are currently not supported.");
+ break;
+ default:
+ s.Faces.Add(ReadFace(line));
+ break;
+ }
+ }
+ return null;
+ }
+
+ private static Face ReadFace(string line)
+ {
+ const NumberStyles ns = NumberStyles.Float;
+
+ var parts = line.Split(' ').ToList();
+
+ Util.Assert(parts[0] == "(");
+ Util.Assert(parts[4] == ")");
+ Util.Assert(parts[5] == "(");
+ Util.Assert(parts[9] == ")");
+ Util.Assert(parts[10] == "(");
+ Util.Assert(parts[14] == ")");
+
+ var a = NumericsExtensions.Parse(parts[1], parts[2], parts[3], ns, CultureInfo.InvariantCulture);
+ var b = NumericsExtensions.Parse(parts[6], parts[7], parts[8], ns, CultureInfo.InvariantCulture);
+ var c = NumericsExtensions.Parse(parts[11], parts[12], parts[13], ns, CultureInfo.InvariantCulture);
+
+ var ab = b - a;
+ var ac = c - a;
+
+ var normal = ac.Cross(ab).Normalise();
+ var d = normal.Dot(a);
+
+ var face = new Face()
+ {
+ Plane = new Plane(normal, d),
+ TextureName = parts[15]
+ };
+
+ // idTech2, idTech3
+ if (parts.Count == 21 || parts.Count == 24)
+ {
+ var direction = ClosestAxisToNormal(face.Plane);
+ face.UAxis = direction == Vector3.UnitX ? Vector3.UnitY : Vector3.UnitX;
+ face.VAxis = direction == Vector3.UnitZ ? -Vector3.UnitY : -Vector3.UnitZ;
+
+ var xshift = float.Parse(parts[16], ns, CultureInfo.InvariantCulture);
+ var yshift = float.Parse(parts[17], ns, CultureInfo.InvariantCulture);
+ var rotate = float.Parse(parts[18], ns, CultureInfo.InvariantCulture);
+ var xscale = float.Parse(parts[19], ns, CultureInfo.InvariantCulture);
+ var yscale = float.Parse(parts[20], ns, CultureInfo.InvariantCulture);
+
+ face.Rotation = rotate;
+ face.XScale = xscale;
+ face.YScale = yscale;
+ face.XShift = xshift;
+ face.YShift = yshift;
+
+ // idTech3
+ if (parts.Count == 24)
+ {
+ face.ContentFlags = int.Parse(parts[18], CultureInfo.InvariantCulture);
+ face.SurfaceFlags = int.Parse(parts[19], CultureInfo.InvariantCulture);
+ face.Value = float.Parse(parts[20], ns, CultureInfo.InvariantCulture);
+ }
+ }
+ // Worldcraft
+ else if (parts.Count == 31)
+ {
+ Util.Assert(parts[16] == "[");
+ Util.Assert(parts[21] == "]");
+ Util.Assert(parts[22] == "[");
+ Util.Assert(parts[27] == "]");
+
+ face.UAxis = NumericsExtensions.Parse(parts[17], parts[18], parts[19], ns, CultureInfo.InvariantCulture);
+ face.XShift = float.Parse(parts[20], ns, CultureInfo.InvariantCulture);
+ face.VAxis = NumericsExtensions.Parse(parts[23], parts[24], parts[25], ns, CultureInfo.InvariantCulture);
+ face.YShift = float.Parse(parts[26], ns, CultureInfo.InvariantCulture);
+ face.Rotation = float.Parse(parts[28], ns, CultureInfo.InvariantCulture);
+ face.XScale = float.Parse(parts[29], ns, CultureInfo.InvariantCulture);
+ face.YScale = float.Parse(parts[30], ns, CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ Util.Assert(false, $"Unknown number of tokens ({parts.Count}) in face definition.");
+ }
+
+ return face;
+ }
+
+ private static Vector3 ClosestAxisToNormal(Plane plane)
+ {
+ var norm = plane.Normal.Absolute();
+ if (norm.Z >= norm.X && norm.Z >= norm.Y) return Vector3.UnitZ;
+ if (norm.X >= norm.Y) return Vector3.UnitX;
+ return Vector3.UnitY;
+ }
+
+ #endregion
+
+ public void Write(Stream stream, MapFile map, string styleHint)
+ {
+ using (var sw = new StreamWriter(stream, Encoding.ASCII, 1024, true))
+ {
+ WriteWorld(sw, map.Worldspawn, styleHint);
+ }
+ }
+
+ #region Writing
+
+
+ private static string FormatVector3(Vector3 c)
+ {
+ return $"{c.X.ToString("0.000", CultureInfo.InvariantCulture)} {c.Y.ToString("0.000", CultureInfo.InvariantCulture)} {c.Z.ToString("0.000", CultureInfo.InvariantCulture)}";
+ }
+
+ private static void CollectNonEntitySolids(List solids, MapObject parent)
+ {
+ foreach (var obj in parent.Children)
+ {
+ switch (obj)
+ {
+ case Solid s:
+ solids.Add(s);
+ break;
+ case Group _:
+ CollectNonEntitySolids(solids, obj);
+ break;
+ }
+ }
+ }
+
+ private static void CollectEntities(List entities, MapObject parent)
+ {
+ foreach (var obj in parent.Children)
+ {
+ switch (obj)
+ {
+ case Entity e:
+ entities.Add(e);
+ break;
+ case Group _:
+ CollectEntities(entities, obj);
+ break;
+ }
+ }
+ }
+
+ private static void WriteFace(StreamWriter sw, Face face, string styleHint)
+ {
+ // ( -128 64 64 ) ( -64 64 64 ) ( -64 0 64 ) AAATRIGGER [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
+ var strings = face.Vertices.Take(3).Select(x => "( " + FormatVector3(x) + " )").ToList();
+ strings.Add(String.IsNullOrWhiteSpace(face.TextureName) ? "NULL" : face.TextureName);
+ switch (styleHint)
+ {
+ case "idTech2":
+ strings.Add("[");
+ strings.Add(face.XShift.ToString("0.000", CultureInfo.InvariantCulture));
+ strings.Add(face.YShift.ToString("0.000", CultureInfo.InvariantCulture));
+ strings.Add(face.Rotation.ToString("0.000", CultureInfo.InvariantCulture));
+ strings.Add(face.XScale.ToString("0.000", CultureInfo.InvariantCulture));
+ strings.Add(face.YScale.ToString("0.000", CultureInfo.InvariantCulture));
+ break;
+ case "idTech3":
+ Util.Assert(false, "idTech3 format maps are currently not supported.");
+ break;
+ case "idTech4":
+ Util.Assert(false, "idTech4 format maps are currently not supported.");
+ break;
+ case "Worldcraft":
+ default:
+ strings.Add("[");
+ strings.Add(FormatVector3(face.UAxis));
+ strings.Add(face.XShift.ToString("0.000", CultureInfo.InvariantCulture));
+ strings.Add("]");
+ strings.Add("[");
+ strings.Add(FormatVector3(face.VAxis));
+ strings.Add(face.YShift.ToString("0.000", CultureInfo.InvariantCulture));
+ strings.Add("]");
+ strings.Add(face.Rotation.ToString("0.000", CultureInfo.InvariantCulture));
+ strings.Add(face.XScale.ToString("0.000", CultureInfo.InvariantCulture));
+ strings.Add(face.YScale.ToString("0.000", CultureInfo.InvariantCulture));
+ break;
+ }
+
+ sw.WriteLine(String.Join(" ", strings));
+ }
+
+ private static void WriteSolid(StreamWriter sw, Solid solid, string styleHint)
+ {
+ sw.WriteLine("{");
+ foreach (var face in solid.Faces)
+ {
+ WriteFace(sw, face, styleHint);
+ }
+ sw.WriteLine("}");
+ }
+
+ private static void WriteProperty(StreamWriter sw, string key, string value)
+ {
+ sw.WriteLine('"' + key + "\" \"" + value + '"');
+ }
+
+ private static void WriteEntity(StreamWriter sw, Entity ent, string styleHint)
+ {
+ var solids = new List();
+ CollectNonEntitySolids(solids, ent);
+ WriteEntityWithSolids(sw, ent, solids, styleHint);
+ }
+
+ private static void WriteEntityWithSolids(StreamWriter sw, Entity e, IEnumerable solids, string styleHint)
+ {
+ sw.WriteLine("{");
+
+ WriteProperty(sw, "classname", e.ClassName);
+
+ if (e.SpawnFlags != 0)
+ {
+ WriteProperty(sw, "spawnflags", e.SpawnFlags.ToString(CultureInfo.InvariantCulture));
+ }
+
+ foreach (var prop in e.Properties)
+ {
+ WriteProperty(sw, prop.Key, prop.Value);
+ }
+
+ foreach (var s in solids)
+ {
+ WriteSolid(sw, s, styleHint);
+ }
+
+ sw.WriteLine("}");
+ }
+
+ private void WriteWorld(StreamWriter sw, Worldspawn world, string styleHint)
+ {
+ var solids = new List();
+ var entities = new List();
+
+ CollectNonEntitySolids(solids, world);
+ CollectEntities(entities, world);
+
+ WriteEntityWithSolids(sw, world, solids, styleHint);
+
+ foreach (var entity in entities)
+ {
+ WriteEntity(sw, entity, styleHint);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Sledge.Formats.Map/Formats/VmfObjects/VmfEditor.cs b/Sledge.Formats.Map/Formats/VmfObjects/VmfEditor.cs
new file mode 100644
index 0000000..a226f79
--- /dev/null
+++ b/Sledge.Formats.Map/Formats/VmfObjects/VmfEditor.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Globalization;
+using System.Linq;
+using Sledge.Formats.Map.Objects;
+using Sledge.Formats.Valve;
+
+namespace Sledge.Formats.Map.Formats.VmfObjects
+{
+ internal class VmfEditor
+ {
+ public Color Color { get; set; }
+ public List VisgroupIDs { get; set; }
+ public int GroupID { get; set; }
+ public int ParentID { get; set; }
+ public Dictionary Properties { get; set; }
+
+ public VmfEditor(SerialisedObject obj)
+ {
+ if (obj == null) obj = new SerialisedObject("editor");
+
+ Color = obj.GetColor("color");
+ ParentID = GroupID = obj.Get("groupid", 0);
+ Properties = new Dictionary();
+ VisgroupIDs = new List();
+
+ foreach (var kv in obj.Properties)
+ {
+ switch (kv.Key.ToLower())
+ {
+ case "visgroupid":
+ if (int.TryParse(kv.Value, out var id)) VisgroupIDs.Add(id);
+ break;
+ case "color":
+ case "groupid":
+ break;
+ default:
+ Properties[kv.Key] = kv.Value;
+ break;
+ }
+ }
+ }
+
+ public VmfEditor(MapObject obj, int groupId, int parentId)
+ {
+ Color = obj.Color;
+ VisgroupIDs = obj.Visgroups.ToList();
+ GroupID = groupId;
+ ParentID = parentId;
+ Properties = new Dictionary();
+ }
+
+ public void Apply(MapObject obj)
+ {
+ obj.Color = Color;
+ obj.Visgroups.AddRange(VisgroupIDs);
+ }
+
+ public SerialisedObject ToSerialisedObject()
+ {
+ var so = new SerialisedObject("editor");
+ so.SetColor("color", Color);
+ if (GroupID > 0) so.Set("groupid", GroupID);
+ foreach (var kv in Properties)
+ {
+ so.Set(kv.Key, kv.Value);
+ }
+ foreach (var id in VisgroupIDs.Distinct())
+ {
+ so.Properties.Add(new KeyValuePair("visgroupid", Convert.ToString(id, CultureInfo.InvariantCulture)));
+ }
+ return so;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Formats/VmfObjects/VmfEntity.cs b/Sledge.Formats.Map/Formats/VmfObjects/VmfEntity.cs
new file mode 100644
index 0000000..1447908
--- /dev/null
+++ b/Sledge.Formats.Map/Formats/VmfObjects/VmfEntity.cs
@@ -0,0 +1,81 @@
+using System.Collections.Generic;
+using System.Linq;
+using Sledge.Formats.Map.Objects;
+using Sledge.Formats.Valve;
+
+namespace Sledge.Formats.Map.Formats.VmfObjects
+{
+ internal class VmfEntity : VmfObject
+ {
+ public List Objects { get; set; }
+ public string ClassName { get; set; }
+ public int SpawnFlags { get; set; }
+ public Dictionary Properties { get; set; }
+
+ private static readonly string[] ExcludedKeys = { "id", "spawnflags", "classname" };
+
+ public VmfEntity(SerialisedObject obj) : base(obj)
+ {
+ Objects = new List();
+ foreach (var so in obj.Children)
+ {
+ var o = Deserialise(so);
+ if (o != null) Objects.Add(o);
+ }
+
+ Properties = new Dictionary();
+ foreach (var kv in obj.Properties)
+ {
+ if (kv.Key == null || ExcludedKeys.Contains(kv.Key.ToLower())) continue;
+ Properties[kv.Key] = kv.Value;
+ }
+ ClassName = obj.Get("classname", "");
+ SpawnFlags = obj.Get("spawnflags", 0);
+ }
+
+ public VmfEntity(Entity ent, int id) : base(ent, id)
+ {
+ Objects = new List();
+ ClassName = ent.ClassName;
+ SpawnFlags = ent.SpawnFlags;
+ Properties = new Dictionary(ent.Properties);
+ }
+
+ public override IEnumerable Flatten()
+ {
+ return Objects.SelectMany(x => x.Flatten()).Union(new[] { this });
+ }
+
+ public override MapObject ToMapObject()
+ {
+ var ent = new Entity
+ {
+ ClassName = ClassName,
+ SpawnFlags = SpawnFlags,
+ Properties = new Dictionary(Properties)
+ };
+
+ Editor.Apply(ent);
+
+ return ent;
+ }
+
+ protected virtual string SerialisedObjectName => "entity";
+
+ public override SerialisedObject ToSerialisedObject()
+ {
+ var so = new SerialisedObject(SerialisedObjectName);
+ so.Set("id", ID);
+ so.Set("classname", ClassName);
+ if (SpawnFlags > 0) so.Set("spawnflags", SpawnFlags);
+ foreach (var prop in Properties)
+ {
+ so.Properties.Add(new KeyValuePair(prop.Key, prop.Value));
+ }
+
+ so.Children.Add(Editor.ToSerialisedObject());
+
+ return so;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Formats/VmfObjects/VmfGroup.cs b/Sledge.Formats.Map/Formats/VmfObjects/VmfGroup.cs
new file mode 100644
index 0000000..d5071e9
--- /dev/null
+++ b/Sledge.Formats.Map/Formats/VmfObjects/VmfGroup.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using Sledge.Formats.Map.Objects;
+using Sledge.Formats.Valve;
+
+namespace Sledge.Formats.Map.Formats.VmfObjects
+{
+ internal class VmfGroup : VmfObject
+ {
+ public VmfGroup(SerialisedObject obj) : base(obj)
+ {
+ }
+
+ public VmfGroup(Group grp, int id) : base(grp, id)
+ {
+ }
+
+ public override IEnumerable Flatten()
+ {
+ yield return this;
+ }
+
+ public override MapObject ToMapObject()
+ {
+ var grp = new Group();
+ Editor.Apply(grp);
+ return grp;
+ }
+
+ public override SerialisedObject ToSerialisedObject()
+ {
+ var so = new SerialisedObject("group");
+ so.Set("id", ID);
+
+ so.Children.Add(Editor.ToSerialisedObject());
+
+ return so;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Formats/VmfObjects/VmfHidden.cs b/Sledge.Formats.Map/Formats/VmfObjects/VmfHidden.cs
new file mode 100644
index 0000000..79e32aa
--- /dev/null
+++ b/Sledge.Formats.Map/Formats/VmfObjects/VmfHidden.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Sledge.Formats.Map.Objects;
+using Sledge.Formats.Valve;
+
+namespace Sledge.Formats.Map.Formats.VmfObjects
+{
+ internal class VmfHidden : VmfObject
+ {
+ public List Objects { get; set; }
+
+ public VmfHidden(SerialisedObject obj) : base(obj)
+ {
+ Objects = new List();
+ foreach (var so in obj.Children)
+ {
+ var o = VmfObject.Deserialise(so);
+ if (o != null) Objects.Add(o);
+ }
+ }
+
+ public override IEnumerable Flatten()
+ {
+ return Objects.SelectMany(x => x.Flatten());
+ }
+
+ public override MapObject ToMapObject()
+ {
+ throw new NotSupportedException();
+ }
+
+ public override SerialisedObject ToSerialisedObject()
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Formats/VmfObjects/VmfObject.cs b/Sledge.Formats.Map/Formats/VmfObjects/VmfObject.cs
new file mode 100644
index 0000000..b21d83a
--- /dev/null
+++ b/Sledge.Formats.Map/Formats/VmfObjects/VmfObject.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using System.Linq;
+using Sledge.Formats.Map.Objects;
+using Sledge.Formats.Valve;
+
+namespace Sledge.Formats.Map.Formats.VmfObjects
+{
+ internal abstract class VmfObject
+ {
+ public int ID { get; set; }
+ public VmfEditor Editor { get; set; }
+
+ protected VmfObject(SerialisedObject obj)
+ {
+ ID = obj.Get("id", 0);
+ Editor = new VmfEditor(obj.Children.FirstOrDefault(x => x.Name == "editor"));
+ }
+
+ protected VmfObject(MapObject obj, int id)
+ {
+ ID = id;
+ Editor = new VmfEditor(obj, 0, 0);
+ }
+
+ public abstract IEnumerable Flatten();
+ public abstract MapObject ToMapObject();
+ public abstract SerialisedObject ToSerialisedObject();
+
+ public static VmfObject Deserialise(SerialisedObject obj)
+ {
+ switch (obj.Name)
+ {
+ case "world":
+ return new VmfWorld(obj);
+ case "entity":
+ return new VmfEntity(obj);
+ case "group":
+ return new VmfGroup(obj);
+ case "solid":
+ return new VmfSolid(obj);
+ case "hidden":
+ return new VmfHidden(obj);
+ default:
+ return null;
+ }
+ }
+
+ public static VmfObject Serialise(MapObject obj, int id)
+ {
+ switch (obj)
+ {
+ case Worldspawn r:
+ return new VmfWorld(r);
+ case Entity e:
+ return new VmfEntity(e, id);
+ case Group g:
+ return new VmfGroup(g, id);
+ case Solid s:
+ return new VmfSolid(s, id);
+ default:
+ return null;
+ }
+ }
+ }
+}
diff --git a/Sledge.Formats.Map/Formats/VmfObjects/VmfSide.cs b/Sledge.Formats.Map/Formats/VmfObjects/VmfSide.cs
new file mode 100644
index 0000000..d874ae8
--- /dev/null
+++ b/Sledge.Formats.Map/Formats/VmfObjects/VmfSide.cs
@@ -0,0 +1,86 @@
+using System.Globalization;
+using Sledge.Formats.Map.Objects;
+using Sledge.Formats.Valve;
+using System.Numerics;
+
+namespace Sledge.Formats.Map.Formats.VmfObjects
+{
+ internal class VmfSide
+ {
+ public long ID { get; set; }
+ public Face Face { get; set; }
+
+ public VmfSide(SerialisedObject obj)
+ {
+ ID = obj.Get("ID", 0L);
+ Face = new Face
+ {
+ TextureName = obj.Get("material", ""),
+ Rotation = obj.Get("rotation", 0f),
+ LightmapScale = obj.Get("lightmapscale", 0),
+ SmoothingGroups = obj.Get("smoothing_groups", "")
+ };
+ if (Util.ParseFloatArray(obj.Get("plane", ""), new[] { ' ', '(', ')' }, 9, out var pl))
+ {
+ Face.Plane = NumericsExtensions.PlaneFromVertices(
+ new Vector3(pl[0], pl[1], pl[2]).Round(),
+ new Vector3(pl[3], pl[4], pl[5]).Round(),
+ new Vector3(pl[6], pl[7], pl[8]).Round()
+ );
+ }
+ else
+ {
+ Face.Plane = new Plane(Vector3.UnitZ, 0);
+ }
+ if (Util.ParseFloatArray(obj.Get("uaxis", ""), new[] { ' ', '[', ']' }, 5, out float[] ua))
+ {
+ Face.UAxis = new Vector3(ua[0], ua[1], ua[2]);
+ Face.XShift = ua[3];
+ Face.XScale = ua[4];
+ }
+ if (Util.ParseFloatArray(obj.Get("vaxis", ""), new[] { ' ', '[', ']' }, 5, out float[] va))
+ {
+ Face.VAxis = new Vector3(va[0], va[1], va[2]);
+ Face.YShift = va[3];
+ Face.YScale = va[4];
+ }
+ }
+
+ public VmfSide(Face face, int id)
+ {
+ ID = id;
+ Face = face;
+ }
+
+ public Face ToFace()
+ {
+ return Face;
+ }
+
+ public SerialisedObject ToSerialisedObject()
+ {
+ var so = new SerialisedObject("side");
+
+ so.Set("id", ID);
+ so.Set("plane", $"({FormatVector3(Face.Vertices[0])}) ({FormatVector3(Face.Vertices[1])}) ({FormatVector3(Face.Vertices[2])})");
+ so.Set("material", Face.TextureName);
+ so.Set("uaxis", $"[{FormatVector3(Face.UAxis)} {FormatDecimal(Face.XShift)}] {FormatDecimal(Face.XScale)}");
+ so.Set("vaxis", $"[{FormatVector3(Face.VAxis)} {FormatDecimal(Face.YShift)}] {FormatDecimal(Face.YScale)}");
+ so.Set("rotation", Face.Rotation);
+ so.Set("lightmapscale", Face.LightmapScale);
+ so.Set("smoothing_groups", Face.SmoothingGroups);
+
+ return so;
+ }
+
+ private static string FormatVector3(Vector3 c)
+ {
+ return $"{FormatDecimal(c.X)} {FormatDecimal(c.Y)} {FormatDecimal(c.Z)}";
+ }
+
+ private static string FormatDecimal(float d)
+ {
+ return d.ToString("0.00####", CultureInfo.InvariantCulture);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Formats/VmfObjects/VmfSolid.cs b/Sledge.Formats.Map/Formats/VmfObjects/VmfSolid.cs
new file mode 100644
index 0000000..42069cc
--- /dev/null
+++ b/Sledge.Formats.Map/Formats/VmfObjects/VmfSolid.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using System.Linq;
+using Sledge.Formats.Map.Objects;
+using Sledge.Formats.Valve;
+
+namespace Sledge.Formats.Map.Formats.VmfObjects
+{
+ internal class VmfSolid : VmfObject
+ {
+ public List Sides { get; set; }
+
+ public VmfSolid(SerialisedObject obj) : base(obj)
+ {
+ Sides = new List();
+ foreach (var so in obj.Children.Where(x => x.Name == "side"))
+ {
+ Sides.Add(new VmfSide(so));
+ }
+ }
+
+ public VmfSolid(Solid sol, int id) : base(sol, id)
+ {
+ Sides = sol.Faces.Select(x => new VmfSide(x, 0)).ToList();
+ }
+
+ public override IEnumerable Flatten()
+ {
+ yield return this;
+ }
+
+ public override MapObject ToMapObject()
+ {
+ var sol = new Solid();
+ Editor.Apply(sol);
+ sol.Faces.AddRange(Sides.Select(x => x.ToFace()));
+ sol.ComputeVertices();
+ return sol;
+ }
+
+ public override SerialisedObject ToSerialisedObject()
+ {
+ var so = new SerialisedObject("solid");
+ so.Set("id", ID);
+ so.Children.AddRange(Sides.Select(x => x.ToSerialisedObject()));
+ so.Children.Add(Editor.ToSerialisedObject());
+ return so;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Formats/VmfObjects/VmfWorld.cs b/Sledge.Formats.Map/Formats/VmfObjects/VmfWorld.cs
new file mode 100644
index 0000000..f41f033
--- /dev/null
+++ b/Sledge.Formats.Map/Formats/VmfObjects/VmfWorld.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using Sledge.Formats.Map.Objects;
+using Sledge.Formats.Valve;
+
+namespace Sledge.Formats.Map.Formats.VmfObjects
+{
+ internal class VmfWorld : VmfEntity
+ {
+ public VmfWorld(SerialisedObject obj) : base(obj)
+ {
+ ID = -1;
+ }
+
+ public VmfWorld(Worldspawn root) : base(root, -1)
+ {
+ }
+
+ public override MapObject ToMapObject()
+ {
+ throw new NotSupportedException();
+ }
+
+ protected override string SerialisedObjectName => "world";
+
+ public override SerialisedObject ToSerialisedObject()
+ {
+ var so = new SerialisedObject(SerialisedObjectName);
+ so.Set("id", 1);
+ so.Set("classname", ClassName);
+ if (SpawnFlags > 0) so.Set("spawnflags", SpawnFlags);
+ foreach (var prop in Properties)
+ {
+ so.Properties.Add(new KeyValuePair(prop.Key, prop.Value));
+ }
+
+ return so;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Formats/WorldcraftRmfFormat.cs b/Sledge.Formats.Map/Formats/WorldcraftRmfFormat.cs
new file mode 100644
index 0000000..53d476d
--- /dev/null
+++ b/Sledge.Formats.Map/Formats/WorldcraftRmfFormat.cs
@@ -0,0 +1,457 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using Sledge.Formats.Map.Objects;
+using Path = Sledge.Formats.Map.Objects.Path;
+
+namespace Sledge.Formats.Map.Formats
+{
+ public class WorldcraftRmfFormat : IMapFormat
+ {
+ public string Name => "Worldcraft RMF";
+ public string Description => "The .rmf file format used by Worldcraft and Valve Hammer Editor 3.";
+ public string ApplicationName => "Worldcraft";
+ public string Extension => "rmf";
+ public string[] AdditionalExtensions => new[] { "rmx" };
+ public string[] SupportedStyleHints => new[] { "2.2" };
+
+ const int MaxVariableStringLength = 127;
+
+ public MapFile Read(Stream stream)
+ {
+ using (var br = new BinaryReader(stream, Encoding.ASCII, true))
+ {
+ // Only RMF version 2.2 is supported for the moment.
+ var version = Math.Round(br.ReadSingle(), 1);
+ Util.Assert(Math.Abs(version - 2.2) < 0.01, $"Unsupported RMF version number. Expected 2.2, got {version}.");
+
+ // RMF header test
+ var header = br.ReadFixedLengthString(Encoding.ASCII, 3);
+ Util.Assert(header == "RMF", $"Incorrect RMF header. Expected 'RMF', got '{header}'.");
+
+ var map = new MapFile();
+
+ ReadVisgroups(map, br);
+ ReadWorldspawn(map, br);
+
+ // Some RMF files might not have the DOCINFO block so we check if we're at the end of the stream
+ if (stream.Position < stream.Length)
+ {
+ // DOCINFO string check
+ var docinfo = br.ReadFixedLengthString(Encoding.ASCII, 8);
+ Util.Assert(docinfo == "DOCINFO", $"Incorrect RMF format. Expected 'DOCINFO', got '{docinfo}'.");
+
+ ReadCameras(map, br);
+ }
+
+ return map;
+ }
+ }
+
+ #region Read
+
+ private static void ReadVisgroups(MapFile map, BinaryReader br)
+ {
+ var numVisgroups = br.ReadInt32();
+ for (var i = 0; i < numVisgroups; i++)
+ {
+ var vis = new Visgroup
+ {
+ Name = br.ReadFixedLengthString(Encoding.ASCII, 128),
+ Color = br.ReadRGBAColour(),
+ ID = br.ReadInt32(),
+ Visible = br.ReadBoolean()
+ };
+ br.ReadBytes(3);
+ map.Visgroups.Add(vis);
+ }
+ }
+
+ private static void ReadWorldspawn(MapFile map, BinaryReader br)
+ {
+ var e = (Worldspawn) ReadObject(map, br);
+
+ map.Worldspawn.SpawnFlags = e.SpawnFlags;
+ foreach (var p in e.Properties) map.Worldspawn.Properties[p.Key] = p.Value;
+ map.Worldspawn.Children.AddRange(e.Children);
+ }
+
+ private static MapObject ReadObject(MapFile map, BinaryReader br)
+ {
+ var type = br.ReadCString();
+ switch (type)
+ {
+ case "CMapWorld":
+ return ReadRoot(map, br);
+ case "CMapGroup":
+ return ReadGroup(map, br);
+ case "CMapSolid":
+ return ReadSolid(map, br);
+ case "CMapEntity":
+ return ReadEntity(map, br);
+ default:
+ throw new ArgumentOutOfRangeException("Unknown RMF map object: " + type);
+ }
+ }
+
+ private static void ReadMapBase(MapFile map, MapObject obj, BinaryReader br)
+ {
+ var visgroupId = br.ReadInt32();
+ if (visgroupId > 0)
+ {
+ obj.Visgroups.Add(visgroupId);
+ }
+
+ obj.Color = br.ReadRGBColour();
+
+ var numChildren = br.ReadInt32();
+ for (var i = 0; i < numChildren; i++)
+ {
+ var child = ReadObject(map, br);
+ if (child != null) obj.Children.Add(child);
+ }
+ }
+
+ private static Worldspawn ReadRoot(MapFile map, BinaryReader br)
+ {
+ var wld = new Worldspawn();
+ ReadMapBase(map, wld, br);
+ ReadEntityData(wld, br);
+ var numPaths = br.ReadInt32();
+ for (var i = 0; i < numPaths; i++)
+ {
+ map.Paths.Add(ReadPath(br));
+ }
+ return wld;
+ }
+
+ private static Path ReadPath(BinaryReader br)
+ {
+ var path = new Path
+ {
+ Name = br.ReadFixedLengthString(Encoding.ASCII, 128),
+ Type = br.ReadFixedLengthString(Encoding.ASCII, 128),
+ Direction = (PathDirection) br.ReadInt32()
+ };
+ var numNodes = br.ReadInt32();
+ for (var i = 0; i < numNodes; i++)
+ {
+ var node = new PathNode
+ {
+ Position = br.ReadVector3(),
+ ID = br.ReadInt32(),
+ Name = br.ReadFixedLengthString(Encoding.ASCII, 128)
+ };
+
+ var numProps = br.ReadInt32();
+ for (var j = 0; j < numProps; j++)
+ {
+ var key = br.ReadCString();
+ var value = br.ReadCString();
+ node.Properties[key] = value;
+ }
+ path.Nodes.Add(node);
+ }
+ return path;
+ }
+
+ private static Group ReadGroup(MapFile map, BinaryReader br)
+ {
+ var grp = new Group();
+ ReadMapBase(map, grp, br);
+ return grp;
+ }
+
+ private static Solid ReadSolid(MapFile map, BinaryReader br)
+ {
+ var sol = new Solid();
+ ReadMapBase(map, sol, br);
+ var numFaces = br.ReadInt32();
+ for (var i = 0; i < numFaces; i++)
+ {
+ var face = ReadFace(br);
+ sol.Faces.Add(face);
+ }
+ return sol;
+ }
+
+ private static Entity ReadEntity(MapFile map, BinaryReader br)
+ {
+ var ent = new Entity();
+ ReadMapBase(map, ent, br);
+ ReadEntityData(ent, br);
+ br.ReadBytes(2); // Unused
+ var origin = br.ReadVector3();
+ ent.Properties["origin"] = $"{origin.X.ToString("0.000", CultureInfo.InvariantCulture)} {origin.Y.ToString("0.000", CultureInfo.InvariantCulture)} {origin.Z.ToString("0.000", CultureInfo.InvariantCulture)}";
+ br.ReadBytes(4); // Unused
+ return ent;
+ }
+
+ private static void ReadEntityData(Entity e, BinaryReader br)
+ {
+ e.ClassName = br.ReadCString();
+ br.ReadBytes(4); // Unused bytes
+ e.SpawnFlags = br.ReadInt32();
+
+ var numProperties = br.ReadInt32();
+ for (var i = 0; i < numProperties; i++)
+ {
+ var key = br.ReadCString();
+ var value = br.ReadCString();
+ if (key == null) continue;
+ e.Properties[key] = value;
+ }
+
+ br.ReadBytes(12); // More unused bytes
+ }
+
+ private static Face ReadFace(BinaryReader br)
+ {
+ var face = new Face();
+ var textureName = br.ReadFixedLengthString(Encoding.ASCII, 256);
+ br.ReadBytes(4); // Unused
+ face.TextureName = textureName;
+ face.UAxis = br.ReadVector3();
+ face.XShift = br.ReadSingle();
+ face.VAxis = br.ReadVector3();
+ face.YShift = br.ReadSingle();
+ face.Rotation = br.ReadSingle();
+ face.XScale = br.ReadSingle();
+ face.YScale = br.ReadSingle();
+ br.ReadBytes(16); // Unused
+ var numVerts = br.ReadInt32();
+ for (var i = 0; i < numVerts; i++)
+ {
+ face.Vertices.Add(br.ReadVector3());
+ }
+ face.Plane = br.ReadPlane();
+ return face;
+ }
+
+ private static void ReadCameras(MapFile map, BinaryReader br)
+ {
+ br.ReadSingle(); // Appears to be a version number for camera data. Unused.
+ var activeCamera = br.ReadInt32();
+
+ var num = br.ReadInt32();
+ for (var i = 0; i < num; i++)
+ {
+ map.Cameras.Add(new Camera
+ {
+ EyePosition = br.ReadVector3(),
+ LookPosition = br.ReadVector3(),
+ IsActive = activeCamera == i
+ });
+ }
+ }
+
+ #endregion
+
+ public void Write(Stream stream, MapFile map, string styleHint)
+ {
+ using (var bw = new BinaryWriter(stream, Encoding.ASCII, true))
+ {
+ // RMF 2.2 header
+ bw.Write(2.2f);
+ bw.WriteFixedLengthString(Encoding.ASCII, 3, "RMF");
+
+ // Body
+ WriteVisgroups(map, bw);
+ WriteWorldspawn(map, bw);
+
+ // Only write docinfo if there's cameras in the document
+ if (map.Cameras.Any())
+ {
+ // Docinfo footer
+ bw.WriteFixedLengthString(Encoding.ASCII, 8, "DOCINFO");
+ WriteCameras(map, bw);
+ }
+ }
+ }
+
+ #region Write
+
+ private static void WriteVisgroups(MapFile map, BinaryWriter bw)
+ {
+ var vis = map.Visgroups;
+ bw.Write(vis.Count);
+ foreach (var visgroup in vis)
+ {
+ bw.WriteFixedLengthString(Encoding.ASCII, 128, visgroup.Name);
+ bw.WriteRGBAColour(visgroup.Color);
+ bw.Write(visgroup.ID);
+ bw.Write(visgroup.Visible);
+ bw.Write(new byte[3]); // Unused
+ }
+ }
+
+ private static void WriteWorldspawn(MapFile map, BinaryWriter bw)
+ {
+ WriteObject(map.Worldspawn, bw);
+ var paths = map.Paths;
+ bw.Write(paths.Count);
+ foreach (var path in paths)
+ {
+ WritePath(bw, path);
+ }
+ }
+
+ private static void WriteObject(MapObject o, BinaryWriter bw)
+ {
+ switch (o)
+ {
+ case Worldspawn r:
+ WriteRoot(r, bw);
+ break;
+ case Group g:
+ WriteGroup(g, bw);
+ break;
+ case Solid s:
+ WriteSolid(s, bw);
+ break;
+ case Entity e:
+ WriteEntity(e, bw);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException("Unsupported RMF map object: " + o.GetType());
+ }
+ }
+
+ private static void WriteMapBase(MapObject obj, BinaryWriter bw)
+ {
+ bw.Write(obj.Visgroups.Any() ? obj.Visgroups[0] : 0);
+ bw.WriteRGBColour(obj.Color);
+ bw.Write(obj.Children.Count);
+ foreach (var child in obj.Children)
+ {
+ WriteObject(child, bw);
+ }
+ }
+
+ private static void WriteRoot(Worldspawn root, BinaryWriter bw)
+ {
+ bw.WriteCString("CMapWorld", MaxVariableStringLength);
+ WriteMapBase(root, bw);
+ WriteEntityData(root, bw);
+ }
+
+ private static void WritePath(BinaryWriter bw, Path path)
+ {
+ bw.WriteFixedLengthString(Encoding.ASCII, 128, path.Name);
+ bw.WriteFixedLengthString(Encoding.ASCII, 128, path.Type);
+ bw.Write((int)path.Direction);
+ bw.Write(path.Nodes.Count);
+ foreach (var node in path.Nodes)
+ {
+ bw.WriteVector3(node.Position);
+ bw.Write(node.ID);
+ bw.WriteFixedLengthString(Encoding.ASCII, 128, node.Name);
+ bw.Write(node.Properties.Count);
+ foreach (var property in node.Properties)
+ {
+ bw.WriteCString(property.Key, MaxVariableStringLength);
+ bw.WriteCString(property.Value, MaxVariableStringLength);
+ }
+ }
+ }
+
+ private static void WriteGroup(Group group, BinaryWriter bw)
+ {
+ bw.WriteCString("CMapGroup", MaxVariableStringLength);
+ WriteMapBase(group, bw);
+ }
+
+ private static void WriteSolid(Solid solid, BinaryWriter bw)
+ {
+ bw.WriteCString("CMapSolid", MaxVariableStringLength);
+ WriteMapBase(solid, bw);
+ var faces = solid.Faces.ToList();
+ bw.Write(faces.Count);
+ foreach (var face in faces)
+ {
+ WriteFace(face, bw);
+ }
+ }
+
+ private static void WriteEntity(Entity entity, BinaryWriter bw)
+ {
+ bw.WriteCString("CMapEntity", MaxVariableStringLength);
+ WriteMapBase(entity, bw);
+ WriteEntityData(entity, bw);
+ bw.Write(new byte[2]); // Unused
+
+ var origin = new Vector3();
+ if (entity.Properties.ContainsKey("origin"))
+ {
+ var o = entity.Properties["origin"];
+ if (!String.IsNullOrWhiteSpace(o))
+ {
+ var parts = o.Split(' ');
+ if (parts.Length == 3)
+ {
+ NumericsExtensions.TryParse(parts[0], parts[1], parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out origin);
+ }
+ }
+ }
+ bw.WriteVector3(origin);
+ bw.Write(new byte[4]); // Unused
+ }
+
+ private static void WriteEntityData(Entity data, BinaryWriter bw)
+ {
+ bw.WriteCString(data.ClassName, MaxVariableStringLength);
+ bw.Write(new byte[4]); // Unused
+ bw.Write(data.SpawnFlags);
+
+ var props = data.Properties.Where(x => !String.IsNullOrWhiteSpace(x.Key)).ToList();
+ bw.Write(props.Count);
+ foreach (var p in props)
+ {
+ bw.WriteCString(p.Key, MaxVariableStringLength);
+ bw.WriteCString(p.Value, MaxVariableStringLength);
+ }
+ bw.Write(new byte[12]); // Unused
+ }
+
+ private static void WriteFace(Face face, BinaryWriter bw)
+ {
+ bw.WriteFixedLengthString(Encoding.ASCII, 256, face.TextureName);
+ bw.Write(new byte[4]);
+ bw.WriteVector3(face.UAxis);
+ bw.Write(face.XShift);
+ bw.WriteVector3(face.VAxis);
+ bw.Write(face.YShift);
+ bw.Write(face.Rotation);
+ bw.Write(face.XScale);
+ bw.Write(face.YScale);
+ bw.Write(new byte[16]);
+ bw.Write(face.Vertices.Count);
+ foreach (var vertex in face.Vertices)
+ {
+ bw.WriteVector3(vertex);
+ }
+ bw.WritePlane(face.Vertices.ToArray());
+ }
+
+ private static void WriteCameras(MapFile map, BinaryWriter bw)
+ {
+ bw.Write(0.2f); // Unused
+
+ var cams = map.Cameras;
+ var active = Math.Max(0, cams.FindIndex(x => x.IsActive));
+
+ bw.Write(active);
+ bw.Write(cams.Count);
+ foreach (var cam in cams)
+ {
+ bw.WriteVector3(cam.EyePosition);
+ bw.WriteVector3(cam.LookPosition);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Sledge.Formats.Map/MapFormatFactory.cs b/Sledge.Formats.Map/MapFormatFactory.cs
new file mode 100644
index 0000000..ad04cd9
--- /dev/null
+++ b/Sledge.Formats.Map/MapFormatFactory.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using Sledge.Formats.Map.Formats;
+
+namespace Sledge.Formats.Map
+{
+ public static class MapFormatFactory
+ {
+ private static readonly List _formats;
+
+ static MapFormatFactory()
+ {
+ _formats = new List
+ {
+ new QuakeMapFormat(),
+ new WorldcraftRmfFormat()
+ };
+ }
+
+ public static void Register(IMapFormat loader)
+ {
+ _formats.Add(loader);
+ }
+ }
+}
diff --git a/Sledge.Formats.Map/Objects/Camera.cs b/Sledge.Formats.Map/Objects/Camera.cs
new file mode 100644
index 0000000..3d5b75f
--- /dev/null
+++ b/Sledge.Formats.Map/Objects/Camera.cs
@@ -0,0 +1,11 @@
+using System.Numerics;
+
+namespace Sledge.Formats.Map.Objects
+{
+ public class Camera
+ {
+ public Vector3 EyePosition { get; set; }
+ public Vector3 LookPosition { get; set; }
+ public bool IsActive { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Objects/Entity.cs b/Sledge.Formats.Map/Objects/Entity.cs
new file mode 100644
index 0000000..5aebe18
--- /dev/null
+++ b/Sledge.Formats.Map/Objects/Entity.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace Sledge.Formats.Map.Objects
+{
+ public class Entity : MapObject
+ {
+ public string ClassName { get; set; }
+ public int SpawnFlags { get; set; }
+ public Dictionary Properties { get; set; }
+
+ public Entity()
+ {
+ Properties = new Dictionary();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Objects/Face.cs b/Sledge.Formats.Map/Objects/Face.cs
new file mode 100644
index 0000000..baf6888
--- /dev/null
+++ b/Sledge.Formats.Map/Objects/Face.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.Numerics;
+
+namespace Sledge.Formats.Map.Objects
+{
+ public class Face
+ {
+ public Plane Plane { get; set; }
+ public List Vertices { get; set; }
+
+ public string TextureName { get; set; }
+ public Vector3 UAxis { get; set; }
+ public Vector3 VAxis { get; set; }
+ public float XScale { get; set; }
+ public float YScale { get; set; }
+ public float XShift { get; set; }
+ public float YShift { get; set; }
+ public float Rotation { get; set; }
+
+ public int ContentFlags { get; set; }
+ public int SurfaceFlags { get; set; }
+ public float Value { get; set; }
+
+ public float LightmapScale { get; set; }
+ public string SmoothingGroups { get; set; }
+
+ public Face()
+ {
+ Vertices = new List();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Objects/Group.cs b/Sledge.Formats.Map/Objects/Group.cs
new file mode 100644
index 0000000..b619e71
--- /dev/null
+++ b/Sledge.Formats.Map/Objects/Group.cs
@@ -0,0 +1,7 @@
+namespace Sledge.Formats.Map.Objects
+{
+ public class Group : MapObject
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Objects/MapFile.cs b/Sledge.Formats.Map/Objects/MapFile.cs
new file mode 100644
index 0000000..8832043
--- /dev/null
+++ b/Sledge.Formats.Map/Objects/MapFile.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using Sledge.Formats.Valve;
+
+namespace Sledge.Formats.Map.Objects
+{
+ public class MapFile
+ {
+ public Worldspawn Worldspawn { get; }
+ public List Visgroups { get; set; }
+ public List Paths { get; set; }
+ public List Cameras { get; set; }
+ public List AdditionalObjects { get; set; }
+
+ public MapFile()
+ {
+ Worldspawn = new Worldspawn();
+ Visgroups = new List();
+ Paths = new List();
+ Cameras = new List();
+ AdditionalObjects = new List();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Objects/MapObject.cs b/Sledge.Formats.Map/Objects/MapObject.cs
new file mode 100644
index 0000000..81c8afe
--- /dev/null
+++ b/Sledge.Formats.Map/Objects/MapObject.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+
+namespace Sledge.Formats.Map.Objects
+{
+ public abstract class MapObject
+ {
+ public List Children { get; set; }
+ public List Visgroups { get; set; }
+ public Color Color { get; set; }
+
+ protected MapObject()
+ {
+ Children = new List();
+ Visgroups = new List();
+ Color = Color.White;
+ }
+
+ public List FindAll()
+ {
+ return Find(x => true);
+ }
+
+ public List Find(Predicate matcher)
+ {
+ var list = new List();
+ FindRecursive(list, matcher);
+ return list;
+ }
+
+ private void FindRecursive(ICollection items, Predicate matcher)
+ {
+ var thisMatch = matcher(this);
+ if (thisMatch)
+ {
+ items.Add(this);
+ }
+ foreach (var mo in Children)
+ {
+ mo.FindRecursive(items, matcher);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Objects/Path.cs b/Sledge.Formats.Map/Objects/Path.cs
new file mode 100644
index 0000000..c570ab3
--- /dev/null
+++ b/Sledge.Formats.Map/Objects/Path.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace Sledge.Formats.Map.Objects
+{
+ public class Path
+ {
+
+ public string Name { get; set; }
+ public string Type { get; set; }
+ public PathDirection Direction { get; set; }
+ public List Nodes { get; set; }
+
+ public Path()
+ {
+ Nodes = new List();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Objects/PathDirection.cs b/Sledge.Formats.Map/Objects/PathDirection.cs
new file mode 100644
index 0000000..5b9dfeb
--- /dev/null
+++ b/Sledge.Formats.Map/Objects/PathDirection.cs
@@ -0,0 +1,9 @@
+namespace Sledge.Formats.Map.Objects
+{
+ public enum PathDirection
+ {
+ OneWay,
+ Circular,
+ PingPong
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Objects/PathNode.cs b/Sledge.Formats.Map/Objects/PathNode.cs
new file mode 100644
index 0000000..15bd35b
--- /dev/null
+++ b/Sledge.Formats.Map/Objects/PathNode.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.Numerics;
+
+namespace Sledge.Formats.Map.Objects
+{
+ public class PathNode
+ {
+ public Vector3 Position { get; set; }
+ public int ID { get; set; }
+ public string Name { get; set; }
+ public Dictionary Properties { get; private set; }
+
+ public PathNode()
+ {
+ Properties = new Dictionary();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Objects/Solid.cs b/Sledge.Formats.Map/Objects/Solid.cs
new file mode 100644
index 0000000..5d03b73
--- /dev/null
+++ b/Sledge.Formats.Map/Objects/Solid.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.Linq;
+using Sledge.Formats.Precision;
+
+namespace Sledge.Formats.Map.Objects
+{
+ public class Solid : MapObject
+ {
+ public List Faces { get; set; }
+
+ public Solid()
+ {
+ Faces = new List();
+ }
+
+ public void ComputeVertices()
+ {
+ var poly = new Polyhedron(Faces.Select(x => new Plane(x.Plane.Normal.ToPrecisionVector3(), x.Plane.D)));
+
+ foreach (var face in Faces)
+ {
+ var pg = poly.Polygons.FirstOrDefault(x => x.Plane.Normal.EquivalentTo(face.Plane.Normal.ToPrecisionVector3(), 0.0075f)); // Magic number that seems to match VHE
+ if (pg != null)
+ {
+ face.Vertices.AddRange(pg.Vertices.Select(x => x.ToStandardVector3()));
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Objects/Visgroup.cs b/Sledge.Formats.Map/Objects/Visgroup.cs
new file mode 100644
index 0000000..2f002b5
--- /dev/null
+++ b/Sledge.Formats.Map/Objects/Visgroup.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Drawing;
+
+namespace Sledge.Formats.Map.Objects
+{
+ public class Visgroup
+ {
+ public int ID { get; set; }
+ public string Name { get; set; }
+ public Color Color { get; set; }
+ public bool Visible { get; set; }
+ public List Children { get; set; }
+
+ public Visgroup()
+ {
+ Children = new List();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Objects/Worldspawn.cs b/Sledge.Formats.Map/Objects/Worldspawn.cs
new file mode 100644
index 0000000..ec507b6
--- /dev/null
+++ b/Sledge.Formats.Map/Objects/Worldspawn.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace Sledge.Formats.Map.Objects
+{
+ public class Worldspawn : Entity
+ {
+ public Worldspawn()
+ {
+ ClassName = "worldspawn";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Map/Sledge.Formats.Map.csproj b/Sledge.Formats.Map/Sledge.Formats.Map.csproj
new file mode 100644
index 0000000..ad06043
--- /dev/null
+++ b/Sledge.Formats.Map/Sledge.Formats.Map.csproj
@@ -0,0 +1,15 @@
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sledge.Formats.Map/Util.cs b/Sledge.Formats.Map/Util.cs
new file mode 100644
index 0000000..69dfec7
--- /dev/null
+++ b/Sledge.Formats.Map/Util.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+namespace Sledge.Formats.Map
+{
+ internal static class Util
+ {
+ public static void Assert(bool b, string message = "Malformed file.")
+ {
+ if (!b) throw new Exception(message);
+ }
+
+ public static bool ParseFloatArray(string input, char[] splitChars, int expected, out float[] array)
+ {
+ var spl = input.Split(splitChars, StringSplitOptions.RemoveEmptyEntries);
+ if (spl.Length == expected)
+ {
+ var parsed = spl.Select(x => float.TryParse(x, NumberStyles.Float, CultureInfo.InvariantCulture, out var o) ? (float?)o : null).ToList();
+ if (parsed.All(x => x.HasValue))
+ {
+ // ReSharper disable once PossibleInvalidOperationException
+ array = parsed.Select(x => x.Value).ToArray();
+ return true;
+ }
+ }
+ array = new float[expected];
+ return false;
+ }
+ }
+}
diff --git a/Sledge.Formats.Tests/Sledge.Formats.Tests.csproj b/Sledge.Formats.Tests/Sledge.Formats.Tests.csproj
new file mode 100644
index 0000000..f58a82f
--- /dev/null
+++ b/Sledge.Formats.Tests/Sledge.Formats.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netcoreapp2.2
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sledge.Formats.Tests/TestBinaryExtensions.cs b/Sledge.Formats.Tests/TestBinaryExtensions.cs
new file mode 100644
index 0000000..c801372
--- /dev/null
+++ b/Sledge.Formats.Tests/TestBinaryExtensions.cs
@@ -0,0 +1,426 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Sledge.Formats.Tests
+{
+ [TestClass]
+ public class TestBinaryExtensions
+ {
+ [TestMethod]
+ public void TestReadFixedLengthString()
+ {
+ var ms = new MemoryStream(new byte[]
+ {
+ 97, 97, 97, 0,
+ 0 , 0 , 0, 0,
+ 0 , 0 , 0, 0,
+ 0 , 0 , 0, 0,
+ });
+ using (var br = new BinaryReader(ms))
+ {
+ var fls = br.ReadFixedLengthString(Encoding.ASCII, 8);
+ Assert.AreEqual("aaa", fls);
+ Assert.AreEqual(8, ms.Position);
+ }
+ }
+
+ [TestMethod]
+ public void TestWriteFixedLengthString()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms))
+ {
+ bw.WriteFixedLengthString(Encoding.ASCII, 8, "aaa");
+ Assert.AreEqual(8, ms.Position);
+ CollectionAssert.AreEqual(
+ new byte[] {97, 97, 97, 0, 0, 0, 0, 0},
+ ms.ToArray()
+ );
+ }
+ }
+
+ [TestMethod]
+ public void TestReadNullTerminatedString()
+ {
+ var ms = new MemoryStream(new byte[]
+ {
+ 97, 97, 97, 0,
+ 0 , 0 , 0, 0,
+ 0 , 0 , 0, 0,
+ 0 , 0 , 0, 0,
+ });
+ using (var br = new BinaryReader(ms))
+ {
+ var fls = br.ReadNullTerminatedString();
+ Assert.AreEqual("aaa", fls);
+ Assert.AreEqual(4, ms.Position);
+ }
+ }
+
+ [TestMethod]
+ public void TestWriteNullTerminatedString()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms))
+ {
+ bw.WriteNullTerminatedString("aaa");
+ Assert.AreEqual(4, ms.Position);
+ CollectionAssert.AreEqual(
+ new byte[] { 97, 97, 97, 0 },
+ ms.ToArray()
+ );
+ }
+ }
+
+ [TestMethod]
+ public void TestReadCString()
+ {
+ var ms = new MemoryStream(new byte[]
+ {
+ 4, 97, 97, 97,
+ 0 , 0 , 0, 0,
+ 0 , 0 , 0, 0,
+ 0 , 0 , 0, 0,
+ });
+ using (var br = new BinaryReader(ms))
+ {
+ var fls = br.ReadCString();
+ Assert.AreEqual("aaa", fls);
+ Assert.AreEqual(5, ms.Position);
+ }
+ }
+
+ [TestMethod]
+ public void TestWriteCString()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms))
+ {
+ bw.WriteCString("aaa", 256);
+ Assert.AreEqual(5, ms.Position);
+ CollectionAssert.AreEqual(
+ new byte[] { 4, 97, 97, 97, 0 },
+ ms.ToArray()
+ );
+ }
+ }
+
+ [TestMethod]
+ public void TestWriteCString_MaxLength()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms))
+ {
+ bw.WriteCString("aaa", 2);
+ Assert.AreEqual(3, ms.Position);
+ CollectionAssert.AreEqual(
+ new byte[] { 2, 97, 0 },
+ ms.ToArray()
+ );
+ }
+ }
+
+ [TestMethod]
+ public void TestReadUshortArray()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms, Encoding.ASCII, true))
+ {
+ bw.Write((ushort) 123);
+ bw.Write((ushort) 456);
+ bw.Write((ushort) 789);
+ }
+ ms.Position = 0;
+
+ using (var br = new BinaryReader(ms))
+ {
+ var a = br.ReadUshortArray(2);
+ CollectionAssert.AreEqual(new ushort[] { 123, 456 }, a);
+ Assert.AreEqual(4, ms.Position);
+ }
+ }
+
+ [TestMethod]
+ public void TestReadShortArray()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms, Encoding.ASCII, true))
+ {
+ bw.Write((short) 123);
+ bw.Write((short) -456);
+ bw.Write((short) 789);
+ }
+ ms.Position = 0;
+
+ using (var br = new BinaryReader(ms))
+ {
+ var a = br.ReadShortArray(2);
+ CollectionAssert.AreEqual(new short[] { 123, -456 }, a);
+ Assert.AreEqual(4, ms.Position);
+ }
+ }
+
+ [TestMethod]
+ public void TestReadIntArray()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms, Encoding.ASCII, true))
+ {
+ bw.Write(123);
+ bw.Write(-456);
+ bw.Write(789);
+ }
+ ms.Position = 0;
+
+ using (var br = new BinaryReader(ms))
+ {
+ var a = br.ReadIntArray(2);
+ CollectionAssert.AreEqual(new [] { 123, -456 }, a);
+ Assert.AreEqual(8, ms.Position);
+ }
+ }
+
+ [TestMethod]
+ public void TestReadSingleArrayAsDecimal()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms, Encoding.ASCII, true))
+ {
+ bw.Write(123f);
+ bw.Write(456f);
+ bw.Write(789f);
+ }
+ ms.Position = 0;
+
+ using (var br = new BinaryReader(ms))
+ {
+ var a = br.ReadSingleArrayAsDecimal(2);
+ CollectionAssert.AreEqual(new[] { 123m, 456m }, a);
+ Assert.AreEqual(8, ms.Position);
+ }
+ }
+
+ [TestMethod]
+ public void TestReadSingleArray()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms, Encoding.ASCII, true))
+ {
+ bw.Write(123f);
+ bw.Write(456f);
+ bw.Write(789f);
+ }
+ ms.Position = 0;
+
+ using (var br = new BinaryReader(ms))
+ {
+ var a = br.ReadSingleArray(2);
+ CollectionAssert.AreEqual(new[] { 123f, 456f }, a);
+ Assert.AreEqual(8, ms.Position);
+ }
+ }
+
+ [TestMethod]
+ public void TestReadSingleAsDecimal()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms, Encoding.ASCII, true))
+ {
+ bw.Write(123f);
+ bw.Write(456f);
+ bw.Write(789f);
+ }
+ ms.Position = 0;
+
+ using (var br = new BinaryReader(ms))
+ {
+ var a = br.ReadSingleAsDecimal();
+ Assert.AreEqual(123m, a);
+ Assert.AreEqual(4, ms.Position);
+ }
+ }
+
+ [TestMethod]
+ public void TestWriteDecimalAsSingle()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms))
+ {
+ bw.WriteDecimalAsSingle(123m);
+ Assert.AreEqual(4, ms.Position);
+ CollectionAssert.AreEqual(BitConverter.GetBytes(123f), ms.ToArray());
+ }
+ }
+
+ [TestMethod]
+ public void TestReadRGBColour()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms, Encoding.ASCII, true))
+ {
+ bw.Write((byte) 255);
+ bw.Write((byte) 0);
+ bw.Write((byte) 0);
+ bw.Write((byte) 0);
+ }
+ ms.Position = 0;
+
+ using (var br = new BinaryReader(ms))
+ {
+ var a = br.ReadRGBColour();
+ Assert.AreEqual(Color.Red.ToArgb(), a.ToArgb());
+ Assert.AreEqual(3, ms.Position);
+ }
+ }
+
+ [TestMethod]
+ public void TestWriteRGBColour()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms))
+ {
+ bw.WriteRGBColour(Color.Red);
+ Assert.AreEqual(3, ms.Position);
+ CollectionAssert.AreEqual(new byte [] { 255, 0, 0 }, ms.ToArray());
+ }
+ }
+
+ [TestMethod]
+ public void TestReadRGBAColour()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms, Encoding.ASCII, true))
+ {
+ bw.Write((byte)255);
+ bw.Write((byte)0);
+ bw.Write((byte)0);
+ bw.Write((byte)255);
+ }
+ ms.Position = 0;
+
+ using (var br = new BinaryReader(ms))
+ {
+ var a = br.ReadRGBAColour();
+ Assert.AreEqual(Color.Red.ToArgb(), a.ToArgb());
+ Assert.AreEqual(4, ms.Position);
+ }
+ }
+
+ [TestMethod]
+ public void TestWriteRGBAColour()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms))
+ {
+ bw.WriteRGBAColour(Color.Red);
+ Assert.AreEqual(4, ms.Position);
+ CollectionAssert.AreEqual(new byte[] { 255, 0, 0, 255 }, ms.ToArray());
+ }
+ }
+
+ [TestMethod]
+ public void TestReadVector3Array()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms, Encoding.ASCII, true))
+ {
+ foreach (var n in Enumerable.Range(1, 9)) bw.Write((float) n);
+ }
+ ms.Position = 0;
+
+ using (var br = new BinaryReader(ms))
+ {
+ var a = br.ReadVector3Array(2);
+ CollectionAssert.AreEqual(new Vector3[] { new Vector3(1, 2, 3), new Vector3(4, 5, 6) }, a);
+ Assert.AreEqual(24, ms.Position);
+ }
+ }
+
+ [TestMethod]
+ public void TestReadVector3()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms, Encoding.ASCII, true))
+ {
+ foreach (var n in Enumerable.Range(1, 9)) bw.Write((float)n);
+ }
+ ms.Position = 0;
+
+ using (var br = new BinaryReader(ms))
+ {
+ var a = br.ReadVector3();
+ Assert.AreEqual(new Vector3(1, 2, 3), a);
+ Assert.AreEqual(12, ms.Position);
+ }
+ }
+
+ [TestMethod]
+ public void TestWriteVector3()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms))
+ {
+ bw.WriteVector3(new Vector3(1, 2, 3));
+ Assert.AreEqual(12, ms.Position);
+ var exp = new List();
+ exp.AddRange(BitConverter.GetBytes(1f));
+ exp.AddRange(BitConverter.GetBytes(2f));
+ exp.AddRange(BitConverter.GetBytes(3f));
+ CollectionAssert.AreEqual(exp, ms.ToArray());
+ }
+ }
+
+ [TestMethod]
+ public void TestReadPlane()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms, Encoding.ASCII, true))
+ {
+ bw.Write(1f);
+ bw.Write(2f);
+ bw.Write(0f);
+ bw.Write(3f);
+ bw.Write(4f);
+ bw.Write(0f);
+ bw.Write(3f);
+ bw.Write(-2f);
+ bw.Write(0f);
+ }
+ ms.Position = 0;
+
+ using (var br = new BinaryReader(ms))
+ {
+ var a = br.ReadPlane();
+ Assert.AreEqual(new Vector3(0, 0, 1), a.Normal);
+ Assert.AreEqual(0f, a.D);
+ Assert.AreEqual(36, ms.Position);
+ }
+ }
+
+ [TestMethod]
+ public void TestWritePlane()
+ {
+ var ms = new MemoryStream();
+ using (var bw = new BinaryWriter(ms))
+ {
+ var vecs = new[] {new Vector3(1, 2, 0), new Vector3(3, 4, 0), new Vector3(3, -2, 0)};
+ bw.WritePlane(vecs);
+ Assert.AreEqual(36, ms.Position);
+ var exp = new List();
+ foreach (var v in vecs)
+ {
+ exp.AddRange(BitConverter.GetBytes(v.X));
+ exp.AddRange(BitConverter.GetBytes(v.Y));
+ exp.AddRange(BitConverter.GetBytes(v.Z));
+ }
+ CollectionAssert.AreEqual(exp, ms.ToArray());
+ }
+ }
+ }
+}
diff --git a/Sledge.Formats.Tests/TestNumericsExtensions.cs b/Sledge.Formats.Tests/TestNumericsExtensions.cs
new file mode 100644
index 0000000..1ff2ad8
--- /dev/null
+++ b/Sledge.Formats.Tests/TestNumericsExtensions.cs
@@ -0,0 +1,145 @@
+using System.Drawing;
+using System.Globalization;
+using System.Numerics;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Sledge.Formats.Tests
+{
+ [TestClass]
+ public class TestNumericsExtensions
+ {
+ [TestMethod]
+ public void TestToVector3()
+ {
+ Assert.AreEqual(
+ new Vector3(1, 2, 0),
+ new Vector2(1, 2).ToVector3()
+ );
+ }
+
+ [TestMethod]
+ public void TestEquivalentTo()
+ {
+ Assert.IsTrue(new Vector3(1, 2, 3).EquivalentTo(new Vector3(1, 2, 3)));
+ Assert.IsTrue(new Vector3(1, 2, 3.0001f).EquivalentTo(new Vector3(1, 2, 3)));
+ Assert.IsTrue(new Vector3(1, 2, 3.01f).EquivalentTo(new Vector3(1, 2, 3), 0.01f));
+ Assert.IsFalse(new Vector3(1, 2, 3.1f).EquivalentTo(new Vector3(1, 2, 3), 0.01f));
+ Assert.IsFalse(new Vector3(1, 2, 3.01f).EquivalentTo(new Vector3(1, 2, 3)));
+ Assert.IsFalse(new Vector3(1, 2, 3.0001f).EquivalentTo(new Vector3(1, 2, 3), 0.00001f));
+ }
+
+ [TestMethod]
+ public void TestParse()
+ {
+ Assert.AreEqual(
+ NumericsExtensions.Parse("1", "2", "3", NumberStyles.Float, CultureInfo.InvariantCulture),
+ new Vector3(1, 2, 3)
+ );
+ Assert.AreEqual(
+ NumericsExtensions.Parse("1,01", "2,02", "3,03", NumberStyles.Float, CultureInfo.GetCultureInfo("es-ES")),
+ new Vector3(1.01f, 2.02f, 3.03f)
+ );
+ }
+
+ [TestMethod]
+ public void TestNormalise()
+ {
+ Assert.AreEqual(Vector3.Normalize(new Vector3(1, 2, 3)), new Vector3(1, 2, 3).Normalise());
+ }
+
+ [TestMethod]
+ public void TestAbsolute()
+ {
+ Assert.AreEqual(Vector3.Abs(new Vector3(-1, 2, -3)), new Vector3(-1, 2, -3).Absolute());
+ }
+
+ [TestMethod]
+ public void TestDot()
+ {
+ Assert.AreEqual(
+ Vector3.Dot(new Vector3(1, 2, 3), new Vector3(4, 5, 6)),
+ new Vector3(1, 2, 3).Dot(new Vector3(4, 5, 6))
+ );
+ }
+
+ [TestMethod]
+ public void TestCross()
+ {
+ Assert.AreEqual(
+ Vector3.Cross(new Vector3(1, 2, 3), new Vector3(4, 5, 6)),
+ new Vector3(1, 2, 3).Cross(new Vector3(4, 5, 6))
+ );
+ }
+
+ [TestMethod]
+ public void TestRound()
+ {
+ Assert.AreEqual(
+ new Vector3(1.01f, 2.02f, 3.03f),
+ new Vector3(1.0099999f, 2.02111111f, 3.034f).Round(2)
+ );
+ }
+
+ [TestMethod]
+ public void TestClosestAxis()
+ {
+ Assert.AreEqual(Vector3.UnitZ, new Vector3(1, 2, 3).ClosestAxis());
+ Assert.AreEqual(Vector3.UnitX, new Vector3(1, 1, 1).ClosestAxis());
+ Assert.AreEqual(Vector3.UnitY, new Vector3(1, 2, 2).ClosestAxis());
+ }
+
+ [TestMethod]
+ public void TestToPrecisionVector3()
+ {
+ Assert.AreEqual(
+ new Precision.Vector3(1, 2, 3),
+ new Vector3(1, 2, 3).ToPrecisionVector3()
+ );
+ }
+
+ [TestMethod]
+ public void TestToVector2()
+ {
+ Assert.AreEqual(
+ new Vector2(1, 2),
+ new Vector3(1, 2, 3).ToVector2()
+ );
+ }
+
+ [TestMethod]
+ public void TestToVector4()
+ {
+ Assert.AreEqual(
+ new Vector4(1, 0, 0, 1),
+ Color.Red.ToVector4()
+ );
+ }
+
+ [TestMethod]
+ public void TestToColor4()
+ {
+ Assert.AreEqual(
+ Color.Red.ToArgb(),
+ new Vector4(1, 0, 0, 1).ToColor().ToArgb()
+ );
+ }
+
+ [TestMethod]
+ public void TestToColor3()
+ {
+ Assert.AreEqual(
+ Color.Red.ToArgb(),
+ new Vector3(1, 0, 0).ToColor().ToArgb()
+ );
+ }
+
+ [TestMethod]
+ public void Transform()
+ {
+ Assert.AreEqual(
+ Vector3.Transform(new Vector3(1, 2, 3), Matrix4x4.CreateRotationX(2)),
+ Matrix4x4.CreateRotationX(2).Transform(new Vector3(1, 2, 3))
+ );
+ }
+ }
+}
diff --git a/Sledge.Formats.Tests/TestStringExtensions.cs b/Sledge.Formats.Tests/TestStringExtensions.cs
new file mode 100644
index 0000000..48bfa72
--- /dev/null
+++ b/Sledge.Formats.Tests/TestStringExtensions.cs
@@ -0,0 +1,41 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Sledge.Formats.Tests
+{
+ [TestClass]
+ public class TestStringExtensions
+ {
+ [TestMethod]
+ public void TestSplitWithQuotes()
+ {
+ CollectionAssert.AreEqual(
+ new [] { "a", "b", "c" },
+ "a b c".SplitWithQuotes()
+ );
+ CollectionAssert.AreEqual(
+ new [] { "a", "b", "c" },
+ "axbxc".SplitWithQuotes(new []{ 'x' })
+ );
+ CollectionAssert.AreEqual(
+ new [] { "axb", "c" },
+ "1axb1xc".SplitWithQuotes(new []{ 'x' }, '1')
+ );
+ CollectionAssert.AreEqual(
+ new [] { "a", "b", "c" },
+ "\"a\" b c".SplitWithQuotes()
+ );
+ CollectionAssert.AreEqual(
+ new [] { "a b", "c" },
+ "\"a b\" c".SplitWithQuotes()
+ );
+ CollectionAssert.AreEqual(
+ new [] { "a b", "c" },
+ "\"a b\" \"c\"".SplitWithQuotes()
+ );
+ CollectionAssert.AreEqual(
+ new [] { "a b", "c" },
+ "\"a b\"\t\"c\"".SplitWithQuotes()
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Tests/Valve/TestLiblist.cs b/Sledge.Formats.Tests/Valve/TestLiblist.cs
new file mode 100644
index 0000000..74fb3cf
--- /dev/null
+++ b/Sledge.Formats.Tests/Valve/TestLiblist.cs
@@ -0,0 +1,77 @@
+using System.IO;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Sledge.Formats.Valve;
+
+namespace Sledge.Formats.Tests.Valve
+{
+ [TestClass]
+ public class TestLiblist
+ {
+ private readonly Stream _libList = new MemoryStream(Encoding.ASCII.GetBytes(@"// Valve Game Info file
+// These are key/value pairs. Certain mods will use different settings.
+//
+game ""Half-Life""
+startmap ""c0a0""
+trainmap ""t0a0""
+mpentity ""info_player_deathmatch""
+gamedll ""dlls\hl.dll""
+gamedll_linux ""dlls/hl.so""
+gamedll_osx ""dlls/hl.dylib""
+secure ""1""
+type ""singleplayer_only""
+
+"));
+ [TestMethod]
+ public void TestLoading()
+ {
+ _libList.Position = 0;
+ var lib = new Liblist(_libList);
+ Assert.AreEqual("Half-Life", lib.Game);
+ Assert.AreEqual("c0a0", lib.StartingMap);
+ Assert.AreEqual("t0a0", lib.TrainingMap);
+ Assert.AreEqual("info_player_deathmatch", lib.MultiplayerEntity);
+ Assert.AreEqual("dlls\\hl.dll", lib.GameDll);
+ Assert.AreEqual("dlls/hl.so", lib.GameDllLinux);
+ Assert.AreEqual("dlls/hl.dylib", lib.GameDllOsx);
+ Assert.AreEqual(true, lib.Secure);
+ Assert.AreEqual("singleplayer_only", lib.Type);
+ }
+
+ [TestMethod]
+ public void TestSaving()
+ {
+ var lib = new Liblist(_libList)
+ {
+ Game = "Half-Life",
+ StartingMap = "c0a0",
+ TrainingMap = "t0a0",
+ MultiplayerEntity = "info_player_deathmatch",
+ GameDll = "dlls\\hl.dll",
+ GameDllLinux = "dlls/hl.so",
+ GameDllOsx = "dlls/hl.dylib",
+ Secure = true,
+ Type = "singleplayer_only"
+ };
+
+ string output;
+ using (var ms = new MemoryStream())
+ {
+ lib.Write(ms);
+ ms.Position = 0;
+ output = Encoding.ASCII.GetString(ms.ToArray());
+ }
+
+ Assert.AreEqual(@"game ""Half-Life""
+startmap ""c0a0""
+trainmap ""t0a0""
+mpentity ""info_player_deathmatch""
+gamedll ""dlls\hl.dll""
+gamedll_linux ""dlls/hl.so""
+gamedll_osx ""dlls/hl.dylib""
+secure ""1""
+type ""singleplayer_only""
+", output);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.sln b/Sledge.Formats.sln
new file mode 100644
index 0000000..3915208
--- /dev/null
+++ b/Sledge.Formats.sln
@@ -0,0 +1,49 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.28803.202
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sledge.Formats.Map", "Sledge.Formats.Map\Sledge.Formats.Map.csproj", "{2DD6382B-F76B-4AB3-B226-7F0BBB8331AC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sledge.Formats.Map.Tests", "Sledge.Formats.Map.Tests\Sledge.Formats.Map.Tests.csproj", "{CD78BC0D-33A8-4E28-AD53-4A6DA174CB1C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sledge.Formats", "Sledge.Formats\Sledge.Formats.csproj", "{5724CCBA-66EB-4715-8092-6F490A71EA43}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sledge.Formats.Tests", "Sledge.Formats.Tests\Sledge.Formats.Tests.csproj", "{F613B3BA-903A-4C42-836F-D288D58AC486}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sledge.Formats.Bsp", "Sledge.Formats.Bsp\Sledge.Formats.Bsp.csproj", "{369D8CA2-7131-406A-8E36-5DC1B7B0A5C6}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {2DD6382B-F76B-4AB3-B226-7F0BBB8331AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2DD6382B-F76B-4AB3-B226-7F0BBB8331AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2DD6382B-F76B-4AB3-B226-7F0BBB8331AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2DD6382B-F76B-4AB3-B226-7F0BBB8331AC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CD78BC0D-33A8-4E28-AD53-4A6DA174CB1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CD78BC0D-33A8-4E28-AD53-4A6DA174CB1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CD78BC0D-33A8-4E28-AD53-4A6DA174CB1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CD78BC0D-33A8-4E28-AD53-4A6DA174CB1C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5724CCBA-66EB-4715-8092-6F490A71EA43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5724CCBA-66EB-4715-8092-6F490A71EA43}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5724CCBA-66EB-4715-8092-6F490A71EA43}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5724CCBA-66EB-4715-8092-6F490A71EA43}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F613B3BA-903A-4C42-836F-D288D58AC486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F613B3BA-903A-4C42-836F-D288D58AC486}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F613B3BA-903A-4C42-836F-D288D58AC486}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F613B3BA-903A-4C42-836F-D288D58AC486}.Release|Any CPU.Build.0 = Release|Any CPU
+ {369D8CA2-7131-406A-8E36-5DC1B7B0A5C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {369D8CA2-7131-406A-8E36-5DC1B7B0A5C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {369D8CA2-7131-406A-8E36-5DC1B7B0A5C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {369D8CA2-7131-406A-8E36-5DC1B7B0A5C6}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {A2AA0883-1321-4A68-ADE4-75C8E9318B67}
+ EndGlobalSection
+EndGlobal
diff --git a/Sledge.Formats/BinaryExtensions.cs b/Sledge.Formats/BinaryExtensions.cs
new file mode 100644
index 0000000..7f77841
--- /dev/null
+++ b/Sledge.Formats/BinaryExtensions.cs
@@ -0,0 +1,289 @@
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+namespace Sledge.Formats
+{
+ ///
+ /// Common binary reader/write extension methods
+ ///
+ public static class BinaryExtensions
+ {
+ // Strings
+
+ ///
+ /// Read a fixed number of bytes from the reader and parse out an optionally null-terminated string
+ ///
+ /// Binary reader
+ /// The text encoding to use
+ /// The number of bytes to read
+ /// The string that was read
+ public static string ReadFixedLengthString(this BinaryReader br, Encoding encoding, int length)
+ {
+ var bstr = br.ReadBytes(length).TakeWhile(b => b != 0).ToArray();
+ return encoding.GetString(bstr);
+ }
+
+ ///
+ /// Write a string to the writer and pad the width with nulls to reach a fixed number of bytes.
+ ///
+ /// Binary writer
+ /// The text encoding to use
+ /// The number of bytes to write
+ /// The string to write
+ public static void WriteFixedLengthString(this BinaryWriter bw, Encoding encoding, int length, string str)
+ {
+ var arr = new byte[length];
+ encoding.GetBytes(str, 0, str.Length, arr, 0);
+ bw.Write(arr, 0, length);
+ }
+
+ ///
+ /// Read a variable number of bytes into a string until a null terminator is reached.
+ ///
+ /// Binary reader
+ /// The string that was read
+ public static string ReadNullTerminatedString(this BinaryReader br)
+ {
+ var str = "";
+ char c;
+ while ((c = br.ReadChar()) != 0)
+ {
+ str += c;
+ }
+ return str;
+ }
+
+ ///
+ /// Write a string followed by a null terminator.
+ ///
+ /// Binary writer
+ /// The string to write
+ public static void WriteNullTerminatedString(this BinaryWriter bw, string str)
+ {
+ bw.Write(str.ToCharArray());
+ bw.Write((char) 0);
+ }
+
+ ///
+ /// Read a length-prefixed string from the reader.
+ ///
+ /// Binary reader
+ /// String that was read
+ public static string ReadCString(this BinaryReader br)
+ {
+ // GH#87: RMF strings aren't prefixed in the same way .NET's BinaryReader expects
+ // Read the byte length and then read that number of characters.
+ var len = br.ReadByte();
+ var chars = br.ReadChars(len);
+ return new string(chars).Trim('\0');
+ }
+
+
+ ///
+ /// Write a length-prefixed string to the writer.
+ ///
+ /// Binary writer
+ /// The string to write
+ /// The maximum length of the string
+ public static void WriteCString(this BinaryWriter bw, string str, int maximumLength)
+ {
+ // GH#87: RMF strings aren't prefixed in the same way .NET's BinaryReader expects
+ // Write the byte length (+1) and then write that number of characters plus the null terminator.
+ // Hammer doesn't like RMF strings longer than 128 bytes...
+ maximumLength--;
+ if (str == null) str = "";
+ if (str.Length > maximumLength) str = str.Substring(0, maximumLength);
+ bw.Write((byte)(str.Length + 1));
+ bw.Write(str.ToCharArray());
+ bw.Write('\0');
+ }
+
+ // Arrays
+
+ ///
+ /// Read an array of short unsigned integers
+ ///
+ /// Binary reader
+ /// The number of values to read
+ /// The resulting array
+ public static ushort[] ReadUshortArray(this BinaryReader br, int num)
+ {
+ var arr = new ushort[num];
+ for (var i = 0; i < num; i++) arr[i] = br.ReadUInt16();
+ return arr;
+ }
+
+ ///
+ /// Read an array of short integers
+ ///
+ /// Binary reader
+ /// The number of values to read
+ /// The resulting array
+ public static short[] ReadShortArray(this BinaryReader br, int num)
+ {
+ var arr = new short[num];
+ for (var i = 0; i < num; i++) arr[i] = br.ReadInt16();
+ return arr;
+ }
+
+ ///
+ /// Read an array of integers
+ ///
+ /// Binary reader
+ /// The number of values to read
+ /// The resulting array
+ public static int[] ReadIntArray(this BinaryReader br, int num)
+ {
+ var arr = new int[num];
+ for (var i = 0; i < num; i++) arr[i] = br.ReadInt32();
+ return arr;
+ }
+
+ ///
+ /// Read an array of floats and cast them to decimals
+ ///
+ /// Binary reader
+ /// The number of values to read
+ /// The resulting array
+ public static decimal[] ReadSingleArrayAsDecimal(this BinaryReader br, int num)
+ {
+ var arr = new decimal[num];
+ for (var i = 0; i < num; i++) arr[i] = br.ReadSingleAsDecimal();
+ return arr;
+ }
+
+ ///
+ /// Read an array of floats
+ ///
+ /// Binary reader
+ /// The number of values to read
+ /// The resulting array
+ public static float[] ReadSingleArray(this BinaryReader br, int num)
+ {
+ var arr = new float[num];
+ for (var i = 0; i < num; i++) arr[i] = br.ReadSingle();
+ return arr;
+ }
+
+ // Decimal <-> Single
+
+ ///
+ /// Read a float and cast it to decimal
+ ///
+ /// Binary reader
+ /// Value that was read
+ public static decimal ReadSingleAsDecimal(this BinaryReader br)
+ {
+ return (decimal) br.ReadSingle();
+ }
+
+ ///
+ /// Write a decimal as a float
+ ///
+ /// Binary writer
+ /// Value to write
+ public static void WriteDecimalAsSingle(this BinaryWriter bw, decimal dec)
+ {
+ bw.Write((float) dec);
+ }
+
+ // Colours
+
+ ///
+ /// Read an RGB colour as 3 bytes
+ ///
+ /// Binary reader
+ /// The colour which was read
+ public static Color ReadRGBColour(this BinaryReader br)
+ {
+ return Color.FromArgb(255, br.ReadByte(), br.ReadByte(), br.ReadByte());
+ }
+
+ ///
+ /// Write an RGB colour as 3 bytes
+ ///
+ /// Binary writer
+ /// The colour to write
+ public static void WriteRGBColour(this BinaryWriter bw, Color c)
+ {
+ bw.Write(c.R);
+ bw.Write(c.G);
+ bw.Write(c.B);
+ }
+
+ ///
+ /// Read an RGBA colour as 4 bytes
+ ///
+ /// Binary reader
+ /// The colour which was read
+ public static Color ReadRGBAColour(this BinaryReader br)
+ {
+ var r = br.ReadByte();
+ var g = br.ReadByte();
+ var b = br.ReadByte();
+ var a = br.ReadByte();
+ return Color.FromArgb(a, r, g, b);
+ }
+
+ ///
+ /// Write an RGBA colour as 4 bytes
+ ///
+ /// Binary writer
+ /// The colour to write
+ public static void WriteRGBAColour(this BinaryWriter bw, Color c)
+ {
+ bw.Write(c.R);
+ bw.Write(c.G);
+ bw.Write(c.B);
+ bw.Write(c.A);
+ }
+
+ public static Vector3[] ReadVector3Array(this BinaryReader br, int num)
+ {
+ var arr = new Vector3[num];
+ for (var i = 0; i < num; i++) arr[i] = br.ReadVector3();
+ return arr;
+ }
+
+ public static Vector3 ReadVector3(this BinaryReader br)
+ {
+ return new Vector3(
+ br.ReadSingle(),
+ br.ReadSingle(),
+ br.ReadSingle()
+ );
+ }
+
+ public static void WriteVector3(this BinaryWriter bw, Vector3 c)
+ {
+ bw.Write(c.X);
+ bw.Write(c.Y);
+ bw.Write(c.Z);
+ }
+
+ public static Plane ReadPlane(this BinaryReader br)
+ {
+ var a = ReadVector3(br);
+ var b = ReadVector3(br);
+ var c = ReadVector3(br);
+
+ var ab = b - a;
+ var ac = c - a;
+
+ var normal = ac.Cross(ab).Normalise();
+ var d = normal.Dot(a);
+
+ return new Plane(normal, d);
+ }
+
+ public static void WritePlane(this BinaryWriter bw, Vector3[] coords)
+ {
+ WriteVector3(bw, coords[0]);
+ WriteVector3(bw, coords[1]);
+ WriteVector3(bw, coords[2]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats/NumericsExtensions.cs b/Sledge.Formats/NumericsExtensions.cs
new file mode 100644
index 0000000..c8b7a62
--- /dev/null
+++ b/Sledge.Formats/NumericsExtensions.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Globalization;
+using System.Linq;
+using System.Numerics;
+
+namespace Sledge.Formats
+{
+ public static class NumericsExtensions
+ {
+ public const float Epsilon = 0.0001f;
+
+ // Vector2
+ public static Vector3 ToVector3(this Vector2 self)
+ {
+ return new Vector3(self, 0);
+ }
+
+ // Vector3
+ public static bool EquivalentTo(this Vector3 self, Vector3 test, float delta = Epsilon)
+ {
+ var xd = Math.Abs(self.X - test.X);
+ var yd = Math.Abs(self.Y - test.Y);
+ var zd = Math.Abs(self.Z - test.Z);
+ return xd < delta && yd < delta && zd < delta;
+ }
+
+ public static Vector3 Parse(string x, string y, string z, NumberStyles ns, IFormatProvider provider)
+ {
+ return new Vector3(float.Parse(x, ns, provider), float.Parse(y, ns, provider), float.Parse(z, ns, provider));
+ }
+
+ public static bool TryParse(string x, string y, string z, NumberStyles ns, IFormatProvider provider, out Vector3 vec)
+ {
+ if (float.TryParse(x, ns, provider, out var a) && float.TryParse(y, ns, provider, out var b) && float.TryParse(z, ns, provider, out var c))
+ {
+ vec = new Vector3(a, b, c);
+ return true;
+ }
+
+ vec = Vector3.Zero;
+ return false;
+ }
+
+ public static Vector3 Normalise(this Vector3 self) => Vector3.Normalize(self);
+ public static Vector3 Absolute(this Vector3 self) => Vector3.Abs(self);
+ public static float Dot(this Vector3 self, Vector3 other) => Vector3.Dot(self, other);
+ public static Vector3 Cross(this Vector3 self, Vector3 other) => Vector3.Cross(self, other);
+ public static Vector3 Round(this Vector3 self, int num = 8) => new Vector3((float) Math.Round(self.X, num), (float) Math.Round(self.Y, num), (float) Math.Round(self.Z, num));
+
+ public static Vector3 ClosestAxis(this Vector3 self)
+ {
+ // VHE prioritises the axes in order of X, Y, Z.
+ var norm = Vector3.Abs(self);
+
+ if (norm.X >= norm.Y && norm.X >= norm.Z) return Vector3.UnitX;
+ if (norm.Y >= norm.Z) return Vector3.UnitY;
+ return Vector3.UnitZ;
+ }
+
+ public static Precision.Vector3 ToPrecisionVector3(this Vector3 self)
+ {
+ return new Precision.Vector3(self.X, self.Y, self.Z);
+ }
+
+ public static Vector2 ToVector2(this Vector3 self)
+ {
+ return new Vector2(self.X, self.Y);
+ }
+
+ // Vector4
+ public static Vector4 ToVector4(this Color self)
+ {
+ return new Vector4(self.R, self.G, self.B, self.A) / 255f;
+ }
+
+ // Color
+ public static Color ToColor(this Vector4 self)
+ {
+ var mul = self * 255;
+ return Color.FromArgb((byte) mul.W, (byte) mul.X, (byte) mul.Y, (byte) mul.Z);
+ }
+
+ public static Color ToColor(this Vector3 self)
+ {
+ var mul = self * 255;
+ return Color.FromArgb(255, (byte) mul.X, (byte) mul.Y, (byte) mul.Z);
+ }
+
+ // Matrix
+ public static Vector3 Transform(this Matrix4x4 self, Vector3 vector) => Vector3.Transform(vector, self);
+
+ // Plane
+ public static Plane PlaneFromVertices(IEnumerable vertices)
+ {
+ var verts = vertices.Take(3).ToList();
+ return PlaneFromVertices(verts[0], verts[1], verts[2]);
+ }
+
+ public static Plane PlaneFromVertices(Vector3 a, Vector3 b, Vector3 c)
+ {
+ var ab = b - a;
+ var ac = c - a;
+
+ var normal = ac.Cross(ab).Normalise();
+ var d = normal.Dot(a);
+
+ return new Plane(normal, d);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats/Precision/Plane.cs b/Sledge.Formats/Precision/Plane.cs
new file mode 100644
index 0000000..bbc00df
--- /dev/null
+++ b/Sledge.Formats/Precision/Plane.cs
@@ -0,0 +1,163 @@
+using System;
+
+namespace Sledge.Formats.Precision
+{
+ ///
+ /// Defines a plane in the form Ax + By + Cz + D = 0. Uses high-precision value types.
+ ///
+ public struct Plane
+ {
+ public Vector3 Normal { get; }
+ public double DistanceFromOrigin { get; }
+ public double A { get; }
+ public double B { get; }
+ public double C { get; }
+ public double D { get; }
+ public Vector3 PointOnPlane { get; }
+
+ public Plane(Vector3 p1, Vector3 p2, Vector3 p3)
+ {
+ var ab = p2 - p1;
+ var ac = p3 - p1;
+
+ Normal = ac.Cross(ab).Normalise();
+ DistanceFromOrigin = Normal.Dot(p1);
+ PointOnPlane = p1;
+
+ A = Normal.X;
+ B = Normal.Y;
+ C = Normal.Z;
+ D = -DistanceFromOrigin;
+ }
+
+ public Plane(Vector3 norm, Vector3 pointOnPlane)
+ {
+ Normal = norm.Normalise();
+ DistanceFromOrigin = Normal.Dot(pointOnPlane);
+ PointOnPlane = pointOnPlane;
+
+ A = Normal.X;
+ B = Normal.Y;
+ C = Normal.Z;
+ D = -DistanceFromOrigin;
+ }
+
+ public Plane(Vector3 norm, double distanceFromOrigin)
+ {
+ Normal = norm.Normalise();
+ DistanceFromOrigin = distanceFromOrigin;
+ PointOnPlane = Normal * DistanceFromOrigin;
+
+ A = Normal.X;
+ B = Normal.Y;
+ C = Normal.Z;
+ D = -DistanceFromOrigin;
+ }
+
+ /// Finds if the given point is above, below, or on the plane.
+ /// The Vector3 to test
+ /// Tolerance value
+ ///
+ /// value == -1 if Vector3 is below the plane
+ /// value == 1 if Vector3 is above the plane
+ /// value == 0 if Vector3 is on the plane.
+ ///
+ public int OnPlane(Vector3 co, double epsilon = 0.0001d)
+ {
+ //eval (s = Ax + By + Cz + D) at point (x,y,z)
+ //if s > 0 then point is "above" the plane (same side as normal)
+ //if s < 0 then it lies on the opposite side
+ //if s = 0 then the point (x,y,z) lies on the plane
+ var res = EvalAtPoint(co);
+ if (Math.Abs(res) < epsilon) return 0;
+ if (res < 0) return -1;
+ return 1;
+ }
+
+ ///
+ /// Gets the point that the line intersects with this plane.
+ ///
+ /// The start of the line to intersect with
+ /// The end of the line to intersect with
+ /// Set to true to ignore the direction
+ /// of the plane and line when intersecting. Defaults to false.
+ /// Set to true to ignore the start and
+ /// end points of the line in the intersection. Defaults to false.
+ /// The point of intersection, or null if the line does not intersect
+ public Vector3? GetIntersectionPoint(Vector3 start, Vector3 end, bool ignoreDirection = false, bool ignoreSegment = false)
+ {
+ // http://softsurfer.com/Archive/algorithm_0104/algorithm_0104B.htm#Line%20Intersections
+ // http://paulbourke.net/geometry/planeline/
+
+ var dir = end - start;
+ var denominator = -Normal.Dot(dir);
+ var numerator = Normal.Dot(start - Normal * DistanceFromOrigin);
+ if (Math.Abs(denominator) < 0.00001d || (!ignoreDirection && denominator < 0)) return null;
+ var u = numerator / denominator;
+ if (!ignoreSegment && (u < 0 || u > 1)) return null;
+ return start + u * dir;
+ }
+
+ ///
+ /// Project a point into the space of this plane. I.e. Get the point closest
+ /// to the provided point that is on this plane.
+ ///
+ /// The point to project
+ /// The point projected onto this plane
+ public Vector3 Project(Vector3 point)
+ {
+ // http://www.gamedev.net/topic/262196-projecting-vector-onto-a-plane/
+ // Projected = Point - ((Point - PointOnPlane) . Normal) * Normal
+ return point - ((point - PointOnPlane).Dot(Normal)) * Normal;
+ }
+
+ public double EvalAtPoint(Vector3 co)
+ {
+ return A * co.X + B * co.Y + C * co.Z + D;
+ }
+
+ ///
+ /// Gets the axis closest to the normal of this plane
+ ///
+ /// Vector3.UnitX, Vector3.UnitY, or Vector3.UnitZ depending on the plane's normal
+ public Vector3 GetClosestAxisToNormal()
+ {
+ // VHE prioritises the axes in order of X, Y, Z.
+ var norm = Normal.Absolute();
+
+ if (norm.X >= norm.Y && norm.X >= norm.Z) return Vector3.UnitX;
+ if (norm.Y >= norm.Z) return Vector3.UnitY;
+ return Vector3.UnitZ;
+ }
+
+ public Plane Clone()
+ {
+ return new Plane(Normal, DistanceFromOrigin);
+ }
+
+ ///
+ /// Intersects three planes and gets the point of their intersection.
+ ///
+ /// The point that the planes intersect at, or null if they do not intersect at a point.
+ public static Vector3? Intersect(Plane p1, Plane p2, Plane p3)
+ {
+ // http://paulbourke.net/geometry/3planes/
+
+ var c1 = p2.Normal.Cross(p3.Normal);
+ var c2 = p3.Normal.Cross(p1.Normal);
+ var c3 = p1.Normal.Cross(p2.Normal);
+
+ var denom = p1.Normal.Dot(c1);
+ if (denom < 0.00001d) return null; // No intersection, planes must be parallel
+
+ var numer = (-p1.D * c1) + (-p2.D * c2) + (-p3.D * c3);
+ return numer / denom;
+ }
+
+ public bool EquivalentTo(Plane other, double delta = 0.0001d)
+ {
+ return Normal.EquivalentTo(other.Normal, delta)
+ && Math.Abs(DistanceFromOrigin - other.DistanceFromOrigin) < delta;
+ }
+ }
+}
diff --git a/Sledge.Formats/Precision/PlaneClassification.cs b/Sledge.Formats/Precision/PlaneClassification.cs
new file mode 100644
index 0000000..1cd6c54
--- /dev/null
+++ b/Sledge.Formats/Precision/PlaneClassification.cs
@@ -0,0 +1,10 @@
+namespace Sledge.Formats.Precision
+{
+ public enum PlaneClassification
+ {
+ Front,
+ Back,
+ OnPlane,
+ Spanning
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats/Precision/Polygon.cs b/Sledge.Formats/Precision/Polygon.cs
new file mode 100644
index 0000000..84378cd
--- /dev/null
+++ b/Sledge.Formats/Precision/Polygon.cs
@@ -0,0 +1,166 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Sledge.Formats.Precision
+{
+ ///
+ /// Represents a coplanar, directed polygon with at least 3 vertices. Uses high-precision value types.
+ ///
+ public class Polygon
+ {
+ public IReadOnlyList Vertices { get; }
+
+ public Plane Plane => new Plane(Vertices[0], Vertices[1], Vertices[2]);
+ public Vector3 Origin => Vertices.Aggregate(Vector3.Zero, (x, y) => x + y) / Vertices.Count;
+
+ ///
+ /// Creates a polygon from a list of points
+ ///
+ /// The vertices of the polygon
+ public Polygon(IEnumerable vertices)
+ {
+ Vertices = vertices.ToList();
+ }
+
+ ///
+ /// Creates a polygon from a plane and a radius.
+ /// Expands the plane to the radius size to create a large polygon with 4 vertices.
+ ///
+ /// The polygon plane
+ /// The polygon radius
+ public Polygon(Plane plane, double radius = 1000000d)
+ {
+ // Get aligned up and right axes to the plane
+ var direction = plane.GetClosestAxisToNormal();
+ var tempV = direction == Vector3.UnitZ ? -Vector3.UnitY : -Vector3.UnitZ;
+ var up = tempV.Cross(plane.Normal).Normalise();
+ var right = plane.Normal.Cross(up).Normalise();
+
+ var verts = new List
+ {
+ plane.PointOnPlane + right + up, // Top right
+ plane.PointOnPlane - right + up, // Top left
+ plane.PointOnPlane - right - up, // Bottom left
+ plane.PointOnPlane + right - up, // Bottom right
+ };
+
+ var origin = verts.Aggregate(Vector3.Zero, (x, y) => x + y) / verts.Count;
+ Vertices = verts.Select(x => (x - origin).Normalise() * radius + origin).ToList();
+ }
+
+ public PlaneClassification ClassifyAgainstPlane(Plane p)
+ {
+ var count = Vertices.Count;
+ var front = 0;
+ var back = 0;
+ var onplane = 0;
+
+ foreach (var t in Vertices)
+ {
+ var test = p.OnPlane(t);
+
+ // Vertices on the plane are both in front and behind the plane in this context
+ if (test <= 0) back++;
+ if (test >= 0) front++;
+ if (test == 0) onplane++;
+ }
+
+ if (onplane == count) return PlaneClassification.OnPlane;
+ if (front == count) return PlaneClassification.Front;
+ if (back == count) return PlaneClassification.Back;
+ return PlaneClassification.Spanning;
+ }
+
+ ///
+ /// Splits this polygon by a clipping plane, returning the back and front planes.
+ /// The original polygon is not modified.
+ ///
+ /// The clipping plane
+ /// The back polygon
+ /// The front polygon
+ /// True if the split was successful
+ public bool Split(Plane clip, out Polygon back, out Polygon front)
+ {
+ return Split(clip, out back, out front, out _, out _);
+ }
+
+ ///
+ /// Splits this polygon by a clipping plane, returning the back and front planes.
+ /// The original polygon is not modified.
+ ///
+ /// The clipping plane
+ /// The back polygon
+ /// The front polygon
+ /// If the polygon rests on the plane and points backward, this will not be null
+ /// If the polygon rests on the plane and points forward, this will not be null
+ /// True if the split was successful
+ public bool Split(Plane clip, out Polygon back, out Polygon front, out Polygon coplanarBack, out Polygon coplanarFront)
+ {
+ const double epsilon = NumericsExtensions.Epsilon;
+
+ var distances = Vertices.Select(clip.EvalAtPoint).ToList();
+
+ int cb = 0, cf = 0;
+ for (var i = 0; i < distances.Count; i++)
+ {
+ if (distances[i] < -epsilon) cb++;
+ else if (distances[i] > epsilon) cf++;
+ else distances[i] = 0;
+ }
+
+ // Check non-spanning cases
+ if (cb == 0 && cf == 0)
+ {
+ // Co-planar
+ back = front = coplanarBack = coplanarFront = null;
+ if (Plane.Normal.Dot(clip.Normal) > 0) coplanarFront = this;
+ else coplanarBack = this;
+ return false;
+ }
+ else if (cb == 0)
+ {
+ // All vertices in front
+ back = coplanarBack = coplanarFront = null;
+ front = this;
+ return false;
+ }
+ else if (cf == 0)
+ {
+ // All vertices behind
+ front = coplanarBack = coplanarFront = null;
+ back = this;
+ return false;
+ }
+
+ // Get the new front and back vertices
+ var backVerts = new List();
+ var frontVerts = new List();
+
+ for (var i = 0; i < Vertices.Count; i++)
+ {
+ var j = (i + 1) % Vertices.Count;
+
+ Vector3 s = Vertices[i], e = Vertices[j];
+ double sd = distances[i], ed = distances[j];
+
+ if (sd <= 0) backVerts.Add(s);
+ if (sd >= 0) frontVerts.Add(s);
+
+ if ((sd < 0 && ed > 0) || (ed < 0 && sd > 0))
+ {
+ var t = sd / (sd - ed);
+ var intersect = s * (1 - t) + e * t;
+
+ backVerts.Add(intersect);
+ frontVerts.Add(intersect);
+ }
+ }
+
+ back = new Polygon(backVerts.Select(x => new Vector3(x.X, x.Y, x.Z)));
+ front = new Polygon(frontVerts.Select(x => new Vector3(x.X, x.Y, x.Z)));
+ coplanarBack = coplanarFront = null;
+
+ return true;
+ }
+ }
+}
diff --git a/Sledge.Formats/Precision/Polyhedron.cs b/Sledge.Formats/Precision/Polyhedron.cs
new file mode 100644
index 0000000..0ece682
--- /dev/null
+++ b/Sledge.Formats/Precision/Polyhedron.cs
@@ -0,0 +1,92 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Sledge.Formats.Precision
+{
+ ///
+ /// Represents a convex polyhedron with at least 4 sides. Uses high-precision value types.
+ ///
+ public class Polyhedron
+ {
+ public IReadOnlyList Polygons { get; }
+
+ public Vector3 Origin => Polygons.Aggregate(Vector3.Zero, (x, y) => x + y.Origin) / Polygons.Count;
+
+ ///
+ /// Creates a polyhedron from a list of polygons which are assumed to be valid.
+ ///
+ public Polyhedron(IEnumerable polygons)
+ {
+ Polygons = polygons.ToList();
+ }
+
+ ///
+ /// Creates a polyhedron by intersecting a set of at least 4 planes.
+ ///
+ public Polyhedron(IEnumerable planes)
+ {
+ var polygons = new List();
+
+ var list = planes.ToList();
+ for (var i = 0; i < list.Count; i++)
+ {
+ // Split the polygon by all the other planes
+ var poly = new Polygon(list[i]);
+ for (var j = 0; j < list.Count; j++)
+ {
+ if (i != j && poly.Split(list[j], out var back, out _))
+ {
+ poly = back;
+ }
+ }
+ polygons.Add(poly);
+ }
+
+ // Ensure all the faces point outwards
+ var origin = polygons.Aggregate(Vector3.Zero, (x, y) => x + y.Origin) / polygons.Count;
+ for (var i = 0; i < polygons.Count; i++)
+ {
+ var face = polygons[i];
+ if (face.Plane.OnPlane(origin) >= 0) polygons[i] = new Polygon(face.Vertices.Reverse());
+ }
+
+ Polygons = polygons;
+ }
+
+ ///
+ /// Splits this polyhedron into two polyhedron by intersecting against a plane.
+ ///
+ /// The splitting plane
+ /// The back side of the polyhedron
+ /// The front side of the polyhedron
+ /// True if the plane splits the polyhedron, false if the plane doesn't intersect
+ public bool Split(Plane plane, out Polyhedron back, out Polyhedron front)
+ {
+ back = front = null;
+
+ // Check that this solid actually spans the plane
+ var classify = Polygons.Select(x => x.ClassifyAgainstPlane(plane)).Distinct().ToList();
+ if (classify.All(x => x != PlaneClassification.Spanning))
+ {
+ if (classify.Any(x => x == PlaneClassification.Back)) back = this;
+ else if (classify.Any(x => x == PlaneClassification.Front)) front = this;
+ return false;
+ }
+
+ var backPlanes = new List { plane };
+ var frontPlanes = new List { new Plane(-plane.Normal, -plane.DistanceFromOrigin) };
+
+ foreach (var face in Polygons)
+ {
+ var classification = face.ClassifyAgainstPlane(plane);
+ if (classification != PlaneClassification.Back) frontPlanes.Add(face.Plane);
+ if (classification != PlaneClassification.Front) backPlanes.Add(face.Plane);
+ }
+
+ back = new Polyhedron(backPlanes);
+ front = new Polyhedron(frontPlanes);
+
+ return true;
+ }
+ }
+}
diff --git a/Sledge.Formats/Precision/Vector3.cs b/Sledge.Formats/Precision/Vector3.cs
new file mode 100644
index 0000000..a8b7d23
--- /dev/null
+++ b/Sledge.Formats/Precision/Vector3.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Globalization;
+
+namespace Sledge.Formats.Precision
+{
+ ///
+ /// A 3-dimensional immutable vector that uses high-precision value types.
+ ///
+ [Serializable]
+ public struct Vector3
+ {
+ public static readonly Vector3 MaxValue = new Vector3(double.MaxValue, double.MaxValue, double.MaxValue);
+ public static readonly Vector3 MinValue = new Vector3(double.MinValue, double.MinValue, double.MinValue);
+ public static readonly Vector3 Zero = new Vector3(0, 0, 0);
+ public static readonly Vector3 One = new Vector3(1, 1, 1);
+ public static readonly Vector3 UnitX = new Vector3(1, 0, 0);
+ public static readonly Vector3 UnitY = new Vector3(0, 1, 0);
+ public static readonly Vector3 UnitZ = new Vector3(0, 0, 1);
+
+ public double X { get; }
+ public double Y { get; }
+ public double Z { get; }
+
+ public Vector3(double x, double y, double z)
+ {
+ X = x;
+ Y = y;
+ Z = z;
+ }
+
+ public bool EquivalentTo(Vector3 test, double delta = 0.0001d)
+ {
+ var xd = Math.Abs(X - test.X);
+ var yd = Math.Abs(Y - test.Y);
+ var zd = Math.Abs(Z - test.Z);
+ return (xd < delta) && (yd < delta) && (zd < delta);
+ }
+
+ public double Dot(Vector3 c)
+ {
+ return X * c.X + Y * c.Y + Z * c.Z;
+ }
+
+ public Vector3 Cross(Vector3 that)
+ {
+ var xv = Y * that.Z - Z * that.Y;
+ var yv = Z * that.X - X * that.Z;
+ var zv = X * that.Y - Y * that.X;
+ return new Vector3(xv, yv, zv);
+ }
+
+ public Vector3 Round(int num = 8)
+ {
+ return new Vector3(Math.Round(X, num), Math.Round(Y, num), Math.Round(Z, num));
+ }
+
+ public Vector3 Snap(double snapTo)
+ {
+ return new Vector3(
+ Math.Round(X / snapTo) * snapTo,
+ Math.Round(Y / snapTo) * snapTo,
+ Math.Round(Z / snapTo) * snapTo
+ );
+ }
+
+ public double Length()
+ {
+ return (double) Math.Sqrt((double) LengthSquared());
+ }
+
+ public double LengthSquared()
+ {
+ return X * X + Y * Y + Z * Z;
+ }
+
+ public Vector3 Normalise()
+ {
+ var len = Length();
+ return Math.Abs(len) < 0.0001 ? new Vector3(0, 0, 0) : new Vector3(X / len, Y / len, Z / len);
+ }
+
+ public Vector3 Absolute()
+ {
+ return new Vector3(Math.Abs(X), Math.Abs(Y), Math.Abs(Z));
+ }
+
+ public static Vector3 operator +(Vector3 c1, Vector3 c2)
+ {
+ return new Vector3(c1.X + c2.X, c1.Y + c2.Y, c1.Z + c2.Z);
+ }
+
+ public static Vector3 operator -(Vector3 c1, Vector3 c2)
+ {
+ return new Vector3(c1.X - c2.X, c1.Y - c2.Y, c1.Z - c2.Z);
+ }
+
+ public static Vector3 operator -(Vector3 c1)
+ {
+ return new Vector3(-c1.X, -c1.Y, -c1.Z);
+ }
+
+ public static Vector3 operator /(Vector3 c, double f)
+ {
+ return Math.Abs(f) < 0.0001 ? new Vector3(0, 0, 0) : new Vector3(c.X / f, c.Y / f, c.Z / f);
+ }
+
+ public static Vector3 operator *(Vector3 c, double f)
+ {
+ return new Vector3(c.X * f, c.Y * f, c.Z * f);
+ }
+
+ public static Vector3 operator *(Vector3 c, Vector3 f)
+ {
+ return new Vector3(c.X * f.X, c.Y * f.Y, c.Z * f.Z);
+ }
+
+ public static Vector3 operator /(Vector3 c, Vector3 f)
+ {
+ return new Vector3(c.X / f.X, c.Y / f.Y, c.Z / f.Z);
+ }
+
+ public static Vector3 operator *(double f, Vector3 c)
+ {
+ return c * f;
+ }
+
+ public bool Equals(Vector3 other)
+ {
+ return X == other.X && Y == other.Y && Z == other.Z;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ return obj is Vector3 other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = X.GetHashCode();
+ hashCode = (hashCode * 397) ^ Y.GetHashCode();
+ hashCode = (hashCode * 397) ^ Z.GetHashCode();
+ return hashCode;
+ }
+ }
+
+ public static bool operator ==(Vector3 left, Vector3 right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(Vector3 left, Vector3 right)
+ {
+ return !left.Equals(right);
+ }
+
+ public override string ToString()
+ {
+ return "(" + X.ToString("0.0000", CultureInfo.InvariantCulture) + " " + Y.ToString("0.0000", CultureInfo.InvariantCulture) + " " + Z.ToString("0.0000", CultureInfo.InvariantCulture) + ")";
+ }
+
+ public Vector3 Clone()
+ {
+ return new Vector3(X, Y, Z);
+ }
+
+ public static Vector3 Parse(string x, string y, string z)
+ {
+ const NumberStyles ns = NumberStyles.Float;
+ return new Vector3(double.Parse(x, ns, CultureInfo.InvariantCulture), double.Parse(y, ns, CultureInfo.InvariantCulture), double.Parse(z, ns, CultureInfo.InvariantCulture));
+ }
+
+ public System.Numerics.Vector3 ToStandardVector3()
+ {
+ const int rounding = 2;
+ return new System.Numerics.Vector3((float) Math.Round(X, rounding), (float) Math.Round(Y, rounding), (float) Math.Round(Z, rounding));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats/Sledge.Formats.csproj b/Sledge.Formats/Sledge.Formats.csproj
new file mode 100644
index 0000000..dcbbb66
--- /dev/null
+++ b/Sledge.Formats/Sledge.Formats.csproj
@@ -0,0 +1,11 @@
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
diff --git a/Sledge.Formats/StringExtensions.cs b/Sledge.Formats/StringExtensions.cs
new file mode 100644
index 0000000..5bce54c
--- /dev/null
+++ b/Sledge.Formats/StringExtensions.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+
+namespace Sledge.Formats
+{
+ ///
+ /// Common string extension methods
+ ///
+ public static class StringExtensions
+ {
+ ///
+ /// Split a string by a delimiter without splitting sequences within quotes.
+ ///
+ /// The string to split
+ /// The characters to split by. Defaults to space and tab characters if not specified.
+ /// The character which indicates the start or end of a quote
+ /// The split result, with split characters removed
+ public static string[] SplitWithQuotes(this string line, char[] splitCharacters = null, char quoteChar = '"')
+ {
+ if (splitCharacters == null) splitCharacters = new[] { ' ', '\t' };
+
+ var result = new List();
+
+ int i;
+ for (i = 0; i < line.Length; i++)
+ {
+ var split = line.IndexOfAny(splitCharacters, i);
+ var quote = line.IndexOf(quoteChar, i);
+
+ if (split < 0) split = line.Length;
+ if (quote < 0) quote = line.Length;
+
+ if (quote < split)
+ {
+ if (quote > i) result.Add(line.Substring(i, quote));
+ var nextQuote = line.IndexOf(quoteChar, quote + 1);
+ if (nextQuote < 0) nextQuote = line.Length;
+ result.Add(line.Substring(quote + 1, nextQuote - quote - 1));
+ i = nextQuote;
+ }
+ else
+ {
+ if (split > i) result.Add(line.Substring(i, split - i));
+ i = split;
+ }
+ }
+ return result.ToArray();
+ }
+ }
+}
diff --git a/Sledge.Formats/Valve/Liblist.cs b/Sledge.Formats/Valve/Liblist.cs
new file mode 100644
index 0000000..8807ee4
--- /dev/null
+++ b/Sledge.Formats/Valve/Liblist.cs
@@ -0,0 +1,260 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Sledge.Formats.Valve
+{
+ ///
+ /// The liblist.gam file, used by Goldsource games and mods.
+ ///
+ public class Liblist : Dictionary
+ {
+ #region Properties
+
+ ///
+ /// The name of the game/mod.
+ ///
+ public string Game
+ {
+ get => TryGetValue("game", out var s) ? s : null;
+ set => this["game"] = value;
+ }
+
+ ///
+ /// A path to an uncompressed, 24bit, 16x16 resolution TGA file, relative to the mod directory, with no file extension.
+ ///
+ public string Icon
+ {
+ get => TryGetValue("icon", out var s) ? s : null;
+ set => this["icon"] = value;
+ }
+
+ ///
+ /// The name of the team or person who created this game/mod.
+ ///
+ public string Developer
+ {
+ get => TryGetValue("developer", out var s) ? s : null;
+ set => this["developer"] = value;
+ }
+
+ ///
+ /// A URL to the developer's website.
+ ///
+ public string DeveloperUrl
+ {
+ get => TryGetValue("developer_url", out var s) ? s : null;
+ set => this["developer_url"] = value;
+ }
+
+ ///
+ /// A URL to the game/mod's manual.
+ ///
+ public string Manual
+ {
+ get => TryGetValue("manual", out var s) ? s : null;
+ set => this["manual"] = value;
+ }
+
+ ///
+ /// The path to the game's DLL file on Windows, relative to the mod directory. e.g. "dlls\hl.dll"
+ ///
+ public string GameDll
+ {
+ get => TryGetValue("gamedll", out var s) ? s : null;
+ set => this["gamedll"] = value;
+ }
+
+ ///
+ /// The path to the game's DLL file on Linux, relative to the mod directory. e.g. "dlls/hl.so"
+ ///
+ public string GameDllLinux
+ {
+ get => TryGetValue("gamedll_linux", out var s) ? s : null;
+ set => this["gamedll_linux"] = value;
+ }
+
+ ///
+ /// The path to the game's DLL file on OSX, relative to the mod directory. e.g. "dlls/hl.dylib"
+ ///
+ public string GameDllOsx
+ {
+ get => TryGetValue("gamedll_osx", out var s) ? s : null;
+ set => this["gamedll_osx"] = value;
+ }
+
+ ///
+ /// Enable VAC security.
+ ///
+ public bool? Secure
+ {
+ get => TryGetValue("secure", out var s) && Int32.TryParse(s, out var b) ? b == 1 : (bool?)null;
+ set => this["secure"] = !value.HasValue ? null : value.Value ? "1" : "0";
+ }
+
+ ///
+ /// If this is a server-only mod.
+ ///
+ public bool? ServerOnly
+ {
+ get => TryGetValue("svonly", out var s) && Int32.TryParse(s, out var b) ? b == 1 : (bool?)null;
+ set => this["svonly"] = !value.HasValue ? null : value.Value ? "1" : "0";
+ }
+
+ ///
+ /// If the mod requires a new client.dll
+ ///
+ public bool? ClientDllRequired
+ {
+ get => TryGetValue("cldll", out var s) && Int32.TryParse(s, out var b) ? b == 1 : (bool?)null;
+ set => this["cldll"] = !value.HasValue ? null : value.Value ? "1" : "0";
+ }
+
+ ///
+ /// The type of game/mod. Usually "singleplayer_only" or "multiplayer_only".
+ ///
+ public string Type
+ {
+ get => TryGetValue("type", out var s) ? s : null;
+ set => this["type"] = value;
+ }
+
+ ///
+ /// The name of the map to load when the player starts a new game, without the extension. e.g. "c0a0"
+ ///
+ public string StartingMap
+ {
+ get => TryGetValue("startmap", out var s) ? s : null;
+ set => this["startmap"] = value;
+ }
+
+ ///
+ /// The name of the map to load when the player starts the training map, without the extension. e.g. "t0a0"
+ ///
+ public string TrainingMap
+ {
+ get => TryGetValue("trainmap", out var s) ? s : null;
+ set => this["trainmap"] = value;
+ }
+
+ ///
+ /// The name of the multiplayer entity class.
+ ///
+ public string MultiplayerEntity
+ {
+ get => TryGetValue("mpentity", out var s) ? s : null;
+ set => this["mpentity"] = value;
+ }
+
+ ///
+ /// Do not show maps with names containing this string in create server dialogue.
+ ///
+ public string MultiplayerFilter
+ {
+ get => TryGetValue("mpfilter", out var s) ? s : null;
+ set => this["mpfilter"] = value;
+ }
+
+ ///
+ /// The mod/game to base this mod/game off of. e.g. "cstrike"
+ ///
+ public string FallbackDirectory
+ {
+ get => TryGetValue("fallback_dir", out var s) ? s : null;
+ set => this["fallback_dir"] = value;
+ }
+
+ ///
+ /// True to load maps from the base game/mod.
+ ///
+ public bool? FallbackMaps
+ {
+ get => TryGetValue("fallback_maps", out var s) && Int32.TryParse(s, out var b) ? b == 1 : (bool?)null;
+ set => this["fallback_maps"] = !value.HasValue ? null : value.Value ? "1" : "0";
+ }
+
+ ///
+ /// Prevent the player model from being anything except player.mdl.
+ ///
+ public bool? NoModels
+ {
+ get => TryGetValue("nomodels", out var s) && Int32.TryParse(s, out var b) ? b == 1 : (bool?)null;
+ set => this["nomodels"] = !value.HasValue ? null : value.Value ? "1" : "0";
+ }
+
+ ///
+ /// Don't allow HD models.
+ ///
+ public bool? NoHighDefinitionModels
+ {
+ get => TryGetValue("nohimodels", out var s) && Int32.TryParse(s, out var b) ? b == 1 : (bool?)null;
+ set => this["nohimodels"] = !value.HasValue ? null : value.Value ? "1" : "0";
+ }
+
+ ///
+ /// Use detailed textures.
+ ///
+ public bool? DetailedTextures
+ {
+ get => TryGetValue("detailed_textures", out var s) && Int32.TryParse(s, out var b) ? b == 1 : (bool?)null;
+ set => this["detailed_textures"] = !value.HasValue ? null : value.Value ? "1" : "0";
+ }
+
+ #endregion
+
+ public Liblist()
+ {
+
+ }
+
+ public Liblist(Stream stream)
+ {
+ using (var sr = new StreamReader(stream, Encoding.ASCII, false, 1024, true))
+ {
+ string line;
+ while ((line = sr.ReadLine()) != null)
+ {
+ var c = line.IndexOf("//", StringComparison.Ordinal);
+ if (c >= 0) line = line.Substring(0, c);
+ line = line.Trim();
+
+ if (String.IsNullOrWhiteSpace(line)) continue;
+
+ c = line.IndexOf(' ');
+ if (c < 0) continue;
+
+ var key = line.Substring(0, c).ToLower();
+ if (String.IsNullOrWhiteSpace(key)) continue;
+
+ var value = line.Substring(c + 1);
+ if (value[0] != '"' || value[value.Length - 1] != '"') continue;
+
+ value = value.Substring(1, value.Length - 2).Trim();
+ this[key] = value;
+ }
+ }
+ }
+
+ public void Write(Stream stream)
+ {
+ using (var sr = new StreamWriter(stream, Encoding.ASCII, 1024, true))
+ {
+ foreach (var kv in this)
+ {
+ sr.WriteLine($"{kv.Key} \"{kv.Value}\"");
+ }
+ }
+ }
+
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+ foreach (var kv in this)
+ {
+ sb.AppendLine($"{kv.Key} \"{kv.Value}\"");
+ }
+ return sb.ToString();
+ }
+ }
+}
diff --git a/Sledge.Formats/Valve/SerialisedObject.cs b/Sledge.Formats/Valve/SerialisedObject.cs
new file mode 100644
index 0000000..6c485e8
--- /dev/null
+++ b/Sledge.Formats/Valve/SerialisedObject.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace Sledge.Formats.Valve
+{
+ ///
+ /// Represents a serialised object with basic features similar to XML.
+ ///
+ [Serializable]
+ public class SerialisedObject : ISerializable
+ {
+ ///
+ /// The name of the object
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// The properties (or attributes) of the object
+ ///
+ public List> Properties { get; set; }
+
+ ///
+ /// A list of child objects
+ ///
+ public List Children { get; set; }
+
+ public SerialisedObject(string name)
+ {
+ Name = name;
+ Properties = new List>();
+ Children = new List();
+ }
+
+ protected SerialisedObject(SerializationInfo info, StreamingContext context)
+ {
+ Name = info.GetString("Name");
+ Properties = (List>) info.GetValue("Properties", typeof(List>));
+ Children = (List) info.GetValue("Children", typeof(List));
+ }
+
+ public void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue("Name", Name);
+ info.AddValue("Properties", Properties);
+ info.AddValue("Children", Children);
+ }
+ }
+}
diff --git a/Sledge.Formats/Valve/SerialisedObjectExtensions.cs b/Sledge.Formats/Valve/SerialisedObjectExtensions.cs
new file mode 100644
index 0000000..cf1a862
--- /dev/null
+++ b/Sledge.Formats/Valve/SerialisedObjectExtensions.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Globalization;
+using System.Linq;
+
+namespace Sledge.Formats.Valve
+{
+ ///
+ /// Common extensions for serialised objects
+ ///
+ public static class SerialisedObjectExtensions
+ {
+ ///
+ /// Set a property value for a serialised object. The value will be converted with a type converter, if one exists.
+ ///
+ /// Value type
+ /// The serialised object
+ /// The property key to set
+ /// The value to set
+ /// True to replace any properties with the same key
+ public static void Set(this SerialisedObject so, string key, T value, bool replace = true)
+ {
+ var conv = TypeDescriptor.GetConverter(typeof(T));
+ var v = conv.ConvertToString(null, CultureInfo.InvariantCulture, value);
+ if (replace) so.Properties.RemoveAll(s => s.Key == key);
+ so.Properties.Add(new KeyValuePair(key, v));
+ }
+
+ ///
+ /// Get a property value from a serialised object. The value will be converted with a type converter, if one exists.
+ ///
+ /// Value type
+ /// The serialised object
+ /// The property key to get
+ /// The default value to use if the key doesn't exists, or couldn't be converted
+ /// The property value, or the default value if the key wasn't found
+ public static T Get(this SerialisedObject so, string key, T defaultValue = default(T))
+ {
+ var match = so.Properties.Where(x => x.Key == key).ToList();
+ if (!match.Any()) return defaultValue;
+ try
+ {
+ var val = match[0].Value;
+ var conv = TypeDescriptor.GetConverter(typeof(T));
+ return (T) conv.ConvertFromString(null, CultureInfo.InvariantCulture, val);
+ }
+ catch
+ {
+ return defaultValue;
+ }
+ }
+
+ ///
+ /// Set a property value to a colour
+ ///
+ /// The serialised object
+ /// The property key to set
+ /// The value to set
+ public static void SetColor(this SerialisedObject so, string key, Color color)
+ {
+ var r = Convert.ToString(color.R, CultureInfo.InvariantCulture);
+ var g = Convert.ToString(color.G, CultureInfo.InvariantCulture);
+ var b = Convert.ToString(color.B, CultureInfo.InvariantCulture);
+ Set(so, key, $"{r} {g} {b}");
+ }
+
+ ///
+ /// Get a colour property from the serialised object
+ ///
+ /// The serialised object
+ /// The property key to get
+ /// The property value as a colour
+ public static Color GetColor(this SerialisedObject so, string key)
+ {
+ var str = Get(so, key) ?? "";
+ var spl = str.Split(' ');
+ if (spl.Length != 3) spl = new[] {"0", "0", "0"};
+ byte.TryParse(spl[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var r);
+ byte.TryParse(spl[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var g);
+ byte.TryParse(spl[2], NumberStyles.Any, CultureInfo.InvariantCulture, out var b);
+ return Color.FromArgb(255, r, g, b);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats/Valve/SerialisedObjectFormatter.cs b/Sledge.Formats/Valve/SerialisedObjectFormatter.cs
new file mode 100644
index 0000000..0d693b5
--- /dev/null
+++ b/Sledge.Formats/Valve/SerialisedObjectFormatter.cs
@@ -0,0 +1,202 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Sledge.Formats.Valve
+{
+ ///
+ /// Handles serialisation of objects using Valve's common definition format.
+ ///
+ public class SerialisedObjectFormatter
+ {
+ ///
+ /// Serialise an array of objects
+ ///
+ /// The stream to serialise into
+ /// The objects to serialise
+ public void Serialize(Stream serializationStream, params SerialisedObject[] objects)
+ {
+ Serialize(serializationStream, objects.AsEnumerable());
+ }
+
+ ///
+ /// Serialise an array of objects
+ ///
+ /// The stream to serialise into
+ /// The objects to serialise
+ public void Serialize(Stream serializationStream, IEnumerable objects)
+ {
+ using (var writer = new StreamWriter(serializationStream, Encoding.UTF8, 1024, true))
+ {
+ foreach (var obj in objects.Where(x => x != null))
+ {
+ Print(obj, writer);
+ }
+ }
+ }
+
+ ///
+ /// Deserialise an array of objects from a stream
+ ///
+ /// The stream to deserialise from
+ /// The deserialised objects
+ public IEnumerable Deserialize(Stream serializationStream)
+ {
+ using (var reader = new StreamReader(serializationStream, Encoding.UTF8, true, 1024, true))
+ {
+ return Parse(reader);
+ }
+ }
+
+ #region Printer
+
+ ///
+ /// Ensure a string doesn't exceed a length limit.
+ ///
+ /// The string to check
+ /// The length limit
+ /// The string, truncated to the limit if it was exceeded
+ private static string LengthLimit(string str, int limit)
+ {
+ return str.Length >= limit ? str.Substring(0, limit - 1) : str;
+ }
+
+ ///
+ /// Print the structure to a stream
+ ///
+ /// The object to print
+ /// The output stream to write to
+ /// The number of tabs to indent this value to
+ private static void Print(SerialisedObject obj, TextWriter tw, int tabs = 0)
+ {
+ var preTabStr = new string(' ', tabs * 4);
+ var postTabStr = new string(' ', (tabs + 1) * 4);
+ tw.Write(preTabStr);
+ tw.WriteLine(obj.Name);
+ tw.Write(preTabStr);
+ tw.WriteLine("{");
+ foreach (var kv in obj.Properties)
+ {
+ tw.Write(postTabStr);
+ tw.Write('"');
+ tw.Write(LengthLimit(kv.Key, 1024));
+ tw.Write('"');
+ tw.Write(' ');
+ tw.Write('"');
+ tw.Write(LengthLimit((kv.Value ?? "").Replace('"', '`'), 1024));
+ tw.Write('"');
+ tw.WriteLine();
+ }
+ foreach (var child in obj.Children)
+ {
+ Print(child, tw, tabs + 1);
+ }
+ tw.Write(preTabStr);
+ tw.WriteLine("}");
+ }
+
+ #endregion
+
+ #region Parser
+
+ ///
+ /// Parse a structure from a stream
+ ///
+ /// The TextReader to parse from
+ /// The parsed structure
+ public static IEnumerable Parse(TextReader reader)
+ {
+ string line;
+ while ((line = CleanLine(reader.ReadLine())) != null)
+ {
+ if (ValidStructStartString(line))
+ {
+ yield return ParseStructure(reader, line);
+ }
+ }
+ }
+
+ ///
+ /// Remove comments and excess whitespace from a line
+ ///
+ /// The unclean line
+ /// The cleaned line
+ private static string CleanLine(string line)
+ {
+ if (line == null) return null;
+ var ret = line;
+ if (ret.Contains("//")) ret = ret.Substring(0, ret.IndexOf("//", StringComparison.Ordinal)); // Comments
+ return ret.Trim();
+ }
+
+ ///
+ /// Parse a structure, given the name of the structure
+ ///
+ /// The TextReader to read from
+ /// The structure's name
+ /// The parsed structure
+ private static SerialisedObject ParseStructure(TextReader reader, string name)
+ {
+ var spl = name.SplitWithQuotes();
+ var gs = new SerialisedObject(spl[0]);
+ string line;
+ if (spl.Length != 2 || spl[1] != "{")
+ {
+ do
+ {
+ line = CleanLine(reader.ReadLine());
+ } while (String.IsNullOrWhiteSpace(line));
+ if (line != "{")
+ {
+ return gs;
+ }
+ }
+ while ((line = CleanLine(reader.ReadLine())) != null)
+ {
+ if (line == "}") break;
+
+ if (ValidStructPropertyString(line)) ParseProperty(gs, line);
+ else if (ValidStructStartString(line)) gs.Children.Add(ParseStructure(reader, line));
+ }
+ return gs;
+ }
+
+ ///
+ /// Check if the given string is a valid structure name
+ ///
+ /// The string to test
+ /// True if this is a valid structure name, false otherwise
+ private static bool ValidStructStartString(string s)
+ {
+ if (string.IsNullOrEmpty(s)) return false;
+ var split = s.SplitWithQuotes();
+ return split.Length == 1 || (split.Length == 2 && split[1] == "{");
+ }
+
+ ///
+ /// Check if the given string is a valid property string in the format: "key" "value"
+ ///
+ /// The string to test
+ /// True if this is a valid property string, false otherwise
+ private static bool ValidStructPropertyString(string s)
+ {
+ if (string.IsNullOrEmpty(s)) return false;
+ var split = s.SplitWithQuotes();
+ return split.Length == 2;
+ }
+
+ ///
+ /// Parse a property string in the format: "key" "value", and add it to the structure
+ ///
+ /// The structure to add the property to
+ /// The property string to parse
+ private static void ParseProperty(SerialisedObject gs, string prop)
+ {
+ var split = prop.SplitWithQuotes();
+ gs.Properties.Add(new KeyValuePair(split[0], (split[1] ?? "").Replace('`', '"')));
+ }
+ #endregion
+ }
+}
\ No newline at end of file