Compare commits

..

8 Commits

8 changed files with 687 additions and 84 deletions

View File

@ -0,0 +1,22 @@
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)
{
var tmpName = Encoding.UTF8.GetString(reader.ReadBytes(256)).Replace("\0", string.Empty);
fileName = tmpName[..Math.Min(255, tmpName.Length)];
}
public void WriteData(BinaryWriter writer)
{
throw new System.NotImplementedException();
}
}

View File

@ -0,0 +1,105 @@
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
{
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 class LinkDataMetaProp : IChunk
{
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();
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace KeepersCompound.LGS.Database.Chunks;
public class PropertyModelName : IChunk
{
public record Property
{
public int objectId;
public int length;
public string modelName;
public Property(BinaryReader reader)
{
objectId = reader.ReadInt32();
length = (int)reader.ReadUInt32();
var tmpName = Encoding.UTF8.GetString(reader.ReadBytes(length)).Replace("\0", string.Empty);
modelName = tmpName[..Math.Min(length - 1, tmpName.Length)];
}
}
public ChunkHeader Header { get; set; }
public List<Property> properties;
public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry)
{
properties = new List<Property>();
while (reader.BaseStream.Position < entry.Offset + entry.Size + 24)
{
properties.Add(new Property(reader));
}
}
public void WriteData(BinaryWriter writer)
{
throw new System.NotImplementedException();
}
}

View File

@ -98,9 +98,13 @@ public class DbFile
{
// "AI_ROOM_DB" => new AiRoomDb(),
// "AICONVERSE" => new AiConverseChunk(),
"GAM_FILE" => new GamFile(),
"TXLIST" => new TxList(),
"WREXT" => new WorldRep(),
"BRLIST" => new BrList(),
"P$ModelName" => new PropertyModelName(),
"LD$MetaProp" => new LinkDataMetaProp(),
_ when entryName.StartsWith("L$") => new LinkChunk(),
_ => new GenericChunk(),
};
}

View File

