commit 2e841276f36650ef5eed000552fae88a8ceb158e Author: Jarrod Doyle Date: Sun Jul 21 22:13:31 2024 +0100 Initial commit 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