using MessagePack; using MessagePack.Formatters; using MessagePack.Resolvers; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; 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 /// Inventory store to write to disk public static void SaveToDisk(string filename, ConcurrentDictionary Items) { try { using (var bw = new BinaryWriter(File.Open(filename, FileMode.Create))) { var options = GetSerializerOptions(); var items = Items.Values.ToList(); Logger.Log($"Caching {items.Count} inventory items to {filename}", Helpers.LogLevel.Info); 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 /// Inventory store being populated from restore /// 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; } } }