@ -0,0 +1,362 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
namespace KeepersCompound.LGS;
// TODO: Support FMs resource paths!
public class ResourcePathManager
{
private record CampaignResources
{
public Dictionary<string, string> missionPathMap;
public Dictionary<string, string> texturePathMap;
public Dictionary<string, string> objectPathMap;
}
private bool _initialised = false;
private readonly string _extractionPath;
private CampaignResources _omResources;
private Dictionary<string, CampaignResources> _fmResources;
public ResourcePathManager(string extractionPath)
{
_extractionPath = extractionPath;
}
public bool Init(string installPath)
{
// TODO: This can be done less awkwardly with the resource paths
if (DirContainsThiefExe(installPath) &&
TryGetInstallCfgPath(installPath, out var installCfgPath) &&
TryBuildOmsPathMap(installPath, installCfgPath, out var omsMap) &&
TryBuildFmsPathMap(installPath, out var fmsMap, out var fmsDir) &&
TryGetPath(installPath, "fam.crf", out var famPath) &&
TryGetPath(installPath, "obj.crf", out var objPath))
{
// Register OM resources
{
var famExtractPath = Path.Join(_extractionPath, "fam");
if (Directory.Exists(famExtractPath))
{
Directory.Delete(famExtractPath, true);
}
ZipFile.OpenRead(famPath).ExtractToDirectory(famExtractPath);
var texturePathMap = GetTexturePaths(_extractionPath);
var objExtractPath = Path.Join(_extractionPath, "obj");
if (Directory.Exists(objExtractPath))
{
Directory.Delete(objExtractPath, true);
}
ZipFile.OpenRead(objPath).ExtractToDirectory(objExtractPath);
var objectPathMap = GetObjectPaths(_extractionPath);
_omResources = new CampaignResources
{
missionPathMap = omsMap,
texturePathMap = texturePathMap,
objectPathMap = objectPathMap,
};
}
{
_fmResources = new Dictionary<string, CampaignResources>();
foreach (var (campaign, missionPathMap) in fmsMap)
{
var texturePathMap = GetTexturePaths(Path.Join(fmsDir, campaign));
var objectPathMap = GetObjectPaths(Path.Join(fmsDir, campaign));
var resource = new CampaignResources
{
missionPathMap = missionPathMap,
texturePathMap = texturePathMap,
objectPathMap = objectPathMap,
};
_fmResources.Add(campaign, resource);
}
}
_initialised = true;
return true;
}
return false;
}
public string GetMissionPath(string missionName)
{
if (!_initialised) return null;
if (_omResources.missionPathMap.TryGetValue(missionName, out var path))
{
return path;
}
return null;
}
public string GetMissionPath(string campaignName, string missionName)
{
if (!_initialised) return null;
if (_fmResources.TryGetValue(campaignName, out var campaign) &&
campaign.missionPathMap.TryGetValue(missionName, out var path))
{
return path;
}
return null;
}
public string GetTexturePath(string textureName)
{
if (!_initialised) return null;
textureName = textureName.ToLower();
if (_omResources.texturePathMap.TryGetValue(textureName, out var path))
{
return path;
}
return null;
}
public string GetTexturePath(string campaignName, string textureName)
{
if (!_initialised) return null;
textureName = textureName.ToLower();
if (_fmResources.TryGetValue(campaignName, out var campaign) &&
campaign.texturePathMap.TryGetValue(textureName, out var path))
{
return path;
}
return null;
}
public string GetObjectPath(string objectName)
{
if (!_initialised) return null;
objectName = objectName.ToLower();
if (_omResources.objectPathMap.TryGetValue(objectName, out var path))
{
return path;
}
return null;
}
public string GetObjectPath(string campaignName, string objectName)
{
if (!_initialised) return null;
objectName = objectName.ToLower();
if (_fmResources.TryGetValue(campaignName, out var campaign) &&
campaign.objectPathMap.TryGetValue(objectName, out var path))
{
return path;
}
return null;
}
// TODO: Handle object textures?
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 ext = Path.GetExtension(path);
if (validExtensions.Contains(ext.ToLower()))
{
var key = Path.GetRelativePath(root, path)[..^ext.Length].ToLower();
pathMap.TryAdd(key, path);
}
}
}
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 key = Path.GetRelativePath(dir, path).ToLower();
pathMap.TryAdd(key, path);
}
}
return pathMap;
}
private static bool TryBuildOmsPathMap(string root, string cfgPath, out Dictionary<string, string> map)
{
map = new Dictionary<string, string>();
var omsPath = "";
foreach (var line in File.ReadLines(cfgPath))
{
if (line.StartsWith("load_path"))
{
// TODO: Do we only need to replace on systems with a different path separator?
var path = line.Split(" ")[1].Replace("\\", "/");
omsPath = Path.GetFullPath(root + path);
break;
}
}
if (omsPath == "")
{
return false;
}
var searchOptions = new EnumerationOptions
{
MatchCasing = MatchCasing.CaseInsensitive,
};
foreach (var path in Directory.GetFiles(root, "*.mis", searchOptions))
{
var baseName = Path.GetFileName(path).ToLower();
map.Add(baseName, path);
}
return true;
}
private static bool TryBuildFmsPathMap(
string root,
out Dictionary<string, Dictionary<string, string>> fmsMap,
out string fmsPath)
{
fmsMap = new Dictionary<string, Dictionary<string, string>>();
fmsPath = root + "/FMs";
var searchOptions = new EnumerationOptions
{
MatchCasing = MatchCasing.CaseInsensitive,
RecurseSubdirectories = true
};
// Cam Mod tells us where any FMs are installed (alongside other things)
// If it doesn't exist then something is wrong with the install directory
if (!TryGetPath(root, "cam_mod.ini", out var camModPath))
{
return false;
}
// We're looking for an uncommented line defining an fm_path, but its
// find if we don't find one because there's a default path.
foreach (var line in File.ReadLines(camModPath))
{
if (line.StartsWith("fm_path"))
{
// TODO: I think this can technically contain multiple paths
var path = line.Split(" ")[1].Replace("\\", "/");
fmsPath = Path.GetFullPath(root + path);
break;
}
}
// Now we can iterate through all the FM directories and map their mission paths
// if they have any.
string[] extensions = { ".mis", ".cow" };
searchOptions.RecurseSubdirectories = false;
foreach (var dir in Directory.GetDirectories(fmsPath))
{
var campaignMap = new Dictionary<string, string>();
foreach (var path in Directory.GetFiles(dir))
{
if (extensions.Contains(Path.GetExtension(path).ToLower()))
{
var baseName = Path.GetFileName(path).ToLower();
campaignMap.Add(baseName, path);
}
}
if (campaignMap.Count != 0)
{
fmsMap.Add(Path.GetFileName(dir), campaignMap);
}
}
return true;
}
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;
}
private static bool TryGetInstallCfgPath(string dir, out string installCfgPath)
{
var searchOptions = new EnumerationOptions
{
MatchCasing = MatchCasing.CaseInsensitive,
RecurseSubdirectories = true
};
foreach (var path in Directory.GetFiles(dir, "*.cfg", searchOptions))
{
var baseName = Path.GetFileName(path).ToLower();
if (baseName == "install.cfg" || baseName == "darkinst.cfg")
{
installCfgPath = path;
return true;
}
}
installCfgPath = "";
return false;
}
private static bool TryGetPath(string dir, string searchPattern, out string path)
{
var searchOptions = new EnumerationOptions
{
MatchCasing = MatchCasing.CaseInsensitive,
RecurseSubdirectories = true
};
var paths = Directory.GetFiles(dir, searchPattern, searchOptions);
if (paths.Length > 0)
{
path = paths[0];
return true;
}
path = "";
return false;
}
}

