docs(reference): import Dune: Awakening server-manager references
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>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="/home/archangel/.cache/CUE4Parse/CUE4Parse/CUE4Parse.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,283 @@
|
||||
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;
|
||||
@@ -0,0 +1,30 @@
|
||||
# Cue4ParsePatents
|
||||
|
||||
Scans Dune Awakening pak files via [CUE4Parse](https://github.com/FabianFG/CUE4Parse) (`GAME_DuneAwakening`) to discover inventory template IDs missing from the wiki-scraped catalog.
|
||||
|
||||
## Requirements
|
||||
|
||||
- .NET 8 SDK (`~/.dotnet` on WSL; set `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1` if `libicu` is missing)
|
||||
- CUE4Parse source cloned to `~/.cache/CUE4Parse` (NuGet 1.2.2 lacks Dune support)
|
||||
- Game install: `DuneAwakening/DuneSandbox/Content/Paks`
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
export DOTNET_ROOT=$HOME/.dotnet PATH="$DOTNET_ROOT:$PATH" DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
cd tools/Cue4ParsePatents
|
||||
dotnet run -c Release
|
||||
```
|
||||
|
||||
Outputs `vehicle-module-ids.txt`, `missing-vehicle-module-ids.txt`, `da-rec-template-ids.txt`, `item-registry-paths.txt`, etc.
|
||||
|
||||
## Central item ID sources (in game files)
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `Content/Dune/Systems/Items/BaseItems/DT_BaseItems_*.uasset` | Master item tables (Vehicles, BuildingSets, Placeables, …) |
|
||||
| `Content/Dune/Systems/Items/CDT_BaseItems.uasset` | Composite table referencing all base item tables |
|
||||
| `Content/Dune/Systems/TechKnowledge/.../DA_REC_*.uasset` | Tech recipes; row name after `DA_REC_` is often the template ID |
|
||||
| `Content/Dune/GUI/.../DT_Admin_QuickItems_Presets.uasset` | Admin quick-spawn presets |
|
||||
|
||||
Full row export from `DT_BaseItems_*` requires Oodle decompression + UE5 `.usmap` mappings in CUE4Parse (not yet wired up here). Pak string grep on `Systems.pak` works as a fallback for many IDs.
|
||||
Reference in New Issue
Block a user