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)";
}
}