From 8f20106aecdd1df4679b67b3d962de94fdb626c0 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:08:57 -0700 Subject: [PATCH] =?UTF-8?q?feat(objects):=20CategorizationSync.Reconcile?= =?UTF-8?q?=20=E2=80=94=20append=20Uncategorized=20+=20report=20orphans?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Objects/CategorizationSyncTests.cs | 44 +++++++++++ .../Object Creation/CategorizationSync.cs | 77 +++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 Projects/UOContent.Tests/Tests/Commands/Objects/CategorizationSyncTests.cs create mode 100644 Projects/UOContent/Commands/Object Creation/CategorizationSync.cs diff --git a/Projects/UOContent.Tests/Tests/Commands/Objects/CategorizationSyncTests.cs b/Projects/UOContent.Tests/Tests/Commands/Objects/CategorizationSyncTests.cs new file mode 100644 index 000000000..8fb4a225d --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Commands/Objects/CategorizationSyncTests.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using Server.Commands; +using Server.Items; +using Xunit; + +namespace UOContent.Tests.Commands.Objects; + +public class CategorizationSyncTests +{ + private static CAGJson Cat(string category, params System.Type[] types) => + new() + { + Category = category, + Objects = types.Select(t => new CAGObject { Type = t }).ToArray() + }; + + [Fact] + public void Reconcile_appends_missing_types_to_uncategorized() + { + var categorization = new List { Cat("Items.Weapons.Swords", typeof(Katana)) }; + var discovered = new List { typeof(Katana), typeof(Runebook) }; + + var (updated, report) = CategorizationSync.Reconcile(categorization, discovered); + + Assert.Contains("Runebook", report.Appended); + Assert.Empty(report.Orphaned); + + var uncategorized = Assert.Single(updated, c => c.Category == "Items.Uncategorized"); + Assert.Contains(uncategorized.Objects, o => o.Type == typeof(Runebook)); + } + + [Fact] + public void Reconcile_reports_orphans_not_in_discovered() + { + var categorization = new List { Cat("Items.Weapons.Swords", typeof(Katana)) }; + var discovered = new List { typeof(Runebook) }; + + var (_, report) = CategorizationSync.Reconcile(categorization, discovered); + + Assert.Contains("Katana", report.Orphaned); + Assert.Contains("Runebook", report.Appended); + } +} diff --git a/Projects/UOContent/Commands/Object Creation/CategorizationSync.cs b/Projects/UOContent/Commands/Object Creation/CategorizationSync.cs new file mode 100644 index 000000000..90b8f0169 --- /dev/null +++ b/Projects/UOContent/Commands/Object Creation/CategorizationSync.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Server.Items; + +namespace Server.Commands; + +public sealed record SyncReport(List Appended, List Orphaned); + +public static class CategorizationSync +{ + public static (List updated, SyncReport report) Reconcile( + List categorization, IReadOnlyList discovered + ) + { + var categorizedTypes = new HashSet(); + foreach (var cag in categorization) + { + foreach (var obj in cag.Objects ?? []) + { + if (obj.Type != null) + { + categorizedTypes.Add(obj.Type); + } + } + } + + var discoveredSet = new HashSet(discovered); + + var orphaned = categorizedTypes + .Where(t => !discoveredSet.Contains(t)) + .Select(t => t.Name) + .OrderBy(n => n, StringComparer.Ordinal) + .ToList(); + + var updated = new List(categorization); + var appended = new List(); + var itemAppend = new List(); + var mobileAppend = new List(); + + foreach (var type in discovered) + { + if (categorizedTypes.Contains(type)) + { + continue; + } + + appended.Add(type.Name); + var target = typeof(Mobile).IsAssignableFrom(type) ? mobileAppend : itemAppend; + target.Add(new CAGObject { Type = type }); + } + + AppendUncategorized(updated, "Items.Uncategorized", itemAppend); + AppendUncategorized(updated, "Mobiles.Uncategorized", mobileAppend); + + return (updated, new SyncReport(appended, orphaned)); + } + + private static void AppendUncategorized(List updated, string category, List toAdd) + { + if (toAdd.Count == 0) + { + return; + } + + var existing = updated.FirstOrDefault(c => c.Category == category); + if (existing == null) + { + updated.Add(new CAGJson { Category = category, Objects = toAdd.ToArray() }); + return; + } + + var merged = new List(existing.Objects ?? []); + merged.AddRange(toAdd); + updated[updated.IndexOf(existing)] = existing with { Objects = merged.ToArray() }; + } +}