using System.Reflection; using ACE.Database.Models.World; using ACE.Server.Factories.Entity; using ACE.Server.Factories.Tables; namespace Aeshnidae.DevKit; /// /// /loot-reload: the server re-reads the world database's loot tables without a restart. /// The staff site's loot editor calls it after every save (Tom, 2026-09-24: the retail loot /// tables editable on the website, live). /// /// ACE holds each of them in memory once read: /// treasure_death cachedDeathTreasure (by treasure type, filled on first ask) /// treasure_wielded cachedWieldedTreasure (the same) /// treasure_material_base / _color / _groups whole-table dictionaries built on first use /// treasure_gem_count GemCountChance's own table, built once in its static constructor /// /// The material tables are rebuilt with ACE's own CacheAll methods, which build the new /// table and then assign it in one step, so a roll in between reads the old one or the new /// one, never half of either. The two per-type caches are cleared and refill from the /// database on the next ask (GetCached* reads the database on a miss). The gem-count table /// is rebuilt in full first and then swapped into GemCountChance's dictionary. /// internal static class LootReload { public static string Run() { var world = DatabaseManager.World; var parts = new List(); world.CacheAllTreasureMaterialBase(); world.CacheAllTreasureMaterialColor(); world.CacheAllTreasureMaterialGroups(); parts.Add("materials rebuilt"); ClearCache(world, "cachedDeathTreasure"); parts.Add("death profiles cleared (refill on next roll)"); world.ClearWieldedTreasureCache(); parts.Add("wielded treasure cleared (refills on next spawn)"); parts.Add(ReloadGemCounts()); return string.Join("; ", parts); } private static void ClearCache(object world, string field) { var f = world.GetType().GetField(field, BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new MissingFieldException(world.GetType().Name, field); var value = f.GetValue(world) ?? throw new InvalidOperationException($"{field} is null"); value.GetType().GetMethod("Clear")!.Invoke(value, null); } private static string ReloadGemCounts() { var f = typeof(GemCountChance).GetField("gemCodes", BindingFlags.Static | BindingFlags.NonPublic); if (f?.GetValue(null) is not Dictionary>> gemCodes) return "gem counts NOT reloaded (GemCountChance.gemCodes not found - ACE changed?)"; // Built in full first, as GemCountChance's static constructor builds it, then swapped in. var fresh = new Dictionary>>(); using (var ctx = new WorldDbContext()) { foreach (var gemCount in ctx.TreasureGemCount.Where(i => i.Chance > 0)) { if (!fresh.TryGetValue(gemCount.GemCode, out var tiers)) fresh[gemCount.GemCode] = tiers = new Dictionary>(); if (!tiers.TryGetValue(gemCount.Tier, out var chances)) tiers[gemCount.Tier] = chances = new ChanceTable(); chances.Add((gemCount.Count, gemCount.Chance)); } } lock (gemCodes) { gemCodes.Clear(); foreach (var (code, tiers) in fresh) gemCodes[code] = tiers; } return $"gem counts rebuilt ({fresh.Count} gem codes)"; } }