View File

@ -1,10 +1,12 @@
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;
@ -37,17 +39,19 @@ public partial class Mission : Node3D
[Export]
public bool Dump = false;
InstallPaths _installPaths;
ResourcePathManager _installPaths;
DbFile _file;
TextureLoader _textureLoader;
public override void _Ready()
{
var extractPath = ProjectSettings.GlobalizePath($"user://extracted/tmp");
_installPaths = new ResourcePathManager(extractPath);
var missionSelector = GetNode<Control>("%MissionSelector") as MissionSelector;
missionSelector.LoadMission += (string rootPath, string missionPath) =>
{
_installPaths = new InstallPaths(rootPath);
var inited = _installPaths.Init(rootPath);
GD.Print($"Inited paths: {inited}");
FileName = missionPath;
Build = true;
};
@ -90,11 +94,53 @@ public partial class Mission : Node3D
{
ClearMap();
_textureLoader = new TextureLoader(_installPaths.famPath, FileName.GetBaseDir());
// TODO: This shouldn't be set for things that aren't actually FMs
var fmName = FileName.GetBaseDir().GetFile();
_textureLoader = new TextureLoader(fmName);
_file = new(FileName);
UseChunk<TxList>("TXLIST", LoadTextures);
UseChunk<WorldRep>("WREXT", BuildWrMeshes);
UseChunk<BrList>("BRLIST", PlaceObjects);
if (
_file.Chunks.TryGetValue("BRLIST", out var brListRaw) &&
_file.Chunks.TryGetValue("P$ModelName", out var modelNamesRaw) &&
_file.Chunks.TryGetValue("L$MetaProp", out var metaPropLinksRaw) &&
_file.Chunks.TryGetValue("LD$MetaProp", out var metaPropLinkDataRaw)
)
{
var brList = (BrList)brListRaw;
var modelNames = (PropertyModelName)modelNamesRaw;
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))
{
GD.Print($"Pre-Merged chunks: {modelNames.properties.Count} {metaPropLinks.links.Count} {metaPropLinkData.linkData.Count}");
modelNames.properties.AddRange(((PropertyModelName)gamChunk1).properties);
metaPropLinks.links.AddRange(((LinkChunk)gamChunk2).links);
metaPropLinkData.linkData.AddRange(((LinkDataMetaProp)gamChunk3).linkData);
GD.Print($"Post-Merged chunks: {modelNames.properties.Count} {metaPropLinks.links.Count} {metaPropLinkData.linkData.Count}");
}
}
}
PlaceObjects(brList, modelNames, metaPropLinks, metaPropLinkData);
}
}
private void UseChunk<T>(string name, Action<T> action)
@ -109,7 +155,11 @@ public partial class Mission : Node3D
}
}
private void PlaceObjects(BrList brList)
private void PlaceObjects(
BrList brList,
PropertyModelName modelNames,
LinkChunk metapropLink,
LinkDataMetaProp metaPropLinkData)
{
foreach (var brush in brList.Brushes)
{
@ -118,12 +168,68 @@ public partial class Mission : Node3D
continue;
}
var pos = brush.position.ToGodotVec3();
var cube = new CsgBox3D
// 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 = "";
while (true)
{
Position = pos
};
AddChild(cube);
// See if there's a modelname property
foreach (var prop in modelNames.properties)
{
if (prop.objectId == id)
{
modelName = prop.modelName;
break;
}
}
if (modelName != "") break;
// No modelname so 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 == "")
{
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 model = new Model();
model.Position = pos;
if (objPath != null)
{
model.BuildModel("", objPath);
}
AddChild(model);
// var pos = brush.position.ToGodotVec3();
// var cube = new CsgBox3D
// {
// Position = pos
// };
// AddChild(cube);
}
}
@ -354,7 +460,7 @@ public partial class Mission : Node3D
for (var i = 0; i < count; i++)
{
var item = textureList.Items[i];
var path = "/";
var path = "";
for (var j = 0; j < item.Tokens.Length; j++)
{
var token = item.Tokens[j];
@ -367,7 +473,7 @@ public partial class Mission : Node3D
}
path += item.Name;
if (!_textureLoader.Load(i, path))
if (!_textureLoader.Load(_installPaths, i, path))
{
GD.Print($"Failed to load texture: {path}");
}

View File

@ -14,7 +14,7 @@ public partial class Model : Node3D
modelSelector.LoadModel += BuildModel;
}
private void BuildModel(string rootPath, string modelPath)
public void BuildModel(string rootPath, string modelPath)
{
foreach (var node in GetChildren())
{
@ -28,7 +28,7 @@ public partial class Model : Node3D
return;
}
// TODO: Remove this disgusting hack
// TODO: Remove this disgusting hack. Not only is it a hack, it doesn't support custom models
var baseDir = ProjectSettings.GlobalizePath($"user://objects/tmp");
var options = new EnumerationOptions
{

View File

@ -1,45 +1,21 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using Godot;
using KeepersCompound.LGS;
namespace KeepersCompound.TMV;
public partial class TextureLoader
{
private readonly string _userTexturesPath;
private readonly string _rootFamPath; // TODO: Load from installation resources
private readonly string _fmPath;
private readonly Dictionary<string, string> _rootTexturePaths = new();
private readonly Dictionary<string, string> _fmTexturePaths = new();
private readonly string _fmName;
private readonly List<ImageTexture> _textureCache = new();
private readonly Dictionary<int, int> _idMap = new();
private readonly Dictionary<string, int> _pathMap = new();
public TextureLoader(string rootFamPath, string fmPath)
public TextureLoader(string fmName)
{
_rootFamPath = rootFamPath;
_fmPath = fmPath;
_userTexturesPath = ProjectSettings.GlobalizePath($"user://textures/tmp");
ExtractRootFamFiles();
_fmName = fmName;
LoadDefaultTexture();
RegisterTexturePaths(_fmPath, _fmTexturePaths);
RegisterTexturePaths(_userTexturesPath, _rootTexturePaths);
}
private void ExtractRootFamFiles()
{
var dir = new DirectoryInfo(_userTexturesPath);
if (dir.Exists)
{
dir.Delete(true);
}
var zip = ZipFile.OpenRead(_rootFamPath);
zip.ExtractToDirectory(_userTexturesPath.PathJoin("fam"));
}
private void LoadDefaultTexture()
@ -50,46 +26,28 @@ public partial class TextureLoader
_textureCache.Add(texture);
}
private static void RegisterTexturePaths(string rootDir, Dictionary<string, string> map)
{
// TODO: Load DDS BMP GIF CEL
string[] validExtensions = { "png", "tga", "pcx", "gif" };
var famOptions = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive };
var textureOptions = new EnumerationOptions
{
MatchCasing = MatchCasing.CaseInsensitive,
RecurseSubdirectories = true,
};
foreach (var dirPath in Directory.EnumerateDirectories(rootDir, "fam", famOptions))
{
foreach (var path in Directory.EnumerateFiles(dirPath, "*", textureOptions))
{
if (validExtensions.Contains(path.GetExtension().ToLower()))
{
// TODO: This only adds the first one found rather than the highest priority
var key = path.TrimPrefix(rootDir).GetBaseName().ToLower();
map.TryAdd(key, path);
}
}
}
GD.Print($"Registered {map.Count} texture paths at : {rootDir}");
}
public bool Load(int id, string path)
public bool Load(ResourcePathManager installManager, int id, string path)
{
var loaded = false;
if (_fmTexturePaths.TryGetValue(path.ToLower(), out var fmTexPath))
string texPath;
if (_fmName != null)
{
_textureCache.Add(LoadTexture(fmTexPath));
loaded = true;
texPath = installManager.GetTexturePath(_fmName, path);
texPath ??= installManager.GetTexturePath(path);
}
else if (_rootTexturePaths.TryGetValue(path.ToLower(), out var rootTexPath))
else
{
_textureCache.Add(LoadTexture(rootTexPath));
loaded = true;
texPath = installManager.GetTexturePath(path);
}
if (texPath != null)
{
var texture = LoadTexture(texPath);
if (texture != null)
{
_textureCache.Add(texture);
loaded = true;
}
}
var index = loaded ? _textureCache.Count - 1 : 0;
@ -101,13 +59,18 @@ public partial class TextureLoader
public static ImageTexture LoadTexture(string path)
{
var ext = path.GetExtension().ToLower();
var texture = ext switch
string[] validExtensions = { "png", "tga", "pcx", "gif" };
if (validExtensions.Contains(ext))
{
"pcx" => LoadPcx(path),
"gif" => LoadGif(path),
_ => ImageTexture.CreateFromImage(Image.LoadFromFile(path)),
};
return texture;
var texture = ext switch
{
"pcx" => LoadPcx(path),
"gif" => LoadGif(path),
_ => ImageTexture.CreateFromImage(Image.LoadFromFile(path)),
};
return texture;
}
return null;
}
public ImageTexture Get(int id)