using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; namespace KeepersCompound.LGS; public enum ResourceType { Mission, Object, ObjectTexture, Texture, } public class ResourcePathManager { private record CampaignResources { public Dictionary missionPathMap; public Dictionary texturePathMap; public Dictionary objectPathMap; public Dictionary objectTexturePathMap; public List GetResourceNames(ResourceType type) { List keys = type switch { ResourceType.Mission => [.. missionPathMap.Keys], ResourceType.Object => [.. objectPathMap.Keys], ResourceType.ObjectTexture => [.. objectTexturePathMap.Keys], ResourceType.Texture => [.. texturePathMap.Keys], _ => throw new ArgumentOutOfRangeException(nameof(type)), }; keys.Sort(); return keys; } public string GetResourcePath(ResourceType type, string name) { var map = type switch { ResourceType.Mission => missionPathMap, ResourceType.Object => objectPathMap, ResourceType.ObjectTexture => objectTexturePathMap, ResourceType.Texture => texturePathMap, _ => throw new ArgumentOutOfRangeException(nameof(type)), }; return map.TryGetValue(name, out var resourcePath) ? resourcePath : null; } } private bool _initialised = false; private readonly string _extractionPath; private string _fmsDir; 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); var objectTexturePathMap = GetObjectTexturePaths(_extractionPath); _omResources = new CampaignResources { missionPathMap = omsMap, texturePathMap = texturePathMap, objectPathMap = objectPathMap, objectTexturePathMap = objectTexturePathMap, }; } { _fmResources = new Dictionary(); foreach (var (campaign, missionPathMap) in fmsMap) { var root = Path.Join(fmsDir, campaign); var texturePathMap = GetTexturePaths(root); var objectPathMap = GetObjectPaths(root); var objectTexturePathMap = GetObjectTexturePaths(root); var resource = new CampaignResources { missionPathMap = missionPathMap, texturePathMap = texturePathMap, objectPathMap = objectPathMap, objectTexturePathMap = objectTexturePathMap, }; _fmResources.Add(campaign, resource); _fmsDir = fmsDir; } } _initialised = true; return true; } return false; } public List GetCampaignNames() { if (!_initialised) return null; var names = new List(_fmResources.Keys); names.Sort(); return names; } public List GetResourceNames(ResourceType type, string campaignName) { if (!_initialised) { throw new InvalidOperationException("Resource Path Manager hasn't been initialised."); } if (campaignName == null || campaignName == "") { return _omResources.GetResourceNames(type); } else if (_fmResources.TryGetValue(campaignName, out var campaign)) { return campaign.GetResourceNames(type); } throw new ArgumentException("No campaign found with given name", nameof(campaignName)); } public (string, string) GetResourcePath( ResourceType type, string campaignName, string resourceName) { if (!_initialised) { throw new InvalidOperationException("Resource Path Manager hasn't been initialised."); } resourceName = resourceName.ToLower(); var omResourcePath = _omResources.GetResourcePath(type, resourceName); if (campaignName == null || campaignName == "" && omResourcePath != null) { return ("", omResourcePath); } else if (_fmResources.TryGetValue(campaignName, out var campaign)) { var fmResourcePath = campaign.GetResourcePath(type, resourceName); if (fmResourcePath != null) { return (campaignName, fmResourcePath); } else if (omResourcePath != null) { return ("", omResourcePath); } } // throw new ArgumentException($"No resource found with given type and name: {type}, {resourceName}", nameof(resourceName)); return (null, null); } private static Dictionary GetObjectTexturePaths(string root) { string[] validExtensions = { ".dds", ".png", ".tga", ".pcx", ".gif", ".bmp", ".cel", }; var dirOptions = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }; var texOptions = 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, "*", texOptions)) { var ext = Path.GetExtension(path); if (validExtensions.Contains(ext.ToLower())) { var key = Path.GetFileNameWithoutExtension(path).ToLower(); pathMap.TryAdd(key, path); } } } return pathMap; } 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(omsPath, "*.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; } }