Files
TeslaSuite/Console/TeslaConsole/PlasmaBitmaps.cs
T
CydandClaude Opus 5 2a5a387381 Console: remember Enable Custom Bitmaps, ship the art; bump suite to 4.11.4.5
Settings -> Enable Custom Bitmaps was a static bool with no backing store:
it defaulted to off on every launch, so an operator had to re-tick it each
session and any custom plasma art was silently ignored until they did. New
ConsoleSettings persists machine-level menu toggles as XML in
%ProgramData%\Tesla Console\console.settings, alongside RPDefaults.rpd /
BTDefaults.btd / local.siteconfig. It is loaded once from Main and never
from a static initializer: the differential suite drives PlasmaBitmaps
directly and must keep seeing the original defaults rather than whatever
this machine has saved. A missing file is the first-run case; a corrupt one
is ignored and rewritten by the next toggle, because losing a menu setting
must never stop the console starting.

Custom art is now version-controlled and rolls with the release. New
Console\Plasma Images\ is copied into the package, and the lookup searches
%ProgramData%\Tesla Console\Plasma Images first and the exe-relative folder
second, so a release can never clobber a site's own name bitmaps.
install.bat creates the data-dir folder before the icacls grant so an
unelevated operator can write it. Ships with three 128x32 name bitmaps
(Deadmeat, Muerte, Phrogg); Muerte arrived as "Muerte_128x32-2.bmp", a name
the lookup can never build, so it is renamed to match its pilot.

Two fixes fell out of making the flag sticky. Path.Combine ran on the raw
participant name outside the try block, so with the option on a pilot named
"A:B" threw ArgumentException straight out of egg generation; names that
cannot be a Windows file name now just render procedurally. And the art was
loaded with Image.FromFile, which keeps the bitmap backed by the file and
locked for its whole lifetime — it is copied out through a stream now, so
art can be swapped between missions without restarting the console.

Verified against the built net40 exe: all three shipped bitmaps resolve by
pilot name, ProgramData wins over the shipped copy, a wrong-size file is
ignored, an illegal-character name does not throw, the file is not left
locked, and the settings round-trip and corrupt-file tolerance both hold.
Diff suite 106/106.

Version bumped 4.11.4.4 -> 4.11.4.5 across Console, Launcher, vPOD, the
install/build banners, the diff-suite version assertion and the README's
latest-release pointer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 13:16:11 -05:00

