2025-05-25 23:02:34 -04:00
using MessagePack ;
using MessagePack.Formatters ;
using MessagePack.Resolvers ;
using System ;
using System.Collections.Concurrent ;
using System.Collections.Generic ;
2025-06-03 11:21:56 -05:00
using System.Collections.Immutable ;
2025-05-25 23:02:34 -04:00
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 ;
/// <summary>
/// Creates MessagePack serializer options for use in inventory cache serializing and deserializing
/// </summary>
/// <returns>MessagePack serializer options</returns>
private static MessagePackSerializerOptions GetSerializerOptions ( )
{
var resolver = MessagePack . Resolvers . CompositeResolver . Create (
new List < IMessagePackFormatter > ( )
{
Formatters . UUIDFormatter . Instance ,
} ,
new List < IFormatterResolver >
{
StandardResolver . Instance ,
}
) ;
return MessagePackSerializerOptions
. Standard
. WithResolver ( resolver ) ;
}
/// <summary>
/// Saves the current inventory structure to a cache file
/// </summary>
/// <param name="filename">Name of the cache file to save to</param>
2025-06-03 11:21:56 -05:00
/// <param name="Items">Inventory store to write to disk</param>
2025-05-25 23:02:34 -04:00
public static void SaveToDisk ( string filename , ConcurrentDictionary < UUID , InventoryNode > Items )
{
try
{
using ( var bw = new BinaryWriter ( File . Open ( filename , FileMode . Create ) ) )
{
2025-05-31 19:03:27 -04:00
var options = GetSerializerOptions ( ) ;
var items = Items . Values . ToList ( ) ;
2025-05-25 23:02:34 -04:00
2025-05-31 19:03:27 -04:00
Logger . Log ( $"Caching {items.Count} inventory items to {filename}" , Helpers . LogLevel . Info ) ;
2025-05-25 23:02:34 -04:00
2025-05-31 19:03:27 -04:00
bw . Write ( Encoding . ASCII . GetBytes ( InventoryCacheMagic ) ) ;
bw . Write ( InventoryCacheVersion ) ;
MessagePackSerializer . Serialize ( bw . BaseStream , items , options ) ;
2025-05-25 23:02:34 -04:00
}
}
catch ( Exception e )
{
Logger . Log ( "Error saving inventory cache to disk" , Helpers . LogLevel . Error , e ) ;
}
}
/// <summary>
/// Loads in inventory cache file into the inventory structure. Note only valid to call after login has been successful.
/// </summary>
/// <param name="filename">Name of the cache file to load</param>
2025-06-03 11:21:56 -05:00
/// <param name="Items">Inventory store being populated from restore</param>
2025-05-25 23:02:34 -04:00
/// <returns>The number of inventory items successfully reconstructed into the inventory node tree, or -1 on error</returns>
public static int RestoreFromDisk ( string filename , ConcurrentDictionary < UUID , InventoryNode > Items )
{
List < InventoryNode > 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 < List < InventoryNode > > ( 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 < UUID > ( ) ;
// 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 ;
}
}
}