UI/ReplayPreviewCache.cs
using System;
using System.Collections.Generic;
using Sandbox;

namespace BlockParty;

/// <summary>Content-shared static replay overlays. Live panels pin entries; up to 128 unused
/// textures are retained for scrolling back, then disposed in least-recently-used order.</summary>
internal static class ReplayPreviewCache
{
	private const int IdleLimit = 128;
	private static readonly Dictionary<string, Entry> Entries = new( StringComparer.Ordinal );
	private static readonly LinkedList<Entry> Idle = new();

	internal sealed class Entry
	{
		internal string Key;
		internal Texture Texture;
		internal int Users;
		internal LinkedListNode<Entry> IdleNode;
	}

	internal static Entry Acquire( LevelDef level )
	{
		// Use the complete canonical wire representation, NOT ContentHash (which strips cosmetics)
		// or the level id (which can name many editor revisions). Full strings avoid hash collisions.
		string key = level is null ? "null" : EditorLevel.FromLevelDef( level ).ToJsonString();
		if ( !Entries.TryGetValue( key, out var entry ) )
		{
			using var bitmap = WorkshopThumbnail.Render( level, 240, previewOverlay: true );
			entry = new Entry { Key = key, Texture = bitmap.ToTexture() };
			Entries.Add( key, entry );
		}
		if ( entry.IdleNode is not null )
		{
			Idle.Remove( entry.IdleNode );
			entry.IdleNode = null;
		}
		entry.Users++;
		return entry;
	}

	internal static void Release( Entry entry )
	{
		if ( entry is null || --entry.Users != 0 ) return;
		entry.IdleNode = Idle.AddLast( entry );
		while ( Idle.Count > IdleLimit )
		{
			var oldest = Idle.First.Value;
			Idle.RemoveFirst();
			oldest.IdleNode = null;
			Entries.Remove( oldest.Key );
			oldest.Texture?.Dispose();
		}
	}
}