From 2ae331bf057dd06cc097aa31dca0cee436599355 Mon Sep 17 00:00:00 2001 From: nooperation Date: Sat, 24 May 2025 00:46:45 -0400 Subject: [PATCH 1/2] Replacing MemoryPack and BinaryFormatter with MessagePack, which covers a much wider range of .net frameworks Removed all old Serialization tags and methods Potentially Breaking Change: Removed InventoryNode.ParentID - This was an old serlization hack. Use Data.ParentUUID instead --- LibreMetaverse/AppearanceManager.cs | 1 - LibreMetaverse/Formatters/UUIDFormatter.cs | 31 ++ LibreMetaverse/Inventory.cs | 69 ++-- LibreMetaverse/InventoryBase.cs | 444 ++++----------------- LibreMetaverse/InventoryNode.cs | 59 +-- LibreMetaverse/InventoryNodeDictionary.cs | 13 +- LibreMetaverse/LibreMetaverse.csproj | 2 +- LibreMetaverse/LockingDictionary.cs | 8 - LibreMetaverse/Login.cs | 1 - LibreMetaverse/Messages/LindenMessages.cs | 9 - LibreMetaverse/Permissions.cs | 10 +- LibreMetaverse/Rendering/Rendering.cs | 1 - LibreMetaverse/TerrainCompressor.cs | 1 - LibreMetaverse/_Packets_.cs | 1 - 14 files changed, 154 insertions(+), 496 deletions(-) create mode 100644 LibreMetaverse/Formatters/UUIDFormatter.cs diff --git a/LibreMetaverse/AppearanceManager.cs b/LibreMetaverse/AppearanceManager.cs index 006bc4c4..0149a522 100644 --- a/LibreMetaverse/AppearanceManager.cs +++ b/LibreMetaverse/AppearanceManager.cs @@ -127,7 +127,6 @@ namespace OpenMetaverse #endregion Enums - [Serializable] public class AppearanceManagerException : Exception { public AppearanceManagerException(string message) diff --git a/LibreMetaverse/Formatters/UUIDFormatter.cs b/LibreMetaverse/Formatters/UUIDFormatter.cs new file mode 100644 index 00000000..40e8271f --- /dev/null +++ b/LibreMetaverse/Formatters/UUIDFormatter.cs @@ -0,0 +1,31 @@ +using MessagePack; +using MessagePack.Formatters; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Text; + +namespace OpenMetaverse.Formatters +{ + public class UUIDFormatter : IMessagePackFormatter + { + public static readonly UUIDFormatter Instance = new UUIDFormatter(); + + public void Serialize(ref MessagePackWriter writer, UUID value, MessagePackSerializerOptions options) + { + writer.Write(value.GetBytes()); + } + + public UUID Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + var sequence = reader.ReadBytes(); + if (sequence == null) + { + throw new MessagePackSerializationException("Invalid UUID"); + } + + var bytes = sequence.Value.ToArray(); + return new UUID(bytes, 0); + } + } +} diff --git a/LibreMetaverse/Inventory.cs b/LibreMetaverse/Inventory.cs index 8584913c..45be2ae0 100644 --- a/LibreMetaverse/Inventory.cs +++ b/LibreMetaverse/Inventory.cs @@ -25,17 +25,15 @@ * POSSIBILITY OF SUCH DAMAGE. */ +using MessagePack; +using MessagePack.Formatters; +using MessagePack.Resolvers; using System; +using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; -#if NET7_0_OR_GREATER -using MemoryPack; -#else -using System.Runtime.Serialization.Formatters.Binary; -#endif - namespace OpenMetaverse { @@ -43,7 +41,6 @@ namespace OpenMetaverse /// /// Exception class to identify inventory exceptions /// - [Serializable] public class InventoryException : Exception { public InventoryException() { } @@ -357,30 +354,42 @@ namespace OpenMetaverse Items.Clear(); } + private MessagePackSerializerOptions GetSerializerOptions() + { + var resolver = MessagePack.Resolvers.CompositeResolver.Create( + new List() + { + Formatters.UUIDFormatter.Instance, + }, + new List + { + StandardResolver.Instance, + } + ); + + return MessagePackSerializerOptions + .Standard + .WithResolver(resolver); + } + /// /// Saves the current inventory structure to a cache file /// /// Name of the cache file to save to public void SaveToDisk(string filename) { - try - { + try + { using (Stream stream = File.Open(filename, FileMode.Create)) { -#if !NET7_0_OR_GREATER - var bformatter = new BinaryFormatter(); -#endif lock (Items) { Logger.Log($"Caching {Items.Count} inventory items to {filename}", Helpers.LogLevel.Info); - foreach (var kvp in Items) - { -#if NET7_0_OR_GREATER - MemoryPackSerializer.SerializeAsync(stream, kvp.Value); -#else - bformatter.Serialize(stream, kvp.Value); -#endif - } + + var options = GetSerializerOptions(); + var items = Items.Values.ToList(); + + MessagePackSerializer.Serialize(stream, items, options); } } } @@ -408,19 +417,9 @@ namespace OpenMetaverse using (var stream = File.Open(filename, FileMode.Open)) { -#if !NET7_0_OR_GREATER - var bformatter = new BinaryFormatter(); -#endif - while (stream.Position < stream.Length) - { -#if NET7_0_OR_GREATER - var node = MemoryPackSerializer.DeserializeAsync(stream); - cacheNodes.Add(node.Result); -#else - var node = (InventoryNode)bformatter.Deserialize(stream); - cacheNodes.Add(node); -#endif - } + var options = GetSerializerOptions(); + + cacheNodes = MessagePackSerializer.Deserialize>(stream, options); } } catch (Exception e) @@ -441,7 +440,7 @@ namespace OpenMetaverse continue; } - if (cacheNode.ParentID == UUID.Zero) + if (cacheNode.Data.ParentUUID == UUID.Zero) { //We don't need the root nodes "My Inventory" etc as they will already exist for the correct // user of this cache. @@ -474,7 +473,7 @@ namespace OpenMetaverse continue; } - if (!Items.TryGetValue(cacheNode.ParentID, out var serverParentNode)) + if (!Items.TryGetValue(cacheNode.Data.ParentUUID, out var serverParentNode)) { // This item does not have a parent in our known inventory. The folder was probably deleted on the server // and our cache is old diff --git a/LibreMetaverse/InventoryBase.cs b/LibreMetaverse/InventoryBase.cs index 1c901b2d..186862c1 100644 --- a/LibreMetaverse/InventoryBase.cs +++ b/LibreMetaverse/InventoryBase.cs @@ -26,56 +26,51 @@ */ using System; -using System.Runtime.Serialization; using OpenMetaverse.StructuredData; -#if NET7_0_OR_GREATER -using MemoryPack; -#endif +using MessagePack; namespace OpenMetaverse { /// /// Base Class for Inventory Items /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] - [MemoryPackUnion(0, typeof(InventoryFolder))] - [MemoryPackUnion(1, typeof(InventoryItem))] - [MemoryPackUnion(2, typeof(InventoryAnimation))] - [MemoryPackUnion(3, typeof(InventoryAttachment))] - [MemoryPackUnion(4, typeof(InventoryCallingCard))] - [MemoryPackUnion(5, typeof(InventoryCategory))] - [MemoryPackUnion(6, typeof(InventoryGesture))] - [MemoryPackUnion(7, typeof(InventoryLSL))] - [MemoryPackUnion(8, typeof(InventoryLandmark))] - [MemoryPackUnion(9, typeof(InventoryMaterial))] - [MemoryPackUnion(10, typeof(InventoryNotecard))] - [MemoryPackUnion(11, typeof(InventoryObject))] - [MemoryPackUnion(12, typeof(InventorySettings))] - [MemoryPackUnion(13, typeof(InventorySnapshot))] - [MemoryPackUnion(14, typeof(InventorySound))] - [MemoryPackUnion(15, typeof(InventoryTexture))] - [MemoryPackUnion(16, typeof(InventoryWearable))] -#endif - public abstract partial class InventoryBase : ISerializable + [MessagePackObject] + [Union(0, typeof(InventoryFolder))] + [Union(1, typeof(InventoryItem))] + [Union(2, typeof(InventoryAnimation))] + [Union(3, typeof(InventoryAttachment))] + [Union(4, typeof(InventoryCallingCard))] + [Union(5, typeof(InventoryCategory))] + [Union(6, typeof(InventoryGesture))] + [Union(7, typeof(InventoryLSL))] + [Union(8, typeof(InventoryLandmark))] + [Union(9, typeof(InventoryMaterial))] + [Union(10, typeof(InventoryNotecard))] + [Union(11, typeof(InventoryObject))] + [Union(12, typeof(InventorySettings))] + [Union(13, typeof(InventorySnapshot))] + [Union(14, typeof(InventorySound))] + [Union(15, typeof(InventoryTexture))] + [Union(16, typeof(InventoryWearable))] + public abstract partial class InventoryBase { /// of item/folder + [Key("UUID")] public UUID UUID; /// of parent folder + [Key("ParentUUID")] public UUID ParentUUID; /// Name of item/folder + [Key("Name")] public string Name; /// Item/Folder Owners + [Key("OwnerID")] public UUID OwnerID; /// /// Constructor, takes an itemID as a parameter /// /// The of the item -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif protected InventoryBase(UUID UUID) { if (UUID == UUID.Zero) @@ -83,32 +78,6 @@ namespace OpenMetaverse this.UUID = UUID; } - /// - /// Get object data - /// - /// - /// - public virtual void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue("UUID", UUID); - info.AddValue("ParentUUID", ParentUUID); - info.AddValue("Name", Name); - info.AddValue("OwnerID", OwnerID); - } - - /// - /// Inventory base ctor - /// - /// - /// - protected InventoryBase(SerializationInfo info, StreamingContext ctxt) - { - UUID = (UUID)info.GetValue("UUID", typeof(UUID)); - ParentUUID = (UUID)info.GetValue("ParentUUID", typeof(UUID)); - Name = (string)info.GetValue("Name", typeof(string)); - OwnerID = (UUID)info.GetValue("OwnerID", typeof(UUID)); - } - /// /// Generates a number corresponding to the value of the object to support the use of a hash table, /// suitable for use in hashing algorithms and data structures such as a hash table @@ -152,10 +121,7 @@ namespace OpenMetaverse /// /// An Item in Inventory /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventoryItem : InventoryBase { public override string ToString() @@ -163,45 +129,58 @@ namespace OpenMetaverse return $"{AssetType} {AssetUUID} ({InventoryType} {UUID}) '{Name}'/'{Description}' {Permissions}"; } /// of the underlying asset + [Key("AssetUUID")] public UUID AssetUUID; /// Combined of the item + [Key("Permissions")] public Permissions Permissions; /// of the underlying asset + [Key("AssetType")] public AssetType AssetType; /// of the item + [Key("InventoryType")] public InventoryType InventoryType; /// of the creator of the item + [Key("CreatorID")] public UUID CreatorID; /// Description of the item + [Key("Description")] public string Description; /// s the item is owned by + [Key("GroupID")] public UUID GroupID; /// If true, item is owned by a group + [Key("GroupOwned")] public bool GroupOwned; /// Price the item can be purchased for + [Key("SalePrice")] public int SalePrice; /// of the item + [Key("SaleType")] public SaleType SaleType; /// Combined flags from + [Key("Flags")] public uint Flags; + /// Time and date the inventory item was created, stored as /// UTC (Coordinated Universal Time) + [Key("CreationDate")] public DateTime CreationDate; /// Used to update the AssetID in requests sent to the server + [Key("TransactionID")] public UUID TransactionID; /// of the previous owner of the item + [Key("LastOwnerID")] public UUID LastOwnerID; /// inventoryID that this item points to, else this item's inventoryID + [IgnoreMember] public UUID ActualUUID => IsLink() ? AssetUUID : UUID; /// /// Construct a new InventoryItem object /// /// The of the item -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryItem(UUID UUID) : base(UUID) { } @@ -221,53 +200,6 @@ namespace OpenMetaverse return AssetType == AssetType.Link || AssetType == AssetType.LinkFolder; } - /// - /// - /// Get object data - /// - /// - /// - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - info.AddValue("AssetUUID", AssetUUID, typeof(UUID)); - info.AddValue("Permissions", Permissions, typeof(Permissions)); - info.AddValue("AssetType", AssetType); - info.AddValue("InventoryType", InventoryType); - info.AddValue("CreatorID", CreatorID); - info.AddValue("Description", Description); - info.AddValue("GroupID", GroupID); - info.AddValue("GroupOwned", GroupOwned); - info.AddValue("SalePrice", SalePrice); - info.AddValue("SaleType", SaleType); - info.AddValue("Flags", Flags); - info.AddValue("CreationDate", CreationDate); - info.AddValue("LastOwnerID", LastOwnerID); - } - - /// - /// Inventory item ctor - /// - /// - /// - public InventoryItem(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - AssetUUID = (UUID)info.GetValue("AssetUUID", typeof(UUID)); - Permissions = (Permissions)info.GetValue("Permissions", typeof(Permissions)); - AssetType = (AssetType)info.GetValue("AssetType", typeof(AssetType)); - InventoryType = (InventoryType)info.GetValue("InventoryType", typeof(InventoryType)); - CreatorID = (UUID)info.GetValue("CreatorID", typeof(UUID)); - Description = (string)info.GetValue("Description", typeof(string)); - GroupID = (UUID)info.GetValue("GroupID", typeof(UUID)); - GroupOwned = (bool)info.GetValue("GroupOwned", typeof(bool)); - SalePrice = (int)info.GetValue("SalePrice", typeof(int)); - SaleType = (SaleType)info.GetValue("SaleType", typeof(SaleType)); - Flags = (uint)info.GetValue("Flags", typeof(uint)); - CreationDate = (DateTime)info.GetValue("CreationDate", typeof(DateTime)); - LastOwnerID = (UUID)info.GetValue("LastOwnerID", typeof(UUID)); - } - /// /// Generates a number corresponding to the value of the object to support the use of a hash table. /// Suitable for use in hashing algorithms and data structures such as a hash table @@ -508,10 +440,7 @@ namespace OpenMetaverse /// InventoryTexture Class representing a graphical image /// /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventoryTexture : InventoryItem { /// @@ -519,33 +448,18 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryTexture(UUID UUID) : base(UUID) { InventoryType = InventoryType.Texture; } - - /// - /// Construct an InventoryTexture object from a serialization stream - /// - public InventoryTexture(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - InventoryType = InventoryType.Texture; - } } /// /// /// InventorySound Class representing a playable sound /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventorySound : InventoryItem { /// @@ -553,33 +467,18 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventorySound(UUID UUID) : base(UUID) { InventoryType = InventoryType.Sound; } - - /// - /// Construct an InventorySound object from a serialization stream - /// - public InventorySound(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - InventoryType = InventoryType.Sound; - } } /// /// /// InventoryCallingCard Class, contains information on another avatar /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventoryCallingCard : InventoryItem { /// @@ -587,33 +486,18 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryCallingCard(UUID UUID) : base(UUID) { InventoryType = InventoryType.CallingCard; } - - /// - /// Construct an InventoryCallingCard object from a serialization stream - /// - public InventoryCallingCard(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - InventoryType = InventoryType.CallingCard; - } } /// /// /// InventoryLandmark Class, contains details on a specific location /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventoryLandmark : InventoryItem { /// @@ -621,28 +505,16 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryLandmark(UUID UUID) : base(UUID) { InventoryType = InventoryType.Landmark; } - /// - /// Construct an InventoryLandmark object from a serialization stream - /// - public InventoryLandmark(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - InventoryType = InventoryType.Landmark; - - } - /// /// Landmarks use the InventoryItemFlags struct and will have a flag of 1 set if they have been visited /// + [IgnoreMember] public bool LandmarkVisited { get => (Flags & 1) != 0; @@ -658,10 +530,7 @@ namespace OpenMetaverse /// /// InventoryObject Class contains details on a primitive or coalesced set of primitives /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventoryObject : InventoryItem { /// @@ -669,27 +538,16 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryObject(UUID UUID) : base(UUID) { InventoryType = InventoryType.Object; } - /// - /// Construct an InventoryObject object from a serialization stream - /// - public InventoryObject(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - InventoryType = InventoryType.Object; - } - /// /// Gets or sets the upper byte of the Flags value /// + [IgnoreMember] public InventoryItemFlags ItemFlags { get => (InventoryItemFlags)(Flags & ~0xFF); @@ -699,6 +557,7 @@ namespace OpenMetaverse /// /// Gets or sets the object attachment point, the lower byte of the Flags value /// + [IgnoreMember] public AttachmentPoint AttachPoint { get => (AttachmentPoint)(Flags & 0xFF); @@ -710,10 +569,7 @@ namespace OpenMetaverse /// /// InventoryNotecard Class, contains details on an encoded text document /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventoryNotecard : InventoryItem { /// @@ -721,33 +577,18 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryNotecard(UUID UUID) : base(UUID) { InventoryType = InventoryType.Notecard; } - - /// - /// Construct an InventoryNotecard object from a serialization stream - /// - public InventoryNotecard(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - InventoryType = InventoryType.Notecard; - } } /// /// /// InventoryCategory Class /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventoryCategory : InventoryItem { /// @@ -755,33 +596,18 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryCategory(UUID UUID) : base(UUID) { InventoryType = InventoryType.Category; } - - /// - /// Construct an InventoryCategory object from a serialization stream - /// - public InventoryCategory(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - InventoryType = InventoryType.Category; - } } /// /// /// InventoryLSL Class, represents a Linden Scripting Language object /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventoryLSL : InventoryItem { /// @@ -789,33 +615,18 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryLSL(UUID UUID) : base(UUID) { InventoryType = InventoryType.LSL; } - - /// - /// Construct an InventoryLSL object from a serialization stream - /// - public InventoryLSL(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - InventoryType = InventoryType.LSL; - } } /// /// /// InventorySnapshot Class, an image taken with the viewer /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventorySnapshot : InventoryItem { /// @@ -824,32 +635,17 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventorySnapshot(UUID UUID) : base(UUID) { InventoryType = InventoryType.Snapshot; } - - /// - /// Construct an InventorySnapshot object from a serialization stream - /// - public InventorySnapshot(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - InventoryType = InventoryType.Snapshot; - } } /// /// InventoryAttachment Class, contains details on an attachable object /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventoryAttachment : InventoryItem { /// @@ -857,27 +653,16 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryAttachment(UUID UUID) : base(UUID) { InventoryType = InventoryType.Attachment; } - /// - /// Construct an InventoryAttachment object from a serialization stream - /// - public InventoryAttachment(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - InventoryType = InventoryType.Attachment; - } - /// /// Get the last AttachmentPoint this object was attached to /// + [IgnoreMember] public AttachmentPoint AttachmentPoint { get => (AttachmentPoint)Flags; @@ -889,10 +674,7 @@ namespace OpenMetaverse /// /// InventoryWearable Class, details on a clothing item or body part /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventoryWearable : InventoryItem { /// @@ -900,23 +682,12 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryWearable(UUID UUID) : base(UUID) { InventoryType = InventoryType.Wearable; } - /// - /// Construct an InventoryWearable object from a serialization stream - /// - public InventoryWearable(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - InventoryType = InventoryType.Wearable; - } - /// /// The , Skin, Shape, Skirt, Etc /// + [IgnoreMember] public WearableType WearableType { get => (WearableType)Flags; @@ -928,10 +699,7 @@ namespace OpenMetaverse /// /// InventoryAnimation Class, A bvh encoded object which animates an avatar /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventoryAnimation : InventoryItem { /// @@ -939,33 +707,18 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryAnimation(UUID UUID) : base(UUID) { InventoryType = InventoryType.Animation; } - - /// - /// Construct an InventoryAnimation object from a serialization stream - /// - public InventoryAnimation(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - InventoryType = InventoryType.Animation; - } } /// /// /// InventoryGesture Class, details on a series of animations, sounds, and actions /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventoryGesture : InventoryItem { /// @@ -973,33 +726,18 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryGesture(UUID UUID) : base(UUID) { InventoryType = InventoryType.Gesture; } - - /// - /// Construct an InventoryGesture object from a serialization stream - /// - public InventoryGesture(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - InventoryType = InventoryType.Gesture; - } } /// /// /// InventorySettings, LLSD settings blob as an asset /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventorySettings : InventoryItem { /// @@ -1007,28 +745,17 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventorySettings(UUID UUID) : base(UUID) { InventoryType = InventoryType.Settings; } - - public InventorySettings(SerializationInfo info, StreamingContext ctxt) : base(info, ctxt) - { - InventoryType = InventoryType.Settings; - } } - + /// /// /// InventoryMaterial, material as an asset /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventoryMaterial : InventoryItem { /// @@ -1036,47 +763,38 @@ namespace OpenMetaverse /// /// A which becomes the /// objects UUID -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryMaterial(UUID UUID) : base(UUID) { InventoryType = InventoryType.Settings; } - - public InventoryMaterial(SerializationInfo info, StreamingContext ctxt) : base(info, ctxt) - { - InventoryType = InventoryType.Settings; - } } - + /// /// /// A folder contains s and has certain attributes specific /// to itself /// - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif + [MessagePackObject] public partial class InventoryFolder : InventoryBase { public const int VERSION_UNKNOWN = -1; /// The Preferred for a folder. + [Key("PreferredType")] public FolderType PreferredType; + /// The Version of this folder + [Key("Version")] public int Version; + /// Number of child items this folder contains. + [Key("DescendentCount")] public int DescendentCount; /// /// Constructor /// /// UUID of the folder -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryFolder(UUID UUID) : base(UUID) { @@ -1094,30 +812,6 @@ namespace OpenMetaverse return Name; } - /// - /// Get Serialization data for this InventoryFolder object - /// - /// - /// - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - info.AddValue("PreferredType", PreferredType, typeof(FolderType)); - info.AddValue("Version", Version); - info.AddValue("DescendentCount", DescendentCount); - } - - /// - /// Construct an InventoryFolder object from a serialization stream - /// - public InventoryFolder(SerializationInfo info, StreamingContext ctxt) - : base(info, ctxt) - { - PreferredType = (FolderType)info.GetValue("PreferredType", typeof(FolderType)); - Version = (int)info.GetValue("Version", typeof(int)); - DescendentCount = (int)info.GetValue("DescendentCount", typeof(int)); - } - /// /// Return int hash code /// diff --git a/LibreMetaverse/InventoryNode.cs b/LibreMetaverse/InventoryNode.cs index 1e4b8c4f..65e59781 100644 --- a/LibreMetaverse/InventoryNode.cs +++ b/LibreMetaverse/InventoryNode.cs @@ -26,30 +26,21 @@ */ using System; -using System.Runtime.Serialization; -#if NET7_0_OR_GREATER -using MemoryPack; -#endif +using MessagePack; namespace OpenMetaverse { - [Serializable] -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif - public partial class InventoryNode : ISerializable + [MessagePackObject] + public partial class InventoryNode { private InventoryBase data; private InventoryNode parent; - private UUID parentID; //used for deseralization private InventoryNodeDictionary nodes; private bool needsUpdate = true; - [NonSerialized] -#if NET7_0_OR_GREATER - [MemoryPackIgnore] -#endif + private object tag; + [Key("Data")] public InventoryBase Data { get => data; @@ -57,27 +48,21 @@ namespace OpenMetaverse } /// User data -#if NET7_0_OR_GREATER - [MemoryPackIgnore] -#endif + [IgnoreMember] public object Tag { get => tag; set => tag = value; } + [IgnoreMember] public InventoryNode Parent { get => parent; set => parent = value; } - public UUID ParentID - { - get => parentID; - private set => parentID = value; - } - + [IgnoreMember] public InventoryNodeDictionary Nodes { get => nodes ?? (nodes = new InventoryNodeDictionary(this)); @@ -88,12 +73,14 @@ namespace OpenMetaverse /// For inventory folder nodes specifies weather the folder needs to be /// refreshed from the server /// + [IgnoreMember] public bool NeedsUpdate { get => needsUpdate; set => needsUpdate = value; } + [IgnoreMember] public DateTime ModifyTime { get @@ -120,9 +107,6 @@ namespace OpenMetaverse Nodes.Sort(); } -#if NET7_0_OR_GREATER - [MemoryPackConstructor] -#endif public InventoryNode() { } @@ -148,29 +132,6 @@ namespace OpenMetaverse } } - /// - /// Serialization handler for the InventoryNode Class - /// - public void GetObjectData(SerializationInfo info, StreamingContext ctxt) - { - info.AddValue("Parent", parent?.Data.UUID ?? UUID.Zero, typeof(UUID)); - info.AddValue("Type", data.GetType(), typeof(Type)); - data.GetObjectData(info, ctxt); - } - - /// - /// De-serialization handler for the InventoryNode Class - /// - public InventoryNode(SerializationInfo info, StreamingContext ctxt) - { - parentID = (UUID)info.GetValue("Parent", typeof(UUID)); - Type type = (Type)info.GetValue("Type", typeof(Type)); - - // Construct a new inventory object based on the Type stored in Type - System.Reflection.ConstructorInfo ctr = type.GetConstructor(new[] {typeof(SerializationInfo),typeof(StreamingContext)}); - if (ctr != null) data = (InventoryBase)ctr.Invoke(new object[] { info, ctxt }); - } - public override string ToString() { return this.Data == null ? "[Empty Node]" : this.Data.ToString(); diff --git a/LibreMetaverse/InventoryNodeDictionary.cs b/LibreMetaverse/InventoryNodeDictionary.cs index 1d26ff06..0b2e93fe 100644 --- a/LibreMetaverse/InventoryNodeDictionary.cs +++ b/LibreMetaverse/InventoryNodeDictionary.cs @@ -27,23 +27,15 @@ using System; using System.Collections.Generic; -#if NET7_0_OR_GREATER -using MemoryPack; -#endif namespace OpenMetaverse { -#if NET7_0_OR_GREATER - [MemoryPackable] -#endif public partial class InventoryNodeDictionary: IComparer { protected readonly SortedDictionary SDictionary; protected readonly Dictionary Dictionary = new Dictionary(); protected InventoryNode parent; -#if NET7_0_OR_GREATER - [MemoryPackIgnore] -#endif + protected readonly object syncRoot = new object(); public int Compare(UUID x, UUID y) { @@ -91,9 +83,6 @@ namespace OpenMetaverse set => parent = value; } -#if NET7_0_OR_GREATER - [MemoryPackIgnore] -#endif public object SyncRoot => syncRoot; public int Count => Dictionary.Count; diff --git a/LibreMetaverse/LibreMetaverse.csproj b/LibreMetaverse/LibreMetaverse.csproj index 1ccc0951..42823599 100644 --- a/LibreMetaverse/LibreMetaverse.csproj +++ b/LibreMetaverse/LibreMetaverse.csproj @@ -28,7 +28,7 @@ - + diff --git a/LibreMetaverse/LockingDictionary.cs b/LibreMetaverse/LockingDictionary.cs index 06fe04dc..f24e5fb7 100644 --- a/LibreMetaverse/LockingDictionary.cs +++ b/LibreMetaverse/LockingDictionary.cs @@ -29,7 +29,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Runtime.Serialization; namespace OpenMetaverse { @@ -90,13 +89,6 @@ namespace OpenMetaverse } - [Obsolete("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")] - public void GetObjectData(SerializationInfo info, StreamingContext context) - { - lock (Dictionary) - Dictionary.GetObjectData(info, context); - } - public void OnDeserialization(object sender) { lock( Dictionary) diff --git a/LibreMetaverse/Login.cs b/LibreMetaverse/Login.cs index 0f02730a..d2799818 100644 --- a/LibreMetaverse/Login.cs +++ b/LibreMetaverse/Login.cs @@ -1061,7 +1061,6 @@ namespace OpenMetaverse #endregion Structs - [Serializable] public class LoginException : Exception { public LoginException(string message) diff --git a/LibreMetaverse/Messages/LindenMessages.cs b/LibreMetaverse/Messages/LindenMessages.cs index abe094cd..4ba0046e 100644 --- a/LibreMetaverse/Messages/LindenMessages.cs +++ b/LibreMetaverse/Messages/LindenMessages.cs @@ -603,7 +603,6 @@ namespace OpenMetaverse.Messages.Linden /// /// The details of a single parcel in a region, also contains some regionwide globals /// - [Serializable] public class ParcelPropertiesMessage : IMessage { /// Simulator-local ID of this parcel @@ -1086,7 +1085,6 @@ namespace OpenMetaverse.Messages.Linden } /// Base class used for the RemoteParcelRequest message - [Serializable] public abstract class RemoteParcelRequestBlock { public abstract OSDMap Serialize(); @@ -1137,7 +1135,6 @@ namespace OpenMetaverse.Messages.Linden /// A message sent from the simulator to the viewer in response to a /// which will contain parcel information /// - [Serializable] public class RemoteParcelRequestReply : RemoteParcelRequestBlock { /// The grid-wide unique parcel ID @@ -1170,7 +1167,6 @@ namespace OpenMetaverse.Messages.Linden /// A message containing a request for a remote parcel from a viewer, or a response /// from the simulator to that request /// - [Serializable] public class RemoteParcelRequestMessage : IMessage { /// The request or response details block @@ -4102,13 +4098,11 @@ namespace OpenMetaverse.Messages.Linden } } - [Serializable] public class DirLandReplyMessage : IMessage { public UUID AgentID; public UUID QueryID; - [Serializable] public class QueryReply { public int ActualArea; @@ -4739,7 +4733,6 @@ namespace OpenMetaverse.Messages.Linden /// Base class used for the ObjectMedia message - [Serializable] public abstract class ObjectMediaBlock { public abstract OSDMap Serialize(); @@ -4941,7 +4934,6 @@ namespace OpenMetaverse.Messages.Linden /// /// Message for setting or getting per face MediaEntry /// - [Serializable] public class ObjectMediaMessage : IMessage { /// The request or response details block @@ -5489,7 +5481,6 @@ namespace OpenMetaverse.Messages.Linden /// /// Message received when requesting AgentProfile for an avatar /// - [Serializable] public class AgentProfileMessage : IMessage { public class GroupData diff --git a/LibreMetaverse/Permissions.cs b/LibreMetaverse/Permissions.cs index 63eb4ba9..b3ebc3e4 100644 --- a/LibreMetaverse/Permissions.cs +++ b/LibreMetaverse/Permissions.cs @@ -24,8 +24,9 @@ * POSSIBILITY OF SUCH DAMAGE. */ -using System; +using MessagePack; using OpenMetaverse.StructuredData; +using System; namespace OpenMetaverse { @@ -61,13 +62,18 @@ namespace OpenMetaverse All = 0x1F } - [Serializable] + [MessagePackObject] public struct Permissions { + [Key("BaseMask")] public PermissionMask BaseMask; + [Key("EveryoneMask")] public PermissionMask EveryoneMask; + [Key("GroupMask")] public PermissionMask GroupMask; + [Key("NextOwnerMask")] public PermissionMask NextOwnerMask; + [Key("OwnerMask")] public PermissionMask OwnerMask; public Permissions(uint baseMask, uint everyoneMask, uint groupMask, uint nextOwnerMask, uint ownerMask) diff --git a/LibreMetaverse/Rendering/Rendering.cs b/LibreMetaverse/Rendering/Rendering.cs index 2e3a36e5..74c96766 100644 --- a/LibreMetaverse/Rendering/Rendering.cs +++ b/LibreMetaverse/Rendering/Rendering.cs @@ -194,7 +194,6 @@ namespace OpenMetaverse.Rendering #region Exceptions - [Serializable] public class RenderingException : Exception { public RenderingException(string message) diff --git a/LibreMetaverse/TerrainCompressor.cs b/LibreMetaverse/TerrainCompressor.cs index 7677b765..e07dcdcd 100644 --- a/LibreMetaverse/TerrainCompressor.cs +++ b/LibreMetaverse/TerrainCompressor.cs @@ -29,7 +29,6 @@ using OpenMetaverse.Packets; namespace OpenMetaverse { - [Serializable] public class TerrainPatch { #region Enums and Structs diff --git a/LibreMetaverse/_Packets_.cs b/LibreMetaverse/_Packets_.cs index 6eaf8c61..2c51a4af 100644 --- a/LibreMetaverse/_Packets_.cs +++ b/LibreMetaverse/_Packets_.cs @@ -49,7 +49,6 @@ namespace OpenMetaverse.Packets /// /// Thrown when a packet could not be successfully deserialized /// - [Serializable] public class MalformedDataException : ApplicationException { /// From bbb2f235d2332f290baee340ecd9e734aded3c73 Mon Sep 17 00:00:00 2001 From: nooperation Date: Sun, 25 May 2025 23:02:34 -0400 Subject: [PATCH 2/2] Added inventory cache version header to the inventory cache Moved inventory caching logic into its own InventoryCache class. Inventory interface not impacted by this change --- LibreMetaverse/Inventory.cs | 145 +-------------------- LibreMetaverse/InventoryCache.cs | 214 +++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 142 deletions(-) create mode 100644 LibreMetaverse/InventoryCache.cs diff --git a/LibreMetaverse/Inventory.cs b/LibreMetaverse/Inventory.cs index 45be2ae0..9af4ee91 100644 --- a/LibreMetaverse/Inventory.cs +++ b/LibreMetaverse/Inventory.cs @@ -25,14 +25,9 @@ * POSSIBILITY OF SUCH DAMAGE. */ -using MessagePack; -using MessagePack.Formatters; -using MessagePack.Resolvers; using System; -using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; -using System.IO; using System.Linq; namespace OpenMetaverse @@ -354,23 +349,6 @@ namespace OpenMetaverse Items.Clear(); } - private MessagePackSerializerOptions GetSerializerOptions() - { - var resolver = MessagePack.Resolvers.CompositeResolver.Create( - new List() - { - Formatters.UUIDFormatter.Instance, - }, - new List - { - StandardResolver.Instance, - } - ); - - return MessagePackSerializerOptions - .Standard - .WithResolver(resolver); - } /// /// Saves the current inventory structure to a cache file @@ -378,134 +356,17 @@ namespace OpenMetaverse /// Name of the cache file to save to public void SaveToDisk(string filename) { - try - { - using (Stream stream = File.Open(filename, FileMode.Create)) - { - lock (Items) - { - Logger.Log($"Caching {Items.Count} inventory items to {filename}", Helpers.LogLevel.Info); - - var options = GetSerializerOptions(); - var items = Items.Values.ToList(); - - MessagePackSerializer.Serialize(stream, items, options); - } - } - } - catch (Exception e) - { - Logger.Log("Error saving inventory cache to disk", Helpers.LogLevel.Error, e); - } + InventoryCache.SaveToDisk(filename, Items); } /// /// Loads in inventory cache file into the inventory structure. Note only valid to call after login has been successful. /// /// Name of the cache file to load - /// The number of inventory items successfully reconstructed into the inventory node tree + /// The number of inventory items successfully reconstructed into the inventory node tree, or -1 on error public int RestoreFromDisk(string filename) { - var cacheNodes = new List(); - - try - { - if (!File.Exists(filename)) - { - return -1; - } - - using (var stream = File.Open(filename, FileMode.Open)) - { - var options = GetSerializerOptions(); - - cacheNodes = MessagePackSerializer.Deserialize>(stream, options); - } - } - catch (Exception e) - { - Logger.Log("Error accessing inventory cache file", Helpers.LogLevel.Error, e); - return -1; - } - - Logger.Log($"Read {cacheNodes.Count} items from inventory cache file", Helpers.LogLevel.Info); - - var dirtyFolders = new HashSet(); - - // First pass: process InventoryFolders - foreach (var cacheNode in cacheNodes) - { - if(!(cacheNode.Data is InventoryFolder cacheFolder)) - { - continue; - } - - if (cacheNode.Data.ParentUUID == UUID.Zero) - { - //We don't need the root nodes "My Inventory" etc as they will already exist for the correct - // user of this cache. - continue; - } - - if (!Items.TryGetValue(cacheNode.Data.UUID, out var serverNode)) - { - // This is an orphaned folder that no longer exists on the server. - continue; - } - - var serverFolder = (InventoryFolder)serverNode.Data; - serverNode.NeedsUpdate = cacheFolder.Version != serverFolder.Version; - - if (serverNode.NeedsUpdate) - { - Logger.DebugLog($"Inventory Cache/Server version mismatch on {cacheNode.Data.Name} {cacheFolder.Version} vs {serverFolder.Version}"); - dirtyFolders.Add(cacheNode.Data.UUID); - } - } - - // Second pass: process InventoryItems - var itemCount = 0; - foreach (var cacheNode in cacheNodes) - { - if (!(cacheNode.Data is InventoryItem cacheItem)) - { - // Only process InventoryItems - continue; - } - - if (!Items.TryGetValue(cacheNode.Data.ParentUUID, out var serverParentNode)) - { - // This item does not have a parent in our known inventory. The folder was probably deleted on the server - // and our cache is old - continue; - } - - if(dirtyFolders.Contains(serverParentNode.Data.UUID)) - { - // This item belongs to a folder that has been marked as dirty, so it too is dirty and must be skipped - continue; - } - - if(Items.ContainsKey(cacheItem.UUID)) - { - // This item was already added to our Items store, likely added from previous server requests during this session - continue; - } - - if (!Items.TryAdd(cacheItem.UUID, cacheNode)) - { - Logger.Log($"Failed to add cache item node {cacheItem.Name} with parent {serverParentNode.Data.Name}", Helpers.LogLevel.Info); - continue; - } - - // Add this cached InventoryItem node to the parent - cacheNode.Parent = serverParentNode; - serverParentNode.Nodes.Add(cacheItem.UUID, cacheNode); - itemCount++; - } - - Logger.Log($"Reassembled {itemCount} items from inventory cache file", Helpers.LogLevel.Info); - return itemCount; + return InventoryCache.RestoreFromDisk(filename, Items); } #region Operators diff --git a/LibreMetaverse/InventoryCache.cs b/LibreMetaverse/InventoryCache.cs new file mode 100644 index 00000000..8b4fd95f --- /dev/null +++ b/LibreMetaverse/InventoryCache.cs @@ -0,0 +1,214 @@ +using MessagePack; +using MessagePack.Formatters; +using MessagePack.Resolvers; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace OpenMetaverse +{ + internal class InventoryCache + { + private static readonly string InventoryCacheMagic = "INVCACHE"; + private static readonly int InventoryCacheVersion = 1; + + /// + /// Creates MessagePack serializer options for use in inventory cache serializing and deserializing + /// + /// MessagePack serializer options + private static MessagePackSerializerOptions GetSerializerOptions() + { + var resolver = MessagePack.Resolvers.CompositeResolver.Create( + new List() + { + Formatters.UUIDFormatter.Instance, + }, + new List + { + StandardResolver.Instance, + } + ); + + return MessagePackSerializerOptions + .Standard + .WithResolver(resolver); + } + + /// + /// Saves the current inventory structure to a cache file + /// + /// Name of the cache file to save to + public static void SaveToDisk(string filename, ConcurrentDictionary Items) + { + try + { + using (var bw = new BinaryWriter(File.Open(filename, FileMode.Create))) + { + lock (Items) + { + Logger.Log($"Caching {Items.Count} inventory items to {filename}", Helpers.LogLevel.Info); + + var options = GetSerializerOptions(); + var items = Items.Values.ToList(); + + bw.Write(Encoding.ASCII.GetBytes(InventoryCacheMagic)); + bw.Write(InventoryCacheVersion); + MessagePackSerializer.Serialize(bw.BaseStream, items, options); + } + } + } + catch (Exception e) + { + Logger.Log("Error saving inventory cache to disk", Helpers.LogLevel.Error, e); + } + } + + /// + /// Loads in inventory cache file into the inventory structure. Note only valid to call after login has been successful. + /// + /// Name of the cache file to load + /// The number of inventory items successfully reconstructed into the inventory node tree, or -1 on error + public static int RestoreFromDisk(string filename, ConcurrentDictionary Items) + { + List cacheNodes; + + try + { + if (!File.Exists(filename)) + { + return -1; + } + + using (var br = new BinaryReader(File.Open(filename, FileMode.Open))) + { + var options = GetSerializerOptions(); + + if (br.BaseStream.Length < InventoryCacheMagic.Length + sizeof(int)) + { + Logger.Log($"Invalid inventory cache file. Missing header.", Helpers.LogLevel.Warning); + return -1; + } + + var magic = br.ReadBytes(InventoryCacheMagic.Length); + if (Encoding.ASCII.GetString(magic) != InventoryCacheMagic) + { + Logger.Log($"Invalid inventory cache file. Missing magic header.", Helpers.LogLevel.Warning); + return -1; + } + + var version = br.ReadInt32(); + if (version != InventoryCacheVersion) + { + Logger.Log($"Invalid inventory cache file. Expected version {InventoryCacheVersion}, got {version}.", Helpers.LogLevel.Warning); + return -1; + } + + cacheNodes = MessagePackSerializer.Deserialize>(br.BaseStream, options); + if (cacheNodes == null) + { + Logger.Log($"Invalid inventory cache file. Failed to deserialize contents.", Helpers.LogLevel.Warning); + return -1; + } + } + } + catch (Exception e) + { + Logger.Log("Error accessing inventory cache file", Helpers.LogLevel.Error, e); + return -1; + } + + Logger.Log($"Read {cacheNodes.Count} items from inventory cache file", Helpers.LogLevel.Info); + + var dirtyFolders = new HashSet(); + + // First pass: process InventoryFolders + foreach (var cacheNode in cacheNodes) + { + if (!(cacheNode.Data is InventoryFolder cacheFolder)) + { + continue; + } + + if (cacheNode.Data.ParentUUID == UUID.Zero) + { + //We don't need the root nodes "My Inventory" etc as they will already exist for the correct + // user of this cache. + continue; + } + + if (!Items.TryGetValue(cacheNode.Data.UUID, out var serverNode)) + { + // This is an orphaned folder that no longer exists on the server. + continue; + } + + if (!(serverNode.Data is InventoryFolder serverFolder)) + { + Logger.Log($"Cached inventory node folder has a parent that is not an InventoryFolder", Helpers.LogLevel.Warning); + continue; + } + + serverNode.NeedsUpdate = cacheFolder.Version != serverFolder.Version; + + if (serverNode.NeedsUpdate) + { + Logger.DebugLog($"Inventory Cache/Server version mismatch on {cacheNode.Data.Name} {cacheFolder.Version} vs {serverFolder.Version}"); + dirtyFolders.Add(cacheNode.Data.UUID); + } + } + + // Second pass: process InventoryItems + var itemCount = 0; + foreach (var cacheNode in cacheNodes) + { + if (!(cacheNode.Data is InventoryItem cacheItem)) + { + // Only process InventoryItems + continue; + } + + if (!Items.TryGetValue(cacheNode.Data.ParentUUID, out var serverParentNode)) + { + // This item does not have a parent in our known inventory. The folder was probably deleted on the server + // and our cache is old + continue; + } + + if (!(serverParentNode.Data is InventoryFolder serverParentFolder)) + { + Logger.Log($"Cached inventory node item {cacheItem.Name} has a parent {serverParentNode.Data.Name} that is not an InventoryFolder", Helpers.LogLevel.Warning); + continue; + } + + if (dirtyFolders.Contains(serverParentFolder.UUID)) + { + // This item belongs to a folder that has been marked as dirty, so it too is dirty and must be skipped + continue; + } + + if (Items.ContainsKey(cacheItem.UUID)) + { + // This item was already added to our Items store, likely added from previous server requests during this session + continue; + } + + if (!Items.TryAdd(cacheItem.UUID, cacheNode)) + { + Logger.Log($"Failed to add cache item node {cacheItem.Name} with parent {serverParentFolder.Name}", Helpers.LogLevel.Info); + continue; + } + + // Add this cached InventoryItem node to the parent + cacheNode.Parent = serverParentNode; + serverParentNode.Nodes.Add(cacheItem.UUID, cacheNode); + itemCount++; + } + + Logger.Log($"Reassembled {itemCount} items from inventory cache file", Helpers.LogLevel.Info); + return itemCount; + } + } +}