using CUE4Parse.FileProvider; using CUE4Parse.UE4.Assets.Exports.Engine; using CUE4Parse.UE4.Versions; using CUE4Parse.Utils; using System.Text.Json; using System.Text.RegularExpressions; const string defaultPaksDir = "/mnt/c/Program Files (x86)/Steam/steamapps/common/DuneAwakening/DuneSandbox/Content/Paks"; var paksDir = args.Length > 0 ? args[0] : defaultPaksDir; var outDir = args.Length > 1 ? args[1] : Directory.GetCurrentDirectory(); if (!Directory.Exists(paksDir)) { Console.Error.WriteLine($"Paks directory not found: {paksDir}"); return 1; } Console.WriteLine($"Scanning: {paksDir}"); var pathComparer = StringComparer.OrdinalIgnoreCase; var provider = new DefaultFileProvider( paksDir, SearchOption.TopDirectoryOnly, versions: new VersionContainer(EGame.GAME_DuneAwakening), pathComparer: pathComparer); provider.Initialize(); var mounted = provider.Mount(); Console.WriteLine($"Mounted {mounted} archives; {provider.Files.Count:N0} files"); var allPaths = provider.Files.Keys.ToList(); var catalogPath = Path.GetFullPath(Path.Combine(outDir, "..", "..", "public", "data", "item-catalog.json")); var catalogIds = new HashSet(StringComparer.OrdinalIgnoreCase); if (File.Exists(catalogPath)) { var json = File.ReadAllText(catalogPath); foreach (Match m in Regex.Matches(json, "\"([^\"]+)\"\\s*:\\s*\\{")) catalogIds.Add(m.Groups[1].Value); Console.WriteLine($"Loaded {catalogIds.Count} catalog IDs"); } // Inventory template IDs for vehicle modules: SandbikeChassis_5, TreadwheelEngine_4, etc. var modulePattern = new Regex( @"^(?Sandbike|Buggy|Treadwheel|OrnithopterTransport|OrnithopterLight|OrnithopterMedium|AssaultOrnithopter|CarrierOrnithopter|Sandcrawler|Groundcar|OrnithopterHeavy)(?(?:Unique_[A-Za-z0-9_]+|[A-Za-z_]+))_(?\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); var moduleIds = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var path in allPaths) { var baseName = Path.GetFileNameWithoutExtension(path); if (string.IsNullOrEmpty(baseName)) continue; if (modulePattern.IsMatch(baseName)) { moduleIds.TryAdd(baseName, path); continue; } if (baseName.StartsWith("DA_REC_", StringComparison.OrdinalIgnoreCase)) { var embedded = baseName["DA_REC_".Length..]; if (modulePattern.IsMatch(embedded)) moduleIds.TryAdd(embedded, path); } } // Also collect item-ish paths under /Items/ for manual review var itemPaths = allPaths .Where(p => p.Contains("/Items/", StringComparison.OrdinalIgnoreCase) && (p.Contains("Treadwheel", StringComparison.OrdinalIgnoreCase) || p.Contains("OrnithopterTransport", StringComparison.OrdinalIgnoreCase) || p.Contains("CargoContainer", StringComparison.OrdinalIgnoreCase) || p.Contains("Sandcrawler", StringComparison.OrdinalIgnoreCase))) .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) .ToList(); Directory.CreateDirectory(outDir); var moduleOut = Path.Combine(outDir, "vehicle-module-ids.txt"); File.WriteAllLines(moduleOut, moduleIds.Keys.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)); var moduleMissing = moduleIds.Keys.Where(id => !catalogIds.Contains(id)).OrderBy(x => x).ToList(); var moduleMissingOut = Path.Combine(outDir, "missing-vehicle-module-ids.txt"); File.WriteAllLines(moduleMissingOut, moduleMissing); File.WriteAllLines(Path.Combine(outDir, "item-path-hints.txt"), itemPaths); Console.WriteLine(); Console.WriteLine($"=== Vehicle module template IDs: {moduleIds.Count} ({moduleMissing.Count} missing from catalog) ==="); foreach (var prefix in new[] { "Treadwheel", "OrnithopterTransport", "CarrierOrnithopter", "Sandcrawler", "Groundcar" }) { var ids = moduleIds.Keys.Where(k => k.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).OrderBy(x => x).ToList(); if (ids.Count == 0) continue; Console.WriteLine(); Console.WriteLine($"-- {prefix} ({ids.Count}) --"); foreach (var id in ids) { var flag = catalogIds.Contains(id) ? "OK" : "MISSING"; Console.WriteLine($" [{flag}] {id}"); } } Console.WriteLine(); Console.WriteLine($"Wrote {moduleOut}"); Console.WriteLine($"Wrote {moduleMissingOut}"); Console.WriteLine($"Wrote {Path.Combine(outDir, "item-path-hints.txt")} ({itemPaths.Count} paths)"); // Broader item-source discovery for future catalog patches var sourceHints = new[] { "/Items/", "ItemTemplate", "ItemDefinition", "ItemData", "DataTable", "DT_", "InventoryItem", "CargoContainer", "StorageContainer", "PlacableSet", "Patent" }; foreach (var hint in sourceHints) { var count = allPaths.Count(p => p.Contains(hint, StringComparison.OrdinalIgnoreCase)); if (count > 0) Console.WriteLine($" paths[{hint}]: {count}"); } var recPattern = new Regex(@"^DA_REC_(.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); var recIds = new SortedSet(StringComparer.OrdinalIgnoreCase); foreach (var path in allPaths) { var baseName = Path.GetFileNameWithoutExtension(path); if (baseName == null) continue; var m = recPattern.Match(baseName); if (m.Success) recIds.Add(m.Groups[1].Value); } var recOut = Path.Combine(outDir, "da-rec-template-ids.txt"); File.WriteAllLines(recOut, recIds); Console.WriteLine($"DA_REC-derived template IDs: {recIds.Count} -> {recOut}"); var registryPaths = allPaths .Where(p => Regex.IsMatch(p, @"(ItemTemplates|ItemDefinitions|ItemRegistry|MasterItem|AllItems|ItemCatalog|DT_.*Item|ItemTable)", RegexOptions.IgnoreCase)) .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) .ToList(); var registryOut = Path.Combine(outDir, "item-registry-paths.txt"); File.WriteAllLines(registryOut, registryPaths); Console.WriteLine($"Item registry candidate paths: {registryPaths.Count} -> {registryOut}"); var cargoPaths = allPaths .Where(p => p.Contains("Cargo", StringComparison.OrdinalIgnoreCase) && (p.Contains("Ornithopter", StringComparison.OrdinalIgnoreCase) || p.Contains("Transport", StringComparison.OrdinalIgnoreCase) || p.Contains("Container", StringComparison.OrdinalIgnoreCase) || p.Contains("Storage", StringComparison.OrdinalIgnoreCase))) .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) .ToList(); var cargoOut = Path.Combine(outDir, "cargo-container-paths.txt"); File.WriteAllLines(cargoOut, cargoPaths); Console.WriteLine($"Cargo/container ornithopter paths: {cargoPaths.Count} -> {cargoOut}"); foreach (var p in cargoPaths.Where(p => p.EndsWith(".uasset", StringComparison.OrdinalIgnoreCase)).Take(15)) Console.WriteLine(" " + p); var baseItemTables = new[] { "DuneSandbox/Content/Dune/Systems/Items/BaseItems/DT_BaseItems_Vehicles", "DuneSandbox/Content/Dune/Systems/Items/BaseItems/DT_BaseItems_BuildingSets", "DuneSandbox/Content/Dune/Systems/Items/BaseItems/DT_BaseItems_Placeables", "DuneSandbox/Content/Dune/Systems/Items/BaseItems/DT_BaseItems_Resources", "DuneSandbox/Content/Dune/Systems/Items/CDT_BaseItems", "DuneSandbox/Content/Dune/GUI/Widgets/Menus/Gameplay/AdminPanel/Items/DT_Admin_QuickItems_Presets", }; Console.WriteLine(); Console.WriteLine("=== DT_BaseItems row keys ==="); foreach (var tablePath in baseItemTables) { var tableName = tablePath.SubstringAfterLast('/'); try { var table = provider.SafeLoadPackageObject(tablePath, tableName); if (table?.RowMap == null) { Console.WriteLine($" {tableName}: failed to load"); continue; } var keys = table.RowMap.Keys.Select(k => k.Text).OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(); var tableOut = Path.Combine(outDir, $"{tableName}-rows.txt"); File.WriteAllLines(tableOut, keys); Console.WriteLine($" {tableName}: {keys.Count} rows -> {tableOut}"); var cargo = keys.Where(k => k.Contains("Cargo", StringComparison.OrdinalIgnoreCase) || k.Contains("Container", StringComparison.OrdinalIgnoreCase) || k.Contains("Treadwheel", StringComparison.OrdinalIgnoreCase) || k.Contains("Patent", StringComparison.OrdinalIgnoreCase)).ToList(); foreach (var k in cargo.Take(20)) Console.WriteLine($" {k}"); } catch (Exception ex) { Console.WriteLine($" {tableName}: {ex.Message}"); } } // Fallback: extract printable template-id-like strings from key table uexp blobs Console.WriteLine(); Console.WriteLine("=== UEXP string fallback (template ID candidates) ==="); var idFromBytes = new Regex(@"\b[A-Z][A-Za-z0-9_]{2,80}\b", RegexOptions.Compiled); foreach (var tablePath in baseItemTables) { var uexpPath = tablePath + ".uexp"; if (!provider.Files.TryGetValue(uexpPath, out var file)) continue; try { var bytes = file.Read(); var text = System.Text.Encoding.ASCII.GetString(bytes); var ids = idFromBytes.Matches(text) .Select(m => m.Value) .Where(v => v.Contains('_') && !v.StartsWith("DA_", StringComparison.Ordinal) && !v.EndsWith("Placeable", StringComparison.OrdinalIgnoreCase) && char.IsUpper(v[0])) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) .ToList(); var tableName = tablePath.SubstringAfterLast('/'); var outPath = Path.Combine(outDir, $"{tableName}-uexp-ids.txt"); File.WriteAllLines(outPath, ids); Console.WriteLine($" {tableName}: {ids.Count} id-like strings -> {outPath}"); foreach (var id in ids.Where(i => i.Contains("Cargo", StringComparison.OrdinalIgnoreCase) || i.Contains("Container", StringComparison.OrdinalIgnoreCase) || i.Contains("Treadwheel", StringComparison.OrdinalIgnoreCase) || i.Contains("Patent", StringComparison.OrdinalIgnoreCase)).Take(12)) Console.WriteLine($" {id}"); } catch (Exception ex) { Console.WriteLine($" {tablePath}: uexp read failed: {ex.Message}"); } } static string TechCategory(string id) { if (id.StartsWith("DA_GRP_", StringComparison.OrdinalIgnoreCase)) return "Group"; if (id.StartsWith("DA_REC_", StringComparison.OrdinalIgnoreCase)) return "Recipe"; if (id.StartsWith("RCP_", StringComparison.OrdinalIgnoreCase)) return "Recipe"; if (id.StartsWith("BLD_", StringComparison.OrdinalIgnoreCase)) return "Building"; return "Other"; } static bool IsTechTreeNodeId(string name) => name.StartsWith("DA_GRP_", StringComparison.OrdinalIgnoreCase) || name.StartsWith("DA_REC_", StringComparison.OrdinalIgnoreCase) || name.StartsWith("RCP_", StringComparison.OrdinalIgnoreCase) || name.StartsWith("BLD_", StringComparison.OrdinalIgnoreCase); var techNodeIds = allPaths .Where(p => p.Contains("/TechKnowledge/", StringComparison.OrdinalIgnoreCase) && p.EndsWith(".uasset", StringComparison.OrdinalIgnoreCase)) .Select(p => Path.GetFileNameWithoutExtension(p)!) .Where(IsTechTreeNodeId) .Concat(allPaths .Where(p => p.EndsWith(".uasset", StringComparison.OrdinalIgnoreCase)) .Select(p => Path.GetFileNameWithoutExtension(p)!) .Where(n => n.StartsWith("RCP_", StringComparison.OrdinalIgnoreCase) || n.StartsWith("BLD_", StringComparison.OrdinalIgnoreCase))) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) .ToList(); var techOut = Path.Combine(outDir, "tech-tree-node-ids.txt"); File.WriteAllLines(techOut, techNodeIds); var techCatalogPath = Path.GetFullPath(Path.Combine(outDir, "..", "..", "public", "data", "tech-recipe-catalog.json")); var recipes = new Dictionary(); foreach (var id in techNodeIds) recipes[id] = new { category = TechCategory(id) }; Directory.CreateDirectory(Path.GetDirectoryName(techCatalogPath)!); File.WriteAllText( techCatalogPath, JsonSerializer.Serialize( new { total = techNodeIds.Count, recipes }, new JsonSerializerOptions { WriteIndented = true })); Console.WriteLine(); Console.WriteLine($"=== Tech tree node IDs: {techNodeIds.Count} ==="); foreach (var prefix in new[] { "DA_GRP_", "DA_REC_", "RCP_", "BLD_" }) { var n = techNodeIds.Count(id => id.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); if (n > 0) Console.WriteLine($" {prefix}: {n}"); } Console.WriteLine($"Wrote {techOut}"); Console.WriteLine($"Wrote {techCatalogPath}"); return 0;