galaxy: add ChildrenByParent index for level-at-a-time browse

This commit is contained in:
Joseph Doherty
2026-05-28 12:51:48 -04:00
parent 2c5c5e5c7e
commit d9eaf4b056
2 changed files with 157 additions and 3 deletions
@@ -7,18 +7,21 @@ public sealed class GalaxyHierarchyIndex
private GalaxyHierarchyIndex(
IReadOnlyList<GalaxyObjectView> objectViews,
IReadOnlyDictionary<int, GalaxyObjectView> objectViewsById,
IReadOnlyDictionary<string, GalaxyTagLookup> tagsByAddress)
IReadOnlyDictionary<string, GalaxyTagLookup> tagsByAddress,
IReadOnlyDictionary<int, IReadOnlyList<GalaxyObjectView>> childrenByParent)
{
ObjectViews = objectViews;
ObjectViewsById = objectViewsById;
TagsByAddress = tagsByAddress;
ChildrenByParent = childrenByParent;
}
/// <summary>Gets an empty Galaxy hierarchy index.</summary>
public static GalaxyHierarchyIndex Empty { get; } = new(
Array.Empty<GalaxyObjectView>(),
new Dictionary<int, GalaxyObjectView>(),
new Dictionary<string, GalaxyTagLookup>(StringComparer.OrdinalIgnoreCase));
new Dictionary<string, GalaxyTagLookup>(StringComparer.OrdinalIgnoreCase),
new Dictionary<int, IReadOnlyList<GalaxyObjectView>>());
/// <summary>Gets the object views.</summary>
public IReadOnlyList<GalaxyObjectView> ObjectViews { get; }
@@ -29,6 +32,9 @@ public sealed class GalaxyHierarchyIndex
/// <summary>Gets tags indexed by address.</summary>
public IReadOnlyDictionary<string, GalaxyTagLookup> TagsByAddress { get; }
/// <summary>Gets direct children grouped by parent gobject id. Root objects (no parent, or self-parented) live under key 0. Each list is sorted areas-first, then by display name (OrdinalIgnoreCase).</summary>
public IReadOnlyDictionary<int, IReadOnlyList<GalaxyObjectView>> ChildrenByParent { get; }
/// <summary>Builds a Galaxy hierarchy index from the given objects.</summary>
/// <param name="objects">The Galaxy objects to index.</param>
/// <returns>A new Galaxy hierarchy index.</returns>
@@ -71,10 +77,39 @@ public sealed class GalaxyHierarchyIndex
}
}
Dictionary<int, List<GalaxyObjectView>> childrenByParent = new();
foreach (GalaxyObjectView view in views)
{
int parentKey = view.Object.ParentGobjectId;
// Treat self-parented (corrupt) rows as roots; matches DashboardBrowseTreeBuilder.
if (parentKey == view.Object.GobjectId)
{
parentKey = 0;
}
if (!childrenByParent.TryGetValue(parentKey, out List<GalaxyObjectView>? bucket))
{
bucket = [];
childrenByParent[parentKey] = bucket;
}
bucket.Add(view);
}
foreach (List<GalaxyObjectView> bucket in childrenByParent.Values)
{
bucket.Sort(CompareByAreaThenDisplayName);
}
Dictionary<int, IReadOnlyList<GalaxyObjectView>> readOnlyChildren = new(childrenByParent.Count);
foreach (KeyValuePair<int, List<GalaxyObjectView>> kvp in childrenByParent)
{
readOnlyChildren[kvp.Key] = kvp.Value;
}
return new GalaxyHierarchyIndex(
views,
viewsById,
tagsByAddress);
tagsByAddress,
readOnlyChildren);
}
private static string BuildContainedPath(
@@ -110,4 +145,27 @@ public sealed class GalaxyHierarchyIndex
return obj.TagName;
}
private static int CompareByAreaThenDisplayName(GalaxyObjectView left, GalaxyObjectView right)
{
if (left.Object.IsArea != right.Object.IsArea)
{
return left.Object.IsArea ? -1 : 1;
}
return string.Compare(DisplayNameOf(left), DisplayNameOf(right), StringComparison.OrdinalIgnoreCase);
}
private static string DisplayNameOf(GalaxyObjectView view)
{
GalaxyObject obj = view.Object;
if (!string.IsNullOrWhiteSpace(obj.BrowseName))
{
return obj.BrowseName;
}
if (!string.IsNullOrWhiteSpace(obj.ContainedName))
{
return obj.ContainedName;
}
return obj.TagName;
}
}