using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; namespace KeepersCompound.LGS; // TODO: Merge the two versions of GetXXX and handle null campaign string as OM 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 List GetCampaignNames() { if (!_initialised) return null; var names = new List(_fmResources.Keys); names.Sort(); return names; } public string GetMissionPath(string campaignName, string missionName) { if (!_initialised) return null; if (campaignName == null || campaignName == "") { if (_omResources.missionPathMap.TryGetValue(missionName, out var omPath)) { return omPath; } } else if ( _fmResources.TryGetValue(campaignName, out var campaign) && campaign.missionPathMap.TryGetValue(missionName, out var fmPath)) { return fmPath; } return null; } public List GetMissionNames(string campaignName) { if (!_initialised) return null; if (campaignName == null || campaignName == "") { var names = new List(_omResources.missionPathMap.Keys); names.Sort(); return names; } else if (_fmResources.TryGetValue(campaignName, out var campaign)) { var names = new List(campaign.missionPathMap.Keys); names.Sort(); return names; } 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 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; } }