feat(objects): ObjectCacheGenerator core + GenObjects command
Wraps introspection, categorization sync, and cache-building into a single file-I/O-free Generate() so the full pipeline is testable without a shard; GenObjects is a thin command that adds file I/O and operator messaging on top. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a55d83fc21
commit
0ac505d7b6
3 changed files with 155 additions and 0 deletions
|
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Server.Commands;
|
||||
using Server.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests.Commands.Objects;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class ObjectCacheGeneratorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Generate_builds_index_and_chunks_from_extracted_objects()
|
||||
{
|
||||
var discovered = new List<Type> { typeof(Runebook), typeof(Katana) };
|
||||
var categorization = new List<CAGJson>(); // empty -> both land in Items.Uncategorized
|
||||
|
||||
var result = ObjectCacheGenerator.Generate(categorization, discovered);
|
||||
|
||||
Assert.Equal(2, result.Index.Objects.Count);
|
||||
Assert.NotEmpty(result.Chunks);
|
||||
|
||||
var runebook = Assert.Single(result.Index.Objects, o => o.Type == "Runebook");
|
||||
Assert.True(runebook.ItemID > 0);
|
||||
Assert.Equal(1041267, runebook.Cliloc);
|
||||
Assert.Equal("items.uncategorized", runebook.Chunk);
|
||||
|
||||
Assert.Contains("Runebook", result.Report.Appended);
|
||||
Assert.Contains("items.uncategorized", result.Chunks.Keys);
|
||||
Assert.Contains("Runebook", result.Chunks["items.uncategorized"].Keys);
|
||||
}
|
||||
}
|
||||
56
Projects/UOContent/Commands/GenObjects.cs
Normal file
56
Projects/UOContent/Commands/GenObjects.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Server.Json;
|
||||
using Server.Logging;
|
||||
|
||||
namespace Server.Commands;
|
||||
|
||||
public static class GenObjects
|
||||
{
|
||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(GenObjects));
|
||||
|
||||
public static void Configure()
|
||||
{
|
||||
CommandSystem.Register("GenObjects", AccessLevel.Developer, GenObjects_OnCommand);
|
||||
}
|
||||
|
||||
[Usage("GenObjects")]
|
||||
[Aliases("GenObjWeb")]
|
||||
[Description("Generates the objects cache (index + detail chunks) and syncs categorization.json.")]
|
||||
private static void GenObjects_OnCommand(CommandEventArgs e)
|
||||
{
|
||||
var baseDir = Core.BaseDirectory;
|
||||
var categorizationPath = Path.Combine(baseDir, "Data", "categorization.json");
|
||||
var categorization = JsonConfig.Deserialize<List<CAGJson>>(categorizationPath) ?? [];
|
||||
var discovered = ObjectIntrospection.DiscoverConstructibleTypes();
|
||||
|
||||
var result = ObjectCacheGenerator.Generate(categorization, discovered);
|
||||
|
||||
var objectsDir = Path.Combine(baseDir, "Data", "objects");
|
||||
var detailDir = Path.Combine(objectsDir, "detail");
|
||||
Directory.CreateDirectory(detailDir);
|
||||
|
||||
JsonConfig.Serialize(Path.Combine(objectsDir, "index.json"), result.Index);
|
||||
foreach (var (chunkKey, map) in result.Chunks)
|
||||
{
|
||||
JsonConfig.Serialize(Path.Combine(detailDir, $"{chunkKey}.json"), map);
|
||||
}
|
||||
|
||||
JsonConfig.Serialize(categorizationPath, result.UpdatedCategorization);
|
||||
|
||||
e.Mobile.SendMessage(
|
||||
$"Objects cache written: {result.Index.Objects.Count} objects, {result.Chunks.Count} chunks. " +
|
||||
$"Appended {result.Report.Appended.Count} to Uncategorized, {result.Report.Orphaned.Count} orphaned."
|
||||
);
|
||||
|
||||
if (result.Report.Appended.Count > 0)
|
||||
{
|
||||
logger.Information("Appended to Uncategorized: {Types}", string.Join(", ", result.Report.Appended));
|
||||
}
|
||||
|
||||
if (result.Report.Orphaned.Count > 0)
|
||||
{
|
||||
logger.Warning("Orphaned categorization entries: {Types}", string.Join(", ", result.Report.Orphaned));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Server.Logging;
|
||||
|
||||
namespace Server.Commands;
|
||||
|
||||
public sealed record ObjectCacheResult(
|
||||
ObjectIndexFile Index,
|
||||
Dictionary<string, Dictionary<string, ObjectDetail>> Chunks,
|
||||
List<CAGJson> UpdatedCategorization,
|
||||
SyncReport Report
|
||||
);
|
||||
|
||||
public static class ObjectCacheGenerator
|
||||
{
|
||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(ObjectCacheGenerator));
|
||||
|
||||
public static ObjectCacheResult Generate(List<CAGJson> categorization, IReadOnlyList<Type> discovered)
|
||||
{
|
||||
var (updatedCategorization, report) = CategorizationSync.Reconcile(categorization, discovered);
|
||||
|
||||
var categoryByType = new Dictionary<Type, string>();
|
||||
foreach (var cag in updatedCategorization)
|
||||
{
|
||||
foreach (var obj in cag.Objects ?? [])
|
||||
{
|
||||
if (obj.Type != null)
|
||||
{
|
||||
categoryByType[obj.Type] = cag.Category;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var generatedUtc = DateTime.UtcNow.ToString("O");
|
||||
var extracted = new List<ExtractedObject>();
|
||||
foreach (var type in discovered)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entity = typeof(Mobile).IsAssignableFrom(type) ? "mobile" : "item";
|
||||
var category = categoryByType.TryGetValue(type, out var c)
|
||||
? c
|
||||
: entity == "mobile" ? "Mobiles.Uncategorized" : "Items.Uncategorized";
|
||||
|
||||
extracted.Add(
|
||||
new ExtractedObject(
|
||||
type,
|
||||
entity,
|
||||
category,
|
||||
ObjectIntrospection.ExtractLean(type),
|
||||
ObjectIntrospection.ExtractCtors(type),
|
||||
ObjectIntrospection.ExtractProperties(type),
|
||||
ObjectIntrospection.ExtractOpl(type),
|
||||
type.BaseType?.Name ?? "object"
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warning(ex, "Failed to introspect {Type}; skipping.", type);
|
||||
}
|
||||
}
|
||||
|
||||
var (index, chunks) = ObjectCacheBuilder.Build(extracted, generatedUtc);
|
||||
return new ObjectCacheResult(index, chunks, updatedCategorization, report);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue