using System; using System.Threading; using ACE.Common; using ACE.Database; using ACE.Entity.Enum; using ACE.Entity.Models; using ACE.Server.Network.GameMessages.Messages; namespace ACE.Server.WorldObjects { partial class WorldObject { private readonly bool biotaOriginatedFromDatabase; public DateTime LastRequestedDatabaseSave { get; protected set; } /// /// This variable is set to true when a change is made, and set to false before a save is requested. /// The primary use for this is to trigger save on add/modify/remove of properties. /// public bool ChangesDetected { get; set; } /// /// Best practice says you should use this lock any time you read/write the Biota. /// However, it's only a requirement to do this for properties/collections that will be modified after the initial biota has been created. /// There are several properties/collections of the biota that are simply duplicates of the original weenie and are never changed. You wouldn't need to use this lock to read those collections. /// /// For absolute maximum performance, if you're willing to assume (and risk) the following: /// - that the biota in the database will not be modified (in a way that adds or removes properties) outside of ACE while ACE is running with a reference to that biota /// - that the biota will only be read/modified by a single thread in ACE /// You can remove the lock usage for any Get/GetAll Property functions. You would simply use it for Set/Remove Property functions because each of these could end up adding/removing to the collections. /// The critical thing is that the collections are not added to or removed from while Entity Framework is iterating over them. /// Mag-nus 2018-08-19 /// public readonly ReaderWriterLockSlim BiotaDatabaseLock = new ReaderWriterLockSlim(); public bool BiotaOriginatedFromOrHasBeenSavedToDatabase() { return biotaOriginatedFromDatabase || LastRequestedDatabaseSave != DateTime.MinValue; } /// /// This will set the LastRequestedDatabaseSave to UtcNow and ChangesDetected to false. /// If enqueueSave is set to true, DatabaseManager.Shard.SaveBiota() will be called for the biota. /// Set enqueueSave to false if you want to perform all the normal routines for a save but not the actual save. This is useful if you're going to collect biotas in bulk for bulk saving. /// public virtual void SaveBiotaToDatabase(bool enqueueSave = true) { // Make sure all of our positions in the biota are up to date with our current cached values. foreach (var kvp in positionCache) { if (kvp.Value != null) Biota.SetPosition(kvp.Key, kvp.Value, BiotaDatabaseLock); } LastRequestedDatabaseSave = DateTime.UtcNow; ChangesDetected = false; if (enqueueSave) { CheckpointTimestamp = Time.GetUnixTime(); //DatabaseManager.Shard.SaveBiota(Biota, BiotaDatabaseLock, null); DatabaseManager.Shard.SaveBiota(Biota, BiotaDatabaseLock, result => { if (!result) { if (this is Player player) { // This will trigger a boot on next player tick player.BiotaSaveFailed = true; } } }); } } /// /// This will set the LastRequestedDatabaseSave to MinValue and ChangesDetected to true. /// If enqueueRemove is set to true, DatabaseManager.Shard.RemoveBiota() will be called for the biota. /// Set enqueueRemove to false if you want to perform all the normal routines for a remove but not the actual removal. This is useful if you're going to collect biotas in bulk for bulk removing. /// public void RemoveBiotaFromDatabase(bool enqueueRemove = true) { // If this entity doesn't exist in the database, let's not queue up work unnecessary database work. if (!BiotaOriginatedFromOrHasBeenSavedToDatabase()) { ChangesDetected = true; return; } LastRequestedDatabaseSave = DateTime.MinValue; ChangesDetected = true; if (enqueueRemove) DatabaseManager.Shard.RemoveBiota(Biota.Id, null); } /// /// A static that should persist to the shard may be a hook with an item, or a house that's been purchased, or a housing chest that isn't empty, etc... /// If the world object originated from the database or has been saved to the database, this will also return true. /// public bool IsStaticThatShouldPersistToShard() { if (!Guid.IsStatic()) return false; if (BiotaOriginatedFromOrHasBeenSavedToDatabase()) return true; if (WeenieType == WeenieType.SlumLord && this is SlumLord slumlord) { if (slumlord.House != null && slumlord.House.HouseOwner.HasValue && slumlord.House.HouseOwner != 0) return true; } if (WeenieType == WeenieType.House && this is House house) { if (house.HouseOwner.HasValue && house.HouseOwner != 0) return true; } if ((WeenieType == WeenieType.Hook || WeenieType == WeenieType.Storage) && this is Container container) { if (container.Inventory.Count > 0) return true; } return false; } /// /// This will filter out the following: /// Ammunition and Spell projectiles. /// Monster corpses. /// Missiles that haven't been saved to the shard yet. /// If the world object originated from the database or has been saved to the database, this will also return true. /// /// public bool IsDynamicThatShouldPersistToShard() { if (!Guid.IsDynamic()) return false; if (BiotaOriginatedFromOrHasBeenSavedToDatabase()) return true; // Don't save generators, and items that were generated by a generator // If the item was generated by a generator and then picked up by a player, the wo.Generator property would be set to null. if (IsGenerator || Generator != null) return false; if (WeenieType == WeenieType.Missile || WeenieType == WeenieType.Ammunition || WeenieType == WeenieType.ProjectileSpell || WeenieType == WeenieType.GamePiece || WeenieType == WeenieType.Pet || WeenieType == WeenieType.CombatPet) return false; if (WeenieType == WeenieType.Corpse && this is Corpse corpse && corpse.IsMonster) return false; if (WeenieType == WeenieType.Portal && this is Portal portal && portal.IsGateway) return false; // Missiles are unique. The only missiles that are persistable are ones that already exist in the database. // TODO: See if we can remove this check by catching the WeenieType above. var missile = Missile; if (missile.HasValue && missile.Value) { log.Warn($"Missile: WeenieClassId: {WeenieClassId}, Name: {Name}, WeenieType: {WeenieType}, detected in IsDynamicThatShouldPersistToShard() that wasn't caught by prior check."); return false; } return true; } } }