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)