From 2e841276f36650ef5eed000552fae88a8ceb158e Mon Sep 17 00:00:00 2001 From: Jarrod Doyle Date: Sun, 21 Jul 2024 22:13:31 +0100 Subject: [PATCH] Initial commit --- .editorconfig | 4 + .gitattributes | 2 + .gitignore | 2 + Thief Mission Viewer.csproj | 9 + Thief Mission Viewer.sln | 19 ++ icon.svg | 1 + icon.svg.import | 37 +++ project.godot | 20 ++ project/code/LGS/Database/Chunk.cs | 68 +++++ .../code/LGS/Database/Chunks/AiConverse.cs | 29 +++ project/code/LGS/Database/Chunks/AiRoomDb.cs | 63 +++++ project/code/LGS/Database/Chunks/WorldRep.cs | 234 ++++++++++++++++++ project/code/LGS/Database/File.cs | 105 ++++++++ project/code/LGS/Database/Serialise.cs | 10 + project/code/LGS/Database/Version.cs | 26 ++ project/code/LGS/Extensions.cs | 12 + project/code/LGS/Utils.cs | 12 + project/code/Mission.cs | 117 +++++++++ project/code/camera.gd | 116 +++++++++ project/scenes/main.tscn | 25 ++ 20 files changed, 911 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Thief Mission Viewer.csproj create mode 100644 Thief Mission Viewer.sln create mode 100644 icon.svg create mode 100644 icon.svg.import create mode 100644 project.godot create mode 100644 project/code/LGS/Database/Chunk.cs create mode 100644 project/code/LGS/Database/Chunks/AiConverse.cs create mode 100644 project/code/LGS/Database/Chunks/AiRoomDb.cs create mode 100644 project/code/LGS/Database/Chunks/WorldRep.cs create mode 100644 project/code/LGS/Database/File.cs create mode 100644 project/code/LGS/Database/Serialise.cs create mode 100644 project/code/LGS/Database/Version.cs create mode 100644 project/code/LGS/Extensions.cs create mode 100644 project/code/LGS/Utils.cs create mode 100644 project/code/Mission.cs create mode 100644 project/code/camera.gd create mode 100644 project/scenes/main.tscn diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..21f0a90 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.{cs,vb}] + +# IDE0130: Namespace does not match folder structure +dotnet_style_namespace_match_folder = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4709183 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Godot 4+ specific ignores +.godot/ diff --git a/Thief Mission Viewer.csproj b/Thief Mission Viewer.csproj new file mode 100644 index 0000000..18b2d7c --- /dev/null +++ b/Thief Mission Viewer.csproj @@ -0,0 +1,9 @@ + + + net6.0 + net7.0 + net8.0 + true + ThiefMissionViewer + + \ No newline at end of file diff --git a/Thief Mission Viewer.sln b/Thief Mission Viewer.sln new file mode 100644 index 0000000..f175d16 --- /dev/null +++ b/Thief Mission Viewer.sln @@ -0,0 +1,19 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thief Mission Viewer", "Thief Mission Viewer.csproj", "{D0FB3ED6-2135-4056-A00B-78F54E8A3C45}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + ExportDebug|Any CPU = ExportDebug|Any CPU + ExportRelease|Any CPU = ExportRelease|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D0FB3ED6-2135-4056-A00B-78F54E8A3C45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0FB3ED6-2135-4056-A00B-78F54E8A3C45}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0FB3ED6-2135-4056-A00B-78F54E8A3C45}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU + {D0FB3ED6-2135-4056-A00B-78F54E8A3C45}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU + {D0FB3ED6-2135-4056-A00B-78F54E8A3C45}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU + {D0FB3ED6-2135-4056-A00B-78F54E8A3C45}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU + EndGlobalSection +EndGlobal diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..3fe4f4a --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..ba3dc20 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b64hn7rvvw68c" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..acd8310 --- /dev/null +++ b/project.godot @@ -0,0 +1,20 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Thief Mission Viewer" +run/main_scene="res://project/scenes/main.tscn" +config/features=PackedStringArray("4.2", "C#", "Forward Plus") +config/icon="res://icon.svg" + +[dotnet] + +project/assembly_name="Thief Mission Viewer" diff --git a/project/code/LGS/Database/Chunk.cs b/project/code/LGS/Database/Chunk.cs new file mode 100644 index 0000000..b008329 --- /dev/null +++ b/project/code/LGS/Database/Chunk.cs @@ -0,0 +1,68 @@ +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) + { + var nameBytes = reader.ReadBytes(12); + var nameTmp = Encoding.UTF8.GetString(nameBytes).Replace("\0", string.Empty); + Name = nameTmp[..Math.Min(11, nameTmp.Length)]; + 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); + } +} \ No newline at end of file diff --git a/project/code/LGS/Database/Chunks/AiConverse.cs b/project/code/LGS/Database/Chunks/AiConverse.cs new file mode 100644 index 0000000..d91d416 --- /dev/null +++ b/project/code/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/project/code/LGS/Database/Chunks/AiRoomDb.cs b/project/code/LGS/Database/Chunks/AiRoomDb.cs new file mode 100644 index 0000000..abbebed --- /dev/null +++ b/project/code/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/project/code/LGS/Database/Chunks/WorldRep.cs b/project/code/LGS/Database/Chunks/WorldRep.cs new file mode 100644 index 0000000..453f813 --- /dev/null +++ b/project/code/LGS/Database/Chunks/WorldRep.cs @@ -0,0 +1,234 @@ +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 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 Lightmap(BinaryReader reader, byte width, byte height, uint bitmask, int bytesPerPixel) + { + var count = 1 + BitOperations.PopCount(bitmask); + Pixels = new byte[count, height, width, bytesPerPixel]; + for (var c = 0; c < count; c++) + { + for (var y = 0; y < height; y++) + { + for (var x = 0; x < width; x++) + { + for (var b = 0; b < bytesPerPixel; b++) + { + Pixels[c, y, x, b] = reader.ReadByte(); + } + } + } + } + } + } + + 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) + { + 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]; + var bpp = 4; // TODO: WREXT only! + 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); + Cells = new Cell[DataHeader.CellCount]; + for (var i = 0; i < DataHeader.CellCount; i++) + { + Cells[i] = new Cell(reader); + } + + // TODO: All the other info lol + } + + public void WriteData(BinaryWriter writer) + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/project/code/LGS/Database/File.cs b/project/code/LGS/Database/File.cs new file mode 100644 index 0000000..a86049b --- /dev/null +++ b/project/code/LGS/Database/File.cs @@ -0,0 +1,105 @@ + +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) + { + var tmpName = Encoding.UTF8.GetString(reader.ReadBytes(12)).Replace("\0", string.Empty); + Name = tmpName[..Math.Min(11, tmpName.Length)]; + 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(), + "WREXT" => new WorldRep(), + _ => new GenericChunk(), + }; + } +} \ No newline at end of file diff --git a/project/code/LGS/Database/Serialise.cs b/project/code/LGS/Database/Serialise.cs new file mode 100644 index 0000000..82deec7 --- /dev/null +++ b/project/code/LGS/Database/Serialise.cs @@ -0,0 +1,10 @@ +using System.IO; + +namespace KeepersCompound.LGS.Database; + +public interface IPersistable +{ + public void Read(BinaryReader reader); + + public void Write(BinaryWriter writer); +} \ No newline at end of file diff --git a/project/code/LGS/Database/Version.cs b/project/code/LGS/Database/Version.cs new file mode 100644 index 0000000..e15f047 --- /dev/null +++ b/project/code/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/project/code/LGS/Extensions.cs b/project/code/LGS/Extensions.cs new file mode 100644 index 0000000..b1df8d9 --- /dev/null +++ b/project/code/LGS/Extensions.cs @@ -0,0 +1,12 @@ +using System.IO; +using System.Numerics; + +namespace KeepersCompound.LGS; + +public static class Extensions +{ + public static Vector3 ReadVec3(this BinaryReader reader) + { + return new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + } +} diff --git a/project/code/LGS/Utils.cs b/project/code/LGS/Utils.cs new file mode 100644 index 0000000..9268dac --- /dev/null +++ b/project/code/LGS/Utils.cs @@ -0,0 +1,12 @@ +using System.Numerics; + +namespace KeepersCompound.LGS; + + +public static class Utils +{ + public static Godot.Vector3 ToGodotVec3(this Vector3 vec, float inverseScale) + { + return new Godot.Vector3(vec.Y, vec.Z, vec.X) / inverseScale; + } +} \ No newline at end of file diff --git a/project/code/Mission.cs b/project/code/Mission.cs new file mode 100644 index 0000000..7511fdd --- /dev/null +++ b/project/code/Mission.cs @@ -0,0 +1,117 @@ +using Godot; +using KeepersCompound.LGS; +using KeepersCompound.LGS.Database; +using KeepersCompound.LGS.Database.Chunks; +using Microsoft.VisualBasic; +using System; +using System.Collections.Generic; + +namespace KeepersCompound; + +[Tool] +public partial class Mission : Node3D +{ + [Export(PropertyHint.GlobalFile, "*.mis")] + public string FileName { get; set; } + [Export] + public bool Build = false; + + + DbFile _file; + + public override void _Ready() + { + RebuildMap(); + } + + public override void _Process(double delta) + { + if (Build) + { + RebuildMap(); + Build = false; + } + } + + public override void _Input(InputEvent @event) + { + if (@event is InputEventKey keyEvent && keyEvent.Pressed) + { + if (keyEvent.Keycode == Key.R) + { + Build = true; + } + } + } + + public void RebuildMap() + { + foreach (var node in GetChildren()) + { + node.QueueFree(); + } + + _file = new(FileName); + var wr = (WorldRep)_file.Chunks["WREXT"]; + + foreach (var cell in wr.Cells) + { + BuildCellMesh(cell); + } + } + + private void BuildCellMesh(WorldRep.Cell cell) + { + var numPolys = cell.PolyCount; + var numRenderPolys = cell.RenderPolyCount; + var numPortalPolys = cell.PortalPolyCount; + + var vertices = new List(); + var normals = new List(); + var idxOffset = 0; + for (int i = 0; i < numPolys; i++) + { + var poly = cell.Polys[i]; + // TODO: Make this break lol + if (i >= numRenderPolys || i >= numPolys - numPortalPolys) + { + idxOffset += poly.VertexCount; + continue; + } + + var plane = cell.Planes[poly.PlaneId]; + var normal = plane.Normal; + + var numPolyVertices = poly.VertexCount; + for (int j = 1; j < numPolyVertices - 1; j++) + { + foreach (var offset in new int[] { 0, j, j + 1 }) + // foreach (var offset in new int[] { j + 1, j, 0 }) + { + var vertex = cell.Vertices[cell.Indices[idxOffset + offset]]; + vertices.Add(vertex.ToGodotVec3(4.0f)); + normals.Add(normal.ToGodotVec3(4.0f)); + // vertices.Add(new Vector3(vertex.Y, vertex.Z, vertex.X)); + // normals.Add(new Vector3(normal.Y, normal.Z, normal.X)); + } + } + + idxOffset += poly.VertexCount; + } + + var arrMesh = new ArrayMesh(); + var arrays = new Godot.Collections.Array(); + arrays.Resize((int)Mesh.ArrayType.Max); + arrays[(int)Mesh.ArrayType.Vertex] = vertices.ToArray(); + arrays[(int)Mesh.ArrayType.Normal] = normals.ToArray(); + + arrMesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, arrays); + var meshInstance = new MeshInstance3D + { + Mesh = arrMesh, + CastShadow = GeometryInstance3D.ShadowCastingSetting.On + }; + + AddChild(meshInstance); + } +} \ No newline at end of file diff --git a/project/code/camera.gd b/project/code/camera.gd new file mode 100644 index 0000000..a57c9a3 --- /dev/null +++ b/project/code/camera.gd @@ -0,0 +1,116 @@ +class_name FreeLookCamera extends Camera3D + +# Modifier keys' speed multiplier +const SHIFT_MULTIPLIER = 2.5 +const ALT_MULTIPLIER = 1.0 / SHIFT_MULTIPLIER + + +@export_range(0.0, 1.0) var sensitivity: float = 0.25 + +# Mouse state +var _mouse_position = Vector2(0.0, 0.0) +var _total_pitch = 0.0 + +# Movement state +var _direction = Vector3(0.0, 0.0, 0.0) +var _velocity = Vector3(0.0, 0.0, 0.0) +var _acceleration = 30 +var _deceleration = -10 +var _vel_multiplier = 4 + +# Keyboard state +var _w = false +var _s = false +var _a = false +var _d = false +var _q = false +var _e = false +var _shift = false +var _alt = false + +func _input(event): + # Receives mouse motion + if event is InputEventMouseMotion: + _mouse_position = event.relative + + # Receives mouse button input + if event is InputEventMouseButton: + match event.button_index: + MOUSE_BUTTON_RIGHT: # Only allows rotation if right click down + Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED if event.pressed else Input.MOUSE_MODE_VISIBLE) + MOUSE_BUTTON_WHEEL_UP: # Increases max velocity + _vel_multiplier = clamp(_vel_multiplier * 1.1, 0.2, 20) + MOUSE_BUTTON_WHEEL_DOWN: # Decereases max velocity + _vel_multiplier = clamp(_vel_multiplier / 1.1, 0.2, 20) + + # Receives key input + if event is InputEventKey: + match event.keycode: + KEY_W: + _w = event.pressed + KEY_S: + _s = event.pressed + KEY_A: + _a = event.pressed + KEY_D: + _d = event.pressed + KEY_Q: + _q = event.pressed + KEY_E: + _e = event.pressed + KEY_SHIFT: + _shift = event.pressed + KEY_ALT: + _alt = event.pressed + +# Updates mouselook and movement every frame +func _process(delta): + _update_mouselook() + _update_movement(delta) + +# Updates camera movement +func _update_movement(delta): + # Computes desired direction from key states + _direction = Vector3( + (_d as float) - (_a as float), + (_e as float) - (_q as float), + (_s as float) - (_w as float) + ) + + # Computes the change in velocity due to desired direction and "drag" + # The "drag" is a constant acceleration on the camera to bring it's velocity to 0 + var offset = _direction.normalized() * _acceleration * _vel_multiplier * delta \ + + _velocity.normalized() * _deceleration * _vel_multiplier * delta + + # Compute modifiers' speed multiplier + var speed_multi = 1 + if _shift: speed_multi *= SHIFT_MULTIPLIER + if _alt: speed_multi *= ALT_MULTIPLIER + + # Checks if we should bother translating the camera + if _direction == Vector3.ZERO and offset.length_squared() > _velocity.length_squared(): + # Sets the velocity to 0 to prevent jittering due to imperfect deceleration + _velocity = Vector3.ZERO + else: + # Clamps speed to stay within maximum value (_vel_multiplier) + _velocity.x = clamp(_velocity.x + offset.x, -_vel_multiplier, _vel_multiplier) + _velocity.y = clamp(_velocity.y + offset.y, -_vel_multiplier, _vel_multiplier) + _velocity.z = clamp(_velocity.z + offset.z, -_vel_multiplier, _vel_multiplier) + + translate(_velocity * delta * speed_multi) + +# Updates mouse look +func _update_mouselook(): + # Only rotates mouse if the mouse is captured + if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: + _mouse_position *= sensitivity + var yaw = _mouse_position.x + var pitch = _mouse_position.y + _mouse_position = Vector2(0, 0) + + # Prevents looking up/down too far + pitch = clamp(pitch, -90 - _total_pitch, 90 - _total_pitch) + _total_pitch += pitch + + rotate_y(deg_to_rad(-yaw)) + rotate_object_local(Vector3(1,0,0), deg_to_rad(-pitch)) diff --git a/project/scenes/main.tscn b/project/scenes/main.tscn new file mode 100644 index 0000000..8c65838 --- /dev/null +++ b/project/scenes/main.tscn @@ -0,0 +1,25 @@ +[gd_scene load_steps=4 format=3 uid="uid://boxi211q3kx6c"] + +[ext_resource type="Script" path="res://project/code/Mission.cs" id="1_xhqt7"] +[ext_resource type="Script" path="res://project/code/camera.gd" id="2_w5otl"] + +[sub_resource type="Environment" id="Environment_oxkvl"] + +[node name="Main" type="Node3D"] + +[node name="Mission" type="Node3D" parent="."] +script = ExtResource("1_xhqt7") +FileName = "/home/jarrod/Dev/thief/de-specs/test_data/wr-test.cow" + +[node name="Camera3D" type="Camera3D" parent="."] +script = ExtResource("2_w5otl") + +[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +environment = SubResource("Environment_oxkvl") + +[node name="OmniLight3D" type="OmniLight3D" parent="."] +shadow_enabled = true + +[node name="OmniLight3D3" type="OmniLight3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.509499, 0.98353, 1.1662) +shadow_enabled = true