diff --git a/KeepersCompound.LGS/Database/Chunk.cs b/KeepersCompound.LGS/Database/Chunk.cs new file mode 100644 index 0000000..791bf9f --- /dev/null +++ b/KeepersCompound.LGS/Database/Chunk.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Text; + +namespace KeepersCompound.LGS.Database; + +public struct ChunkHeader +{ + public string Name { get; set; } + public Version Version { get; set; } + + public ChunkHeader(BinaryReader reader) + { + Name = reader.ReadNullString(12); + Version = new(reader); + reader.ReadBytes(4); + } + + public readonly void Write(BinaryWriter writer) + { + var writeBytes = new byte[12]; + var nameBytes = Encoding.UTF8.GetBytes(Name); + nameBytes[..Math.Min(12, nameBytes.Length)].CopyTo(writeBytes, 0); + writer.Write(writeBytes); + Version.Write(writer); + writer.Write(new byte[4]); + } +} + +public interface IChunk +{ + public ChunkHeader Header { get; set; } + + public void Read(BinaryReader reader, DbFile.TableOfContents.Entry entry) + { + reader.BaseStream.Seek(entry.Offset, SeekOrigin.Begin); + + Header = new(reader); + ReadData(reader, entry); + } + + public void Write(BinaryWriter writer) + { + Header.Write(writer); + WriteData(writer); + } + + public abstract void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry); + public abstract void WriteData(BinaryWriter writer); +} + +public class GenericChunk : IChunk +{ + public ChunkHeader Header { get; set; } + public byte[] Data { get; set; } + + public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry) + { + Data = reader.ReadBytes((int)entry.Size); + } + + public void WriteData(BinaryWriter writer) + { + writer.Write(Data); + } +} + +public interface IMergable +{ + public abstract void Merge(IMergable other); +} \ No newline at end of file diff --git a/KeepersCompound.LGS/Database/Chunks/AiConverse.cs b/KeepersCompound.LGS/Database/Chunks/AiConverse.cs new file mode 100644 index 0000000..d91d416 --- /dev/null +++ b/KeepersCompound.LGS/Database/Chunks/AiConverse.cs @@ -0,0 +1,29 @@ +using System.IO; + +namespace KeepersCompound.LGS.Database.Chunks; + +class AiConverseChunk : IChunk +{ + public ChunkHeader Header { get; set; } + public uint Count; + public uint[] Ids; + + public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry) + { + Count = reader.ReadUInt32(); + Ids = new uint[Count]; + for (var i = 0; i < Count; i++) + { + Ids[i] = reader.ReadUInt32(); + } + } + + public void WriteData(BinaryWriter writer) + { + writer.Write(Count); + for (var i = 0; i < Count; i++) + { + writer.Write(Ids[i]); + } + } +} \ No newline at end of file diff --git a/KeepersCompound.LGS/Database/Chunks/AiRoomDb.cs b/KeepersCompound.LGS/Database/Chunks/AiRoomDb.cs new file mode 100644 index 0000000..abbebed --- /dev/null +++ b/KeepersCompound.LGS/Database/Chunks/AiRoomDb.cs @@ -0,0 +1,63 @@ +using System.IO; + +namespace KeepersCompound.LGS.Database.Chunks; + +class AiRoomDb : IChunk +{ + public struct Cell + { + int Size { get; set; } + uint[] CellIds { get; set; } + + public Cell(BinaryReader reader) + { + Size = reader.ReadInt32(); + CellIds = new uint[Size]; + for (var i = 0; i < Size; i++) + { + CellIds[i] = reader.ReadUInt32(); + } + } + + public readonly void Write(BinaryWriter writer) + { + writer.Write(Size); + for (var i = 0; i < Size; i++) + { + writer.Write(CellIds[i]); + } + } + } + + public ChunkHeader Header { get; set; } + public bool IsEmpty { get; set; } + public int ValidCellCount { get; set; } + public int CellCount { get; set; } + public Cell[] Cells { get; set; } + + public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry) + { + IsEmpty = reader.ReadBoolean(); + reader.ReadBytes(3); + ValidCellCount = reader.ReadInt32(); + CellCount = reader.ReadInt16(); + Cells = new Cell[CellCount]; + for (var i = 0; i < CellCount; i++) + { + Cells[i] = new Cell(reader); + } + } + + public void WriteData(BinaryWriter writer) + { + writer.Write(IsEmpty); + writer.Write(new byte[3]); + writer.Write(ValidCellCount); + writer.Write(CellCount); + for (var i = 0; i < CellCount; i++) + { + Cells[i].Write(writer); + } + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/KeepersCompound.LGS/Database/Chunks/BrList.cs b/KeepersCompound.LGS/Database/Chunks/BrList.cs new file mode 100644 index 0000000..5d65316 --- /dev/null +++ b/KeepersCompound.LGS/Database/Chunks/BrList.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Text; + +namespace KeepersCompound.LGS.Database.Chunks; + +public class BrList : IChunk +{ + // TODO: Add better handling of the different brush types + public record Brush + { + public enum Media + { + Room = 0xFB, + Flow = 0xFC, + Object = 0xFD, + Area = 0xFE, + Light = 0xFF, + FillSolid = 0x00, + FillAir = 0x01, + FillWater = 0x02, + Flood = 0x03, + Evaporate = 0x04, + SolidToWater = 0x05, + SolidToAir = 0x06, + AirToSolid = 0x07, + WaterToSolid = 0x08, + Blockable = 0x09, + }; + + public record TexInfo + { + public short id; + public ushort rot; + public short scale; + public ushort x; + public ushort y; + + public TexInfo(BinaryReader reader) + { + id = reader.ReadInt16(); + rot = reader.ReadUInt16(); + scale = reader.ReadInt16(); + x = reader.ReadUInt16(); + y = reader.ReadUInt16(); + } + }; + + public short id; + public short time; + public uint brushInfo; + public short textureId; + public Media media; + public sbyte flags; + public Vector3 position; + public Vector3 size; + public Vector3 angle; + public short currentFaceIndex; + public float gridLineSpacing; + public Vector3 gridPhaseShift; + public Vector3 gridOrientation; + public bool gridEnabled; + public byte numFaces; + public sbyte edgeSelected; + public sbyte pointSelected; + public sbyte useFlag; + public sbyte groupId; + public TexInfo[] txs; + + public Brush(BinaryReader reader) + { + id = reader.ReadInt16(); + time = reader.ReadInt16(); + brushInfo = reader.ReadUInt32(); + textureId = reader.ReadInt16(); + media = (Media)reader.ReadByte(); + flags = reader.ReadSByte(); + position = reader.ReadVec3(); + size = reader.ReadVec3(); + angle = reader.ReadRotation(); + currentFaceIndex = reader.ReadInt16(); + gridLineSpacing = reader.ReadSingle(); + gridPhaseShift = reader.ReadVec3(); + gridOrientation = reader.ReadRotation(); + gridEnabled = reader.ReadBoolean(); + numFaces = reader.ReadByte(); + edgeSelected = reader.ReadSByte(); + pointSelected = reader.ReadSByte(); + useFlag = reader.ReadSByte(); + groupId = reader.ReadSByte(); + reader.ReadBytes(4); + if ((sbyte)media >= 0) + { + txs = new TexInfo[numFaces]; + for (var i = 0; i < numFaces; i++) + { + txs[i] = new TexInfo(reader); + } + } + else + { + txs = Array.Empty(); + } + } + } + + public ChunkHeader Header { get; set; } + public List Brushes { get; set; } + + public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry) + { + Brushes = new List(); + while (reader.BaseStream.Position < entry.Offset + entry.Size + 24) + { + Brushes.Add(new Brush(reader)); + } + } + + public void WriteData(BinaryWriter writer) + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/KeepersCompound.LGS/Database/Chunks/GamFile.cs b/KeepersCompound.LGS/Database/Chunks/GamFile.cs new file mode 100644 index 0000000..9d6e9aa --- /dev/null +++ b/KeepersCompound.LGS/Database/Chunks/GamFile.cs @@ -0,0 +1,21 @@ +using System; +using System.IO; +using System.Text; + +namespace KeepersCompound.LGS.Database.Chunks; + +public class GamFile : IChunk +{ + public ChunkHeader Header { get; set; } + public string fileName; + + public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry) + { + fileName = reader.ReadNullString(256); + } + + public void WriteData(BinaryWriter writer) + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/KeepersCompound.LGS/Database/Chunks/Link.cs b/KeepersCompound.LGS/Database/Chunks/Link.cs new file mode 100644 index 0000000..b079e64 --- /dev/null +++ b/KeepersCompound.LGS/Database/Chunks/Link.cs @@ -0,0 +1,116 @@ + +using System.Collections.Generic; +using System.IO; + +namespace KeepersCompound.LGS.Database.Chunks; + +public record LinkId +{ + private readonly uint _data; + + public LinkId(uint data) + { + _data = data; + } + + public uint GetId() + { + return _data & 0xFFFF; + } + + public bool IsConcrete() + { + return (_data & 0xF0000) != 0; + } + + public uint GetRelation() + { + return (_data >> 20) & 0xFFF; + } + + public uint GetRaw() + { + return _data; + } +} + +public class LinkChunk : IChunk, IMergable +{ + public record Link + { + public LinkId linkId; + public int source; + public int destination; + public ushort relation; + + public Link(BinaryReader reader) + { + linkId = new LinkId(reader.ReadUInt32()); + source = reader.ReadInt32(); + destination = reader.ReadInt32(); + relation = reader.ReadUInt16(); + } + } + + public ChunkHeader Header { get; set; } + public List links; + + public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry) + { + links = new List(); + while (reader.BaseStream.Position < entry.Offset + entry.Size + 24) + { + links.Add(new Link(reader)); + } + } + + public void WriteData(BinaryWriter writer) + { + throw new System.NotImplementedException(); + } + + public void Merge(IMergable other) + { + links.AddRange(((LinkChunk)other).links); + } +} + +// TODO: This should be generic like Property +public class LinkDataMetaProp : IChunk, IMergable +{ + public record LinkData + { + public LinkId linkId; + public int priority; + + public LinkData(BinaryReader reader) + { + linkId = new LinkId(reader.ReadUInt32()); + priority = reader.ReadInt32(); + } + } + + public ChunkHeader Header { get; set; } + public int DataSize; + public List linkData; + + public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry) + { + DataSize = reader.ReadInt32(); + linkData = new List(); + while (reader.BaseStream.Position < entry.Offset + entry.Size + 24) + { + linkData.Add(new LinkData(reader)); + } + } + + public void WriteData(BinaryWriter writer) + { + throw new System.NotImplementedException(); + } + + public void Merge(IMergable other) + { + linkData.AddRange(((LinkDataMetaProp)other).linkData); + } +} \ No newline at end of file diff --git a/KeepersCompound.LGS/Database/Chunks/Property.cs b/KeepersCompound.LGS/Database/Chunks/Property.cs new file mode 100644 index 0000000..56aa7d8 --- /dev/null +++ b/KeepersCompound.LGS/Database/Chunks/Property.cs @@ -0,0 +1,290 @@ +using System.Collections.Generic; +using System.IO; +using System.Numerics; + +namespace KeepersCompound.LGS.Database.Chunks; + +public class Property +{ + public int objectId; + public int length; + + public virtual void Read(BinaryReader reader) + { + objectId = reader.ReadInt32(); + length = (int)reader.ReadUInt32(); + } +} + +public class PropertyChunk : IChunk, IMergable where T : Property, new() +{ + public ChunkHeader Header { get; set; } + public List properties; + + public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry) + { + properties = new List(); + while (reader.BaseStream.Position < entry.Offset + entry.Size + 24) + { + var prop = new T(); + prop.Read(reader); + properties.Add(prop); + } + } + + public void WriteData(BinaryWriter writer) + { + throw new System.NotImplementedException(); + } + + public void Merge(IMergable other) + { + properties.AddRange(((PropertyChunk)other).properties); + } +} + +public class PropGeneric : Property +{ + public byte[] data; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + data = reader.ReadBytes(length); + } +} + +public class PropBool : Property +{ + public bool value; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + value = reader.ReadInt32() != 0; + } +} + +public class PropInt : Property +{ + public int value; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + value = reader.ReadInt32(); + } +} + +public class PropLabel : Property +{ + public string value; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + value = reader.ReadNullString(length); + } +} + +public class PropString : Property +{ + public int stringLength; + public string value; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + stringLength = reader.ReadInt32(); + value = reader.ReadNullString(stringLength); + } +} + +public class PropFloat : Property +{ + public float value; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + value = reader.ReadSingle(); + } +} + +public class PropVector : Property +{ + public Vector3 value; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + value = reader.ReadVec3(); + } +} + +public class PropRenderType : Property +{ + public enum Mode + { + Normal, + NotRendered, + Unlit, + EditorOnly, + CoronaOnly, + } + + public Mode mode; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + mode = (Mode)reader.ReadUInt32(); + } +} + +public class PropSlayResult : Property +{ + public enum Effect + { + Normal, NoEffect, Terminate, Destroy, + } + + public Effect effect; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + effect = (Effect)reader.ReadUInt32(); + } +} + +public class PropInventoryType : Property +{ + public enum Slot + { + Junk, Item, Weapon, + } + + public Slot type; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + type = (Slot)reader.ReadUInt32(); + } +} + +public class PropCollisionType : Property +{ + public bool Bounce; + public bool DestroyOnImpact; + public bool SlayOnImpact; + public bool NoCollisionSound; + public bool NoResult; + public bool FullCollisionSound; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + var flags = reader.ReadUInt32(); + Bounce = (flags & 0x1) != 0; + DestroyOnImpact = (flags & (0x1 << 1)) != 0; + SlayOnImpact = (flags & (0x1 << 2)) != 0; + NoCollisionSound = (flags & (0x1 << 3)) != 0; + NoResult = (flags & (0x1 << 4)) != 0; + FullCollisionSound = (flags & (0x1 << 5)) != 0; + } +} + +public class PropPosition : Property +{ + public Vector3 Location; + public Vector3 Rotation; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + Location = reader.ReadVec3(); + reader.ReadBytes(4); // Runtime Cell/Hint in editor + Rotation = reader.ReadRotation(); + } +} + +public class PropLight : Property +{ + public float Brightness; + public Vector3 Offset; + public float Radius; + public float InnerRadius; + public bool QuadLit; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + Brightness = reader.ReadSingle(); + Offset = reader.ReadVec3(); + Radius = reader.ReadSingle(); + QuadLit = reader.ReadBoolean(); + reader.ReadBytes(3); + InnerRadius = reader.ReadSingle(); + } +} + +public class PropLightColor : Property +{ + public float Hue; + public float Saturation; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + Hue = reader.ReadSingle(); + Saturation = reader.ReadSingle(); + } +} + +public class PropSpotlight : Property +{ + public float InnerAngle; + public float OuterAngle; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + InnerAngle = reader.ReadSingle(); + OuterAngle = reader.ReadSingle(); + reader.ReadBytes(4); // Z is unused + } +} + +public class PropSpotlightAndAmbient : Property +{ + public float InnerAngle; + public float OuterAngle; + public float SpotBrightness; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + InnerAngle = reader.ReadSingle(); + OuterAngle = reader.ReadSingle(); + SpotBrightness = reader.ReadSingle(); + } +} + +// TODO: Work out what this property actually does +public class PropLightBasedAlpha : Property +{ + public bool Enabled; + public Vector2 AlphaRange; + public Vector2 LightRange; + + public override void Read(BinaryReader reader) + { + base.Read(reader); + Enabled = reader.ReadBoolean(); + reader.ReadBytes(3); + AlphaRange = reader.ReadVec2(); + LightRange = new Vector2(reader.ReadInt32(), reader.ReadInt32()); + } +} diff --git a/KeepersCompound.LGS/Database/Chunks/TxList.cs b/KeepersCompound.LGS/Database/Chunks/TxList.cs new file mode 100644 index 0000000..5aaafd7 --- /dev/null +++ b/KeepersCompound.LGS/Database/Chunks/TxList.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using System.Text; + +namespace KeepersCompound.LGS.Database.Chunks; + +public class TxList : IChunk +{ + public struct Item + { + public byte[] Tokens { get; set; } + public string Name { get; set; } + + public Item(BinaryReader reader) + { + Tokens = reader.ReadBytes(4); + Name = reader.ReadNullString(16); + } + } + + + public ChunkHeader Header { get; set; } + + public int BlockSize { get; set; } + public int ItemCount { get; set; } + public int TokenCount { get; set; } + public string[] Tokens { get; set; } + public Item[] Items { get; set; } + + + public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry) + { + BlockSize = reader.ReadInt32(); + ItemCount = reader.ReadInt32(); + TokenCount = reader.ReadInt32(); + Tokens = new string[TokenCount]; + for (var i = 0; i < TokenCount; i++) + { + Tokens[i] = reader.ReadNullString(16); + } + Items = new Item[ItemCount]; + for (var i = 0; i < ItemCount; i++) + { + Items[i] = new Item(reader); + } + } + + public void WriteData(BinaryWriter writer) + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/KeepersCompound.LGS/Database/Chunks/WorldRep.cs b/KeepersCompound.LGS/Database/Chunks/WorldRep.cs new file mode 100644 index 0000000..ef442e4 --- /dev/null +++ b/KeepersCompound.LGS/Database/Chunks/WorldRep.cs @@ -0,0 +1,304 @@ +using System; +using System.IO; +using System.Numerics; + +namespace KeepersCompound.LGS.Database.Chunks; + +public class WorldRep : IChunk +{ + public struct WrHeader + { + // Extended header content + public int Size { get; set; } + public int Version { get; set; } + public int Flags { get; set; } + public uint LightmapFormat { get; set; } + public int LightmapScale { get; set; } + + // Standard header + public uint DataSize { get; set; } + public uint CellCount { get; set; } + + public WrHeader(BinaryReader reader) + { + Size = reader.ReadInt32(); + Version = reader.ReadInt32(); + Flags = reader.ReadInt32(); + LightmapFormat = reader.ReadUInt32(); + LightmapScale = reader.ReadInt32(); + DataSize = reader.ReadUInt32(); + CellCount = reader.ReadUInt32(); + } + + public readonly float LightmapScaleMultiplier() + { + return Math.Sign(LightmapScale) switch + { + 1 => LightmapScale, + -1 => 1.0f / LightmapScale, + _ => 1.0f, + }; + } + } + + public struct Cell + { + public struct Poly + { + public byte Flags { get; set; } + public byte VertexCount { get; set; } + public byte PlaneId { get; set; } + public byte ClutId { get; set; } + public ushort Destination { get; set; } + public byte MotionIndex { get; set; } + + public Poly(BinaryReader reader) + { + Flags = reader.ReadByte(); + VertexCount = reader.ReadByte(); + PlaneId = reader.ReadByte(); + ClutId = reader.ReadByte(); + Destination = reader.ReadUInt16(); + MotionIndex = reader.ReadByte(); + reader.ReadByte(); + } + } + + public struct RenderPoly + { + public (Vector3, Vector3) TextureVectors { get; set; } + public (float, float) TextureBases { get; set; } + public ushort TextureId { get; set; } + public ushort CachedSurface { get; set; } + public float TextureMagnitude { get; set; } + public Vector3 Center { get; set; } + + public RenderPoly(BinaryReader reader) + { + TextureVectors = (reader.ReadVec3(), reader.ReadVec3()); + TextureBases = (reader.ReadSingle(), reader.ReadSingle()); + TextureId = reader.ReadUInt16(); + CachedSurface = reader.ReadUInt16(); + TextureMagnitude = reader.ReadSingle(); + Center = reader.ReadVec3(); + } + } + + public struct LightmapInfo + { + public (short, short) Bases { get; set; } + public short PaddedWidth { get; set; } + public byte Height { get; set; } + public byte Width { get; set; } + public uint DataPtr { get; set; } + public uint DynamicLightPtr { get; set; } + public uint AnimLightBitmask { get; set; } + + public LightmapInfo(BinaryReader reader) + { + Bases = (reader.ReadInt16(), reader.ReadInt16()); + PaddedWidth = reader.ReadInt16(); + Height = reader.ReadByte(); + Width = reader.ReadByte(); + DataPtr = reader.ReadUInt32(); + DynamicLightPtr = reader.ReadUInt32(); + AnimLightBitmask = reader.ReadUInt32(); + } + } + + public struct Lightmap + { + public byte[] Pixels { get; set; } + + public int Layers; + public int Width; + public int Height; + public int Bpp; + + public Lightmap(BinaryReader reader, byte width, byte height, uint bitmask, int bytesPerPixel) + { + var count = 1 + BitOperations.PopCount(bitmask); + var length = bytesPerPixel * width * height * count; + Pixels = reader.ReadBytes(length); + Layers = count; + Width = width; + Height = height; + Bpp = bytesPerPixel; + } + + public readonly Vector4 GetPixel(uint layer, uint x, uint y) + { + if (layer >= Layers || x >= Width || y >= Height) + { + return Vector4.Zero; + } + + var idx = 0 + x * Bpp + y * Bpp * Width + layer * Bpp * Width * Height; + switch (Bpp) + { + case 1: + var raw1 = Pixels[idx]; + return new Vector4(raw1, raw1, raw1, 255) / 255.0f; + case 2: + var raw2 = Pixels[idx] + (Pixels[idx + 1] << 8); + return new Vector4(raw2 & 31, (raw2 >> 5) & 31, (raw2 >> 10) & 31, 31) / 31.0f; + case 4: + return new Vector4(Pixels[idx + 2], Pixels[idx + 1], Pixels[idx], Pixels[idx + 3]) / 255.0f; + default: + return Vector4.Zero; + } + } + + public readonly byte[] AsBytesRgba(int layer) + { + ArgumentOutOfRangeException.ThrowIfLessThan(layer, 0, nameof(layer)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(layer, Layers, nameof(layer)); + + var pIdx = layer * Bpp * Width * Height; + var length = 4 * Width * Height; + var bytes = new byte[length]; + for (var i = 0; i < length; i += 4, pIdx += Bpp) + { + switch (Bpp) + { + case 1: + var raw1 = Pixels[pIdx]; + bytes[i] = raw1; + bytes[i + 1] = raw1; + bytes[i + 2] = raw1; + bytes[i + 3] = 255; + break; + case 2: + var raw2 = Pixels[pIdx] + (Pixels[pIdx + 1] << 8); + bytes[i] = (byte)(255 * (raw2 & 31) / 31.0f); + bytes[i + 1] = (byte)(255 * ((raw2 >> 5) & 31) / 31.0f); + bytes[i + 2] = (byte)(255 * ((raw2 >> 10) & 31) / 31.0f); + bytes[i + 3] = 255; + break; + case 4: + bytes[i] = Pixels[pIdx + 2]; + bytes[i + 1] = Pixels[pIdx + 1]; + bytes[i + 2] = Pixels[pIdx]; + bytes[i + 3] = Pixels[pIdx + 3]; + break; + } + } + + return bytes; + } + } + + public byte VertexCount { get; set; } + public byte PolyCount { get; set; } + public byte RenderPolyCount { get; set; } + public byte PortalPolyCount { get; set; } + public byte PlaneCount { get; set; } + public byte Medium { get; set; } + public byte Flags { get; set; } + public int PortalVertices { get; set; } + public ushort NumVList { get; set; } + public byte AnimLightCount { get; set; } + public byte MotionIndex { get; set; } + public Vector3 SphereCenter { get; set; } + public float SphereRadius { get; set; } + public Vector3[] Vertices { get; set; } + public Poly[] Polys { get; set; } + public RenderPoly[] RenderPolys { get; set; } + public uint IndexCount { get; set; } + public byte[] Indices { get; set; } + public Plane[] Planes { get; set; } + public ushort[] AnimLights { get; set; } + public LightmapInfo[] LightList { get; set; } + public Lightmap[] Lightmaps { get; set; } + public int LightIndexCount { get; set; } + public ushort[] LightIndices { get; set; } + + public Cell(BinaryReader reader, int bpp) + { + VertexCount = reader.ReadByte(); + PolyCount = reader.ReadByte(); + RenderPolyCount = reader.ReadByte(); + PortalPolyCount = reader.ReadByte(); + PlaneCount = reader.ReadByte(); + Medium = reader.ReadByte(); + Flags = reader.ReadByte(); + PortalVertices = reader.ReadInt32(); + NumVList = reader.ReadUInt16(); + AnimLightCount = reader.ReadByte(); + MotionIndex = reader.ReadByte(); + SphereCenter = reader.ReadVec3(); + SphereRadius = reader.ReadSingle(); + Vertices = new Vector3[VertexCount]; + for (var i = 0; i < VertexCount; i++) + { + Vertices[i] = reader.ReadVec3(); + } + Polys = new Poly[PolyCount]; + for (var i = 0; i < PolyCount; i++) + { + Polys[i] = new Poly(reader); + } + RenderPolys = new RenderPoly[RenderPolyCount]; + for (var i = 0; i < RenderPolyCount; i++) + { + RenderPolys[i] = new RenderPoly(reader); + } + IndexCount = reader.ReadUInt32(); + Indices = new byte[IndexCount]; + for (var i = 0; i < IndexCount; i++) + { + Indices[i] = reader.ReadByte(); + } + Planes = new Plane[PlaneCount]; + for (var i = 0; i < PlaneCount; i++) + { + Planes[i] = new Plane(reader.ReadVec3(), reader.ReadSingle()); + } + AnimLights = new ushort[AnimLightCount]; + for (var i = 0; i < AnimLightCount; i++) + { + AnimLights[i] = reader.ReadUInt16(); + } + LightList = new LightmapInfo[RenderPolyCount]; + for (var i = 0; i < RenderPolyCount; i++) + { + LightList[i] = new LightmapInfo(reader); + } + Lightmaps = new Lightmap[RenderPolyCount]; + for (var i = 0; i < RenderPolyCount; i++) + { + var info = LightList[i]; + Lightmaps[i] = new Lightmap(reader, info.Width, info.Height, info.AnimLightBitmask, bpp); + } + LightIndexCount = reader.ReadInt32(); + LightIndices = new ushort[LightIndexCount]; + for (var i = 0; i < LightIndexCount; i++) + { + LightIndices[i] = reader.ReadUInt16(); + } + } + } + + public ChunkHeader Header { get; set; } + public WrHeader DataHeader { get; set; } + public Cell[] Cells { get; set; } + + public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry) + { + DataHeader = new(reader); + var bpp = (DataHeader.LightmapFormat == 0) ? 2 : 4; + + Cells = new Cell[DataHeader.CellCount]; + for (var i = 0; i < DataHeader.CellCount; i++) + { + Cells[i] = new Cell(reader, bpp); + } + + // TODO: All the other info lol + } + + public void WriteData(BinaryWriter writer) + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/KeepersCompound.LGS/Database/File.cs b/KeepersCompound.LGS/Database/File.cs new file mode 100644 index 0000000..89ed448 --- /dev/null +++ b/KeepersCompound.LGS/Database/File.cs @@ -0,0 +1,118 @@ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using KeepersCompound.LGS.Database.Chunks; + +namespace KeepersCompound.LGS.Database; + +public class DbFile +{ + public struct FHeader + { + public uint TocOffset { get; set; } + public Version Version { get; } + public string Deadbeef { get; } + + public FHeader(BinaryReader reader) + { + TocOffset = reader.ReadUInt32(); + Version = new Version(reader); + reader.ReadBytes(256); + Deadbeef = BitConverter.ToString(reader.ReadBytes(4)); + } + + public readonly void Write(BinaryWriter writer) + { + writer.Write(TocOffset); + Version.Write(writer); + writer.Write(new byte[256]); + writer.Write(Array.ConvertAll(Deadbeef.Split('-'), s => byte.Parse(s, System.Globalization.NumberStyles.HexNumber))); + } + } + + public readonly struct TableOfContents + { + public readonly struct Entry + { + public string Name { get; } + public uint Offset { get; } + public uint Size { get; } + + public Entry(BinaryReader reader) + { + Name = reader.ReadNullString(12); + Offset = reader.ReadUInt32(); + Size = reader.ReadUInt32(); + } + + public override string ToString() + { + // return $"Name: {Name}, Offset: {O}" + return base.ToString(); + } + } + + public uint ItemCount { get; } + public List Items { get; } + + public TableOfContents(BinaryReader reader) + { + ItemCount = reader.ReadUInt32(); + Items = new List(); + for (var i = 0; i < ItemCount; i++) + Items.Add(new Entry(reader)); + Items.Sort((a, b) => a.Offset.CompareTo(b.Offset)); + } + } + + public FHeader Header { get; private set; } + public TableOfContents Toc { get; } + public Dictionary Chunks { get; set; } + + public DbFile(string filename) + { + if (!File.Exists(filename)) return; + + using MemoryStream stream = new(File.ReadAllBytes(filename)); + using BinaryReader reader = new(stream, Encoding.UTF8, false); + + Header = new(reader); + stream.Seek(Header.TocOffset, SeekOrigin.Begin); + Toc = new(reader); + + Chunks = new Dictionary(); + foreach (var entry in Toc.Items) + { + var chunk = NewChunk(entry.Name); + chunk.Read(reader, entry); + Chunks.Add(entry.Name, chunk); + } + } + + private static IChunk NewChunk(string entryName) + { + return entryName switch + { + // "AI_ROOM_DB" => new AiRoomDb(), + // "AICONVERSE" => new AiConverseChunk(), + "GAM_FILE" => new GamFile(), + "TXLIST" => new TxList(), + "WREXT" => new WorldRep(), + "BRLIST" => new BrList(), + "P$ModelName" => new PropertyChunk(), + "P$Scale" => new PropertyChunk(), + "P$RenderTyp" => new PropertyChunk(), + "P$OTxtRepr0" => new PropertyChunk(), + "P$OTxtRepr1" => new PropertyChunk(), + "P$OTxtRepr2" => new PropertyChunk(), + "P$OTxtRepr3" => new PropertyChunk(), + "P$RenderAlp" => new PropertyChunk(), + "LD$MetaProp" => new LinkDataMetaProp(), + _ when entryName.StartsWith("L$") => new LinkChunk(), + _ when entryName.StartsWith("P$") => new PropertyChunk(), + _ => new GenericChunk(), + }; + } +} \ No newline at end of file diff --git a/KeepersCompound.LGS/Database/ObjectHierarchy.cs b/KeepersCompound.LGS/Database/ObjectHierarchy.cs new file mode 100644 index 0000000..e76d3cc --- /dev/null +++ b/KeepersCompound.LGS/Database/ObjectHierarchy.cs @@ -0,0 +1,129 @@ + +using System; +using System.Collections.Generic; +using KeepersCompound.LGS.Database.Chunks; + +namespace KeepersCompound.LGS.Database; + +public class ObjectHierarchy +{ + public class DarkObject + { + public int objectId; + public int parentId; + public Dictionary properties; + + public DarkObject(int id) + { + objectId = id; + parentId = 0; + properties = new Dictionary(); + } + + public T GetProperty(string propName) where T : Property + { + if (properties.TryGetValue(propName, out var prop)) + { + return (T)prop; + } + return null; + } + } + + private Dictionary _objects; + + public ObjectHierarchy(DbFile db, DbFile gam = null) + { + _objects = new Dictionary(); + + T GetMergedChunk(string name) where T : IMergable + { + if (db.Chunks.TryGetValue(name, out var rawChunk)) + { + var chunk = (T)rawChunk; + if (gam != null && gam.Chunks.TryGetValue(name, out var rawGamChunk)) + { + var gamChunk = (T)rawGamChunk; + chunk.Merge(gamChunk); + } + return chunk; + } + + throw new ArgumentException($"No chunk with name ({name}) found", nameof(name)); + } + + // Add parentages + var metaPropLinks = GetMergedChunk("L$MetaProp"); + var metaPropLinkData = GetMergedChunk("LD$MetaProp"); + var length = metaPropLinks.links.Count; + for (var i = 0; i < length; i++) + { + var link = metaPropLinks.links[i]; + var linkData = metaPropLinkData.linkData[i]; + var childId = link.source; + var parentId = link.destination; + if (!_objects.ContainsKey(childId)) + { + _objects.Add(childId, new DarkObject(childId)); + } + if (!_objects.ContainsKey(parentId)) + { + _objects.Add(parentId, new DarkObject(parentId)); + } + + if (linkData.priority == 0) + { + _objects[childId].parentId = parentId; + } + } + + void AddProp(string name) where T : Property, new() + { + var chunk = GetMergedChunk>(name); + foreach (var prop in chunk.properties) + { + var id = prop.objectId; + if (!_objects.ContainsKey(id)) + { + _objects.Add(id, new DarkObject(id)); + } + _objects[id].properties.TryAdd(name, prop); + } + } + + AddProp("P$ModelName"); + AddProp("P$Scale"); + AddProp("P$RenderTyp"); + AddProp("P$OTxtRepr0"); + AddProp("P$OTxtRepr1"); + AddProp("P$OTxtRepr2"); + AddProp("P$OTxtRepr3"); + AddProp("P$RenderAlp"); + } + + public T GetProperty(int objectId, string propName) where T : Property + { + if (!_objects.ContainsKey(objectId)) + { + return null; + } + + var parentId = objectId; + while (parentId != 0) + { + if (!_objects.TryGetValue(parentId, out var obj)) + { + return null; + } + + var prop = obj.GetProperty(propName); + if (prop != null) + { + return prop; + } + parentId = obj.parentId; + } + + return null; + } +} \ No newline at end of file diff --git a/KeepersCompound.LGS/Database/Version.cs b/KeepersCompound.LGS/Database/Version.cs new file mode 100644 index 0000000..e15f047 --- /dev/null +++ b/KeepersCompound.LGS/Database/Version.cs @@ -0,0 +1,26 @@ +using System.IO; + +namespace KeepersCompound.LGS.Database; + +public struct Version +{ + public uint Major { get; set; } + public uint Minor { get; set; } + + public Version(BinaryReader reader) + { + Major = reader.ReadUInt32(); + Minor = reader.ReadUInt32(); + } + + public readonly void Write(BinaryWriter writer) + { + writer.Write(Major); + writer.Write(Minor); + } + + public override readonly string ToString() + { + return $"{Major}.{Minor}"; + } +} \ No newline at end of file diff --git a/KeepersCompound.LGS/Extensions.cs b/KeepersCompound.LGS/Extensions.cs new file mode 100644 index 0000000..078e5a9 --- /dev/null +++ b/KeepersCompound.LGS/Extensions.cs @@ -0,0 +1,32 @@ +using System.IO; +using System.Numerics; +using System.Text; + +namespace KeepersCompound.LGS; + +public static class Extensions +{ + public static Vector3 ReadRotation(this BinaryReader reader) + { + var raw = new Vector3(reader.ReadUInt16(), reader.ReadUInt16(), reader.ReadUInt16()); + return raw * 360 / (ushort.MaxValue + 1); + } + + public static Vector3 ReadVec3(this BinaryReader reader) + { + return new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + } + + public static Vector2 ReadVec2(this BinaryReader reader) + { + return new Vector2(reader.ReadSingle(), reader.ReadSingle()); + } + + public static string ReadNullString(this BinaryReader reader, int length) + { + var tmpName = Encoding.UTF8.GetString(reader.ReadBytes(length)); + var idx = tmpName.IndexOf('\0'); + if (idx >= 0) tmpName = tmpName[..idx]; + return tmpName; + } +} diff --git a/KeepersCompound.LGS/KeepersCompound.LGS.csproj b/KeepersCompound.LGS/KeepersCompound.LGS.csproj new file mode 100644 index 0000000..2150e37 --- /dev/null +++ b/KeepersCompound.LGS/KeepersCompound.LGS.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/KeepersCompound.LGS/ModelFile.cs b/KeepersCompound.LGS/ModelFile.cs new file mode 100644 index 0000000..64bb42a --- /dev/null +++ b/KeepersCompound.LGS/ModelFile.cs @@ -0,0 +1,206 @@ +using System; +using System.IO; +using System.Numerics; +using System.Text; + +namespace KeepersCompound.LGS; + +// TODO: Remove all the things that don't actually need to be stored +public class ModelFile +{ + public readonly struct BHeader + { + public string Signature { get; } + public int Version { get; } + + public BHeader(BinaryReader reader) + { + Signature = reader.ReadNullString(4); + Version = reader.ReadInt32(); + } + } + + public readonly struct MHeader + { + public string Name { get; } + public float Radius { get; } + public float MaxPolygonRadius { get; } + public Vector3 MaxBounds { get; } + public Vector3 MinBounds { get; } + public Vector3 Center { get; } + + public ushort PolygonCount { get; } + public ushort VertexCount { get; } + public ushort ParameterCount { get; } + public byte MaterialCount { get; } + public byte VCallCount { get; } + public byte VHotCount { get; } + public byte ObjectCount { get; } + + public uint ObjectOffset { get; } + public uint MaterialOffset { get; } + public uint UvOffset { get; } + public uint VHotOffset { get; } + public uint VertexOffset { get; } + public uint LightOffset { get; } + public uint NormalOffset { get; } + public uint PolygonOffset { get; } + public uint NodeOffset { get; } + + public uint ModelSize { get; } + + public uint AuxMaterialFlags { get; } + public uint AuxMaterialOffset { get; } + public uint AuxMaterialSize { get; } + + public MHeader(BinaryReader reader, int version) + { + Name = reader.ReadNullString(8); + Radius = reader.ReadSingle(); + MaxPolygonRadius = reader.ReadSingle(); + MaxBounds = reader.ReadVec3(); + MinBounds = reader.ReadVec3(); + Center = reader.ReadVec3(); + PolygonCount = reader.ReadUInt16(); + VertexCount = reader.ReadUInt16(); + ParameterCount = reader.ReadUInt16(); + MaterialCount = reader.ReadByte(); + VCallCount = reader.ReadByte(); + VHotCount = reader.ReadByte(); + ObjectCount = reader.ReadByte(); + ObjectOffset = reader.ReadUInt32(); + MaterialOffset = reader.ReadUInt32(); + UvOffset = reader.ReadUInt32(); + VHotOffset = reader.ReadUInt32(); + VertexOffset = reader.ReadUInt32(); + LightOffset = reader.ReadUInt32(); + NormalOffset = reader.ReadUInt32(); + PolygonOffset = reader.ReadUInt32(); + NodeOffset = reader.ReadUInt32(); + ModelSize = reader.ReadUInt32(); + + if (version == 4) + { + AuxMaterialFlags = reader.ReadUInt32(); + AuxMaterialOffset = reader.ReadUInt32(); + AuxMaterialSize = reader.ReadUInt32(); + } + else + { + AuxMaterialFlags = 0; + AuxMaterialOffset = 0; + AuxMaterialSize = 0; + } + } + } + + public struct Polygon + { + public ushort Index; + public ushort Data; + public byte Type; + public byte VertexCount; + public ushort Normal; + public float D; + public ushort[] VertexIndices; + public ushort[] LightIndices; + public ushort[] UvIndices; + public byte Material; + + public Polygon(BinaryReader reader, int version) + { + Index = reader.ReadUInt16(); + Data = reader.ReadUInt16(); + Type = reader.ReadByte(); + VertexCount = reader.ReadByte(); + Normal = reader.ReadUInt16(); + D = reader.ReadSingle(); + VertexIndices = new ushort[VertexCount]; + for (var i = 0; i < VertexCount; i++) + { + VertexIndices[i] = reader.ReadUInt16(); + } + LightIndices = new ushort[VertexCount]; + for (var i = 0; i < VertexCount; i++) + { + LightIndices[i] = reader.ReadUInt16(); + } + UvIndices = new ushort[Type == 0x1B ? VertexCount : 0]; + for (var i = 0; i < UvIndices.Length; i++) + { + UvIndices[i] = reader.ReadUInt16(); + } + + Material = version == 4 ? reader.ReadByte() : (byte)0; + } + } + + public struct Material + { + public string Name; + public byte Type; + public byte Slot; + public uint Handle; + public float Uv; + + public Material(BinaryReader reader) + { + Name = reader.ReadNullString(16); + Type = reader.ReadByte(); + Slot = reader.ReadByte(); + Handle = reader.ReadUInt32(); + Uv = reader.ReadSingle(); + } + } + + public BHeader BinHeader { get; set; } + public MHeader Header { get; set; } + public Vector3[] Vertices { get; } + public Vector2[] Uvs { get; } + public Vector3[] Normals { get; } + public Polygon[] Polygons { get; } + public Material[] Materials { get; } + + public ModelFile(string filename) + { + if (!File.Exists(filename)) return; + + using MemoryStream stream = new(File.ReadAllBytes(filename)); + using BinaryReader reader = new(stream, Encoding.UTF8, false); + + BinHeader = new BHeader(reader); + if (BinHeader.Signature != "LGMD") return; + + Header = new MHeader(reader, BinHeader.Version); + stream.Seek(Header.VertexOffset, SeekOrigin.Begin); + Vertices = new Vector3[Header.VertexCount]; + for (var i = 0; i < Vertices.Length; i++) + { + Vertices[i] = reader.ReadVec3(); + } + stream.Seek(Header.UvOffset, SeekOrigin.Begin); + Uvs = new Vector2[(Header.VHotOffset - Header.UvOffset) / 8]; + for (var i = 0; i < Uvs.Length; i++) + { + Uvs[i] = reader.ReadVec2(); + } + stream.Seek(Header.NormalOffset, SeekOrigin.Begin); + Normals = new Vector3[(Header.PolygonOffset - Header.NormalOffset) / 12]; + for (var i = 0; i < Normals.Length; i++) + { + Normals[i] = reader.ReadVec3(); + } + stream.Seek(Header.PolygonOffset, SeekOrigin.Begin); + Polygons = new Polygon[Header.PolygonCount]; + for (var i = 0; i < Polygons.Length; i++) + { + Polygons[i] = new Polygon(reader, BinHeader.Version); + } + stream.Seek(Header.MaterialOffset, SeekOrigin.Begin); + Materials = new Material[Header.MaterialCount]; + for (var i = 0; i < Materials.Length; i++) + { + Materials[i] = new Material(reader); + } + } +} \ No newline at end of file diff --git a/KeepersCompound.LGS/ResourcePathManager.cs b/KeepersCompound.LGS/ResourcePathManager.cs new file mode 100644 index 0000000..6a27c77 --- /dev/null +++ b/KeepersCompound.LGS/ResourcePathManager.cs @@ -0,0 +1,449 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; + +namespace KeepersCompound.LGS; + +// TODO: Make this nicer to use! +// Rather than navigating through the path manager Context should hold the current campaign resources +// Campaign resources should be lazy loaded (only get the paths when we first set it as campaign) +// Campaign resources should extend off of the base game resources and just overwrite any resource paths it needs to + +enum ConfigFile +{ + Cam, + CamExt, + CamMod, + Game, + Install, + User, + ConfigFileCount, +} + +public enum ResourceType +{ + Mission, + Object, + ObjectTexture, + Texture, +} + +public class ResourcePathManager +{ + public record CampaignResources + { + public bool initialised = false; + public string name; + private readonly Dictionary _missionPathMap = []; + private readonly Dictionary _texturePathMap = []; + private readonly Dictionary _objectPathMap = []; + private readonly Dictionary _objectTexturePathMap = []; + + public List GetResourceNames(ResourceType type) + { + List keys = type switch + { + ResourceType.Mission => [.. _missionPathMap.Keys], + ResourceType.Object => [.. _objectPathMap.Keys], + ResourceType.ObjectTexture => [.. _objectTexturePathMap.Keys], + ResourceType.Texture => [.. _texturePathMap.Keys], + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; + keys.Sort(); + return keys; + } + + public string GetResourcePath(ResourceType type, string name) + { + var map = type switch + { + ResourceType.Mission => _missionPathMap, + ResourceType.Object => _objectPathMap, + ResourceType.ObjectTexture => _objectTexturePathMap, + ResourceType.Texture => _texturePathMap, + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; + return map.TryGetValue(name.ToLower(), out var resourcePath) ? resourcePath : null; + } + + public void Initialise(string misPath, string resPath) + { + foreach (var path in Directory.GetFiles(misPath)) + { + var convertedPath = ConvertSeparator(path); + var ext = Path.GetExtension(convertedPath).ToLower(); + if (ext == ".mis" || ext == ".cow") + { + var baseName = Path.GetFileName(convertedPath).ToLower(); + _missionPathMap[baseName] = convertedPath; + } + } + + foreach (var (name, path) in GetTexturePaths(resPath)) + { + _texturePathMap[name] = path; + } + foreach (var (name, path) in GetObjectPaths(resPath)) + { + _objectPathMap[name] = path; + } + foreach (var (name, path) in GetObjectTexturePaths(resPath)) + { + _objectTexturePathMap[name] = path; + } + } + + public void Initialise(string misPath, string resPath, CampaignResources parent) + { + foreach (var (name, path) in parent._texturePathMap) + { + _texturePathMap[name] = path; + } + foreach (var (name, path) in parent._objectPathMap) + { + _objectPathMap[name] = path; + } + foreach (var (name, path) in parent._objectTexturePathMap) + { + _objectTexturePathMap[name] = path; + } + + Initialise(misPath, resPath); + } + } + + private bool _initialised = false; + private readonly string _extractionPath; + private string _fmsDir; + private CampaignResources _omResources; + private Dictionary _fmResources; + + public ResourcePathManager(string extractionPath) + { + _extractionPath = extractionPath; + } + + public static string ConvertSeparator(string path) + { + return path.Replace('\\', '/'); + } + + public void Init(string installPath) + { + // TODO: + // - Determine if folder is a thief install + // - Load all the (relevant) config files + // - Get base paths from configs + // - Build list of FM campaigns + // - Initialise OM campaign resource paths + // - Lazy load FM campaign resource paths (that inherit OM resources) + + if (!DirContainsThiefExe(installPath)) + { + throw new ArgumentException($"No Thief installation found at {installPath}", nameof(installPath)); + } + + // TODO: Should these paths be stored? + if (!TryGetConfigPaths(installPath, out var configPaths)) + { + throw new InvalidOperationException("Failed to find all installation config paths."); + } + + // Get the paths of the base Fam and Obj resources so we can extract them. + var installCfgLines = File.ReadAllLines(configPaths[(int)ConfigFile.Install]); + FindConfigVar(installCfgLines, "resname_base", out var resPaths); + var baseFamPath = ""; + var baseObjPath = ""; + foreach (var resPath in resPaths.Split('+')) + { + var dir = Path.Join(installPath, ConvertSeparator(resPath)); + foreach (var path in Directory.GetFiles(dir)) + { + var name = Path.GetFileName(path).ToLower(); + if (name == "fam.crf" && baseFamPath == "") + { + baseFamPath = path; + } + else if (name == "obj.crf" && baseObjPath == "") + { + baseObjPath = path; + } + } + } + + // Do the extraction bro + (string, string)[] resources = [("fam", baseFamPath), ("obj", baseObjPath)]; + foreach (var (extractName, zipPath) in resources) + { + var extractPath = Path.Join(_extractionPath, extractName); + if (Directory.Exists(extractPath)) + { + Directory.Delete(extractPath, true); + } + ZipFile.OpenRead(zipPath).ExtractToDirectory(extractPath); + } + + FindConfigVar(installCfgLines, "load_path", out var omsPath); + omsPath = Path.Join(installPath, ConvertSeparator(omsPath)); + _omResources = new CampaignResources(); + _omResources.name = ""; + _omResources.Initialise(omsPath, _extractionPath); + + var camModLines = File.ReadAllLines(configPaths[(int)ConfigFile.CamMod]); + FindConfigVar(camModLines, "fm_path", out var fmsPath, "FMs"); + _fmsDir = Path.Join(installPath, fmsPath); + + // Build up the map of FM campaigns. These are uninitialised, we just want + // to have their name + _fmResources = new Dictionary(); + foreach (var dir in Directory.GetDirectories(_fmsDir)) + { + var name = Path.GetFileName(dir); + var fmResource = new CampaignResources(); + fmResource.name = name; + _fmResources.Add(name, fmResource); + } + + _initialised = true; + } + + public List GetCampaignNames() + { + if (!_initialised) return null; + + var names = new List(_fmResources.Keys); + names.Sort(); + return names; + } + + public CampaignResources GetCampaign(string campaignName) + { + if (campaignName == null || campaignName == "") + { + return _omResources; + } + else if (_fmResources.TryGetValue(campaignName, out var campaign)) + { + if (!campaign.initialised) + { + var fmPath = Path.Join(_fmsDir, campaignName); + campaign.Initialise(fmPath, fmPath, _omResources); + } + return campaign; + } + + throw new ArgumentException("No campaign found with given name", nameof(campaignName)); + } + + private static Dictionary GetObjectTexturePaths(string root) + { + string[] validExtensions = { ".dds", ".png", ".tga", ".pcx", ".gif", ".bmp", ".cel", }; + + var dirOptions = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }; + var texOptions = new EnumerationOptions + { + MatchCasing = MatchCasing.CaseInsensitive, + RecurseSubdirectories = true, + }; + + var pathMap = new Dictionary(); + foreach (var dir in Directory.EnumerateDirectories(root, "obj", dirOptions)) + { + foreach (var path in Directory.EnumerateFiles(dir, "*", texOptions)) + { + var convertedPath = ConvertSeparator(path); + var ext = Path.GetExtension(convertedPath); + if (validExtensions.Contains(ext.ToLower())) + { + var key = Path.GetFileNameWithoutExtension(convertedPath).ToLower(); + pathMap.TryAdd(key, convertedPath); + } + } + } + + return pathMap; + } + + private static Dictionary GetTexturePaths(string root) + { + string[] validExtensions = { ".dds", ".png", ".tga", ".pcx", ".gif", ".bmp", ".cel", }; + + var famOptions = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }; + var textureOptions = new EnumerationOptions + { + MatchCasing = MatchCasing.CaseInsensitive, + RecurseSubdirectories = true, + }; + + var pathMap = new Dictionary(); + foreach (var dir in Directory.EnumerateDirectories(root, "fam", famOptions)) + { + foreach (var path in Directory.EnumerateFiles(dir, "*", textureOptions)) + { + var convertedPath = ConvertSeparator(path); + var ext = Path.GetExtension(convertedPath); + if (validExtensions.Contains(ext.ToLower())) + { + var key = Path.GetRelativePath(root, convertedPath)[..^ext.Length].ToLower(); + pathMap.TryAdd(key, convertedPath); + } + } + } + + return pathMap; + } + + private static Dictionary GetObjectPaths(string root) + { + var dirOptions = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }; + var binOptions = new EnumerationOptions + { + MatchCasing = MatchCasing.CaseInsensitive, + RecurseSubdirectories = true, + }; + + var pathMap = new Dictionary(); + foreach (var dir in Directory.EnumerateDirectories(root, "obj", dirOptions)) + { + foreach (var path in Directory.EnumerateFiles(dir, "*.bin", binOptions)) + { + var convertedPath = ConvertSeparator(path); + var key = Path.GetRelativePath(dir, convertedPath).ToLower(); + pathMap.TryAdd(key, convertedPath); + } + } + + return pathMap; + } + + /// + /// Determine if the given directory contains a Thief executable at the top level. + /// + /// The directory to search + /// true if a Thief executable was found, false otherwise. + private static bool DirContainsThiefExe(string dir) + { + var searchOptions = new EnumerationOptions + { + MatchCasing = MatchCasing.CaseInsensitive, + }; + + foreach (var path in Directory.GetFiles(dir, "*.exe", searchOptions)) + { + var baseName = Path.GetFileName(path).ToLower(); + if (baseName.Contains("thief")) + { + return true; + } + } + + return false; + } + + /// + /// Get an array of of all the Dark config file paths. + /// + /// Root directory of the Thief installation. + /// Output array of config file paths + /// true if all config files were found, false otherwise. + private static bool TryGetConfigPaths(string installPath, out string[] configPaths) + { + configPaths = new string[(int)ConfigFile.ConfigFileCount]; + + var searchOptions = new EnumerationOptions + { + MatchCasing = MatchCasing.CaseInsensitive, + }; + + // `cam.cfg`, `cam_ext.cfg`, and `cam_mod.ini` are always in the root of the install. + // The first two configs will tell us if any other configs are in non-default locations. + // We can't just do a recursive search for everything else because they can potentially + // be *outside* of the Thief installation. + foreach (var path in Directory.GetFiles(installPath, "cam*", searchOptions)) + { + var name = Path.GetFileName(path).ToLower(); + if (name == "cam.cfg") + { + configPaths[(int)ConfigFile.Cam] = path; + } + else if (name == "cam_ext.cfg") + { + configPaths[(int)ConfigFile.CamExt] = path; + } + else if (name == "cam_mod.ini") + { + configPaths[(int)ConfigFile.CamMod] = path; + } + } + + var camExtLines = File.ReadAllLines(configPaths[(int)ConfigFile.CamExt]); + var camLines = File.ReadAllLines(configPaths[(int)ConfigFile.Cam]); + + bool FindCamVar(string varName, out string value, string defaultValue = "") + { + return FindConfigVar(camExtLines, varName, out value, defaultValue) || + FindConfigVar(camLines, varName, out value, defaultValue); + } + + FindCamVar("include_path", out var includePath, "./"); + FindCamVar("game", out var gameName); + FindCamVar($"{gameName}_include_install_cfg", out var installCfgName); + FindCamVar("include_user_cfg", out var userCfgName); + + // TODO: How to handle case-insensitive absolute paths? + // Fixup the include path to "work" cross-platform + includePath = ConvertSeparator(includePath); + includePath = Path.Join(installPath, includePath); + if (!Directory.Exists(includePath)) + { + return false; + } + + foreach (var path in Directory.GetFiles(includePath, "*.cfg", searchOptions)) + { + var name = Path.GetFileName(path).ToLower(); + if (name == $"{gameName}.cfg") + { + configPaths[(int)ConfigFile.Game] = path; + } + else if (name == installCfgName.ToLower()) + { + configPaths[(int)ConfigFile.Install] = path; + } + else if (name == userCfgName.ToLower()) + { + configPaths[(int)ConfigFile.User] = path; + } + } + + // Check we found everything + var i = 0; + foreach (var path in configPaths) + { + if (path == null || path == "") + { + return false; + } + i++; + } + return true; + } + + private static bool FindConfigVar(string[] lines, string varName, out string value, string defaultValue = "") + { + value = defaultValue; + + foreach (var line in lines) + { + if (line.StartsWith(varName)) + { + value = line[(line.IndexOf(' ') + 1)..]; + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/lightmapper.sln b/lightmapper.sln index 58ea566..8430893 100644 --- a/lightmapper.sln +++ b/lightmapper.sln @@ -3,6 +3,10 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeepersCompound.Lightmapper", "KeepersCompound.Lightmapper\KeepersCompound.Lightmapper.csproj", "{24FE5A7B-3E74-43CE-8F61-92AEFB07C544}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeepersCompound.LGS", "KeepersCompound.LGS\KeepersCompound.LGS.csproj", "{7262E507-A972-4693-8C99-67E163E8BF7C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -11,4 +15,14 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {24FE5A7B-3E74-43CE-8F61-92AEFB07C544}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24FE5A7B-3E74-43CE-8F61-92AEFB07C544}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24FE5A7B-3E74-43CE-8F61-92AEFB07C544}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24FE5A7B-3E74-43CE-8F61-92AEFB07C544}.Release|Any CPU.Build.0 = Release|Any CPU + {7262E507-A972-4693-8C99-67E163E8BF7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7262E507-A972-4693-8C99-67E163E8BF7C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7262E507-A972-4693-8C99-67E163E8BF7C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7262E507-A972-4693-8C99-67E163E8BF7C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection EndGlobal