184 lines
5.5 KiB
C#

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
using System.IO;
using System.Text;
using System.Windows.Forms;
namespace TeslaConsole;
public class PlasmaBitmaps
{
public const string DefaultFontFamily = "Microsoft Sans Serif";
private static bool sEnableCustomBitmaps;
public static bool EnableCustomBitmaps
{
get
{
return sEnableCustomBitmaps;
}
set
{
sEnableCustomBitmaps = value;
}
}
public static void GenerateBitmaps(out Bitmap large, out Bitmap small, string str)
{
GenerateBitmaps(out large, out small, "Microsoft Sans Serif", str);
}
public static void GenerateBitmaps(out Bitmap large, out Bitmap small, string fontName, string str)
{
large = GenerateBitmap(128, 32, fontName, str);
small = GenerateBitmap(64, 16, fontName, str);
}
public static Bitmap GenerateBitmap(int width, int height, string str)
{
return GenerateBitmap(width, height, "Microsoft Sans Serif", str);
}
public static Bitmap GenerateBitmap(int width, int height, string fontName, string str)
{
if (sEnableCustomBitmaps)
{
Bitmap bitmap = LoadCustomBitmap(width, height, str);
if (bitmap != null)
{
return bitmap;
}
}
Bitmap bitmap2 = new Bitmap(width, height);
using Graphics graphics = Graphics.FromImage(bitmap2);
graphics.SmoothingMode = SmoothingMode.None;
graphics.CompositingQuality = CompositingQuality.AssumeLinear;
graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
graphics.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit;
graphics.Clear(Color.Black);
int num = 24;
while (true)
{
using Font font = new Font(fontName, num);
SizeF sizeF = graphics.MeasureString(str, font);
if (sizeF.Width <= (float)bitmap2.Width && sizeF.Height <= (float)bitmap2.Height)
{
Rectangle rectangle = new Rectangle(0, 0, bitmap2.Width, bitmap2.Height);
StringFormat stringFormat = new StringFormat();
stringFormat.Alignment = StringAlignment.Center;
stringFormat.LineAlignment = StringAlignment.Center;
graphics.DrawString(str, font, Brushes.White, rectangle, stringFormat);
return bitmap2;
}
num = ((num <= 2) ? (num / 2) : (num - 2));
}
}
/// <summary>
/// The operator-supplied plasma image for this string at this size, or null
/// when there is none (the caller then renders the text procedurally).
///
/// Two folders are searched, in order:
/// 1. %ProgramData%\Tesla Console\Plasma Images - this machine's own art,
/// which survives reinstalls and wins over anything shipped.
/// 2. Plasma Images\ next to TeslaConsole.exe - the set that rolls with
/// the release, so art can be version-controlled and deployed.
/// The file name is "&lt;text&gt;_&lt;width&gt;x&lt;height&gt;.bmp", e.g. Camera_128x32.bmp.
///
/// Nothing here may throw: participant names come from operator input, and a
/// name containing a character that is illegal in a path used to take out
/// egg generation entirely once this option was switched on.
/// </summary>
private static Bitmap LoadCustomBitmap(int width, int height, string str)
{
string fileName = $"{str}_{width}x{height}.bmp";
if (fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
{
return null;
}
string[] searchRoots = new string[2]
{
Program.GetCommonAppDataDirectory(),
Path.GetDirectoryName(Application.ExecutablePath)
};
foreach (string searchRoot in searchRoots)
{
try
{
string path = Path.Combine(Path.Combine(searchRoot, "Plasma Images"), fileName);
if (!File.Exists(path))
{
continue;
}
// Copied out of the file rather than Image.FromFile'd: that keeps the
// bitmap backed by the file for its whole lifetime, which locks the art
// against an operator swapping it while the console is running.
using FileStream stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using Image image = Image.FromStream(stream);
if (image.Width == width && image.Height == height)
{
return new Bitmap(image);
}
}
catch
{
}
}
return null;
}
public static void GenerateStrings(out string large, out string small, string str)
{
GenerateStrings(out large, out small, "Microsoft Sans Serif", str);
}
public static void GenerateStrings(out string large, out string small, string fontName, string str)
{
large = GenerateString(128, 32, fontName, str);
small = GenerateString(64, 16, fontName, str);
}
public static string GenerateString(int width, int height, string str)
{
return GenerateString(width, height, "Microsoft Sans Serif", str);
}
public static string GenerateString(int width, int height, string fontName, string str)
{
using Bitmap bitmap = GenerateBitmap(width, height, fontName, str);
return ConvertBitmap(bitmap);
}
public static string ConvertBitmap(Bitmap bitmap)
{
if (bitmap.Width % 8 != 0 || bitmap.Height % 8 != 0)
{
throw new ArgumentOutOfRangeException("bitmap");
}
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < bitmap.Height; i++)
{
stringBuilder.Append("bitmap=");
for (int j = 0; j < bitmap.Width / 8; j++)
{
byte b = 0;
for (int k = 0; k < 8; k++)
{
if (bitmap.GetPixel(j * 8 + k, i).GetBrightness() >= 0.5f)
{
b = (byte)(b | (byte)(128 >> k));
}
}
stringBuilder.Append(b.ToString("X2"));
}
stringBuilder.Append("\n");
}
stringBuilder.AppendFormat("x={0}\n", bitmap.Width);
stringBuilder.AppendFormat("y={0}\n", bitmap.Height);
return stringBuilder.ToString();
}
}