using Godot; using KeepersCompound.LGS; using KeepersCompound.LGS.Database; using KeepersCompound.LGS.Database.Chunks; using KeepersCompound.TMV.UI; using RectpackSharp; using System; using System.Collections.Generic; using System.IO; namespace KeepersCompound.TMV; public partial class Mission : Node3D { private readonly struct LightmapRectData { public readonly int cellIndex; public readonly int lightmapIndex; public readonly int textureId; public readonly int uvStart; public readonly int uvEnd; public LightmapRectData(int cellIndex, int lightmapIndex, int textureId, int uvStart, int uvEnd) { this.cellIndex = cellIndex; this.lightmapIndex = lightmapIndex; this.textureId = textureId; this.uvStart = uvStart; this.uvEnd = uvEnd; } } [Export(PropertyHint.GlobalFile, "*.mis")] public string FileName { get; set; } [Export] public bool Build = false; [Export] public bool Clear = false; [Export] public bool Dump = false; string _campaignName; string _missionName; ResourcePathManager _installPaths; DbFile _file; TextureLoader _textureLoader; public override void _Ready() { var extractPath = ProjectSettings.GlobalizePath($"user://extracted/tmp"); _installPaths = new ResourcePathManager(extractPath); var missionSelector = GetNode("%MissionSelector") as MissionSelector; missionSelector.pathManager = _installPaths; missionSelector.MissionSelected += (string campaign, string mission) => { _campaignName = campaign; _missionName = mission; FileName = _installPaths.GetMissionPath(campaign, mission); Build = true; }; } public override void _Process(double delta) { if (Build) { RebuildMap(); Build = false; } if (Clear) { ClearMap(); Clear = false; } } public override void _Input(InputEvent @event) { if (@event is InputEventKey keyEvent && keyEvent.Pressed) { if (keyEvent.Keycode == Key.R) { Build = true; } } } public void ClearMap() { foreach (var node in GetChildren()) { node.QueueFree(); } } public void RebuildMap() { ClearMap(); _textureLoader = new TextureLoader(_campaignName); _file = new(FileName); UseChunk("TXLIST", LoadTextures); UseChunk("WREXT", BuildWrMeshes); if ( _file.Chunks.TryGetValue("BRLIST", out var brListRaw) && _file.Chunks.TryGetValue("P$ModelName", out var modelNamesRaw) && _file.Chunks.TryGetValue("P$Scale", out var scalesRaw) && _file.Chunks.TryGetValue("P$RenderTyp", out var renderTypesRaw) && _file.Chunks.TryGetValue("L$MetaProp", out var metaPropLinksRaw) && _file.Chunks.TryGetValue("LD$MetaProp", out var metaPropLinkDataRaw) ) { var brList = (BrList)brListRaw; var modelNames = (PropertyChunk)modelNamesRaw; var scales = (PropertyChunk)scalesRaw; var renderTypes = (PropertyChunk)renderTypesRaw; var metaPropLinks = (LinkChunk)metaPropLinksRaw; var metaPropLinkData = (LinkDataMetaProp)metaPropLinkDataRaw; // TODO: Do this somewhere else lol if (_file.Chunks.TryGetValue("GAM_FILE", out var gamFileChunk)) { GD.Print("GAM_FILE detected"); var options = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }; var name = ((GamFile)gamFileChunk).fileName; GD.Print($"Searching for GAM: {FileName.GetBaseDir()}/{name}"); var paths = Directory.GetFiles(FileName.GetBaseDir(), name, options); GD.Print($"Found paths: {paths.Length}"); if (!paths.IsEmpty()) { GD.Print($"Attempting to load GAM at: {paths[0]}"); var gamFile = new DbFile(paths[0]); if (gamFile.Chunks.TryGetValue("P$ModelName", out var gamChunk1) && gamFile.Chunks.TryGetValue("L$MetaProp", out var gamChunk2) && gamFile.Chunks.TryGetValue("LD$MetaProp", out var gamChunk3) && gamFile.Chunks.TryGetValue("P$Scale", out var gamChunk4) && gamFile.Chunks.TryGetValue("P$RenderTyp", out var gamChunk5)) { GD.Print($"Pre-Merged chunks: {modelNames.properties.Count} {metaPropLinks.links.Count} {metaPropLinkData.linkData.Count}"); modelNames.properties.AddRange(((PropertyChunk)gamChunk1).properties); metaPropLinks.links.AddRange(((LinkChunk)gamChunk2).links); metaPropLinkData.linkData.AddRange(((LinkDataMetaProp)gamChunk3).linkData); scales.properties.AddRange(((PropertyChunk)gamChunk4).properties); renderTypes.properties.AddRange(((PropertyChunk)gamChunk5).properties); GD.Print($"Post-Merged chunks: {modelNames.properties.Count} {metaPropLinks.links.Count} {metaPropLinkData.linkData.Count}"); } } } PlaceObjects(brList, modelNames, scales, renderTypes, metaPropLinks, metaPropLinkData); } } private void UseChunk(string name, Action action) { if (_file.Chunks.TryGetValue(name, out var value)) { action((T)value); } else { GD.Print($"No chunk found/loaded: {name}"); } } private void PlaceObjects( BrList brList, PropertyChunk modelNames, PropertyChunk scales, PropertyChunk renderTypes, LinkChunk metapropLink, LinkDataMetaProp metaPropLinkData) { foreach (var brush in brList.Brushes) { if (brush.media != BrList.Brush.Media.Object) { continue; } // TODO: Build an actual hierarchy and such :) // TODO: We need to load the gamesys :) // Determine if we have a model name :)) var id = (int)brush.brushInfo; var modelName = ""; var scale = Vector3.One; var scaleFound = false; var renderType = PropRenderType.Mode.Normal; var renderTypeFound = false; while (true) { // See if there's a modelname property if (modelName == "") { foreach (var prop in modelNames.properties) { if (prop.objectId == id) { modelName = prop.modelName; break; } } } if (!scaleFound) { foreach (var prop in scales.properties) { if (prop.objectId == id) { scale = prop.scale.ToGodotVec3(false); scaleFound = true; break; } } } if (!renderTypeFound) { foreach (var prop in renderTypes.properties) { if (prop.objectId == id) { renderType = prop.mode; renderTypeFound = true; break; } } } if (modelName != "" && scaleFound && renderTypeFound) { break; } // Check for a parent var length = metapropLink.links.Count; var prevId = id; for (var i = 0; i < length; i++) { var link = metapropLink.links[i]; var linkData = metaPropLinkData.linkData[i]; if (link.source == id && linkData.priority == 0) { id = link.destination; break; } } // No parent found if (id == prevId) { break; } } if (modelName == "" || renderType == PropRenderType.Mode.NotRendered) { continue; } // Let's try and place an object :) var fmName = FileName.GetBaseDir().GetFile(); var objPath = _installPaths.GetObjectPath(fmName, modelName + ".bin"); objPath ??= _installPaths.GetObjectPath(modelName + ".bin"); var pos = brush.position.ToGodotVec3(); var rawRot = brush.angle; var rot = new Vector3(rawRot.Y, rawRot.Z, rawRot.X) * 360 / ushort.MaxValue; var model = new Model { Position = pos, RotationDegrees = rot, Scale = scale, }; if (objPath != null) { model.BuildModel("", objPath); } AddChild(model); } } private void BuildWrMeshes(WorldRep worldRep) { var cells = worldRep.Cells; var lmHdr = worldRep.DataHeader.LightmapFormat == 2; GD.Print($"HDR Lightmap: {lmHdr}"); var packingRects = new List(); var surfaceDataMap = new Dictionary(); var rectDataMap = new Dictionary(); for (var cellIdx = 0; cellIdx < cells.Length; cellIdx++) { var cell = cells[cellIdx]; var numPolys = cell.PolyCount; var numRenderPolys = cell.RenderPolyCount; var numPortalPolys = cell.PortalPolyCount; // There's nothing to render if (numRenderPolys == 0 || numPortalPolys >= numPolys) { continue; } // You'd think these would be the same number, but apparently not // I think it's because water counts as a render poly and a portal poly var maxPolyIdx = Math.Min(numRenderPolys, numPolys - numPortalPolys); var cellIdxOffset = 0; for (int polyIdx = 0; polyIdx < maxPolyIdx; polyIdx++) { var lightmapRectData = ProcessCellSurfaceData(surfaceDataMap, cell, cellIdx, polyIdx, cellIdxOffset); rectDataMap.Add(packingRects.Count, lightmapRectData); var light = cell.LightList[polyIdx]; var rect = new PackingRectangle(0, 0, light.Width, light.Height, packingRects.Count); packingRects.Add(rect); cellIdxOffset += cell.Polys[polyIdx].VertexCount; } } var lightmapTexture = BuildLightmapTexture(cells, packingRects.ToArray(), rectDataMap, surfaceDataMap); foreach (var (textureId, surface) in surfaceDataMap) { if (surface.Empty) { continue; } var albedoTexture = _textureLoader.Get(textureId); var mesh = new ArrayMesh(); mesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, surface.BuildSurfaceArray()); mesh.SurfaceSetMaterial(0, BuildMaterial(albedoTexture, lightmapTexture, lmHdr)); var meshInstance = new MeshInstance3D { Mesh = mesh }; AddChild(meshInstance); } } private LightmapRectData ProcessCellSurfaceData(Dictionary surfaceDataMap, WorldRep.Cell cell, int cellIdx, int polyIdx, int indicesOffset) { var poly = cell.Polys[polyIdx]; var normal = cell.Planes[poly.PlaneId].Normal.ToGodotVec3(); var vertices = new List(); var textureUvs = new List(); var lightmapUvs = new List(); var numPolyVertices = poly.VertexCount; for (var j = 0; j < numPolyVertices; j++) { var vertex = cell.Vertices[cell.Indices[indicesOffset + j]]; vertices.Add(vertex.ToGodotVec3()); } var renderPoly = cell.RenderPolys[polyIdx]; var light = cell.LightList[polyIdx]; var textureId = CalcBaseUV(cell, poly, renderPoly, light, textureUvs, lightmapUvs, indicesOffset); if (!surfaceDataMap.ContainsKey(textureId)) { surfaceDataMap.Add(textureId, new MeshSurfaceData()); } var surfaceData = surfaceDataMap[textureId]; var (start, end) = surfaceData.AddPolygon(vertices, normal, textureUvs, lightmapUvs); if (polyIdx >= cell.Lightmaps.Length) GD.Print("HUH"); return new LightmapRectData(cellIdx, polyIdx, textureId, start, end); } private static Texture BuildLightmapTexture(WorldRep.Cell[] cells, PackingRectangle[] packingRects, Dictionary rectDataMap, Dictionary surfaceDataMap) { RectanglePacker.Pack(packingRects, out var bounds); var image = Image.Create((int)bounds.Width, (int)bounds.Height, false, Image.Format.Rgba8); foreach (var rect in packingRects) { if (!rectDataMap.ContainsKey(rect.Id)) GD.Print("Invalid rectDataMap key"); var info = rectDataMap[rect.Id]; if (info.cellIndex >= cells.Length) GD.Print($"CellIndex too big: {info.cellIndex}/{cells.Length}"); if (info.lightmapIndex >= cells[info.cellIndex].Lightmaps.Length) GD.Print($"LightmapIndex too big: {info.lightmapIndex}/{cells[info.cellIndex].Lightmaps.Length}"); var lightmap = cells[info.cellIndex].Lightmaps[info.lightmapIndex]; var layers = (uint)lightmap.Pixels.GetLength(0); var height = (uint)lightmap.Pixels.GetLength(1); var width = (uint)lightmap.Pixels.GetLength(2); for (uint y = 0; y < height; y++) { for (uint x = 0; x < width; x++) { var rawColour = System.Numerics.Vector4.Zero; for (uint l = 0; l < layers; l++) { rawColour += lightmap.GetPixel(l, x, y); } var colour = new Color(MathF.Min(rawColour.X, 1.0f), MathF.Min(rawColour.Y, 1.0f), MathF.Min(rawColour.Z, 1.0f), MathF.Min(rawColour.W, 1.0f)); image.SetPixel((int)(rect.X + x), (int)(rect.Y + y), colour); } } if (!surfaceDataMap.ContainsKey(info.textureId)) GD.Print("Invalid SurfaceDataMap key"); surfaceDataMap[info.textureId].TransformUv2s(info.uvStart, info.uvEnd, (uv) => { var u = uv.X; var v = uv.Y; // Clamp uv range to [0..1] u %= 1; v %= 1; if (u < 0) u = Math.Abs(u); if (v < 0) v = Math.Abs(v); // Transform! u = (rect.X + rect.Width * u) / (int)bounds.Width; v = (rect.Y + rect.Height * v) / (int)bounds.Height; return new Vector2(u, v); }); } return ImageTexture.CreateFromImage(image); } private int CalcBaseUV( WorldRep.Cell cell, WorldRep.Cell.Poly poly, WorldRep.Cell.RenderPoly renderPoly, WorldRep.Cell.LightmapInfo light, List textureUvs, List lightmapUvs, int cellIdxOffset) { // TODO: This is slightly hardcoded for ND. Check other stuff at some point. Should be handled in LG side imo // TODO: This is a mess lol var textureId = renderPoly.TextureId; var texture = _textureLoader.Get(textureId); var texU = renderPoly.TextureVectors.Item1.ToGodotVec3(); var texV = renderPoly.TextureVectors.Item2.ToGodotVec3(); var baseU = renderPoly.TextureBases.Item1; var baseV = renderPoly.TextureBases.Item2; var txUScale = 64.0f / texture.GetWidth(); var txVScale = 64.0f / texture.GetHeight(); var lmUScale = 4.0f / light.Width; var lmVScale = 4.0f / light.Height; var txUBase = baseU * txUScale; var txVBase = baseV * txVScale; var lmUBase = lmUScale * (baseU + (0.5f - light.Bases.Item1) / 4.0f); var lmVBase = lmVScale * (baseV + (0.5f - light.Bases.Item2) / 4.0f); var uu = texU.Dot(texU); var vv = texV.Dot(texV); var uv = texU.Dot(texV); var anchor = cell.Vertices[cell.Indices[cellIdxOffset + 0]].ToGodotVec3(); // TODO: This probably shouldn't be hardcoded idx 0 if (uv == 0.0) { var txUVec = texU * txUScale / uu; var txVVec = texV * txVScale / vv; var lmUVec = texU * lmUScale / uu; var lmVVec = texV * lmVScale / vv; for (var i = 0; i < poly.VertexCount; i++) { var v = cell.Vertices[cell.Indices[cellIdxOffset + i]].ToGodotVec3(); var delta = new Vector3(v.X - anchor.X, v.Y - anchor.Y, v.Z - anchor.Z); var txUV = new Vector2(delta.Dot(txUVec) + txUBase, delta.Dot(txVVec) + txVBase); var lmUV = new Vector2(delta.Dot(lmUVec) + lmUBase, delta.Dot(lmVVec) + lmVBase); textureUvs.Add(txUV); lightmapUvs.Add(lmUV); } } else { var denom = 1.0f / (uu * vv - uv * uv); var txUu = uu * txVScale * denom; var txVv = vv * txUScale * denom; var txUvu = txUScale * denom * uv; var txUvv = txVScale * denom * uv; var lmUu = uu * lmVScale * denom; var lmVv = vv * lmUScale * denom; var lmUvu = lmUScale * denom * uv; var lmUvv = lmVScale * denom * uv; for (var i = 0; i < poly.VertexCount; i++) { var v = cell.Vertices[cell.Indices[cellIdxOffset + i]].ToGodotVec3(); var delta = new Vector3(v.X - anchor.X, v.Y - anchor.Y, v.Z - anchor.Z); var du = delta.Dot(texU); var dv = delta.Dot(texV); var txUV = new Vector2(txUBase + txVv * du - txUvu * dv, txVBase + txUu * dv - txUvv * du); var lmUV = new Vector2(lmUBase + lmVv * du - lmUvu * dv, lmVBase + lmUu * dv - lmUvv * du); textureUvs.Add(txUV); lightmapUvs.Add(lmUV); } } return textureId; } private void LoadTextures(TxList textureList) { // TODO: Use PathJoin var count = textureList.ItemCount; for (var i = 0; i < count; i++) { var item = textureList.Items[i]; var path = ""; for (var j = 0; j < item.Tokens.Length; j++) { var token = item.Tokens[j]; if (token == 0) { break; } path += $"{textureList.Tokens[token - 1]}/"; } path += item.Name; if (!_textureLoader.Load(_installPaths, i, path)) { GD.Print($"Failed to load texture: {path}"); } } if (Dump) DumpTextureList(textureList); } private static void DumpTextureList(TxList textureList) { GD.Print($"TXLIST:\n BlockSize: {textureList.BlockSize}\n ItemCount: {textureList.ItemCount}\n TokenCount: {textureList.TokenCount}\n Tokens:"); for (var i = 0; i < textureList.TokenCount; i++) { GD.Print($" {i}: {textureList.Tokens[i]}"); } GD.Print($" Items:"); for (var i = 0; i < textureList.ItemCount; i++) { var item = textureList.Items[i]; GD.Print($" {i}:\n Tokens: [{item.Tokens[0]}, {item.Tokens[1]}, {item.Tokens[2]}, {item.Tokens[3]}]\n Name: {item.Name}"); } } const string MATERIAL_PATH = "res://project/materials/base.tres"; private static Material BuildMaterial(Texture albedoTexture, Texture lightmapTexture, bool lmHdr) { var material = ResourceLoader.Load(MATERIAL_PATH).Duplicate() as ShaderMaterial; material.SetShaderParameter("texture_albedo", albedoTexture); material.SetShaderParameter("lightmap_albedo", lightmapTexture); material.SetShaderParameter("lightmap_2x", lmHdr); return material; } }