diff --git a/project/code/LGS/ResourcePathManager.cs b/project/code/LGS/ResourcePathManager.cs new file mode 100644 index 0000000..8cf2048 --- /dev/null +++ b/project/code/LGS/ResourcePathManager.cs @@ -0,0 +1,360 @@ +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 missionPathMap; + public Dictionary texturePathMap; + public Dictionary objectPathMap; + } + + private bool _initialised = false; + private readonly string _extractionPath; + private CampaignResources _omResources; + private Dictionary _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(); + 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; + + if (_omResources.objectPathMap.TryGetValue(objectName, out var path)) + { + return path; + } + return null; + } + + public string GetObjectPath(string campaignName, string objectName) + { + if (!_initialised) return null; + + 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 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(); + 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 GetObjectPaths(string root) + { + var dirOptions = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }; + var binOptions = new EnumerationOptions + { + MatchCasing = MatchCasing.CaseInsensitive, + RecurseSubdirectories = true, + }; + + var pathMap = new Dictionary(); + 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 map) + { + map = new Dictionary(); + + 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> fmsMap, + out string fmsPath) + { + fmsMap = new Dictionary>(); + 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(); + 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; + } +} \ No newline at end of file