using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ACE.Common;
using ACE.Common.Performance;
using ACE.Entity;
using ACE.Entity.Enum;
using ACE.Server.Entity;
using ACE.Server.WorldObjects;
using log4net;
namespace ACE.Server.Managers
{
///
/// Handles loading/unloading landblocks, and their adjacencies
///
public static class LandblockManager
{
private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
///
/// Locking mechanism provides concurrent access to collections
///
private static readonly ReaderWriterLockSlim landblockLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
///
/// A table of all the landblocks in the world map
/// Landblocks which aren't currently loaded will be null here
///
private static readonly Landblock[,] landblocks = new Landblock[255, 255];
///
/// A lookup table of all the currently loaded landblocks
///
private static readonly HashSet loadedLandblocks = new HashSet();
private static readonly List landblockGroupPendingAdditions = new List();
private static readonly List landblockGroups = new List();
public static int LandblockGroupsCount
{
get
{
landblockLock.EnterReadLock();
try
{
return landblockGroups.Count;
}
finally
{
landblockLock.ExitReadLock();
}
}
}
public static List GetLoadedLandblockGroups()
{
landblockLock.EnterReadLock();
try
{
return landblockGroups.ToList();
}
finally
{
landblockLock.ExitReadLock();
}
}
///
/// DestructionQueue is concurrent because it can be added to by multiple threads at once, publicly via AddToDestructionQueue()
///
private static readonly ConcurrentBag destructionQueue = new ConcurrentBag();
///
/// Permaloads a list of configurable landblocks if server option is set
///
public static void PreloadConfigLandblocks()
{
if (!ConfigManager.Config.Server.LandblockPreloading)
{
log.Info("Preloading Landblocks Disabled...");
log.Warn("Events may not function correctly as Preloading of Landblocks has disabled.");
return;
}
log.Info("Preloading Landblocks...");
if (ConfigManager.Config.Server.PreloadedLandblocks == null)
{
log.Info("No configuration found for PreloadedLandblocks, please refer to Config.js.example");
log.Warn("Initializing PreloadedLandblocks with single default for Hebian-To (Global Events)");
log.Warn("Add a PreloadedLandblocks section to your Config.js file and adjust to meet your needs");
ConfigManager.Config.Server.PreloadedLandblocks = new List { new PreloadedLandblocks { Id = "E74EFFFF", Description = "Hebian-To (Global Events)", Permaload = true, IncludeAdjacents = true, Enabled = true } };
}
log.InfoFormat("Found {0} landblock entries in PreloadedLandblocks configuration, {1} are set to preload.", ConfigManager.Config.Server.PreloadedLandblocks.Count, ConfigManager.Config.Server.PreloadedLandblocks.Count(x => x.Enabled == true));
foreach (var preloadLandblock in ConfigManager.Config.Server.PreloadedLandblocks)
{
if (!preloadLandblock.Enabled)
{
log.DebugFormat("Landblock {0:X4} specified but not enabled in config, skipping", preloadLandblock.Id);
continue;
}
if (uint.TryParse(preloadLandblock.Id, NumberStyles.HexNumber, CultureInfo.CurrentCulture, out uint landblock))
{
if (landblock == 0)
{
switch (preloadLandblock.Description)
{
case "Apartment Landblocks":
log.InfoFormat("Preloading landblock group: {0}, IncludeAdjacents = {1}, Permaload = {2}", preloadLandblock.Description, preloadLandblock.IncludeAdjacents, preloadLandblock.Permaload);
foreach (var apt in apartmentLandblocks)
PreloadLandblock(apt, preloadLandblock);
break;
}
}
else
PreloadLandblock(landblock, preloadLandblock);
}
}
}
private static void PreloadLandblock(uint landblock, PreloadedLandblocks preloadLandblock)
{
var landblockID = new LandblockId(landblock);
GetLandblock(landblockID, preloadLandblock.IncludeAdjacents, preloadLandblock.Permaload);
log.DebugFormat("Landblock {0:X4}, ({1}) preloaded. IncludeAdjacents = {2}, Permaload = {3}", landblockID.Landblock, preloadLandblock.Description, preloadLandblock.IncludeAdjacents, preloadLandblock.Permaload);
}
private static readonly uint[] apartmentLandblocks =
{
0x7200FFFF,
0x7300FFFF,
0x7400FFFF,
0x7500FFFF,
0x7600FFFF,
0x7700FFFF,
0x7800FFFF,
0x7900FFFF,
0x7A00FFFF,
0x7B00FFFF,
0x7C00FFFF,
0x7D00FFFF,
0x7E00FFFF,
0x7F00FFFF,
0x8000FFFF,
0x8100FFFF,
0x8200FFFF,
0x8300FFFF,
0x8400FFFF,
0x8500FFFF,
0x8600FFFF,
0x8700FFFF,
0x8800FFFF,
0x8900FFFF,
0x8A00FFFF,
0x8B00FFFF,
0x8C00FFFF,
0x8D00FFFF,
0x8E00FFFF,
0x8F00FFFF,
0x9000FFFF,
0x9100FFFF,
0x9200FFFF,
0x9300FFFF,
0x9400FFFF,
0x9500FFFF,
0x9600FFFF,
0x9700FFFF,
0x9800FFFF,
0x9900FFFF,
0x5360FFFF,
0x5361FFFF,
0x5362FFFF,
0x5363FFFF,
0x5364FFFF,
0x5365FFFF,
0x5366FFFF,
0x5367FFFF,
0x5368FFFF,
0x5369FFFF
};
private static void ProcessPendingLandblockGroupAdditions()
{
if (landblockGroupPendingAdditions.Count == 0)
return;
landblockLock.EnterWriteLock();
try
{
for (int i = landblockGroupPendingAdditions.Count - 1; i >= 0; i--)
{
if (landblockGroupPendingAdditions[i].IsDungeon)
{
// Each dungeon exists in its own group
var landblockGroup = new LandblockGroup(landblockGroupPendingAdditions[i]);
landblockGroups.Add(landblockGroup);
}
else
{
// Find out how many groups this landblock is eligible for
var landblockGroupsIndexMatchesByDistance = new List();
for (int j = 0; j < landblockGroups.Count; j++)
{
if (landblockGroups[j].IsDungeon)
continue;
if (landblockGroups[j].ShouldBeAddedToThisLandblockGroup(landblockGroupPendingAdditions[i]))
landblockGroupsIndexMatchesByDistance.Add(j);
}
if (landblockGroupsIndexMatchesByDistance.Count > 0)
{
// Add the landblock to the first eligible group
landblockGroups[landblockGroupsIndexMatchesByDistance[0]].Add(landblockGroupPendingAdditions[i]);
if (landblockGroupsIndexMatchesByDistance.Count > 1)
{
// Merge the additional eligible groups into the first one
for (int j = landblockGroupsIndexMatchesByDistance.Count - 1; j > 0; j--)
{
// Copy the j down into 0
foreach (var landblock in landblockGroups[landblockGroupsIndexMatchesByDistance[j]])
landblockGroups[landblockGroupsIndexMatchesByDistance[0]].Add(landblock);
landblockGroups.RemoveAt(landblockGroupsIndexMatchesByDistance[j]);
}
}
}
else
{
// No close groups were found
var landblockGroup = new LandblockGroup(landblockGroupPendingAdditions[i]);
landblockGroups.Add(landblockGroup);
}
}
landblockGroupPendingAdditions.RemoveAt(i);
}
// Debugging todo: comment this out after enough testing
var count = 0;
foreach (var group in landblockGroups)
count += group.Count;
if (count != loadedLandblocks.Count)
log.Error($"[LANDBLOCK GROUP] ProcessPendingAdditions count ({count}) != loadedLandblocks.Count ({loadedLandblocks.Count})");
}
finally
{
landblockLock.ExitWriteLock();
}
}
public static void Tick(double portalYearTicks)
{
// update positions through physics engine
ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.LandblockManager_TickPhysics);
TickPhysics(portalYearTicks);
ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.LandblockManager_TickPhysics);
// Tick all of our Landblocks and WorldObjects (Work that can be multi-threaded)
ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.LandblockManager_TickMultiThreadedWork);
TickMultiThreadedWork();
ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.LandblockManager_TickMultiThreadedWork);
// Tick all of our Landblocks and WorldObjects (Work that must be single threaded)
ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.LandblockManager_TickSingleThreadedWork);
TickSingleThreadedWork();
ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.LandblockManager_TickSingleThreadedWork);
// clean up inactive landblocks
UnloadLandblocks();
}
///
/// Used to debug cross-landblock group (and potentially cross-thread) operations
///
public static bool CurrentlyTickingLandblockGroupsMultiThreaded { get; private set; }
///
/// Used to debug cross-landblock group (and potentially cross-thread) operations
///
public static readonly ThreadLocal CurrentMultiThreadedTickingLandblockGroup = new ThreadLocal();
public static readonly RollingAmountOverTimeTracker TickPhysicsEfficiencyTracker = new RollingAmountOverTimeTracker(TimeSpan.FromMinutes(1));
public static readonly RollingAmountOverTimeTracker TickMultiThreadedWorkEfficiencyTracker = new RollingAmountOverTimeTracker(TimeSpan.FromMinutes(1));
///
/// Processes physics objects in all active landblocks for updating
///
private static void TickPhysics(double portalYearTicks)
{
ProcessPendingLandblockGroupAdditions();
var movedObjects = new ConcurrentBag();
if (ConfigManager.Config.Server.Threading.MultiThreadedLandblockGroupPhysicsTicking)
{
CurrentlyTickingLandblockGroupsMultiThreaded = true;
var partitioner = Partitioner.Create(landblockGroups.OrderByDescending(r => r.Count).ThenByDescending(r => r.TickPhysicsTracker.AverageAmount));
var sw = new Stopwatch();
sw.Start();
Parallel.ForEach(partitioner, ConfigManager.Config.Server.Threading.LandblockManagerParallelOptions, landblockGroup =>
{
CurrentMultiThreadedTickingLandblockGroup.Value = landblockGroup;
var swInner = new Stopwatch();
swInner.Start();
foreach (var landblock in landblockGroup)
landblock.TickPhysics(portalYearTicks, movedObjects);
swInner.Stop();
landblockGroup.TickPhysicsTracker.RegisterAmount(swInner.Elapsed.TotalSeconds);
CurrentMultiThreadedTickingLandblockGroup.Value = null;
});
sw.Stop();
// Calculate Tick Efficiency
if (landblockGroups.Count > 0)
{
var totalSecondsUsedInParallel = landblockGroups.Sum(r => r.TickPhysicsTracker.LastAmount);
var totalThreadsUsed = Math.Min(landblockGroups.Count, ConfigManager.Config.Server.Threading.LandblockManagerParallelOptions.MaxDegreeOfParallelism);
var efficiency = (totalSecondsUsedInParallel / (sw.Elapsed.TotalSeconds * totalThreadsUsed)) * 100;
TickPhysicsEfficiencyTracker.RegisterAmount(efficiency);
}
CurrentlyTickingLandblockGroupsMultiThreaded = false;
}
else
{
foreach (var landblockGroup in landblockGroups)
{
foreach (var landblock in landblockGroup)
landblock.TickPhysics(portalYearTicks, movedObjects);
}
}
// iterate through objects that have changed landblocks
foreach (var movedObject in movedObjects)
{
// NOTE: The object's Location can now be null, if a player logs out, or an item is picked up
if (movedObject.Location == null)
continue;
// assume adjacency move here?
RelocateObjectForPhysics(movedObject, true);
}
}
private static void TickMultiThreadedWork()
{
ProcessPendingLandblockGroupAdditions();
if (ConfigManager.Config.Server.Threading.MultiThreadedLandblockGroupTicking)
{
CurrentlyTickingLandblockGroupsMultiThreaded = true;
var partitioner = Partitioner.Create(landblockGroups.OrderByDescending(r => r.Count).ThenByDescending(r => r.TickMultiThreadedWorkTracker.AverageAmount));
var sw = new Stopwatch();
sw.Start();
Parallel.ForEach(partitioner, ConfigManager.Config.Server.Threading.LandblockManagerParallelOptions, landblockGroup =>
{
CurrentMultiThreadedTickingLandblockGroup.Value = landblockGroup;
var swInner = new Stopwatch();
swInner.Start();
foreach (var landblock in landblockGroup)
landblock.TickMultiThreadedWork(Time.GetUnixTime());
swInner.Stop();
landblockGroup.TickMultiThreadedWorkTracker.RegisterAmount(swInner.Elapsed.TotalSeconds);
CurrentMultiThreadedTickingLandblockGroup.Value = null;
});
sw.Stop();
// Calculate Tick Efficiency
if (landblockGroups.Count > 0)
{
var totalSecondsUsedInParallel = landblockGroups.Sum(r => r.TickMultiThreadedWorkTracker.LastAmount);
var totalThreadsUsed = Math.Min(landblockGroups.Count, ConfigManager.Config.Server.Threading.LandblockManagerParallelOptions.MaxDegreeOfParallelism);
var efficiency = (totalSecondsUsedInParallel / (sw.Elapsed.TotalSeconds * totalThreadsUsed)) * 100;
TickMultiThreadedWorkEfficiencyTracker.RegisterAmount(efficiency);
}
CurrentlyTickingLandblockGroupsMultiThreaded = false;
}
else
{
foreach (var landblockGroup in landblockGroups)
{
foreach (var landblock in landblockGroup)
landblock.TickMultiThreadedWork(Time.GetUnixTime());
}
}
}
private static void TickSingleThreadedWork()
{
ProcessPendingLandblockGroupAdditions();
foreach (var landblockGroup in landblockGroups)
{
foreach (var landblock in landblockGroup)
landblock.TickSingleThreadedWork(Time.GetUnixTime());
}
}
///
/// Adds a WorldObject to the landblock defined by the object's location
///
/// If TRUE, ensures all of the adjacent landblocks for this WorldObject are loaded
public static bool AddObject(WorldObject worldObject, bool loadAdjacents = false)
{
var block = GetLandblock(worldObject.Location.LandblockId, loadAdjacents);
return block.AddWorldObject(worldObject);
}
///
/// Relocates an object to the appropriate landblock -- Should only be called from physics/worldmanager -- not player!
///
public static void RelocateObjectForPhysics(WorldObject worldObject, bool adjacencyMove)
{
var oldBlock = worldObject.CurrentLandblock;
var newBlock = GetLandblock(worldObject.Location.LandblockId, true);
if (newBlock.IsDormant && worldObject is SpellProjectile)
{
worldObject.PhysicsObj.set_active(false);
worldObject.Destroy();
return;
}
// Remove from the old landblock -- force
if (oldBlock != null)
oldBlock.RemoveWorldObjectForPhysics(worldObject.Guid, adjacencyMove);
// Add to the new landblock
newBlock.AddWorldObjectForPhysics(worldObject);
}
public static bool IsLoaded(LandblockId landblockId)
{
landblockLock.EnterReadLock();
try
{
return landblocks[landblockId.LandblockX, landblockId.LandblockY] != null;
}
finally
{
landblockLock.ExitReadLock();
}
}
///
/// Returns a reference to a landblock, loading the landblock if not already active
///
public static Landblock GetLandblock(LandblockId landblockId, bool loadAdjacents, bool permaload = false)
{
Landblock landblock;
landblockLock.EnterUpgradeableReadLock();
try
{
bool setAdjacents = false;
landblock = landblocks[landblockId.LandblockX, landblockId.LandblockY];
if (landblock == null)
{
landblockLock.EnterWriteLock();
try
{
// load up this landblock
landblock = landblocks[landblockId.LandblockX, landblockId.LandblockY] = new Landblock(landblockId);
if (!loadedLandblocks.Add(landblock))
{
log.Error($"LandblockManager: failed to add {landblock.Id.Raw:X8} to active landblocks!");
return landblock;
}
landblockGroupPendingAdditions.Add(landblock);
}
finally
{
landblockLock.ExitWriteLock();
}
landblock.Init();
setAdjacents = true;
}
if (permaload)
landblock.Permaload = true;
// load adjacents, if applicable
if (loadAdjacents)
{
var adjacents = GetAdjacentIDs(landblock);
foreach (var adjacent in adjacents)
GetLandblock(adjacent, false, permaload);
setAdjacents = true;
}
// cache adjacencies
if (setAdjacents)
SetAdjacents(landblock, true, true);
}
finally
{
landblockLock.ExitUpgradeableReadLock();
}
return landblock;
}
///
/// Returns the list of all loaded landblocks
///
public static List GetLoadedLandblocks()
{
landblockLock.EnterReadLock();
try
{
return loadedLandblocks.ToList();
}
finally
{
landblockLock.ExitReadLock();
}
}
///
/// Returns the active, non-null adjacents for a landblock
///
private static List GetAdjacents(Landblock landblock)
{
var adjacentIDs = GetAdjacentIDs(landblock);
var adjacents = new List();
foreach (var adjacentID in adjacentIDs)
{
var adjacent = landblocks[adjacentID.LandblockX, adjacentID.LandblockY];
if (adjacent != null)
adjacents.Add(adjacent);
}
return adjacents;
}
///
/// Returns the list of adjacent landblock IDs for a landblock
///
private static List GetAdjacentIDs(Landblock landblock)
{
var adjacents = new List();
if (landblock.IsDungeon)
return adjacents; // dungeons have no adjacents
var north = GetAdjacentID(landblock.Id, Adjacency.North);
var south = GetAdjacentID(landblock.Id, Adjacency.South);
var west = GetAdjacentID(landblock.Id, Adjacency.West);
var east = GetAdjacentID(landblock.Id, Adjacency.East);
var northwest = GetAdjacentID(landblock.Id, Adjacency.NorthWest);
var northeast = GetAdjacentID(landblock.Id, Adjacency.NorthEast);
var southwest = GetAdjacentID(landblock.Id, Adjacency.SouthWest);
var southeast = GetAdjacentID(landblock.Id, Adjacency.SouthEast);
if (north != null)
adjacents.Add(north.Value);
if (south != null)
adjacents.Add(south.Value);
if (west != null)
adjacents.Add(west.Value);
if (east != null)
adjacents.Add(east.Value);
if (northwest != null)
adjacents.Add(northwest.Value);
if (northeast != null)
adjacents.Add(northeast.Value);
if (southwest != null)
adjacents.Add(southwest.Value);
if (southeast != null)
adjacents.Add(southeast.Value);
return adjacents;
}
///
/// Returns an adjacent landblock ID for a landblock
///
private static LandblockId? GetAdjacentID(LandblockId landblock, Adjacency adjacency)
{
int lbx = landblock.LandblockX;
int lby = landblock.LandblockY;
switch (adjacency)
{
case Adjacency.North:
lby += 1;
break;
case Adjacency.South:
lby -= 1;
break;
case Adjacency.West:
lbx -= 1;
break;
case Adjacency.East:
lbx += 1;
break;
case Adjacency.NorthWest:
lby += 1;
lbx -= 1;
break;
case Adjacency.NorthEast:
lby += 1;
lbx += 1;
break;
case Adjacency.SouthWest:
lby -= 1;
lbx -= 1;
break;
case Adjacency.SouthEast:
lby -= 1;
lbx += 1;
break;
}
if (lbx < 0 || lbx > 254 || lby < 0 || lby > 254)
return null;
return new LandblockId((byte)lbx, (byte)lby);
}
///
/// Rebuilds the adjacency cache for a landblock, and optionally rebuilds the adjacency caches
/// for the adjacent landblocks if traverse is true
///
private static void SetAdjacents(Landblock landblock, bool traverse = true, bool pSync = false)
{
landblock.Adjacents = GetAdjacents(landblock);
if (pSync)
landblock.PhysicsLandblock.SetAdjacents(landblock.Adjacents);
if (traverse)
{
foreach (var adjacent in landblock.Adjacents)
SetAdjacents(adjacent, false);
}
}
///
/// Queues a landblock for thread-safe unloading
///
public static void AddToDestructionQueue(Landblock landblock)
{
destructionQueue.Add(landblock);
}
private static readonly System.Diagnostics.Stopwatch swTrySplitEach = new System.Diagnostics.Stopwatch();
///
/// Processes the destruction queue in a thread-safe manner
///
private static void UnloadLandblocks()
{
while (!destructionQueue.IsEmpty)
{
if (destructionQueue.TryTake(out Landblock landblock))
{
landblock.Unload();
bool unloadFailed = false;
landblockLock.EnterWriteLock();
try
{
// remove from list of managed landblocks
if (loadedLandblocks.Remove(landblock))
{
landblocks[landblock.Id.LandblockX, landblock.Id.LandblockY] = null;
// remove from landblock group
for (int i = landblockGroups.Count - 1; i >= 0 ; i--)
{
if (landblockGroups[i].Remove(landblock))
{
if (landblockGroups[i].Count == 0)
landblockGroups.RemoveAt(i);
else if (ConfigManager.Config.Server.Threading.MultiThreadedLandblockGroupPhysicsTicking || ConfigManager.Config.Server.Threading.MultiThreadedLandblockGroupTicking) // Only try to split if multi-threading is enabled
{
swTrySplitEach.Restart();
var splits = landblockGroups[i].TryThrottledSplit();
swTrySplitEach.Stop();
if (swTrySplitEach.Elapsed.TotalMilliseconds > 3)
log.WarnFormat("[LANDBLOCK GROUP] TrySplit for {0} took: {1:N2} ms", landblockGroups[i], swTrySplitEach.Elapsed.TotalMilliseconds);
else if (swTrySplitEach.Elapsed.TotalMilliseconds > 1)
log.DebugFormat("[LANDBLOCK GROUP] TrySplit for {0} took: {1:N2} ms", landblockGroups[i], swTrySplitEach.Elapsed.TotalMilliseconds);
if (splits != null)
{
if (splits.Count > 0)
{
log.DebugFormat("[LANDBLOCK GROUP] TrySplit resulted in {0} split(s) and took: {1:N2} ms", splits.Count, swTrySplitEach.Elapsed.TotalMilliseconds);
log.DebugFormat("[LANDBLOCK GROUP] split for old: {0}", landblockGroups[i]);
}
foreach (var split in splits)
{
landblockGroups.Add(split);
log.DebugFormat("[LANDBLOCK GROUP] split and new: {0}", split);
}
}
}
break;
}
}
NotifyAdjacents(landblock);
}
else
unloadFailed = true;
}
finally
{
landblockLock.ExitWriteLock();
}
if (unloadFailed)
log.Error($"LandblockManager: failed to unload {landblock.Id.Raw:X8}");
}
}
}
///
/// Notifies the adjacent landblocks to rebuild their adjacency cache
/// Called when a landblock is unloaded
///
private static void NotifyAdjacents(Landblock landblock)
{
var adjacents = GetAdjacents(landblock);
foreach (var adjacent in adjacents)
SetAdjacents(adjacent, false, true);
}
///
/// Used on server shutdown
///
public static void AddAllActiveLandblocksToDestructionQueue()
{
landblockLock.EnterWriteLock();
try
{
foreach (var landblock in loadedLandblocks)
AddToDestructionQueue(landblock);
}
finally
{
landblockLock.ExitWriteLock();
}
}
public static EnvironChangeType? GlobalFogColor;
private static void SetGlobalFogColor(EnvironChangeType environChangeType)
{
if (environChangeType.IsFog())
{
if (environChangeType == EnvironChangeType.Clear)
GlobalFogColor = null;
else
GlobalFogColor = environChangeType;
foreach (var landblock in loadedLandblocks)
landblock.SendCurrentEnviron();
}
}
private static void SendGlobalEnvironSound(EnvironChangeType environChangeType)
{
if (environChangeType.IsSound())
{
foreach (var landblock in loadedLandblocks)
landblock.SendEnvironChange(environChangeType);
}
}
public static void DoEnvironChange(EnvironChangeType environChangeType)
{
landblockLock.EnterReadLock();
try
{
if (environChangeType.IsFog())
SetGlobalFogColor(environChangeType);
else
SendGlobalEnvironSound(environChangeType);
}
finally
{
landblockLock.ExitReadLock();
}
}
}
}