Phase 2 references for the host-agent Dune adapter, moved out of volatile /tmp
into docs/reference-repos/ (per Commander). Three upstream projects, .git +
node_modules + compiled binaries stripped (16MB source). Nested AI-instruction
files (.claude/, CLAUDE.md) removed so they don't pollute Corrosion sessions.
- icehunter/ dune-admin (Go+React) — 4 control planes; SETUP_DOCKER.md is the
closest analog to our agent's Dune docker control plane (compose
lifecycle, docker logs, RabbitMQ-via-exec, dune Postgres schema)
- adainrivers/ Rust/Tauri desktop — SSH+k8s BattleGroup control, maintenance
daemon, in-game admin console (Rust idiom reference)
- the4rchangel/ Node web UI replacing battlegroup.bat — matches the Commander's
Hyper-V self-host path + game-config schema
See docs/reference-repos/README.md for the full index + how we use each.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
284 lines
13 KiB
C#
284 lines
13 KiB
C#
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<string>(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(
|
|
@"^(?<prefix>Sandbike|Buggy|Treadwheel|OrnithopterTransport|OrnithopterLight|OrnithopterMedium|AssaultOrnithopter|CarrierOrnithopter|Sandcrawler|Groundcar|OrnithopterHeavy)(?<part>(?:Unique_[A-Za-z0-9_]+|[A-Za-z_]+))_(?<tier>\d+)$",
|
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
|
|
var moduleIds = new Dictionary<string, string>(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<string>(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<UDataTable>(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<string, object>();
|
|
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;
|