namespace Aeshnidae.InstancesNoDat;
///
/// A per-copy view of ACE's global physics object registry.
///
/// ServerObjectManager keeps one ConcurrentDictionary keyed by guid for the whole
/// server, and landblock instances have DETERMINISTIC guids - a drudge authored in
/// 002B is 0x7002B668 in the master and 0x7002B668 in every copy of it. So all of them
/// compete for a single slot, last write wins, and both the master and the copies
/// populate themselves on background tasks: which one ends up registered is a race.
///
/// Fourteen call sites read that registry - targeting, sticky projectiles, and the
/// voyeur checks that decide who gets told about whom - so losing the race does not
/// merely lose a lookup. It hands physics a different copy's object.
///
/// The fix is the same one this mod already uses for landblock and cell lookups: scope
/// the answer to the copy being processed. A guid is only ambiguous within one landblock
/// id, and only for static guids:
///
/// 0x50000001-0x5FFFFFFF players unique, must stay global
/// 0x70000000-0x7FFFFFFF landblock instances COLLIDE, one per copy
/// 0x80000000+ corpses, loot, projectiles unique, must stay global
///
/// So players and dynamics are left entirely alone - they are unique already, and
/// hiding a player from a global lookup would break far more than it fixed. Only static
/// guids are held here, and only for our copies. The master keeps the global slot,
/// which means a lookup with no ambient copy still answers exactly what it always did.
///
public static class Registry
{
private static readonly ConcurrentDictionary<(ushort Landblock, int Copy, uint Guid), PhysicsObj> _objects = new();
public static int Count => _objects.Count;
///
/// Whether this guid is one of the ambiguous ones. Anything else belongs in ACE's
/// own registry and is none of our business.
///
public static bool IsAmbiguous(uint guid) => ObjectGuid.IsStatic(guid);
/// The copy currently being processed, if it is one of ours.
private static (ushort Landblock, int Copy)? Scope()
{
var ambient = Context.Ambient;
if (ambient is null)
return null;
var copy = InstanceWorld.CopyOf(ambient);
if (copy <= 0)
return null; // the master, or a landblock we do not own
return ((ushort)((ambient.Id.LandblockX << 8) | ambient.Id.LandblockY), copy);
}
///
/// Registers a copy's object here instead of globally. Returns false when the
/// object is not ours to hold, in which case ACE's own registry should take it.
///
public static bool Add(PhysicsObj obj)
{
if (obj is null || !IsAmbiguous(obj.ID))
return false;
if (Scope() is not { } scope)
return false;
_objects[(scope.Landblock, scope.Copy, obj.ID)] = obj;
return true;
}
public static bool Remove(PhysicsObj obj)
{
if (obj is null || !IsAmbiguous(obj.ID))
return false;
if (Scope() is not { } scope)
return false;
_objects.TryRemove((scope.Landblock, scope.Copy, obj.ID), out _);
return true;
}
///
/// The object for a guid within the copy being processed, or null to fall through
/// to ACE's global answer.
///
public static PhysicsObj? Get(uint guid)
{
if (!IsAmbiguous(guid))
return null;
if (Scope() is not { } scope)
return null;
return _objects.TryGetValue((scope.Landblock, scope.Copy, guid), out var obj) ? obj : null;
}
/// Drops everything a closed copy was holding.
public static int Forget(ushort landblock, int copy)
{
var dropped = 0;
foreach (var key in _objects.Keys.ToList())
{
if (key.Landblock == landblock && key.Copy == copy && _objects.TryRemove(key, out _))
dropped++;
}
return dropped;
}
public static void ForgetAll() => _objects.Clear();
}