UI/FlagSheet.cs
namespace BlockParty;
/// <summary>
/// Country flags packed into one CSS-sprite sheet (tools/bake.py convert_flag_sheet):
/// sprites/flags_sheet.png + sprites/flags.json (per-flag pixel rects). <see cref="Style"/>
/// reproduces the old loose-png look — `background-size: cover; background-position: center`
/// inside a fixed box — by scaling the whole sheet so the flag covers the box and windowing
/// the box onto it. The cover window is always a sub-rect of the flag, so neighbouring
/// cells can't bleed in.
/// </summary>
public static class FlagSheet
{
private class Manifest
{
public string Sheet { get; set; }
public int[] SheetSize { get; set; }
public Dictionary<string, int[]> Flags { get; set; }
}
private static Manifest _manifest;
private static bool _loadTried;
private static Manifest Load()
{
if ( _manifest is null && !_loadTried )
{
// Don't latch a failed load: the mounted filesystem can lag at boot (same race
// Levels/DailyTemplates guard against).
try
{
if ( FileSystem.Mounted.FileExists( "sprites/flags.json" ) )
{
_manifest = Json.Deserialize<Manifest>( FileSystem.Mounted.ReadAllText( "sprites/flags.json" ) );
_loadTried = true;
}
}
catch ( Exception ex )
{
Log.Warning( $"[BlockParty] flags.json failed to load: {ex.Message}" );
_loadTried = true;
}
}
return _manifest;
}
public static bool Has( string code )
=> !string.IsNullOrEmpty( code ) && Load()?.Flags?.ContainsKey( code ) == true;
/// <summary>English country or organization name for the flag's hover tooltip.</summary>
public static string CountryName( string code )
{
if ( string.IsNullOrWhiteSpace( code ) ) return "";
code = code.ToUpperInvariant();
// These bundled flags are not recognized by RegionInfo on every platform.
if ( code == "EH" ) return "Western Sahara";
if ( code == "EU" ) return "European Union";
if ( code == "UN" ) return "United Nations";
try
{
return new System.Globalization.RegionInfo( code ).EnglishName;
}
catch ( ArgumentException )
{
return code;
}
}
/// <summary>Inline style drawing the flag cover+centered in a boxW x boxH element.
/// Everything is emitted in PERCENTAGES of the box: the engine multiplies pixel
/// background-sizes by ScaleToScreen but leaves pixel background-positions unscaled
/// (ImageRect.Calculate), so px units fall apart under the 1080-reference ScreenPanel
/// scaling — percentages of the panel box are scale-invariant on both.</summary>
public static string Style( string code, float boxW, float boxH )
{
var manifest = Load();
if ( manifest is null || !manifest.Flags.TryGetValue( code, out var rect ) )
return "";
float x = rect[0], y = rect[1], w = rect[2], h = rect[3];
float scale = MathF.Max( boxW / w, boxH / h );
// Window the box onto the scaled sheet so the flag's centre lands on the box centre.
float posX = boxW * 0.5f - (x + w * 0.5f) * scale;
float posY = boxH * 0.5f - (y + h * 0.5f) * scale;
return string.Create( System.Globalization.CultureInfo.InvariantCulture,
$"background-image: url( {manifest.Sheet} ); " +
$"background-size: {manifest.SheetSize[0] * scale / boxW * 100f:0.####}% {manifest.SheetSize[1] * scale / boxH * 100f:0.####}% ; " +
$"background-position: {posX / boxW * 100f:0.####}% {posY / boxH * 100f:0.####}% ;" );
}
}