Add LGS project (from TMV)

This commit is contained in:
Jarrod Doyle 2024-09-20 16:28:44 +01:00
parent 50bcb41134
commit 27b52f32d8
Signed by: Jayrude
GPG Key ID: 38B57B16E7C0ADF7
17 changed files with 2055 additions and 0 deletions

View File

@ -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);
}

View File

@ -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]);
}
}
}

View File

@ -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();
}
}

View File

@ -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<TexInfo>();
}
}
}
public ChunkHeader Header { get; set; }
public List<Brush> Brushes { get; set; }
public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry)
{
Brushes = new List<Brush>();
while (reader.BaseStream.Position < entry.Offset + entry.Size + 24)
{
Brushes.Add(new Brush(reader));
}
}
public void WriteData(BinaryWriter writer)
{
throw new System.NotImplementedException();
}
}

View File

@ -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();
}
}

View File

@ -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<Link> links;
public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry)
{
links = new List<Link>();
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> linkData;
public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry)
{
DataSize = reader.ReadInt32();
linkData = new List<LinkData>();
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);
}
}

View File

@ -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<T> : IChunk, IMergable where T : Property, new()
{
public ChunkHeader Header { get; set; }
public List<T> properties;
public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry)
{
properties = new List<T>();
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<T>)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());
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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<Entry> Items { get; }
public TableOfContents(BinaryReader reader)
{
ItemCount = reader.ReadUInt32();
Items = new List<Entry>();
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<string, IChunk> 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<string, IChunk>();
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<PropLabel>(),
"P$Scale" => new PropertyChunk<PropVector>(),
"P$RenderTyp" => new PropertyChunk<PropRenderType>(),
"P$OTxtRepr0" => new PropertyChunk<PropString>(),
"P$OTxtRepr1" => new PropertyChunk<PropString>(),
"P$OTxtRepr2" => new PropertyChunk<PropString>(),
"P$OTxtRepr3" => new PropertyChunk<PropString>(),
"P$RenderAlp" => new PropertyChunk<PropFloat>(),
"LD$MetaProp" => new LinkDataMetaProp(),
_ when entryName.StartsWith("L$") => new LinkChunk(),
_ when entryName.StartsWith("P$") => new PropertyChunk<PropGeneric>(),
_ => new GenericChunk(),
};
}
}

View File

@ -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<string, Property> properties;
public DarkObject(int id)
{
objectId = id;
parentId = 0;
properties = new Dictionary<string, Property>();
}
public T GetProperty<T>(string propName) where T : Property
{
if (properties.TryGetValue(propName, out var prop))
{
return (T)prop;
}
return null;
}
}
private Dictionary<int, DarkObject> _objects;
public ObjectHierarchy(DbFile db, DbFile gam = null)
{
_objects = new Dictionary<int, DarkObject>();
T GetMergedChunk<T>(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<LinkChunk>("L$MetaProp");
var metaPropLinkData = GetMergedChunk<LinkDataMetaProp>("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<T>(string name) where T : Property, new()
{
var chunk = GetMergedChunk<PropertyChunk<T>>(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<PropLabel>("P$ModelName");
AddProp<PropVector>("P$Scale");
AddProp<PropRenderType>("P$RenderTyp");
AddProp<PropString>("P$OTxtRepr0");
AddProp<PropString>("P$OTxtRepr1");
AddProp<PropString>("P$OTxtRepr2");
AddProp<PropString>("P$OTxtRepr3");
AddProp<PropFloat>("P$RenderAlp");
}
public T GetProperty<T>(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<T>(propName);
if (prop != null)
{
return prop;
}
parentId = obj.parentId;
}
return null;
}
}

View File

@ -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}";
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -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);
}
}
}

View File

@ -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<string, string> _missionPathMap = [];
private readonly Dictionary<string, string> _texturePathMap = [];
private readonly Dictionary<string, string> _objectPathMap = [];
private readonly Dictionary<string, string> _objectTexturePathMap = [];
public List<string> GetResourceNames(ResourceType type)
{
List<string> 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<string, CampaignResources> _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<string, CampaignResources>();
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<string> GetCampaignNames()
{
if (!_initialised) return null;
var names = new List<string>(_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<string, string> 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<string, string>();
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<string, string> 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<string, string>();
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<string, string> GetObjectPaths(string root)
{
var dirOptions = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive };
var binOptions = new EnumerationOptions
{
MatchCasing = MatchCasing.CaseInsensitive,
RecurseSubdirectories = true,
};
var pathMap = new Dictionary<string, string>();
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;
}
/// <summary>
/// Determine if the given directory contains a Thief executable at the top level.
/// </summary>
/// <param name="dir">The directory to search</param>
/// <returns><c>true</c> if a Thief executable was found, <c>false</c> otherwise.</returns>
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;
}
/// <summary>
/// Get an array of of all the Dark config file paths.
/// </summary>
/// <param name="installPath">Root directory of the Thief installation.</param>
/// <param name="configPaths">Output array of config file paths</param>
/// <returns><c>true</c> if all config files were found, <c>false</c> otherwise.</returns>
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;
}
}

View File

@ -3,6 +3,10 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59 VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -11,4 +15,14 @@ Global
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection 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 EndGlobal