diff --git a/Source/ACE.Common/OfflineConfiguration.cs b/Source/ACE.Common/OfflineConfiguration.cs index ef4d4c4df2..d6b800fef2 100644 --- a/Source/ACE.Common/OfflineConfiguration.cs +++ b/Source/ACE.Common/OfflineConfiguration.cs @@ -8,8 +8,8 @@ namespace ACE.Common public class OfflineConfiguration { /// - /// Purge characters that have been deleted longer than PruneDeletedCharactersDays - /// These characters, and their associated biotas, will be deleted permanantly! + /// Purge characters that have been deleted longer than PurgeDeletedCharactersDays + /// These characters, and their associated biotas, will be deleted permanently! /// public bool PurgeDeletedCharacters { get; set; } = false; @@ -25,6 +25,17 @@ public class OfflineConfiguration /// public bool PurgeOrphanedBiotas { get; set; } = false; + /// + /// Purge biota that have been deleted longer than PurgeReleasedBiotasDays + /// These biota will be deleted permanently! + /// + public bool PurgeReleasedBiotas { get; set; } = false; + + /// + /// Number of days a biota must have been deleted for before eligible for purging + /// + public int PurgeReleasedBiotasDays { get; set; } = 30; + /// /// Prune deleted characters from all friend lists /// diff --git a/Source/ACE.Database/ShardDatabase.cs b/Source/ACE.Database/ShardDatabase.cs index 15f93ea95d..4cc595cd30 100644 --- a/Source/ACE.Database/ShardDatabase.cs +++ b/Source/ACE.Database/ShardDatabase.cs @@ -94,8 +94,11 @@ public uint GetMaxGuidFoundInRange(uint min, uint max) " JOIN biota" + Environment.NewLine + " WHERE id > " + min + Environment.NewLine + " ORDER BY id" + Environment.NewLine + - " ) AS z" + Environment.NewLine + - "WHERE z.gap_ends_at_not_inclusive!=0 AND @available_ids<" + limitAvailableIDsReturned + "; "; + " ) AS z" + Environment.NewLine; + if (limitAvailableIDsReturned != uint.MaxValue) + sql += "WHERE z.gap_ends_at_not_inclusive!=0 AND @available_ids<" + limitAvailableIDsReturned + "; "; + else + sql += "WHERE z.gap_ends_at_not_inclusive!=0;"; using (var context = new ShardDbContext()) { diff --git a/Source/ACE.Database/ShardDatabaseOfflineTools.cs b/Source/ACE.Database/ShardDatabaseOfflineTools.cs index 386508513c..6a07ad1528 100644 --- a/Source/ACE.Database/ShardDatabaseOfflineTools.cs +++ b/Source/ACE.Database/ShardDatabaseOfflineTools.cs @@ -333,6 +333,7 @@ public static void PurgeOrphanedBiotasInParallel(ShardDbContext context, out int HashSet playerBiotaIds = null; HashSet characterIds = null; + HashSet releasedIds = null; Dictionary biotas = null; Dictionary containerPointers = null; @@ -428,6 +429,9 @@ from c in combined.DefaultIfEmpty() }); } + context.Database.SetCommandTimeout(900); + releasedIds = context.BiotaPropertiesFloat.AsNoTracking().Where(r => r.Type == (ushort)PropertyFloat.ReleasedTimestamp).Select(r => r.ObjectId).ToHashSet(); + // Purge contained items that belong to a parent container that no longer exists { // select * from biota_properties_i_i_d iid left join biota on biota.id=iid.`value` where iid.`type`=2 and biota.id is null; @@ -460,7 +464,7 @@ from b in combined.DefaultIfEmpty() Parallel.ForEach(results, ConfigManager.Config.Server.Threading.DatabaseParallelOptions, result => { - if (PurgeBiota(result.ObjectId, "Parent container not found")) + if (!releasedIds.Contains(result.ObjectId) && PurgeBiota(result.ObjectId, "Parent container not found")) Interlocked.Increment(ref totalNumberOfBiotasPurged); }); @@ -498,7 +502,7 @@ from b in combined.DefaultIfEmpty() Parallel.ForEach(results, ConfigManager.Config.Server.Threading.DatabaseParallelOptions, result => { - if (PurgeBiota(result.ObjectId, "Parent wielder not found")) + if (!releasedIds.Contains(result.ObjectId) && PurgeBiota(result.ObjectId, "Parent wielder not found")) Interlocked.Increment(ref totalNumberOfBiotasPurged); }); @@ -512,6 +516,10 @@ from b in combined.DefaultIfEmpty() foreach (var kvp in biotas) { + // exclude released objects + if (releasedIds.Contains(kvp.Key)) + continue; + // exclude allegiances if (kvp.Value == WeenieType.Allegiance) continue; @@ -919,6 +927,55 @@ public static void PurgeOrphanedBiotasInParallel(out int numberOfBiotasPurged) PurgeOrphanedBiotasInParallel(context, out numberOfBiotasPurged); } + public static void PurgeReleasedBiotasInParallel(ShardDbContext context, int daysLimiter, out int numberOfBiotasPurged) + { + var deleteLimit = Time.GetUnixTime(DateTime.UtcNow.AddDays(-daysLimiter)); + + context.Database.SetCommandTimeout(900); + + var releasedIds = context.BiotaPropertiesFloat.AsNoTracking().Where(r => r.Type == (ushort)PropertyFloat.ReleasedTimestamp && r.Value <= deleteLimit).Select(r => new { r.ObjectId, r.Value }).AsEnumerable().Select(r => (Id: r.ObjectId, ReleasedTimestamp: r.Value)).ToHashSet(); + + int biotaPurgedTotal = 0; + + Parallel.ForEach(releasedIds, ConfigManager.Config.Server.Threading.DatabaseParallelOptions, result => + { + if (PurgeBiota(result.Id, $"Released on {Time.GetDateTimeFromTimestamp(result.ReleasedTimestamp).ToLocalTime()} which is older than {daysLimiter} days")) + Interlocked.Increment(ref biotaPurgedTotal); + }); + + numberOfBiotasPurged = biotaPurgedTotal; + } + + public static void PurgeDoDBiotasInParallel(out int numberOfBiotasPurged) + { + using (var context = new ShardDbContext()) + { + context.Database.SetCommandTimeout(900); + PurgeDoDBiotasInParallel(context, out numberOfBiotasPurged); + } + } + + public static void PurgeDoDBiotasInParallel(ShardDbContext context, out int numberOfBiotasPurged) + { + var destroyedIds = context.BiotaPropertiesInt.AsNoTracking().Where(r => r.Type == (ushort)PropertyInt.Bonded && r.Value == (int)BondedStatus.Destroy).Select(r => r.ObjectId).ToHashSet(); + + int biotaPurgedTotal = 0; + + Parallel.ForEach(destroyedIds, ConfigManager.Config.Server.Threading.DatabaseParallelOptions, result => + { + if (PurgeBiota(result, $"Biota has BondedStatus.Destroy")) + Interlocked.Increment(ref biotaPurgedTotal); + }); + + numberOfBiotasPurged = biotaPurgedTotal; + } + + public static void PurgeReleasedBiotasInParallel(int daysLimiter, out int numberOfBiotasPurged) + { + using (var context = new ShardDbContext()) + PurgeReleasedBiotasInParallel(context, daysLimiter, out numberOfBiotasPurged); + } + /// /// This is temporary and can be removed in the near future, 2020-04-05 Mag-nus /// diff --git a/Source/ACE.Entity/Enum/Properties/PositionType.cs b/Source/ACE.Entity/Enum/Properties/PositionType.cs index d08731fdd8..5f3bc25a57 100644 --- a/Source/ACE.Entity/Enum/Properties/PositionType.cs +++ b/Source/ACE.Entity/Enum/Properties/PositionType.cs @@ -183,6 +183,9 @@ public enum PositionType : ushort TeleportedCharacter = 27, [ServerOnly] - PCAPRecordedLocation = 8040 + PCAPRecordedLocation = 8040, + + [ServerOnly] + PreDestroyLocation = 9000 } } diff --git a/Source/ACE.Entity/Enum/Properties/PropertyString.cs b/Source/ACE.Entity/Enum/Properties/PropertyString.cs index 864b7fcc40..9f88ebf867 100644 --- a/Source/ACE.Entity/Enum/Properties/PropertyString.cs +++ b/Source/ACE.Entity/Enum/Properties/PropertyString.cs @@ -96,6 +96,16 @@ public enum PropertyString : ushort GodState = 9006, [ServerOnly] TinkerLog = 9007, + [ServerOnly] + DestroyStackLog = 9008, + [ServerOnly] + PreviousOwners = 9009, + [ServerOnly] + PreviousWielders = 9010, + [ServerOnly] + PreviousOwnerStackLog = 9011, + [ServerOnly] + PreviousWielderStackLog = 9012, } public static class PropertyStringExtensions diff --git a/Source/ACE.Server/Config.js.docker b/Source/ACE.Server/Config.js.docker index ed0d55397b..c62eb6a464 100644 --- a/Source/ACE.Server/Config.js.docker +++ b/Source/ACE.Server/Config.js.docker @@ -179,14 +179,21 @@ // This section can trigger events that may happen before the world starts up, or after it shuts down // The shard should be in a disconnected state from any running ACE world "Offline": { - // Purge characters that have been deleted longer than PruneDeletedCharactersDays - // These characters, and their associated biotas, will be deleted permanantly! + // Purge characters that have been deleted longer than PurgeDeletedCharactersDays + // These characters, and their associated biotas, will be deleted permanently! "PurgeDeletedCharacters": false, // Number of days a character must have been deleted for before eligible for purging "PurgeDeletedCharactersDays": 30, - // This will purge biotas that are completely disconnected from the world + // Purge biotas that have been released longer than PurgeReleasedBiotasDays + // These biotas (World Objects) will be deleted permanently! + "PurgeReleasedBiotas": false, + + // Number of days a biota must have been released for before eligible for purging + "PurgeReleasedBiotasDays": 30, + + // This will purge biotas that are completely disconnected from the world, excluding biotas marked as released // These may have been items that were never deleted properly, items that were given to the town crier before delete was implemented, etc... // This can be time consuming so it's not something you would have set to true for every server startup. You might run this once every few months "PurgeOrphanedBiotas": false, diff --git a/Source/ACE.Server/Config.js.example b/Source/ACE.Server/Config.js.example index a8cdbb1a3f..ee89285714 100644 --- a/Source/ACE.Server/Config.js.example +++ b/Source/ACE.Server/Config.js.example @@ -177,14 +177,21 @@ // This section can trigger events that may happen before the world starts up, or after it shuts down // The shard should be in a disconnected state from any running ACE world "Offline": { - // Purge characters that have been deleted longer than PruneDeletedCharactersDays - // These characters, and their associated biotas, will be deleted permanantly! + // Purge characters that have been deleted longer than PurgeDeletedCharactersDays + // These characters, and their associated biotas, will be deleted permanently! "PurgeDeletedCharacters": false, // Number of days a character must have been deleted for before eligible for purging "PurgeDeletedCharactersDays": 30, - // This will purge biotas that are completely disconnected from the world + // Purge biotas that have been released longer than PurgeReleasedBiotasDays + // These biotas (World Objects) will be deleted permanently! + "PurgeReleasedBiotas": false, + + // Number of days a biota must have been released for before eligible for purging + "PurgeReleasedBiotasDays": 30, + + // This will purge biotas that are completely disconnected from the world, excluding biotas marked as released // These may have been items that were never deleted properly, items that were given to the town crier before delete was implemented, etc... // This can be time consuming so it's not something you would have set to true for every server startup. You might run this once every few months "PurgeOrphanedBiotas": false, diff --git a/Source/ACE.Server/Entity/Landblock.cs b/Source/ACE.Server/Entity/Landblock.cs index 23e911f913..71ae2be170 100644 --- a/Source/ACE.Server/Entity/Landblock.cs +++ b/Source/ACE.Server/Entity/Landblock.cs @@ -874,6 +874,11 @@ private bool AddWorldObjectInternal(WorldObject wo) 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()}"); + if (PropertyManager.GetBool("landblock_destroys_failed_unstuck_spawns").Item && wo is not Creature && !wo.Stuck && wo.Generator is null) + { + wo.Destroy(); + } + return false; } } diff --git a/Source/ACE.Server/Managers/GuidManager.cs b/Source/ACE.Server/Managers/GuidManager.cs index acc3dfd680..c3ba81bc7c 100644 --- a/Source/ACE.Server/Managers/GuidManager.cs +++ b/Source/ACE.Server/Managers/GuidManager.cs @@ -127,7 +127,7 @@ private class DynamicGuidAllocator private bool useSequenceGapExhaustedMessageDisplayed; private LinkedList<(uint start, uint end)> availableIDs = new LinkedList<(uint start, uint end)>(); - public DynamicGuidAllocator(uint min, uint max, string name) + public DynamicGuidAllocator(uint min, uint max, string name, bool unlimitedGaps) { this.max = max; @@ -164,7 +164,7 @@ public DynamicGuidAllocator(uint min, uint max, string name) lock (this) { bool done = false; - Database.DatabaseManager.Shard.GetSequenceGaps(ObjectGuid.DynamicMin, limitAvailableIDsReturnedInGetSequenceGaps, gaps => + Database.DatabaseManager.Shard.GetSequenceGaps(ObjectGuid.DynamicMin, unlimitedGaps ? uint.MaxValue : limitAvailableIDsReturnedInGetSequenceGaps, gaps => { lock (this) { @@ -294,7 +294,7 @@ public override string ToString() public static void Initialize() { playerAlloc = new PlayerGuidAllocator(ObjectGuid.PlayerMin, ObjectGuid.PlayerMax, "player"); - dynamicAlloc = new DynamicGuidAllocator(ObjectGuid.DynamicMin, ObjectGuid.DynamicMax, "dynamic"); + dynamicAlloc = new DynamicGuidAllocator(ObjectGuid.DynamicMin, ObjectGuid.DynamicMax, "dynamic", PropertyManager.GetBool("unlimited_sequence_gaps").Item); } /// diff --git a/Source/ACE.Server/Managers/PropertyManager.cs b/Source/ACE.Server/Managers/PropertyManager.cs index d32b6bd629..02b9d266bc 100644 --- a/Source/ACE.Server/Managers/PropertyManager.cs +++ b/Source/ACE.Server/Managers/PropertyManager.cs @@ -548,6 +548,7 @@ public static void LoadDefaultProperties() ("craft_exact_msg", new Property(false, "If TRUE, and player has crafting chance of success dialog enabled, shows them an additional message in their chat window with exact %")), ("creature_name_check", new Property(true, "if enabled, creature names in world database restricts player names during character creation")), ("creatures_drop_createlist_wield", new Property(false, "If FALSE then Wielded items in CreateList will not drop. Retail defaulted to TRUE but there are currently data errors")), + ("destroy_deletes_from_database", new Property(true, "when an item is destroyed, if it was stored in database it will be immediately deleted")), ("equipmentsetid_enabled", new Property(true, "enable this to allow adding EquipmentSetIDs to loot armor")), ("equipmentsetid_name_decoration", new Property(false, "enable this to add the EquipmentSet name to loot armor name")), ("fastbuff", new Property(true, "If TRUE, enables the fast buffing trick from retail.")), @@ -567,6 +568,7 @@ public static void LoadDefaultProperties() ("house_rent_enabled", new Property(true, "If FALSE then rent is not required")), ("iou_trades", new Property(false, "(non-retail function) If enabled, IOUs can be traded for objects that are missing in DB but added/restored later on")), ("item_dispel", new Property(false, "if enabled, allows players to dispel items. defaults to end of retail, where item dispels could only target creatures")), + ("landblock_destroys_failed_unstuck_spawns", new Property(false, "if enabled, any unmanaged/uncontrolled unstuck object that fails to spawn on a landblock will be destroyed")), ("legacy_loot_system", new Property(false, "use the previous iteration of the ace lootgen system")), ("lifestone_broadcast_death", new Property(true, "if true, player deaths are additionally broadcast to other players standing near the destination lifestone")), ("loot_quality_mod", new Property(true, "if FALSE then the loot quality modifier of a Death Treasure profile does not affect loot generation")), @@ -586,6 +588,10 @@ public static void LoadDefaultProperties() ("rares_real_time", new Property(true, "allow for second chance roll based on an rng seeded timestamp for a rare on rare eligible kills that do not generate a rare, rares_max_seconds_between defines maximum seconds before second chance kicks in")), ("rares_real_time_v2", new Property(false, "chances for a rare to be generated on rare eligible kills are modified by the last time one was found per each player, rares_max_days_between defines maximum days before guaranteed rare generation")), ("runrate_add_hooks", new Property(false, "if TRUE, adds some runrate hooks that were missing from retail (exhaustion done, raise skill/attribute")), + ("record_destroy_stacktrace", new Property(false, "logs stack trace when object is destroyed")), + ("record_dequip_stacktrace", new Property(false, "logs stack trace when object is dequiped")), + ("record_remove_stacktrace", new Property(false, "logs stack trace when object is removed")), + ("recycle_guids", new Property(true, "allows dynamic objects guids to be recycled")), ("reportbug_enabled", new Property(false, "toggles the /reportbug player command")), ("require_spell_comps", new Property(true, "if FALSE, spell components are no longer required to be in inventory to cast spells. defaults to enabled, as in retail")), ("safe_spell_comps", new Property(false, "if TRUE, disables spell component burning for everyone")), @@ -605,6 +611,7 @@ public static void LoadDefaultProperties() ("trajectory_alt_solver", new Property(false, "use the alternate trajectory solver for missiles and spell projectiles")), ("universal_masteries", new Property(true, "if TRUE, matches end of retail masteries - players wielding almost any weapon get +5 DR, except if the weapon \"seems tough to master\". " + "if FALSE, players start with mastery of 1 melee and 1 ranged weapon type based on heritage, and can later re-select these 2 masteries")), + ("unlimited_sequence_gaps", new Property(false, "upon startup, allows server to find all unused guids in a range instead of a set hard limit")), ("use_generator_rotation_offset", new Property(true, "enables or disables using the generator's current rotation when offseting relative positions")), ("use_turbine_chat", new Property(true, "enables or disables global chat channels (General, LFG, Roleplay, Trade, Olthoi, Society, Allegience)")), ("use_wield_requirements", new Property(true, "disable this to bypass wield requirements. mostly for dev debugging")), @@ -621,6 +628,7 @@ public static void LoadDefaultProperties() ("chat_requires_player_level", new Property(0, "the level a player is required to have for global chat privileges")), ("corpse_spam_limit", new Property(15, "the number of corpses a player is allowed to leave on a landblock at one time")), ("default_subscription_level", new Property(1, "retail defaults to 1, 1 = standard subscription (same as 2 and 3), 4 grants ToD pre-order bonus item Asheron's Benediction")), + ("destroy_saves_older_than_seconds", new Property(86400, "the amount of time in seconds a destroyed object, which is not Stuck, must have existed for before it is worth saving from immediate destruction. Object can then later be purged via startup database maintanence")), ("fellowship_even_share_level", new Property(50, "level when fellowship XP sharing is no longer restricted")), ("mansion_min_rank", new Property(6, "overrides the default allegiance rank required to own a mansion")), ("max_chars_per_account", new Property(11, "retail defaults to 11, client supports up to 20")), diff --git a/Source/ACE.Server/Program.cs b/Source/ACE.Server/Program.cs index 4224e0aa48..baa79e4bd6 100644 --- a/Source/ACE.Server/Program.cs +++ b/Source/ACE.Server/Program.cs @@ -166,6 +166,20 @@ public static void Main(string[] args) log.Info($"Purged {charactersPurged:N0} characters, {playerBiotasPurged:N0} player biotas and {possessionsPurged:N0} possessions."); } + if (ConfigManager.Config.Offline.PurgeReleasedBiotas) + { + log.Info($"Purging released biotas older than {ConfigManager.Config.Offline.PurgeReleasedBiotasDays} days ({DateTime.Now.AddDays(-ConfigManager.Config.Offline.PurgeReleasedBiotasDays)})..."); + ShardDatabaseOfflineTools.PurgeReleasedBiotasInParallel(ConfigManager.Config.Offline.PurgeReleasedBiotasDays, out var numberOfBiotasPurged); + log.Info($"Purged {numberOfBiotasPurged:N0} biotas."); + } + + //if (ConfigManager.Config.Offline.PurgeBondedDestroyBiotas) + //{ + // log.Info($"Purging Bonded.Destroy biotas..."); + // ShardDatabaseOfflineTools.PurgeDoDBiotasInParallel(out var numberOfBiotasPurged); + // log.Info($"Purged {numberOfBiotasPurged:N0} biotas."); + //} + if (ConfigManager.Config.Offline.PurgeOrphanedBiotas) { log.Info($"Purging orphaned biotas..."); diff --git a/Source/ACE.Server/WorldObjects/Container.cs b/Source/ACE.Server/WorldObjects/Container.cs index 3eafb1b238..3f8eb4d7c7 100644 --- a/Source/ACE.Server/WorldObjects/Container.cs +++ b/Source/ACE.Server/WorldObjects/Container.cs @@ -648,6 +648,34 @@ public bool TryRemoveFromInventory(ObjectGuid objectGuid, out WorldObject item, { int removedItemsPlacementPosition = item.PlacementPosition ?? 0; + var currentPrevOwners = item.GetProperty(PropertyString.PreviousOwners) ?? ""; + var currentOwner = item.OwnerId ?? 0; + if (!ObjectGuid.IsPlayer(currentOwner)) + { + if (item.Container is Container && item.Container.Container is Player) + currentOwner = item.Container.Container.Guid.Full; + } + + //if (!ObjectGuid.IsPlayer(currentOwner) && !ObjectGuid.IsStatic(currentOwner)) + if (!ObjectGuid.IsPlayer(currentOwner)) + currentOwner = 0; + + if (currentOwner > 0) + { + var owners = currentPrevOwners.Split(";", StringSplitOptions.RemoveEmptyEntries); + if (owners.Length > 0) + { + var lastOwner = owners[owners.Length - 1]; + if (Convert.ToUInt32(lastOwner[0..10], 16) != currentOwner) + item.SetProperty(PropertyString.PreviousOwners, currentPrevOwners + $"0x{currentOwner:X8}:{Common.Time.GetUnixTime()};"); + } + else + item.SetProperty(PropertyString.PreviousOwners, currentPrevOwners + $"0x{currentOwner:X8}:{Common.Time.GetUnixTime()};"); + } + + if (PropertyManager.GetBool("record_remove_stacktrace").Item) + item.SetProperty(PropertyString.PreviousOwnerStackLog, Environment.StackTrace); + item.OwnerId = null; item.ContainerId = null; item.Container = null; diff --git a/Source/ACE.Server/WorldObjects/Creature_Equipment.cs b/Source/ACE.Server/WorldObjects/Creature_Equipment.cs index b77b1bba3c..4f7d829a68 100644 --- a/Source/ACE.Server/WorldObjects/Creature_Equipment.cs +++ b/Source/ACE.Server/WorldObjects/Creature_Equipment.cs @@ -385,6 +385,29 @@ public bool TryDequipObject(ObjectGuid objectGuid, out WorldObject worldObject, RemoveItemFromEquippedItemsRatingCache(worldObject); + var currentPrevOwners = worldObject.GetProperty(PropertyString.PreviousWielders) ?? ""; + var currentOwner = worldObject.WielderId ?? 0; + + //if (!ObjectGuid.IsPlayer(currentOwner) && !ObjectGuid.IsStatic(currentOwner)) + if (!ObjectGuid.IsPlayer(currentOwner)) + currentOwner = 0; + + if (currentOwner > 0) + { + var owners = currentPrevOwners.Split(";", StringSplitOptions.RemoveEmptyEntries); + if (owners.Length > 0) + { + var lastOwner = owners[owners.Length - 1]; + if (Convert.ToUInt32(lastOwner[0..10], 16) != currentOwner) + worldObject.SetProperty(PropertyString.PreviousWielders, currentPrevOwners + $"0x{currentOwner:X8}:{Time.GetUnixTime()};"); + } + else + worldObject.SetProperty(PropertyString.PreviousWielders, currentPrevOwners + $"0x{currentOwner:X8}:{Time.GetUnixTime()};"); + } + + if (PropertyManager.GetBool("record_dequip_stacktrace").Item) + worldObject.SetProperty(PropertyString.PreviousWielderStackLog, Environment.StackTrace); + wieldedLocation = worldObject.CurrentWieldedLocation ?? EquipMask.None; worldObject.RemoveProperty(PropertyInt.CurrentWieldedLocation); diff --git a/Source/ACE.Server/WorldObjects/WorldObject.cs b/Source/ACE.Server/WorldObjects/WorldObject.cs index cc02fe60b9..c2d3d23c5d 100644 --- a/Source/ACE.Server/WorldObjects/WorldObject.cs +++ b/Source/ACE.Server/WorldObjects/WorldObject.cs @@ -844,7 +844,10 @@ public void Destroy(bool raiseNotifyOfDestructionEvent = true, bool fromLandbloc IsDestroyed = true; - ReleasedTimestamp = Time.GetUnixTime(); + var utcNow = DateTime.UtcNow; + var timestamp = Time.GetUnixTime(utcNow); + + ReleasedTimestamp = timestamp; if (this is Container container) { @@ -889,10 +892,93 @@ public void Destroy(bool raiseNotifyOfDestructionEvent = true, bool fromLandbloc CurrentLandblock?.RemoveWorldObject(Guid); - RemoveBiotaFromDatabase(); + var recycleGuids = PropertyManager.GetBool("recycle_guids").Item; + var destroyItem = PropertyManager.GetBool("destroy_deletes_from_database").Item; + + var creationTimestamp = CreationTimestamp; + var createdOn = creationTimestamp.HasValue ? Time.GetDateTimeFromTimestamp(creationTimestamp.Value) : utcNow; + var destroyLimiter = PropertyManager.GetLong("destroy_saves_older_than_seconds").Item; + var destroyLimit = utcNow.AddSeconds(-destroyLimiter); + var isOlderThanLimit = createdOn < destroyLimit; + + //if (destroyItem || Stuck || ((ValidLocations ?? 0) < EquipMask.HeadWear) || (Container?.Guid.IsStatic() ?? false) || (!Wielder?.Guid.IsPlayer() ?? false) || (Container is Corpse && !Container.Level.HasValue) || (Container is Creature and not Player) || (Container is Chest and not Storage) || (this is Missile) || (this is Ammunition) || fromLandblockUnload) + if (destroyItem || Stuck || (Container is Creature and not Player) || (Container is Chest and not Storage) || (Container?.Guid.IsStatic() ?? false) || (Container is Corpse && !Container.Level.HasValue) || (!Wielder?.Guid.IsPlayer() ?? false) || !isOlderThanLimit) + { + RemoveBiotaFromDatabase(); - if (Guid.IsDynamic()) - GuidManager.RecycleDynamicGuid(Guid); + if (Guid.IsDynamic() && recycleGuids) + GuidManager.RecycleDynamicGuid(Guid); + } + else + { + var logline = "[DESTROY] "; + if (StackSize > 1) + logline += $"{StackSize:N0}x "; + logline += $"{GetNameWithMaterial(StackSize)} "; + logline += $"({Name} | {WeenieClassId} | 0x{Guid}) "; + logline += "has been destroyed but not deleted. "; + logline += $"OwnerId: 0x{OwnerId ?? 0:X8} | WielderId: 0x{WielderId ?? 0:X8} | ContainerId: 0x{ContainerId ?? 0:X8}\n"; + logline += $"CreationTimestamp: {createdOn.ToLocalTime():G} ({creationTimestamp})\n"; + if (OwnerId > 0) + OwnerId = null; + if (WielderId > 0) + WielderId = null; + if (ContainerId > 0) + ContainerId = null; + if (Location != null && Location.LandblockId.Raw > 0) + { + logline += $"LOC: {Location.ToLOCString()}\n"; + SetPosition(PositionType.PreDestroyLocation, new Position(Location)); + Location = null; + } + else + logline += $"No Previous Location\n"; + var previousOwners = GetProperty(PropertyString.PreviousOwners) ?? ""; + var prevOwners = previousOwners.Split(";", StringSplitOptions.RemoveEmptyEntries); + if (prevOwners.Length > 0) + { + logline += "Previous Owners: "; + foreach (var p in prevOwners) + { + var po = PlayerManager.FindByGuid(new ObjectGuid(Convert.ToUInt32(p[0..10], 16))); + if (po != null) + logline += $"{po.Name} (0x{po.Guid}) ({Time.GetDateTimeFromTimestamp(Convert.ToDouble(p[11..])).ToLocalTime():G}), "; + else + logline += $"{p[0..10]} ({Time.GetDateTimeFromTimestamp(Convert.ToDouble(p[11..])).ToLocalTime():G}), "; + } + if (logline.EndsWith(", ")) + logline = logline[..^2] + "\n"; + } + else + logline += $"No Previous Owners\n"; + var previousWielders = GetProperty(PropertyString.PreviousWielders) ?? ""; + var prevWielders = previousWielders.Split(";", StringSplitOptions.RemoveEmptyEntries); + if (prevWielders.Length > 0) + { + logline += "Previous Wielders: "; + foreach (var p in prevWielders) + { + var po = PlayerManager.FindByGuid(new ObjectGuid(Convert.ToUInt32(p[0..10], 16))); + if (po != null) + logline += $"{po.Name} (0x{po.Guid}) ({Time.GetDateTimeFromTimestamp(Convert.ToDouble(p[11..])).ToLocalTime():G}), "; + else + logline += $"{p[0..10]} ({Time.GetDateTimeFromTimestamp(Convert.ToDouble(p[11..])).ToLocalTime():G}), "; + } + if (logline.EndsWith(", ")) + logline = logline[..^2] + "\n"; + } + else + logline += $"No Previous Wielders\n"; + if (PropertyManager.GetBool("record_destroy_stacktrace").Item) + { + var loglineStackTrace = System.Environment.StackTrace; + //logline += $"StackTrace: {loglineStackTrace}"; + + SetProperty(PropertyString.DestroyStackLog, loglineStackTrace); + } + log.Debug(logline); + SaveBiotaToDatabase(); + } } public void FadeOutAndDestroy(bool raiseNotifyOfDestructionEvent = true)