mirror of
https://github.com/ACEmulator/ACE
synced 2026-08-17 12:26:06 -04:00
1366 lines
54 KiB
C#
1366 lines
54 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
using log4net;
|
|
|
|
using ACE.Common.Performance;
|
|
using ACE.Database;
|
|
using ACE.Database.Models.World;
|
|
using ACE.DatLoader;
|
|
using ACE.DatLoader.FileTypes;
|
|
using ACE.Entity;
|
|
using ACE.Entity.Enum;
|
|
using ACE.Entity.Enum.Properties;
|
|
using ACE.Entity.Models;
|
|
using ACE.Server.Entity.Actions;
|
|
using ACE.Server.Factories;
|
|
using ACE.Server.Managers;
|
|
using ACE.Server.Physics.Common;
|
|
using ACE.Server.Network.GameMessages;
|
|
using ACE.Server.WorldObjects;
|
|
|
|
using Position = ACE.Entity.Position;
|
|
|
|
namespace ACE.Server.Entity
|
|
{
|
|
/// <summary>
|
|
/// the gist of a landblock is that, generally, everything on it publishes
|
|
/// to and subscribes to everything else in the landblock. x/y in an outdoor
|
|
/// landblock goes from 0 to 192. "indoor" (dungeon) landblocks have no
|
|
/// functional limit as players can't freely roam in/out of them
|
|
/// </summary>
|
|
public class Landblock : IActor
|
|
{
|
|
private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
|
|
|
public static float AdjacencyLoadRange { get; } = 96f;
|
|
public static float OutdoorChatRange { get; } = 75f;
|
|
public static float IndoorChatRange { get; } = 25f;
|
|
public static float MaxXY { get; } = 192f;
|
|
public static float MaxObjectRange { get; } = 192f;
|
|
public static float MaxObjectGhostRange { get; } = 250f;
|
|
|
|
|
|
public LandblockId Id { get; }
|
|
|
|
/// <summary>
|
|
/// Flag indicates if this landblock is permanently loaded (for example, towns on high-traffic servers)
|
|
/// </summary>
|
|
public bool Permaload = false;
|
|
|
|
/// <summary>
|
|
/// Flag indicates if this landblock has no keep alive objects
|
|
/// </summary>
|
|
public bool HasNoKeepAliveObjects = true;
|
|
|
|
/// <summary>
|
|
/// This must be true before a player enters a landblock.
|
|
/// This prevents a player from possibly pasing through a door that hasn't spawned in yet, and other scenarios.
|
|
/// </summary>
|
|
public bool CreateWorldObjectsCompleted { get; private set; }
|
|
|
|
private DateTime lastActiveTime;
|
|
|
|
/// <summary>
|
|
/// Dormant landblocks suppress Monster AI ticking and physics processing
|
|
/// </summary>
|
|
public bool IsDormant;
|
|
|
|
private readonly Dictionary<ObjectGuid, WorldObject> worldObjects = new Dictionary<ObjectGuid, WorldObject>();
|
|
private readonly Dictionary<ObjectGuid, WorldObject> pendingAdditions = new Dictionary<ObjectGuid, WorldObject>();
|
|
private readonly List<ObjectGuid> pendingRemovals = new List<ObjectGuid>();
|
|
|
|
// Cache used for Tick efficiency
|
|
private readonly List<Player> players = new List<Player>();
|
|
private readonly LinkedList<Creature> sortedCreaturesByNextTick = new LinkedList<Creature>();
|
|
private readonly LinkedList<WorldObject> sortedWorldObjectsByNextHeartbeat = new LinkedList<WorldObject>();
|
|
private readonly LinkedList<WorldObject> sortedGeneratorsByNextGeneratorUpdate = new LinkedList<WorldObject>();
|
|
private readonly LinkedList<WorldObject> sortedGeneratorsByNextRegeneration = new LinkedList<WorldObject>();
|
|
|
|
/// <summary>
|
|
/// This is used to detect and manage cross-landblock group (which is potentially cross-thread) operations.
|
|
/// </summary>
|
|
public LandblockGroup CurrentLandblockGroup { get; internal set; }
|
|
|
|
public List<Landblock> Adjacents = new List<Landblock>();
|
|
|
|
private readonly ActionQueue actionQueue = new ActionQueue();
|
|
|
|
/// <summary>
|
|
/// Landblocks heartbeat every 5 seconds
|
|
/// </summary>
|
|
private static readonly TimeSpan heartbeatInterval = TimeSpan.FromSeconds(5);
|
|
|
|
private DateTime lastHeartBeat = DateTime.MinValue;
|
|
|
|
/// <summary>
|
|
/// Landblock items will be saved to the database every 5 minutes
|
|
/// </summary>
|
|
private static readonly TimeSpan databaseSaveInterval = TimeSpan.FromMinutes(5);
|
|
|
|
private DateTime lastDatabaseSave = DateTime.MinValue;
|
|
|
|
/// <summary>
|
|
/// Landblocks which have been inactive for this many seconds will be dormant
|
|
/// </summary>
|
|
private static readonly TimeSpan dormantInterval = TimeSpan.FromMinutes(1);
|
|
|
|
/// <summary>
|
|
/// Landblocks which have been inactive for this many seconds will be unloaded
|
|
/// </summary>
|
|
public static readonly TimeSpan UnloadInterval = TimeSpan.FromMinutes(5);
|
|
|
|
|
|
/// <summary>
|
|
/// The clientlib backing store landblock
|
|
/// Eventually these classes could be merged, but for now they are separate...
|
|
/// </summary>
|
|
public Physics.Common.Landblock PhysicsLandblock { get; }
|
|
|
|
public CellLandblock CellLandblock { get; }
|
|
public LandblockInfo LandblockInfo { get; }
|
|
|
|
/// <summary>
|
|
/// The landblock static meshes for
|
|
/// collision detection and physics simulation
|
|
/// </summary>
|
|
public LandblockMesh LandblockMesh { get; private set; }
|
|
public List<ModelMesh> LandObjects { get; private set; }
|
|
public List<ModelMesh> Buildings { get; private set; }
|
|
public List<ModelMesh> WeenieMeshes { get; private set; }
|
|
public List<ModelMesh> Scenery { get; private set; }
|
|
|
|
|
|
public readonly RateMonitor Monitor5m = new RateMonitor();
|
|
private readonly TimeSpan last5mClearInteval = TimeSpan.FromMinutes(5);
|
|
private DateTime last5mClear;
|
|
public readonly RateMonitor Monitor1h = new RateMonitor();
|
|
private readonly TimeSpan last1hClearInteval = TimeSpan.FromHours(1);
|
|
private DateTime last1hClear;
|
|
private bool monitorsRequireEventStart = true;
|
|
|
|
// Used for cumulative ServerPerformanceMonitor event recording
|
|
private readonly Stopwatch stopwatch = new Stopwatch();
|
|
|
|
|
|
private EnvironChangeType fogColor;
|
|
|
|
public EnvironChangeType FogColor
|
|
{
|
|
get
|
|
{
|
|
if (LandblockManager.GlobalFogColor.HasValue)
|
|
return LandblockManager.GlobalFogColor.Value;
|
|
|
|
return fogColor;
|
|
}
|
|
set => fogColor = value;
|
|
}
|
|
|
|
|
|
public Landblock(LandblockId id)
|
|
{
|
|
//log.DebugFormat("Landblock({0:X8})", (id.Raw | 0xFFFF));
|
|
|
|
Id = id;
|
|
|
|
CellLandblock = DatManager.CellDat.ReadFromDat<CellLandblock>(Id.Raw | 0xFFFF);
|
|
LandblockInfo = DatManager.CellDat.ReadFromDat<LandblockInfo>((uint)Id.Landblock << 16 | 0xFFFE);
|
|
|
|
lastActiveTime = DateTime.UtcNow;
|
|
|
|
var cellLandblock = DBObj.GetCellLandblock(Id.Raw | 0xFFFF);
|
|
PhysicsLandblock = new Physics.Common.Landblock(cellLandblock);
|
|
}
|
|
|
|
public void Init(bool reload = false)
|
|
{
|
|
if (!reload)
|
|
PhysicsLandblock.PostInit();
|
|
|
|
Task.Run(() =>
|
|
{
|
|
CreateWorldObjects();
|
|
|
|
SpawnDynamicShardObjects();
|
|
|
|
SpawnEncounters();
|
|
});
|
|
|
|
//LoadMeshes(objects);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Monster Locations, Generators<para />
|
|
/// This will be called from a separate task from our constructor. Use thread safety when interacting with this landblock.
|
|
/// </summary>
|
|
private void CreateWorldObjects()
|
|
{
|
|
var objects = DatabaseManager.World.GetCachedInstancesByLandblock(Id.Landblock);
|
|
var shardObjects = DatabaseManager.Shard.BaseDatabase.GetStaticObjectsByLandblock(Id.Landblock);
|
|
var factoryObjects = WorldObjectFactory.CreateNewWorldObjects(objects, shardObjects);
|
|
|
|
actionQueue.EnqueueAction(new ActionEventDelegate(() =>
|
|
{
|
|
// for mansion linking
|
|
var houses = new List<House>();
|
|
|
|
foreach (var fo in factoryObjects)
|
|
{
|
|
WorldObject parent = null;
|
|
if (fo.WeenieType == WeenieType.House)
|
|
{
|
|
var house = fo as House;
|
|
Houses.Add(house);
|
|
|
|
if (fo.HouseType == HouseType.Mansion)
|
|
{
|
|
houses.Add(house);
|
|
house.LinkedHouses.Add(houses[0]);
|
|
|
|
if (houses.Count > 1)
|
|
{
|
|
houses[0].LinkedHouses.Add(house);
|
|
parent = houses[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
AddWorldObject(fo);
|
|
fo.ActivateLinks(objects, shardObjects, parent);
|
|
|
|
if (fo.PhysicsObj != null)
|
|
fo.PhysicsObj.Order = 0;
|
|
}
|
|
|
|
CreateWorldObjectsCompleted = true;
|
|
|
|
PhysicsLandblock.SortObjects();
|
|
}));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Corpses<para />
|
|
/// This will be called from a separate task from our constructor. Use thread safety when interacting with this landblock.
|
|
/// </summary>
|
|
private void SpawnDynamicShardObjects()
|
|
{
|
|
var dynamics = DatabaseManager.Shard.BaseDatabase.GetDynamicObjectsByLandblock(Id.Landblock);
|
|
var factoryShardObjects = WorldObjectFactory.CreateWorldObjects(dynamics);
|
|
|
|
actionQueue.EnqueueAction(new ActionEventDelegate(() =>
|
|
{
|
|
foreach (var fso in factoryShardObjects)
|
|
AddWorldObject(fso);
|
|
}));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawns the semi-randomized monsters scattered around the outdoors<para />
|
|
/// This will be called from a separate task from our constructor. Use thread safety when interacting with this landblock.
|
|
/// </summary>
|
|
private void SpawnEncounters()
|
|
{
|
|
// get the encounter spawns for this landblock
|
|
var encounters = DatabaseManager.World.GetCachedEncountersByLandblock(Id.Landblock);
|
|
|
|
foreach (var encounter in encounters)
|
|
{
|
|
var wo = WorldObjectFactory.CreateNewWorldObject(encounter.WeenieClassId);
|
|
|
|
if (wo == null) continue;
|
|
|
|
actionQueue.EnqueueAction(new ActionEventDelegate(() =>
|
|
{
|
|
var xPos = Math.Clamp(encounter.CellX * 24.0f, 0.5f, 191.5f);
|
|
var yPos = Math.Clamp(encounter.CellY * 24.0f, 0.5f, 191.5f);
|
|
|
|
var pos = new Physics.Common.Position();
|
|
pos.ObjCellID = (uint)(Id.Landblock << 16) | 1;
|
|
pos.Frame = new Physics.Animation.AFrame(new Vector3(xPos, yPos, 0), Quaternion.Identity);
|
|
pos.adjust_to_outside();
|
|
|
|
pos.Frame.Origin.Z = PhysicsLandblock.GetZ(pos.Frame.Origin);
|
|
|
|
wo.Location = new Position(pos.ObjCellID, pos.Frame.Origin, pos.Frame.Orientation);
|
|
|
|
var sortCell = LScape.get_landcell(pos.ObjCellID) as SortCell;
|
|
if (sortCell != null && sortCell.has_building())
|
|
{
|
|
wo.Destroy();
|
|
return;
|
|
}
|
|
|
|
if (PropertyManager.GetBool("override_encounter_spawn_rates").Item)
|
|
{
|
|
wo.RegenerationInterval = PropertyManager.GetDouble("encounter_regen_interval").Item;
|
|
|
|
wo.ReinitializeHeartbeats();
|
|
|
|
if (wo.Biota.PropertiesGenerator != null)
|
|
{
|
|
// While this may be ugly, it's done for performance reasons.
|
|
// Common weenie properties are not cloned into the bota on creation. Instead, the biota references simply point to the weenie collections.
|
|
// The problem here is that we want to update one of those common collection properties. If the biota is referencing the weenie collection,
|
|
// then we'll end up updating the global weenie (from the cache), instead of just this specific biota.
|
|
if (wo.Biota.PropertiesGenerator == wo.Weenie.PropertiesGenerator)
|
|
{
|
|
wo.Biota.PropertiesGenerator = new List<PropertiesGenerator>(wo.Weenie.PropertiesGenerator.Count);
|
|
|
|
foreach (var record in wo.Weenie.PropertiesGenerator)
|
|
wo.Biota.PropertiesGenerator.Add(record.Clone());
|
|
}
|
|
|
|
foreach (var profile in wo.Biota.PropertiesGenerator)
|
|
profile.Delay = (float)PropertyManager.GetDouble("encounter_delay").Item;
|
|
}
|
|
}
|
|
|
|
if (!AddWorldObject(wo))
|
|
wo.Destroy();
|
|
}));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the meshes for the landblock<para />
|
|
/// This isn't used by ACE, but we still retain it for the following reason:<para />
|
|
/// its useful, concise, high level overview code for everything needed to load landblocks, all their objects, scenery, polygons
|
|
/// without getting into all of the low level methods that acclient uses to do it
|
|
/// </summary>
|
|
private void LoadMeshes(List<LandblockInstance> objects)
|
|
{
|
|
LandblockMesh = new LandblockMesh(Id);
|
|
LoadLandObjects();
|
|
LoadBuildings();
|
|
LoadWeenies(objects);
|
|
LoadScenery();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the meshes for the static landblock objects,
|
|
/// also known as obstacles
|
|
/// </summary>
|
|
private void LoadLandObjects()
|
|
{
|
|
LandObjects = new List<ModelMesh>();
|
|
|
|
foreach (var obj in LandblockInfo.Objects)
|
|
LandObjects.Add(new ModelMesh(obj.Id, obj.Frame));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the meshes for the buildings on the landblock
|
|
/// </summary>
|
|
private void LoadBuildings()
|
|
{
|
|
Buildings = new List<ModelMesh>();
|
|
|
|
foreach (var obj in LandblockInfo.Buildings)
|
|
Buildings.Add(new ModelMesh(obj.ModelId, obj.Frame));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the meshes for the weenies on the landblock
|
|
/// </summary>
|
|
private void LoadWeenies(List<LandblockInstance> objects)
|
|
{
|
|
WeenieMeshes = new List<ModelMesh>();
|
|
|
|
foreach (var obj in objects)
|
|
{
|
|
var weenie = DatabaseManager.World.GetCachedWeenie(obj.WeenieClassId);
|
|
WeenieMeshes.Add(
|
|
new ModelMesh(weenie.GetProperty(PropertyDataId.Setup) ?? 0,
|
|
new DatLoader.Entity.Frame(new Position(obj.ObjCellId, obj.OriginX, obj.OriginY, obj.OriginZ, obj.AnglesX, obj.AnglesY, obj.AnglesZ, obj.AnglesW))));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the meshes for the scenery on the landblock
|
|
/// </summary>
|
|
private void LoadScenery()
|
|
{
|
|
Scenery = Entity.Scenery.Load(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This should be called before TickLandblockGroupThreadSafeWork() and before Tick()
|
|
/// </summary>
|
|
public void TickPhysics(double portalYearTicks, ConcurrentBag<WorldObject> movedObjects)
|
|
{
|
|
if (IsDormant)
|
|
return;
|
|
|
|
Monitor5m.Restart();
|
|
Monitor1h.Restart();
|
|
monitorsRequireEventStart = false;
|
|
|
|
ProcessPendingWorldObjectAdditionsAndRemovals();
|
|
|
|
foreach (WorldObject wo in worldObjects.Values)
|
|
{
|
|
// set to TRUE if object changes landblock
|
|
var landblockUpdate = wo.UpdateObjectPhysics();
|
|
|
|
if (landblockUpdate)
|
|
movedObjects.Add(wo);
|
|
}
|
|
|
|
Monitor5m.Pause();
|
|
Monitor1h.Pause();
|
|
}
|
|
|
|
/// <summary>
|
|
/// This will tick anything that can be multi-threaded safely using LandblockGroups as thread boundaries
|
|
/// This should be called after TickPhysics() and before Tick()
|
|
/// </summary>
|
|
public void TickMultiThreadedWork(double currentUnixTime)
|
|
{
|
|
if (monitorsRequireEventStart)
|
|
{
|
|
Monitor5m.Restart();
|
|
Monitor1h.Restart();
|
|
}
|
|
else
|
|
{
|
|
Monitor5m.Resume();
|
|
Monitor1h.Resume();
|
|
}
|
|
|
|
stopwatch.Restart();
|
|
// This will consist of the following work:
|
|
// - this.CreateWorldObjects
|
|
// - this.SpawnDynamicShardObjects
|
|
// - this.SpawnEncounters
|
|
// - Adding items back onto the landblock from failed player movements: Player_Inventory.cs DoHandleActionPutItemInContainer()
|
|
// - Executing trade between two players: Player_Trade.cs FinalizeTrade()
|
|
actionQueue.RunActions();
|
|
ServerPerformanceMonitor.AddToCumulativeEvent(ServerPerformanceMonitor.CumulativeEventHistoryType.Landblock_Tick_RunActions, stopwatch.Elapsed.TotalSeconds);
|
|
|
|
ProcessPendingWorldObjectAdditionsAndRemovals();
|
|
|
|
// When a WorldObject Ticks, it can end up adding additional WorldObjects to this landblock
|
|
if (!IsDormant)
|
|
{
|
|
stopwatch.Restart();
|
|
while (sortedCreaturesByNextTick.Count > 0) // Monster_Tick()
|
|
{
|
|
var first = sortedCreaturesByNextTick.First.Value;
|
|
|
|
// If they wanted to run before or at now
|
|
if (first.NextMonsterTickTime <= currentUnixTime)
|
|
{
|
|
sortedCreaturesByNextTick.RemoveFirst();
|
|
first.Monster_Tick(currentUnixTime);
|
|
sortedCreaturesByNextTick.AddLast(first); // All creatures tick at a fixed interval
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
ServerPerformanceMonitor.AddToCumulativeEvent(ServerPerformanceMonitor.CumulativeEventHistoryType.Landblock_Tick_Monster_Tick, stopwatch.Elapsed.TotalSeconds);
|
|
}
|
|
|
|
stopwatch.Restart();
|
|
while (sortedGeneratorsByNextGeneratorUpdate.Count > 0)
|
|
{
|
|
var first = sortedGeneratorsByNextGeneratorUpdate.First.Value;
|
|
|
|
// If they wanted to run before or at now
|
|
if (first.NextGeneratorUpdateTime <= currentUnixTime)
|
|
{
|
|
sortedGeneratorsByNextGeneratorUpdate.RemoveFirst();
|
|
first.GeneratorUpdate(currentUnixTime);
|
|
//InsertWorldObjectIntoSortedGeneratorUpdateList(first);
|
|
sortedGeneratorsByNextGeneratorUpdate.AddLast(first);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
ServerPerformanceMonitor.AddToCumulativeEvent(ServerPerformanceMonitor.CumulativeEventHistoryType.Landblock_Tick_GeneratorUpdate, stopwatch.Elapsed.TotalSeconds);
|
|
|
|
stopwatch.Restart();
|
|
while (sortedGeneratorsByNextRegeneration.Count > 0) // GeneratorRegeneration()
|
|
{
|
|
var first = sortedGeneratorsByNextRegeneration.First.Value;
|
|
|
|
//Console.WriteLine($"{first.Name}.Landblock_Tick_GeneratorRegeneration({currentUnixTime})");
|
|
|
|
// If they wanted to run before or at now
|
|
if (first.NextGeneratorRegenerationTime <= currentUnixTime)
|
|
{
|
|
sortedGeneratorsByNextRegeneration.RemoveFirst();
|
|
first.GeneratorRegeneration(currentUnixTime);
|
|
InsertWorldObjectIntoSortedGeneratorRegenerationList(first); // Generators can have regnerations at different intervals
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
ServerPerformanceMonitor.AddToCumulativeEvent(ServerPerformanceMonitor.CumulativeEventHistoryType.Landblock_Tick_GeneratorRegeneration, stopwatch.Elapsed.TotalSeconds);
|
|
|
|
// Heartbeat
|
|
stopwatch.Restart();
|
|
if (lastHeartBeat + heartbeatInterval <= DateTime.UtcNow)
|
|
{
|
|
var thisHeartBeat = DateTime.UtcNow;
|
|
|
|
ProcessPendingWorldObjectAdditionsAndRemovals();
|
|
|
|
// Decay world objects
|
|
if (lastHeartBeat != DateTime.MinValue)
|
|
{
|
|
foreach (var wo in worldObjects.Values)
|
|
{
|
|
if (wo.IsDecayable())
|
|
wo.Decay(thisHeartBeat - lastHeartBeat);
|
|
}
|
|
}
|
|
|
|
if (!Permaload && HasNoKeepAliveObjects)
|
|
{
|
|
if (lastActiveTime + dormantInterval < thisHeartBeat)
|
|
{
|
|
if (!IsDormant)
|
|
{
|
|
var spellProjectiles = worldObjects.Values.Where(i => i is SpellProjectile).ToList();
|
|
foreach (var spellProjectile in spellProjectiles)
|
|
{
|
|
spellProjectile.PhysicsObj.set_active(false);
|
|
spellProjectile.Destroy();
|
|
}
|
|
}
|
|
|
|
IsDormant = true;
|
|
}
|
|
if (lastActiveTime + UnloadInterval < thisHeartBeat)
|
|
LandblockManager.AddToDestructionQueue(this);
|
|
}
|
|
|
|
//log.Info($"Landblock {Id.ToString()}.Tick({currentUnixTime}).Landblock_Tick_Heartbeat: thisHeartBeat: {thisHeartBeat.ToString()} | lastHeartBeat: {lastHeartBeat.ToString()} | worldObjects.Count: {worldObjects.Count()}");
|
|
lastHeartBeat = thisHeartBeat;
|
|
}
|
|
ServerPerformanceMonitor.AddToCumulativeEvent(ServerPerformanceMonitor.CumulativeEventHistoryType.Landblock_Tick_Heartbeat, stopwatch.Elapsed.TotalSeconds);
|
|
|
|
// Database Save
|
|
stopwatch.Restart();
|
|
if (lastDatabaseSave + databaseSaveInterval <= DateTime.UtcNow)
|
|
{
|
|
ProcessPendingWorldObjectAdditionsAndRemovals();
|
|
|
|
SaveDB();
|
|
lastDatabaseSave = DateTime.UtcNow;
|
|
}
|
|
ServerPerformanceMonitor.AddToCumulativeEvent(ServerPerformanceMonitor.CumulativeEventHistoryType.Landblock_Tick_Database_Save, stopwatch.Elapsed.TotalSeconds);
|
|
|
|
Monitor5m.Pause();
|
|
Monitor1h.Pause();
|
|
}
|
|
|
|
/// <summary>
|
|
/// This will tick everything that should be done single threaded on the main ACE World thread
|
|
/// This should be called after TickPhysics() and after Tick()
|
|
/// </summary>
|
|
public void TickSingleThreadedWork(double currentUnixTime)
|
|
{
|
|
if (monitorsRequireEventStart)
|
|
{
|
|
Monitor5m.Restart();
|
|
Monitor1h.Restart();
|
|
}
|
|
else
|
|
{
|
|
Monitor5m.Resume();
|
|
Monitor1h.Resume();
|
|
}
|
|
|
|
ProcessPendingWorldObjectAdditionsAndRemovals();
|
|
|
|
stopwatch.Restart();
|
|
foreach (var player in players)
|
|
player.Player_Tick(currentUnixTime);
|
|
ServerPerformanceMonitor.AddToCumulativeEvent(ServerPerformanceMonitor.CumulativeEventHistoryType.Landblock_Tick_Player_Tick, stopwatch.Elapsed.TotalSeconds);
|
|
|
|
stopwatch.Restart();
|
|
while (sortedWorldObjectsByNextHeartbeat.Count > 0) // Heartbeat()
|
|
{
|
|
var first = sortedWorldObjectsByNextHeartbeat.First.Value;
|
|
|
|
// If they wanted to run before or at now
|
|
if (first.NextHeartbeatTime <= currentUnixTime)
|
|
{
|
|
sortedWorldObjectsByNextHeartbeat.RemoveFirst();
|
|
first.Heartbeat(currentUnixTime);
|
|
InsertWorldObjectIntoSortedHeartbeatList(first); // WorldObjects can have heartbeats at different intervals
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
ServerPerformanceMonitor.AddToCumulativeEvent(ServerPerformanceMonitor.CumulativeEventHistoryType.Landblock_Tick_WorldObject_Heartbeat, stopwatch.Elapsed.TotalSeconds);
|
|
|
|
Monitor5m.RegisterEventEnd();
|
|
Monitor1h.RegisterEventEnd();
|
|
monitorsRequireEventStart = true;
|
|
|
|
if (DateTime.UtcNow - last5mClear >= last5mClearInteval)
|
|
{
|
|
Monitor5m.ClearEventHistory();
|
|
last5mClear = DateTime.UtcNow;
|
|
}
|
|
|
|
if (DateTime.UtcNow - last1hClear >= last1hClearInteval)
|
|
{
|
|
Monitor1h.ClearEventHistory();
|
|
last1hClear = DateTime.UtcNow;
|
|
}
|
|
}
|
|
|
|
private void ProcessPendingWorldObjectAdditionsAndRemovals()
|
|
{
|
|
if (pendingAdditions.Count > 0)
|
|
{
|
|
foreach (var kvp in pendingAdditions)
|
|
{
|
|
worldObjects[kvp.Key] = kvp.Value;
|
|
|
|
if (kvp.Value is Player player)
|
|
players.Add(player);
|
|
else if (kvp.Value is Creature creature)
|
|
sortedCreaturesByNextTick.AddLast(creature);
|
|
|
|
InsertWorldObjectIntoSortedHeartbeatList(kvp.Value);
|
|
InsertWorldObjectIntoSortedGeneratorUpdateList(kvp.Value);
|
|
InsertWorldObjectIntoSortedGeneratorRegenerationList(kvp.Value);
|
|
|
|
if (kvp.Value.WeenieClassId == 80007) // Landblock KeepAlive weenie (ACE custom)
|
|
HasNoKeepAliveObjects = false;
|
|
}
|
|
|
|
pendingAdditions.Clear();
|
|
}
|
|
|
|
if (pendingRemovals.Count > 0)
|
|
{
|
|
foreach (var objectGuid in pendingRemovals)
|
|
{
|
|
if (worldObjects.Remove(objectGuid, out var wo))
|
|
{
|
|
if (wo is Player player)
|
|
players.Remove(player);
|
|
else if (wo is Creature creature)
|
|
sortedCreaturesByNextTick.Remove(creature);
|
|
|
|
sortedWorldObjectsByNextHeartbeat.Remove(wo);
|
|
sortedGeneratorsByNextGeneratorUpdate.Remove(wo);
|
|
sortedGeneratorsByNextRegeneration.Remove(wo);
|
|
|
|
if (wo.WeenieClassId == 80007) // Landblock KeepAlive weenie (ACE custom)
|
|
{
|
|
var keepAliveObject = worldObjects.Values.FirstOrDefault(w => w.WeenieClassId == 80007);
|
|
|
|
if (keepAliveObject == null)
|
|
HasNoKeepAliveObjects = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
pendingRemovals.Clear();
|
|
}
|
|
}
|
|
|
|
private void InsertWorldObjectIntoSortedHeartbeatList(WorldObject worldObject)
|
|
{
|
|
// If you want to add checks to exclude certain object types from heartbeating, you would do it here
|
|
if (worldObject.NextHeartbeatTime == double.MaxValue)
|
|
return;
|
|
|
|
if (sortedWorldObjectsByNextHeartbeat.Count == 0)
|
|
{
|
|
sortedWorldObjectsByNextHeartbeat.AddFirst(worldObject);
|
|
return;
|
|
}
|
|
|
|
if (sortedWorldObjectsByNextHeartbeat.Last.Value.NextHeartbeatTime <= worldObject.NextHeartbeatTime)
|
|
{
|
|
sortedWorldObjectsByNextHeartbeat.AddLast(worldObject);
|
|
return;
|
|
}
|
|
|
|
var currentNode = sortedWorldObjectsByNextHeartbeat.First;
|
|
|
|
while (currentNode != null)
|
|
{
|
|
if (worldObject.NextHeartbeatTime <= currentNode.Value.NextHeartbeatTime)
|
|
{
|
|
sortedWorldObjectsByNextHeartbeat.AddBefore(currentNode, worldObject);
|
|
return;
|
|
}
|
|
|
|
currentNode = currentNode.Next;
|
|
}
|
|
|
|
sortedWorldObjectsByNextHeartbeat.AddLast(worldObject); // This line really shouldn't be hit
|
|
}
|
|
|
|
private void InsertWorldObjectIntoSortedGeneratorUpdateList(WorldObject worldObject)
|
|
{
|
|
// If you want to add checks to exclude certain object types from heartbeating, you would do it here
|
|
if (worldObject.NextGeneratorUpdateTime == double.MaxValue)
|
|
return;
|
|
|
|
if (sortedGeneratorsByNextGeneratorUpdate.Count == 0)
|
|
{
|
|
sortedGeneratorsByNextGeneratorUpdate.AddFirst(worldObject);
|
|
return;
|
|
}
|
|
|
|
if (sortedGeneratorsByNextGeneratorUpdate.Last.Value.NextGeneratorUpdateTime <= worldObject.NextGeneratorUpdateTime)
|
|
{
|
|
sortedGeneratorsByNextGeneratorUpdate.AddLast(worldObject);
|
|
return;
|
|
}
|
|
|
|
var currentNode = sortedGeneratorsByNextGeneratorUpdate.First;
|
|
|
|
while (currentNode != null)
|
|
{
|
|
if (worldObject.NextGeneratorUpdateTime <= currentNode.Value.NextGeneratorUpdateTime)
|
|
{
|
|
sortedGeneratorsByNextGeneratorUpdate.AddBefore(currentNode, worldObject);
|
|
return;
|
|
}
|
|
|
|
currentNode = currentNode.Next;
|
|
}
|
|
|
|
sortedGeneratorsByNextGeneratorUpdate.AddLast(worldObject); // This line really shouldn't be hit
|
|
}
|
|
|
|
private void InsertWorldObjectIntoSortedGeneratorRegenerationList(WorldObject worldObject)
|
|
{
|
|
// If you want to add checks to exclude certain object types from heartbeating, you would do it here
|
|
if (worldObject.NextGeneratorRegenerationTime == double.MaxValue)
|
|
return;
|
|
|
|
if (sortedGeneratorsByNextRegeneration.Count == 0)
|
|
{
|
|
sortedGeneratorsByNextRegeneration.AddFirst(worldObject);
|
|
return;
|
|
}
|
|
|
|
if (sortedGeneratorsByNextRegeneration.Last.Value.NextGeneratorRegenerationTime <= worldObject.NextGeneratorRegenerationTime)
|
|
{
|
|
sortedGeneratorsByNextRegeneration.AddLast(worldObject);
|
|
return;
|
|
}
|
|
|
|
var currentNode = sortedGeneratorsByNextRegeneration.First;
|
|
|
|
while (currentNode != null)
|
|
{
|
|
if (worldObject.NextGeneratorRegenerationTime <= currentNode.Value.NextGeneratorRegenerationTime)
|
|
{
|
|
sortedGeneratorsByNextRegeneration.AddBefore(currentNode, worldObject);
|
|
return;
|
|
}
|
|
|
|
currentNode = currentNode.Next;
|
|
}
|
|
|
|
sortedGeneratorsByNextRegeneration.AddLast(worldObject); // This line really shouldn't be hit
|
|
}
|
|
|
|
public void ResortWorldObjectIntoSortedGeneratorRegenerationList(WorldObject worldObject)
|
|
{
|
|
if (sortedGeneratorsByNextRegeneration.Contains(worldObject))
|
|
{
|
|
sortedGeneratorsByNextRegeneration.Remove(worldObject);
|
|
InsertWorldObjectIntoSortedGeneratorRegenerationList(worldObject);
|
|
}
|
|
}
|
|
|
|
public void EnqueueAction(IAction action)
|
|
{
|
|
actionQueue.EnqueueAction(action);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This will fail if the wo doesn't have a valid location.
|
|
/// </summary>
|
|
public bool AddWorldObject(WorldObject wo)
|
|
{
|
|
if (wo.Location == null)
|
|
{
|
|
log.DebugFormat("Landblock 0x{0} failed to add 0x{1:X8} {2}. Invalid Location", Id, wo.Biota.Id, wo.Name);
|
|
return false;
|
|
}
|
|
|
|
return AddWorldObjectInternal(wo);
|
|
}
|
|
|
|
public void AddWorldObjectForPhysics(WorldObject wo)
|
|
{
|
|
AddWorldObjectInternal(wo);
|
|
}
|
|
|
|
private bool AddWorldObjectInternal(WorldObject wo)
|
|
{
|
|
if (LandblockManager.CurrentlyTickingLandblockGroupsMultiThreaded)
|
|
{
|
|
if (CurrentLandblockGroup != null && CurrentLandblockGroup != LandblockManager.CurrentMultiThreadedTickingLandblockGroup.Value)
|
|
{
|
|
// Prevent possible multi-threaded crash
|
|
// The following scenario can happen rarely in ACE, all in the same call stack with no ActionQueue usage:
|
|
// Moster successfully lands an attack on a player that procs a cloak spell
|
|
// The code goes through and does the LaunchSpellProjectiles() which adds the spell projectiles to (presumably) the players landblock
|
|
// For some unknown reason, the LandblockGroup/Thread where the monster exists seems to be a different LandblockGroup/Thread where the spells are added to.
|
|
// Maybe there's a player death race condition? Maybe it's a teleport race condition? I dunno.
|
|
// Because this happens so rarely, and, it only seems to affect cloak projectiles, and, cloak projectiles are pretty benign, we simply don't add the object, and only log it as a warning.
|
|
if (wo.WeenieType == WeenieType.ProjectileSpell)
|
|
{
|
|
log.Warn($"Landblock 0x{Id} entered AddWorldObjectInternal in a cross-thread operation for a ProjectileSpell. This is normally not an issue unless it's happening more than once an hour.");
|
|
return false;
|
|
}
|
|
|
|
log.Error($"Landblock 0x{Id} entered AddWorldObjectInternal in a cross-thread operation.");
|
|
log.Error($"Landblock 0x{Id} CurrentLandblockGroup: {CurrentLandblockGroup}");
|
|
log.Error($"LandblockManager.CurrentMultiThreadedTickingLandblockGroup.Value: {LandblockManager.CurrentMultiThreadedTickingLandblockGroup.Value}");
|
|
|
|
log.Error($"wo: 0x{wo.Guid}:{wo.Name} [{wo.WeenieClassId} - {wo.WeenieType}], previous landblock 0x{wo.CurrentLandblock?.Id}");
|
|
|
|
if (wo.WeenieType == WeenieType.ProjectileSpell)
|
|
{
|
|
if (wo.ProjectileSource != null)
|
|
log.Error($"wo.ProjectileSource: 0x{wo.ProjectileSource?.Guid}:{wo.ProjectileSource?.Name}, position: {wo.ProjectileSource?.Location}");
|
|
|
|
if (wo.ProjectileTarget != null)
|
|
log.Error($"wo.ProjectileTarget: 0x{wo.ProjectileTarget?.Guid}:{wo.ProjectileTarget?.Name}, position: {wo.ProjectileTarget?.Location}");
|
|
}
|
|
|
|
log.Error(System.Environment.StackTrace);
|
|
|
|
log.Error("PLEASE REPORT THIS TO THE ACE DEV TEAM !!!");
|
|
|
|
// This may still crash...
|
|
}
|
|
}
|
|
|
|
wo.CurrentLandblock = this;
|
|
|
|
if (wo.PhysicsObj == null)
|
|
wo.InitPhysicsObj();
|
|
else
|
|
wo.PhysicsObj.set_object_guid(wo.Guid); // re-add to ServerObjectManager
|
|
|
|
if (wo.PhysicsObj.CurCell == null)
|
|
{
|
|
var success = wo.AddPhysicsObj();
|
|
if (!success)
|
|
{
|
|
wo.CurrentLandblock = null;
|
|
|
|
if (wo.Generator != null)
|
|
{
|
|
if (log.IsDebugEnabled)
|
|
log.Debug($"AddWorldObjectInternal: couldn't spawn 0x{wo.Guid}:{wo.Name} [{wo.WeenieClassId} - {wo.WeenieType}] at {wo.Location.ToLOCString()} from generator {wo.Generator.WeenieClassId} - 0x{wo.Generator.Guid}:{wo.Generator.Name}");
|
|
wo.NotifyOfEvent(RegenerationType.PickUp); // Notify generator the generated object is effectively destroyed, use Pickup to catch both cases.
|
|
}
|
|
else if (wo.IsGenerator) // Some generators will fail random spawns if they're circumference spans over water or cliff edges
|
|
{
|
|
if (log.IsDebugEnabled)
|
|
log.Debug($"AddWorldObjectInternal: couldn't spawn generator 0x{wo.Guid}:{wo.Name} [{wo.WeenieClassId} - {wo.WeenieType}] at {wo.Location.ToLOCString()}");
|
|
}
|
|
else if (wo.ProjectileTarget == null && !(wo is SpellProjectile))
|
|
log.Warn($"AddWorldObjectInternal: couldn't spawn 0x{wo.Guid}:{wo.Name} [{wo.WeenieClassId} - {wo.WeenieType}] at {wo.Location.ToLOCString()}");
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!worldObjects.ContainsKey(wo.Guid))
|
|
pendingAdditions[wo.Guid] = wo;
|
|
else
|
|
pendingRemovals.Remove(wo.Guid);
|
|
|
|
// broadcast to nearby players
|
|
wo.NotifyPlayers();
|
|
|
|
if (wo is Player player)
|
|
player.SetFogColor(FogColor);
|
|
|
|
if (wo is Corpse && wo.Level.HasValue)
|
|
{
|
|
var corpseLimit = PropertyManager.GetLong("corpse_spam_limit").Item;
|
|
var corpseList = worldObjects.Values.Union(pendingAdditions.Values).Where(w => w is Corpse && w.Level.HasValue && w.VictimId == wo.VictimId).OrderBy(w => w.CreationTimestamp);
|
|
|
|
if (corpseList.Count() > corpseLimit)
|
|
{
|
|
var corpse = GetObject(corpseList.First(w => w.TimeToRot > Corpse.EmptyDecayTime).Guid);
|
|
|
|
if (corpse != null)
|
|
{
|
|
log.Warn($"[CORPSE] Landblock.AddWorldObjectInternal(): {wo.Name} (0x{wo.Guid}) exceeds the per player limit of {corpseLimit} corpses for 0x{Id.Landblock:X4}. Adjusting TimeToRot for oldest {corpse.Name} (0x{corpse.Guid}), CreationTimestamp: {corpse.CreationTimestamp} ({Common.Time.GetDateTimeFromTimestamp(corpse.CreationTimestamp ?? 0).ToLocalTime():yyyy-MM-dd HH:mm:ss}), to Corpse.EmptyDecayTime({Corpse.EmptyDecayTime}).");
|
|
corpse.TimeToRot = Corpse.EmptyDecayTime;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void RemoveWorldObject(ObjectGuid objectId, bool adjacencyMove = false, bool fromPickup = false, bool showError = true)
|
|
{
|
|
RemoveWorldObjectInternal(objectId, adjacencyMove, fromPickup, showError);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Should only be called by physics/relocation engines -- not from player
|
|
/// </summary>
|
|
/// <param name="objectId">The object ID to be removed from the current landblock</param>
|
|
/// <param name="adjacencyMove">Flag indicates if object is moving to an adjacent landblock</param>
|
|
public void RemoveWorldObjectForPhysics(ObjectGuid objectId, bool adjacencyMove = false)
|
|
{
|
|
RemoveWorldObjectInternal(objectId, adjacencyMove);
|
|
}
|
|
|
|
private void RemoveWorldObjectInternal(ObjectGuid objectId, bool adjacencyMove = false, bool fromPickup = false, bool showError = true)
|
|
{
|
|
if (LandblockManager.CurrentlyTickingLandblockGroupsMultiThreaded)
|
|
{
|
|
if (CurrentLandblockGroup != null && CurrentLandblockGroup != LandblockManager.CurrentMultiThreadedTickingLandblockGroup.Value)
|
|
{
|
|
log.Error($"Landblock 0x{Id} entered RemoveWorldObjectInternal in a cross-thread operation.");
|
|
log.Error($"Landblock 0x{Id} CurrentLandblockGroup: {CurrentLandblockGroup}");
|
|
log.Error($"LandblockManager.CurrentMultiThreadedTickingLandblockGroup.Value: {LandblockManager.CurrentMultiThreadedTickingLandblockGroup.Value}");
|
|
|
|
log.Error($"objectId: 0x{objectId}");
|
|
|
|
log.Error(System.Environment.StackTrace);
|
|
|
|
log.Error("PLEASE REPORT THIS TO THE ACE DEV TEAM !!!");
|
|
|
|
// This may still crash...
|
|
}
|
|
}
|
|
|
|
if (worldObjects.TryGetValue(objectId, out var wo))
|
|
pendingRemovals.Add(objectId);
|
|
else if (!pendingAdditions.Remove(objectId, out wo))
|
|
{
|
|
if (showError)
|
|
log.Warn($"RemoveWorldObjectInternal: Couldn't find {objectId.Full:X8}");
|
|
return;
|
|
}
|
|
|
|
wo.CurrentLandblock = null;
|
|
|
|
// Weenies can come with a default of 0 (Instant Rot) or -1 (Never Rot). If they still have that value, we want to retain it.
|
|
// We also want to make sure fromPickup is true so that we're not clearing out TimeToRot on server shutdown (unloads all landblocks and removed all objects).
|
|
if (fromPickup && wo.TimeToRot.HasValue && wo.TimeToRot != 0 && wo.TimeToRot != -1)
|
|
wo.TimeToRot = null;
|
|
|
|
if (!adjacencyMove)
|
|
{
|
|
// really remove it - send message to client to remove object
|
|
wo.EnqueueActionBroadcast(p => p.RemoveTrackedObject(wo, fromPickup));
|
|
|
|
wo.PhysicsObj.DestroyObject();
|
|
}
|
|
}
|
|
|
|
public void EmitSignal(WorldObject emitter, string message)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(message)) return;
|
|
|
|
foreach (var wo in worldObjects.Values.Where(w => w.HearLocalSignals).ToList())
|
|
{
|
|
if (emitter == wo) continue;
|
|
|
|
if (emitter.IsWithinUseRadiusOf(wo, wo.HearLocalSignalsRadius))
|
|
{
|
|
//Console.WriteLine($"{wo.Name}.EmoteManager.OnLocalSignal({emitter.Name}, {message})");
|
|
wo.EmoteManager.OnLocalSignal(emitter, message);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check to see if we are close enough to interact. Adds a fudge factor of 1.5f
|
|
/// </summary>
|
|
public bool WithinUseRadius(Player player, ObjectGuid targetGuid, out bool validTargetGuid, float? useRadius = null)
|
|
{
|
|
var target = GetObject(targetGuid);
|
|
|
|
validTargetGuid = target != null;
|
|
|
|
if (target != null)
|
|
return player.IsWithinUseRadiusOf(target, useRadius);
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns landblock objects with physics initialized
|
|
/// </summary>
|
|
public ICollection<WorldObject> GetWorldObjectsForPhysicsHandling()
|
|
{
|
|
// If a missile is destroyed when it runs it's UpdateObjectPhysics(), it will remove itself from the landblock, thus, modifying the worldObjects collection.
|
|
|
|
ProcessPendingWorldObjectAdditionsAndRemovals();
|
|
|
|
return worldObjects.Values;
|
|
}
|
|
|
|
public List<WorldObject> GetAllWorldObjectsForDiagnostics()
|
|
{
|
|
// We do not ProcessPending here, and we return ToList() to avoid cross-thread issues.
|
|
// This can happen if we "loadalllandblocks" and do a "serverstatus".
|
|
return worldObjects.Values.ToList();
|
|
}
|
|
|
|
public WorldObject GetObject(uint objectId)
|
|
{
|
|
return GetObject(new ObjectGuid(objectId));
|
|
}
|
|
|
|
/// <summary>
|
|
/// This will return null if the object was not found in the current or adjacent landblocks.
|
|
/// </summary>
|
|
public WorldObject GetObject(ObjectGuid guid, bool searchAdjacents = true)
|
|
{
|
|
if (pendingRemovals.Contains(guid))
|
|
return null;
|
|
|
|
if (worldObjects.TryGetValue(guid, out var worldObject) || pendingAdditions.TryGetValue(guid, out worldObject))
|
|
return worldObject;
|
|
|
|
if (searchAdjacents)
|
|
{
|
|
foreach (Landblock lb in Adjacents)
|
|
{
|
|
if (lb != null)
|
|
{
|
|
var wo = lb.GetObject(guid, false);
|
|
|
|
if (wo != null)
|
|
return wo;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public WorldObject GetWieldedObject(uint objectGuid, bool searchAdjacents = true)
|
|
{
|
|
return GetWieldedObject(new ObjectGuid(objectGuid), searchAdjacents); // todo fix
|
|
}
|
|
|
|
/// <summary>
|
|
/// Searches this landblock (and possibly adjacents) for an ObjectGuid wielded by a creature
|
|
/// </summary>
|
|
public WorldObject GetWieldedObject(ObjectGuid guid, bool searchAdjacents = true)
|
|
{
|
|
// search creature wielded items in current landblock
|
|
var creatures = worldObjects.Values.OfType<Creature>();
|
|
foreach (var creature in creatures)
|
|
{
|
|
var wieldedItem = creature.GetEquippedItem(guid);
|
|
if (wieldedItem != null)
|
|
{
|
|
if ((wieldedItem.CurrentWieldedLocation & EquipMask.Selectable) != 0)
|
|
return wieldedItem;
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// try searching adjacent landblocks if not found
|
|
if (searchAdjacents)
|
|
{
|
|
foreach (var adjacent in Adjacents)
|
|
{
|
|
if (adjacent == null) continue;
|
|
|
|
var wieldedItem = adjacent.GetWieldedObject(guid, false);
|
|
if (wieldedItem != null)
|
|
return wieldedItem;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a landblock to active state, with the current time as the LastActiveTime
|
|
/// </summary>
|
|
/// <param name="isAdjacent">Public calls to this function should always set isAdjacent to false</param>
|
|
public void SetActive(bool isAdjacent = false)
|
|
{
|
|
lastActiveTime = DateTime.UtcNow;
|
|
IsDormant = false;
|
|
|
|
if (isAdjacent || PhysicsLandblock == null || PhysicsLandblock.IsDungeon) return;
|
|
|
|
// for outdoor landblocks, recursively call 1 iteration to set adjacents to active
|
|
foreach (var landblock in Adjacents)
|
|
{
|
|
if (landblock != null)
|
|
landblock.SetActive(true);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the cleanup process for a landblock
|
|
/// This method is called by LandblockManager
|
|
/// </summary>
|
|
public void Unload()
|
|
{
|
|
var landblockID = Id.Raw | 0xFFFF;
|
|
|
|
//log.DebugFormat("Landblock.Unload({0:X8})", landblockID);
|
|
|
|
ProcessPendingWorldObjectAdditionsAndRemovals();
|
|
|
|
SaveDB();
|
|
|
|
// remove all objects
|
|
foreach (var wo in worldObjects.ToList())
|
|
{
|
|
if (!wo.Value.BiotaOriginatedFromOrHasBeenSavedToDatabase())
|
|
wo.Value.Destroy(false, true);
|
|
else
|
|
RemoveWorldObjectInternal(wo.Key);
|
|
}
|
|
|
|
ProcessPendingWorldObjectAdditionsAndRemovals();
|
|
|
|
actionQueue.Clear();
|
|
|
|
// remove physics landblock
|
|
LScape.unload_landblock(landblockID);
|
|
|
|
PhysicsLandblock.release_shadow_objs();
|
|
}
|
|
|
|
public void DestroyAllNonPlayerObjects()
|
|
{
|
|
ProcessPendingWorldObjectAdditionsAndRemovals();
|
|
|
|
SaveDB();
|
|
|
|
// remove all objects
|
|
foreach (var wo in worldObjects.Where(i => !(i.Value is Player)).ToList())
|
|
{
|
|
if (!wo.Value.BiotaOriginatedFromOrHasBeenSavedToDatabase())
|
|
wo.Value.Destroy(false);
|
|
else
|
|
RemoveWorldObjectInternal(wo.Key);
|
|
}
|
|
|
|
ProcessPendingWorldObjectAdditionsAndRemovals();
|
|
|
|
actionQueue.Clear();
|
|
}
|
|
|
|
private void SaveDB()
|
|
{
|
|
var biotas = new Collection<(Biota biota, ReaderWriterLockSlim rwLock)>();
|
|
|
|
foreach (var wo in worldObjects.Values)
|
|
{
|
|
if (wo.IsStaticThatShouldPersistToShard() || wo.IsDynamicThatShouldPersistToShard())
|
|
AddWorldObjectToBiotasSaveCollection(wo, biotas);
|
|
}
|
|
|
|
DatabaseManager.Shard.SaveBiotasInParallel(biotas, null);
|
|
}
|
|
|
|
private void AddWorldObjectToBiotasSaveCollection(WorldObject wo, Collection<(Biota biota, ReaderWriterLockSlim rwLock)> biotas)
|
|
{
|
|
if (wo.ChangesDetected)
|
|
{
|
|
wo.SaveBiotaToDatabase(false);
|
|
biotas.Add((wo.Biota, wo.BiotaDatabaseLock));
|
|
}
|
|
|
|
if (wo is Container container)
|
|
{
|
|
foreach (var item in container.Inventory.Values)
|
|
AddWorldObjectToBiotasSaveCollection(item, biotas);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is only used for very specific instances, such as broadcasting player deaths to the destination lifestone block
|
|
/// This is a rarely used method to broadcast network messages to all of the players within a landblock,
|
|
/// and possibly the adjacent landblocks.
|
|
/// </summary>
|
|
public void EnqueueBroadcast(ICollection<Player> excludeList, bool adjacents, Position pos = null, float? maxRangeSq = null, params GameMessage[] msgs)
|
|
{
|
|
var players = worldObjects.Values.OfType<Player>();
|
|
|
|
// for landblock death broadcasts:
|
|
// exclude players that have already been broadcast to within range of the death
|
|
if (excludeList != null)
|
|
players = players.Except(excludeList);
|
|
|
|
// broadcast messages to player in this landblock
|
|
foreach (var player in players)
|
|
{
|
|
if (pos != null && maxRangeSq != null)
|
|
{
|
|
var distSq = player.Location.SquaredDistanceTo(pos);
|
|
if (distSq > maxRangeSq)
|
|
continue;
|
|
}
|
|
player.Session.Network.EnqueueSend(msgs);
|
|
}
|
|
|
|
// if applicable, iterate into adjacent landblocks
|
|
if (adjacents)
|
|
{
|
|
foreach (var adjacent in this.Adjacents.Where(adj => adj != null))
|
|
adjacent.EnqueueBroadcast(excludeList, false, pos, maxRangeSq, msgs);
|
|
}
|
|
}
|
|
|
|
private bool? isDungeon;
|
|
|
|
/// <summary>
|
|
/// Returns TRUE if this landblock is a dungeon,
|
|
/// with no traversable overworld
|
|
/// </summary>
|
|
public bool IsDungeon
|
|
{
|
|
get
|
|
{
|
|
// return cached value
|
|
if (isDungeon != null)
|
|
return isDungeon.Value;
|
|
|
|
// hack for NW island
|
|
// did a worldwide analysis for adding watercells into the formula,
|
|
// but they are inconsistently defined for some of the edges of map unfortunately
|
|
if (Id.LandblockX < 0x08 && Id.LandblockY > 0xF8)
|
|
{
|
|
isDungeon = false;
|
|
return isDungeon.Value;
|
|
}
|
|
|
|
// a dungeon landblock is determined by:
|
|
// - all heights being 0
|
|
// - having at least 1 EnvCell (0x100+)
|
|
// - contains no buildings
|
|
foreach (var height in CellLandblock.Height)
|
|
{
|
|
if (height != 0)
|
|
{
|
|
isDungeon = false;
|
|
return isDungeon.Value;
|
|
}
|
|
}
|
|
isDungeon = LandblockInfo != null && LandblockInfo.NumCells > 0 && LandblockInfo.Buildings != null && LandblockInfo.Buildings.Count == 0;
|
|
return isDungeon.Value;
|
|
}
|
|
}
|
|
|
|
private bool? hasDungeon;
|
|
|
|
/// <summary>
|
|
/// Returns TRUE if this landblock contains a dungeon
|
|
//
|
|
/// If a landblock contains both a dungeon + traversable overworld,
|
|
/// this field will return TRUE, whereas IsDungeon will return FALSE
|
|
///
|
|
/// This property should only be used in very specific scenarios,
|
|
/// such as determining if a landblock contains a mansion basement
|
|
/// </summary>
|
|
public bool HasDungeon
|
|
{
|
|
get
|
|
{
|
|
// return cached value
|
|
if (hasDungeon != null)
|
|
return hasDungeon.Value;
|
|
|
|
hasDungeon = LandblockInfo != null && LandblockInfo.NumCells > 0 && LandblockInfo.Buildings != null && LandblockInfo.Buildings.Count == 0;
|
|
return hasDungeon.Value;
|
|
}
|
|
}
|
|
|
|
|
|
public List<House> Houses = new List<House>();
|
|
|
|
public void SetFogColor(EnvironChangeType environChangeType)
|
|
{
|
|
if (environChangeType.IsFog())
|
|
{
|
|
FogColor = environChangeType;
|
|
|
|
foreach (var lb in Adjacents)
|
|
lb.FogColor = environChangeType;
|
|
|
|
foreach(var player in players)
|
|
{
|
|
player.SetFogColor(FogColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SendEnvironSound(EnvironChangeType environChangeType)
|
|
{
|
|
if (environChangeType.IsSound())
|
|
{
|
|
SendEnvironChange(environChangeType);
|
|
|
|
foreach (var lb in Adjacents)
|
|
lb.SendEnvironChange(environChangeType);
|
|
}
|
|
}
|
|
|
|
public void SendEnvironChange(EnvironChangeType environChangeType)
|
|
{
|
|
foreach (var player in players)
|
|
{
|
|
player.SendEnvironChange(environChangeType);
|
|
}
|
|
}
|
|
|
|
public void SendCurrentEnviron()
|
|
{
|
|
foreach (var player in players)
|
|
{
|
|
if (FogColor.IsFog())
|
|
{
|
|
player.SetFogColor(FogColor);
|
|
}
|
|
else
|
|
{
|
|
player.SendEnvironChange(FogColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void DoEnvironChange(EnvironChangeType environChangeType)
|
|
{
|
|
if (environChangeType.IsFog())
|
|
SetFogColor(environChangeType);
|
|
else
|
|
SendEnvironSound(environChangeType);
|
|
}
|
|
}
|
|
}
|