Static sprite catalog for UI, maps ids to embedded or package PNGs, caches created Texture objects, supports warmup and invalidation, and logs missing sprites.
namespace NoChillquarium;
/// <summary>
/// Loads game sprites as <see cref="Texture"/> for UI panels.
/// Runtime uses embedded PNG bytes (reliable); package paths under
/// <c>Assets/sprites/</c> remain for the Sprite Editor.
/// </summary>
public static class SpriteCatalog
{
static readonly Dictionary<string, Texture> _textures = new( StringComparer.OrdinalIgnoreCase );
static readonly Dictionary<string, bool> _failed = new( StringComparer.OrdinalIgnoreCase );
static int _version;
static bool _quietLoad;
static readonly Dictionary<string, string> FishPaths = new( StringComparer.OrdinalIgnoreCase )
{
["goldfish"] = "sprites/fish/goldfish.png",
["neon_tetra"] = "sprites/fish/neon_tetra.png",
["betta"] = "sprites/fish/betta.png",
["clownfish"] = "sprites/fish/clownfish.png",
["blue_tang"] = "sprites/fish/blue_tang.png",
["cocaine_shark"] = "sprites/fish/cocaine_shark.png",
};
static readonly Dictionary<string, string> DecorPaths = new( StringComparer.OrdinalIgnoreCase )
{
["plant_green"] = "sprites/decor/plant_green.png",
["treasure_chest"] = "sprites/decor/treasure_chest.png",
["bubble_wand"] = "sprites/decor/bubble_wand.png",
["forty_oz"] = "sprites/decor/forty_oz.png",
["handcuffs_skeleton"] = "sprites/decor/handcuffs_skeleton.png",
["old_boot"] = "sprites/decor/old_boot.png",
["medicine_bottles"] = "sprites/decor/medicine_bottles.png",
["doge_statue"] = "sprites/decor/doge_statue.png",
["m80_crate"] = "sprites/decor/m80_crate.png",
};
static readonly Dictionary<string, string> UiPaths = new( StringComparer.OrdinalIgnoreCase )
{
["wallet_font"] = "sprites/ui/wallet_font.png",
["inv_food"] = "sprites/ui/inv_food.png",
["inv_m80"] = "sprites/ui/inv_m80.png",
["inv_syringe"] = "sprites/ui/inv_syringe.png",
["inv_meth"] = "sprites/ui/inv_meth.png",
["menu_wallpaper"] = "sprites/ui/menu_wallpaper.png",
["ui_laptop"] = "sprites/ui/ui_laptop.png",
["ui_fish_counter"] = "sprites/ui/ui_fish_counter.png",
};
public static int Version => _version;
public static bool Has( string idOrFile ) => GetTexture( idOrFile ) is not null;
public static Texture GetTexture( string idOrFile )
{
if ( string.IsNullOrWhiteSpace( idOrFile ) )
return null;
var key = NormalizeKey( idOrFile );
if ( _textures.TryGetValue( key, out var cached ) && cached is not null )
return cached;
if ( _failed.ContainsKey( key ) )
return null;
var tex = TryCreateFromEmbedded( key ) ?? TryLoadPackageTexture( key );
if ( tex is not null )
{
_textures[key] = tex;
_version++;
return tex;
}
_failed[key] = true;
_version++;
if ( !_quietLoad )
Log.Warning( $"[NO-CHILLquarium] Sprite FAILED: {key}" );
return null;
}
public static void Warmup()
{
_textures.Clear();
_failed.Clear();
_quietLoad = true;
var total = 0;
var ok = 0;
foreach ( var k in FishPaths.Keys )
{
total++;
if ( GetTexture( k ) is not null )
ok++;
}
foreach ( var k in DecorPaths.Keys )
{
total++;
if ( GetTexture( k ) is not null )
ok++;
}
foreach ( var k in UiPaths.Keys )
{
total++;
if ( GetTexture( k ) is not null )
ok++;
}
_quietLoad = false;
Log.Info( $"[NO-CHILLquarium] {GameInfo.VersionLabel} sprites {ok}/{total}" );
if ( ok < total )
Log.Warning( $"[NO-CHILLquarium] {total - ok} sprite(s) missing — check Assets/ui/sprites embeds." );
}
static string NormalizeKey( string idOrFile )
{
var s = idOrFile.Trim().Replace( '\\', '/' );
if ( s.Contains( '/' ) )
{
var name = s.Split( '/' )[^1];
if ( name.EndsWith( ".png", StringComparison.OrdinalIgnoreCase ) )
name = name[..^4];
if ( name.EndsWith( ".sprite", StringComparison.OrdinalIgnoreCase ) )
name = name[..^7];
return name;
}
if ( s.EndsWith( ".png", StringComparison.OrdinalIgnoreCase ) )
s = s[..^4];
if ( s.EndsWith( ".sprite", StringComparison.OrdinalIgnoreCase ) )
s = s[..^7];
return s;
}
static string ResolvePath( string key )
{
if ( FishPaths.TryGetValue( key, out var fp ) )
return fp;
if ( DecorPaths.TryGetValue( key, out var dp ) )
return dp;
if ( UiPaths.TryGetValue( key, out var up ) )
return up;
return null;
}
static Texture TryLoadPackageTexture( string key )
{
// Prefer embeds; package Texture.Load can spam "failed to load resource"
// when a PNG hasn't been imported yet. Only try quiet fallbacks.
var path = ResolvePath( key );
if ( path is null )
return null;
// Try common package path forms without throwing to the console if possible.
foreach ( var candidate in new[]
{
path,
path.EndsWith( ".png", StringComparison.OrdinalIgnoreCase ) ? path : path + ".png",
"assets/" + path,
"/" + path
} )
{
try
{
var tex = Texture.Load( candidate );
if ( tex is not null )
return tex;
}
catch
{
// ignore — next candidate
}
try
{
#pragma warning disable CS0618
var tex = Texture.Load( FileSystem.Mounted, candidate );
#pragma warning restore CS0618
if ( tex is not null )
return tex;
}
catch
{
// ignore
}
}
return null;
}
static Texture TryCreateFromEmbedded( string key )
{
if ( !SpriteEmbedded.PngBase64.TryGetValue( key, out var b64 ) || string.IsNullOrEmpty( b64 ) )
return null;
byte[] bytes;
try
{
bytes = Convert.FromBase64String( b64 );
}
catch
{
return null;
}
try
{
var bmp = Bitmap.CreateFromBytes( bytes );
if ( bmp is null )
return null;
var size = bmp.Size;
var w = Math.Max( 1, (int)MathF.Round( size.x ) );
var h = Math.Max( 1, (int)MathF.Round( size.y ) );
var tex = Texture.Create( w, h )
.WithName( $"nochill_embed_{key}" )
.Finish();
if ( tex is null )
return null;
tex.Update( bmp );
return tex;
}
catch ( Exception e )
{
if ( !_quietLoad )
Log.Warning( $"[NO-CHILLquarium] Embed texture failed ({key}): {e.Message}" );
return null;
}
}
public static void Invalidate( string key )
{
if ( string.IsNullOrWhiteSpace( key ) )
return;
key = NormalizeKey( key );
_textures.Remove( key );
_failed.Remove( key );
_version++;
}
}