using System;
using System.Collections.Generic;
using System.Threading;
using log4net;
using ACE.Entity;
namespace ACE.Server.Managers
{
///
/// Used to assign global guids and ensure they are unique to server.
///
public static class GuidManager
{
private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
// Running server is guid master - database only read as startup to get current max per range.
// weenie class templates Max 65,535 - took Turbine 17 years to get to 10K
// these will be added by developers and not in game.
// Nothing in this range is persisted by the game. Only developers or content creators can create them to be persisted.
// this is only here for documentation purposes.
// Fragmentation: None
///
/// Is equal to uint.MaxValue
///
public static uint InvalidGuid { get; } = uint.MaxValue;
private const uint LowIdLimit = 0x1000;
private class PlayerGuidAllocator
{
private readonly uint min;
private readonly uint max;
private uint current;
private readonly string name;
public PlayerGuidAllocator(uint min, uint max, string name)
{
this.min = min;
this.max = max;
// Read current value out of ShardDatabase
lock (this)
{
bool done = false;
Database.DatabaseManager.Shard.GetMaxGuidFoundInRange(min, max, dbVal =>
{
lock (this)
{
current = dbVal;
done = true;
Monitor.Pulse(this);
}
});
while (!done)
Monitor.Wait(this);
if (current == InvalidGuid)
current = min;
else
// Need to start allocating at current value in db +1
current++;
log.DebugFormat("{0} GUID Allocator current is now {1:X8} of {2:X8}", name, current, max);
if ((max - current) < LowIdLimit)
log.WarnFormat("Dangerously low on {0} GUIDs: {1:X8} of {2:X8}", name, current, max);
}
this.name = name;
}
public uint Alloc()
{
lock (this)
{
if (current == max)
{
log.FatalFormat("Out of {0} GUIDs!", name);
return InvalidGuid;
}
if (current == max - LowIdLimit)
log.WarnFormat("Running dangerously low on {0} GUIDs, need to defrag", name);
uint ret = current;
current += 1;
return ret;
}
}
///
/// For information purposes only, do not use the result. Use Alloc() instead
/// This value represents the current DbMax + 1
///
public uint Current()
{
return current;
}
public uint Min()
{
return min;
}
public uint Max()
{
return max;
}
}
///
/// On a server with ~500 players, about 10,000,000 dynamic GUID's will be requested every 24hr period.
///
private class DynamicGuidAllocator
{
private readonly uint min;
private readonly uint max;
private uint current;
private readonly string name;
private static readonly TimeSpan recycleTime = TimeSpan.FromMinutes(360);
private readonly Queue> recycledGuids = new Queue>();
///
/// The value here is the result of two factors:
/// - A: The total number of GUIDs that are generated during a period of recycledTime (defined above)
/// - B: The total number of GUIDs that are consumed and saved to the shard between server resets
/// A safe value might be (2 * A) + (2 * B)
/// On a shard with severe id fragmentation, this can end up eating more memory to store all the smaller gaps
/// Once sequence gaps are depleted and there are no available id's in the recycle queue, DB Max + 1 is used
/// You can monitor the amount of available id's using /serverstatus
///
private const int limitAvailableIDsReturnedInGetSequenceGaps = 10000000;
private bool useSequenceGapExhaustedMessageDisplayed;
private LinkedList<(uint start, uint end)> availableIDs = new LinkedList<(uint start, uint end)>();
public DynamicGuidAllocator(uint min, uint max, string name, bool unlimitedGaps)
{
this.min = min;
this.max = max;
// Read current value out of ShardDatabase
lock (this)
{
bool done = false;
Database.DatabaseManager.Shard.GetMaxGuidFoundInRange(min, max, dbVal =>
{
lock (this)
{
current = dbVal;
done = true;
Monitor.Pulse(this);
}
});
while (!done)
Monitor.Wait(this);
if (current == InvalidGuid)
current = min;
else
// Need to start allocating at current value in db +1
current++;
log.DebugFormat("{0} GUID Allocator current is now {1:X8} of {2:X8}", name, current, max);
if ((max - current) < LowIdLimit)
log.WarnFormat("Dangerously low on {0} GUIDs: {1:X8} of {2:X8}", name, current, max);
}
// Get available ids in the form of sequence gaps
lock (this)
{
bool done = false;
Database.DatabaseManager.Shard.GetSequenceGaps(ObjectGuid.DynamicMin, unlimitedGaps ? uint.MaxValue : limitAvailableIDsReturnedInGetSequenceGaps, gaps =>
{
lock (this)
{
availableIDs = new LinkedList<(uint start, uint end)>(gaps);
uint total = 0;
foreach (var pair in availableIDs)
total += (pair.end - pair.start) + 1;
log.DebugFormat("{0} GUID Sequence gaps initialized with total availableIDs of {1:N0}", name, total);
done = true;
Monitor.Pulse(this);
}
});
while (!done)
Monitor.Wait(this);
}
this.name = name;
}
public uint Alloc()
{
lock (this)
{
// First, try to use a recycled Guid
if (recycledGuids.TryPeek(out var result) && DateTime.UtcNow - result.Item1 > recycleTime)
{
recycledGuids.Dequeue();
return result.Item2;
}
// Second, try to use a known available Guid
if (availableIDs.First != null)
{
var id = availableIDs.First.Value.start;
if (availableIDs.First.Value.start == availableIDs.First.Value.end)
{
availableIDs.RemoveFirst();
//if (availableIDs.First == null)
// log.Warn($"Sequence gap GUIDs depleted on {name}");
}
else
availableIDs.First.Value = (availableIDs.First.Value.start + 1, availableIDs.First.Value.end);
return id;
}
else
{
if (!useSequenceGapExhaustedMessageDisplayed)
{
log.DebugFormat("{0} GUID Sequence gaps exhausted. Any new, non-recycled GUID will be current + 1. current is now {1:X8}", name, current);
useSequenceGapExhaustedMessageDisplayed = true;
}
}
// Lastly, use an id that increments our max
if (current == max)
{
log.FatalFormat("Out of {0} GUIDs!", name);
return InvalidGuid;
}
if (current == max - LowIdLimit)
log.WarnFormat("Running dangerously low on {0} GUIDs, need to defrag", name);
uint ret = current;
current += 1;
return ret;
}
}
///
/// For information purposes only, do not use the result. Use Alloc() instead
/// This is the value that might be used in the event that there are no recycled guid available and sequence gap guids have been exhausted
/// This value represents the current DbMax + 1
///
public uint Current()
{
return current;
}
public uint Min()
{
return min;
}
public uint Max()
{
return max;
}
public int SequenceGapPairsTotal => availableIDs.Count;
public uint SequenceGapTotalAvailable()
{
lock (this)
{
uint total = 0;
foreach (var (start, end) in availableIDs)
total += end - start + 1;
return total;
}
}
public int RecycledGuidsTotal => recycledGuids.Count;
public void Recycle(uint guid)
{
lock (this)
recycledGuids.Enqueue(new Tuple(DateTime.UtcNow, guid));
}
public override string ToString()
{
lock (this)
{
uint total = 0;
foreach (var pair in availableIDs)
total += (pair.end - pair.start) + 1;
return $"DynamicGuidAllocator: {name}, current: 0x{current:X8}, max: 0x{max:X8}, sequence gap GUIDs available: {total:N0}, recycled GUIDs available: {recycledGuids.Count:N0}";
}
}
public (DateTime nextRecycleTime, int totalPendingRecycledGuids, uint totalSequenceGapGuids) GetRecycleDebugInfo()
{
var nextRecycleTime = DateTime.MinValue;
int totalPendingRecycledGuids;
uint totalSequenceGapGuids = 0;
lock (this)
{
if (recycledGuids.TryPeek(out var firstRecycledGuid))
nextRecycleTime = firstRecycledGuid.Item1 + recycleTime;
totalPendingRecycledGuids = recycledGuids.Count;
foreach (var pair in availableIDs)
totalSequenceGapGuids += (pair.end - pair.start) + 1;
}
return (nextRecycleTime, totalPendingRecycledGuids, totalSequenceGapGuids);
}
}
private static PlayerGuidAllocator playerAlloc;
private static DynamicGuidAllocator dynamicAlloc;
public static void Initialize()
{
playerAlloc = new PlayerGuidAllocator(ObjectGuid.PlayerMin, ObjectGuid.PlayerMax, "player");
dynamicAlloc = new DynamicGuidAllocator(ObjectGuid.DynamicMin, ObjectGuid.DynamicMax, "dynamic", PropertyManager.GetBool("unlimited_sequence_gaps").Item);
}
///
/// Returns New Player Guid
///
public static ObjectGuid NewPlayerGuid()
{
return new ObjectGuid(playerAlloc.Alloc());
}
///
/// These represent items are generated in the world.
/// Some of them will be saved to the Shard db.
/// They can be monsters, loot, etc..
///
public static ObjectGuid NewDynamicGuid()
{
return new ObjectGuid(dynamicAlloc.Alloc());
}
///
/// Guid will be added to the recycle queue, and available for use in GuidAllocator.recycleTime
///
///
public static void RecycleDynamicGuid(ObjectGuid guid)
{
dynamicAlloc.Recycle(guid.Full);
}
public static string GetDynamicGuidDebugInfo()
{
return dynamicAlloc.ToString();
}
public static string GetIdListCommandOutput()
{
var playerGuidCurrent = playerAlloc.Current();
var dynamicGuidCurrent = dynamicAlloc.Current();
var dynamicDebugInfo = dynamicAlloc.GetRecycleDebugInfo();
string message = $"The next Player GUID to be allocated is expected to be: 0x{playerGuidCurrent:X}\n";
if (dynamicDebugInfo.nextRecycleTime == DateTime.MinValue)
message += $"After {dynamicDebugInfo.totalSequenceGapGuids:N0} sequence gap ids have been consumed, and {dynamicDebugInfo.totalPendingRecycledGuids:N0} recycled ids have been consumed, the next id will be {dynamicGuidCurrent:X8}";
else
{
var nextDynamicIsAvailIn = dynamicDebugInfo.nextRecycleTime - DateTime.UtcNow;
if (nextDynamicIsAvailIn.TotalSeconds <= 0)
message += $"After {dynamicDebugInfo.totalSequenceGapGuids:N0} sequence gap ids have been consumed, and {dynamicDebugInfo.totalPendingRecycledGuids:N0} recycled ids have been consumed, the next of which are available now, the next id will be: 0x{dynamicGuidCurrent:X8}";
else
message += $"After {dynamicDebugInfo.totalSequenceGapGuids:N0} sequence gap ids have been consumed, and {dynamicDebugInfo.totalPendingRecycledGuids:N0} recycled ids have been consumed, the next of which is available in {nextDynamicIsAvailIn.TotalMinutes:N1} m, the next id will be: 0x{dynamicGuidCurrent:X8}";
}
return message;
}
public static uint PlayerMin => playerAlloc?.Min() ?? 0;
public static uint PlayerCurrent => playerAlloc?.Current() ?? 0;
public static uint PlayerMax => playerAlloc?.Max() ?? 0;
public static uint DynamicMin => dynamicAlloc?.Min() ?? 0;
public static uint DynamicCurrent => dynamicAlloc?.Current() ?? 0;
public static uint DynamicMax => dynamicAlloc?.Max() ?? 0;
public static int SequenceGapPairsTotal => dynamicAlloc?.SequenceGapPairsTotal ?? 0;
public static int RecycledGuidsTotal => dynamicAlloc?.RecycledGuidsTotal ?? 0;
}
}