smeargle/Smeargle.cs

371 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using Tommy;
namespace Smeargle {
class Game {
public string Name { get; }
public Dictionary<string, Font> Fonts { get; }
public List<Script> Scripts { get; }
public Game(TomlTable config) {
Name = config["name"];
Fonts = new Dictionary<string, Font>();
Scripts = new List<Script>();
foreach (string key in config["font"].Keys) {
TomlTable font_table;
Font font;
using (StreamReader reader = new StreamReader(File.OpenRead(config["font"][key]))) {
try {
font_table = TOML.Parse(reader);
font = new Font(font_table);
Fonts[font.Name] = font;
} catch (TomlParseException ex) {
Console.WriteLine($"Error parsing {config["font"][key]}:");
foreach (TomlSyntaxException synex in ex.SyntaxErrors) {
Console.WriteLine($"{synex.Line}:{synex.Column}: {synex.Message}");
}
System.Environment.Exit(1);
}
}
}
foreach (string key in config["script"].Keys) {
Script script = new Script(config["script"][key].AsTable, this);
Scripts.Add(script);
}
}
public void RenderScript(Script script, bool output=true) {
List<Line> lines;
if (output) { Console.WriteLine("Rendering text..."); }
lines = script.RenderLines();
if (output) { Console.WriteLine("Text rendered."); }
if (output) { Console.Write("Generating tilemap..."); }
Tilemap tilemap = script.GenerateTilemap(lines);
if (output) { Console.WriteLine($"{tilemap.Tiles} tiles generated, {tilemap.UniqueTiles} unique."); }
if (output) { Console.WriteLine("Writing compressed tiles..."); }
script.RenderTilesToFile(tilemap.Compressed, script.DedupeFilename);
if (output) { Console.WriteLine("Writing raw tiles..."); }
script.RenderTilesToFile(tilemap.Raw, script.RawFilename);
if (output) { Console.WriteLine("Writing map indices..."); }
using (StreamWriter writer = new StreamWriter(File.OpenWrite(script.TilemapFilename))) {
foreach (Tuple<Line, string> line in tilemap.Indices) {
if (script.OutputFormat == "thingy") {
writer.WriteLine($"{line.Item2}={line.Item1.Text}");
}
else {
writer.WriteLine($"{line.Item1.Text} = {line.Item2}");
}
}
}
if (output) {
Console.WriteLine($"\nRaw tiles: {script.RawFilename}");
Console.WriteLine($"Compressed tiles: {script.DedupeFilename}");
Console.WriteLine($"Tile<->text: {script.TilemapFilename}");
}
}
}
class Font {
public string Name { get; }
private string Filename;
public int Bits { get; }
public int Width { get; }
public int Height { get; }
public ColorPalette Palette { get; }
public Dictionary<string, Tuple<int, int>> Map { get; }
private Bitmap Image;
public Font(TomlTable config) {
Name = config["name"];
Filename = config["filename"];
Bits = config["bits_per_pixel"].AsInteger;
Width = config["width"].AsInteger;
Height = config["height"].AsInteger;
Map = new Dictionary<string, Tuple<int, int>>();
foreach (string key in config["map"].Keys) {
int width = config["map"][key][1].AsInteger;
int index = config["map"][key][0].AsInteger;
Tuple<int, int> data = new Tuple<int, int>(index, width);
Map[key] = data;
}
Image = new Bitmap(Filename);
if (Image.PixelFormat == PixelFormat.Indexed) {
Palette = Image.Palette;
} else {
Console.WriteLine($"WARNING: {Filename} is not color-indexed; output will be unpredictable.");
}
}
public Bitmap Index(int index) {
int tile_width = Image.Width / Width;
int row = (index - 1) / tile_width;
int col = (index - 1) % tile_width;
int x = col * Width;
int y = row * Height;
if ((x > Image.Width) || (y > Image.Height)) {
throw new IndexOutOfRangeException("Index was outside the bounds of the font.");
}
return Image.Clone(new Rectangle(x, y, Width, Height), PixelFormat.DontCare);
}
public int Length(string text) {
int ret = 0;
foreach (char character in text) {
ret += Map[character.ToString()].Item2;
}
return ret;
}
}
struct Line {
public string Text { get; }
public Bitmap Image { get; }
public int Length { get; }
public Line(string text, Bitmap image, int length) {
Text = text;
Image = image;
Length = length;
}
}
struct Tilemap {
public Dictionary<Color[], Bitmap> Map;
public List<Bitmap> Raw;
public List<Bitmap> Compressed;
public Dictionary<Color[], string> MapIndices;
public List<Tuple<Line,string>> Indices;
public int Tiles;
public int UniqueTiles;
public Tilemap(Dictionary<Color[], Bitmap> tilemap, List<Bitmap> raw, List<Bitmap> compressed, Dictionary<Color[], string> map_indices, List<Tuple<Line, string>> indices, int tiles, int unique) {
Map = tilemap;
Raw = raw;
Compressed = compressed;
MapIndices = map_indices;
Indices = indices;
Tiles = tiles;
UniqueTiles = unique;
}
}
class Script {
private string Filename;
private Font Font;
private int MinimumTilesPerLine;
private int MaximumTilesPerLine;
public string OutputFormat { get; }
private bool LeadingZeroes;
private int TileOffset;
private bool LittleEndian;
public string RawFilename { get; }
public string DedupeFilename { get; }
public string TilemapFilename { get; }
private string[] Text;
private int GetOrDefault(TomlTable table, string key, int def) {
if (table.Keys.Contains(key)) {
return table[key];
}
return def;
}
private string GetOrDefault(TomlTable table, string key, string def) {
if (table.Keys.Contains(key)) {
return table[key];
}
return def;
}
private bool GetOrDefault(TomlTable table, string key, bool def) {
if (table.Keys.Contains(key)) {
return table[key];
}
return def;
}
public Script(TomlTable config, Game game) {
Filename = config["filename"];
Font = game.Fonts[config["font"]];
MinimumTilesPerLine = GetOrDefault(config, "min_tiles_per_line", 0);
MaximumTilesPerLine = GetOrDefault(config, "max_tiles_per_line", 0);
OutputFormat = GetOrDefault(config, "tilemap_format", null);
LeadingZeroes = GetOrDefault(config, "leading_zeroes", false);
TileOffset = GetOrDefault(config, "tile_offset", 0);
LittleEndian = GetOrDefault(config, "little_endian", false);
RawFilename = GetOrDefault(config, "raw_filename", $"{Filename} Data/raw.png");
DedupeFilename = GetOrDefault(config, "dedupe_filename", $"{Filename} Data/dedupe.png");
TilemapFilename = GetOrDefault(config, "tilemap_filename", $"{Filename} Data/tilemap.txt");
using (StreamReader reader = new StreamReader(File.OpenRead(Filename))) {
List<string> input = new List<string>();
while (reader.Peek() >= 0) {
input.Add(reader.ReadLine());
}
Text = input.ToArray();
}
}
public List<Line> RenderLines() {
Dictionary<string, Tuple<int, int>> map = this.Font.Map;
int max_tiles = MaximumTilesPerLine * this.Font.Width;
int min_tiles = MinimumTilesPerLine * this.Font.Width;
List<Line> lines = new List<Line>();
foreach (string line in Text) {
if (line.Length < 1) {
continue;
}
int length = (int)Math.Ceiling((double)Font.Length(line) / Font.Width) * Font.Width;
int position = 0;
if ((max_tiles > 0) && (max_tiles < length)) {
Console.WriteLine($"WARNING: '{line}' exceeds {MaximumTilesPerLine} tiles by {length - max_tiles}px; truncating.");
length = max_tiles;
}
if ((min_tiles > 0) && (length < min_tiles)) {
Console.WriteLine($"INFO: '{line}' is shorter than {min_tiles} tiles by {min_tiles - length}px.");
length = min_tiles;
}
Bitmap image = new Bitmap(length, Font.Height);
Graphics painter = Graphics.FromImage(image);
painter.Clear(Font.Index(Font.Map[" "].Item1).GetPixel(0, 0));
foreach (char character in line) {
Tuple<int,int> data = Font.Map[character.ToString()];
int width = data.Item2;
if (((position + width) > max_tiles) && (max_tiles > 0)) {
break;
}
painter.DrawImage(Font.Index(data.Item1), position, 0);
position += width;
}
lines.Add(new Line(line, image, length));
}
return lines;
}
public Tilemap GenerateTilemap(List<Line> lines) {
Dictionary<Color[], Bitmap> tilemap = new Dictionary<Color[], Bitmap>();
List<Bitmap> raw = new List<Bitmap>();
List<Bitmap> compressed = new List<Bitmap>();
Dictionary<Color[], string> map_indices = new Dictionary<Color[], string>();
int unique = 0, total = 0;
List<Tuple<Line, string>> indices = new List<Tuple<Line, string>>();
foreach (Line line in lines) {
List<string> tile_index = new List<string>();
int tiles = line.Length / Font.Width;
int column = 0;
while (tiles > 0) {
Bitmap tile = line.Image.Clone(new Rectangle(column, 0, Font.Width, Font.Height), PixelFormat.Indexed);
Color[] hash = new Color[Font.Width * Font.Height];
foreach (int y in Enumerable.Range(0, Font.Height)) {
foreach (int x in Enumerable.Range(0, Font.Width)) {
hash[y * Font.Height + x] = tile.GetPixel(x, y);
}
}
if (!tilemap.Keys.Contains(hash)) {
unique++;
tilemap[hash] = tile;
compressed.Add(tile);
int index = unique + TileOffset;
int upper = index / 256;
int lower = index % 256;
if ((upper > 0) || LeadingZeroes) {
if (LittleEndian) {
int temp = upper;
upper = lower;
lower = temp;
}
if (OutputFormat.Equals("atlas")) {
map_indices[hash] = $"<${upper,2:x}><${lower,2:x}>";
} else if (OutputFormat.Equals("thingy")) {
map_indices[hash] = $"{upper,2:x}{lower,2:x}";
} else {
map_indices[hash] = $"0x{upper,2:x}{lower,2:x}";
}
} else {
if (OutputFormat.Equals("atlas")) {
map_indices[hash] = $"<${lower,2:x}>";
} else if (OutputFormat.Equals("thingy")) {
map_indices[hash] = $"{lower,2:x}";
} else {
map_indices[hash] = $"0x{lower,2:x}";
}
}
}
raw.Add(tile);
tile_index.Add(map_indices[hash]);
total++;
column += Font.Width;
tiles--;
}
if (OutputFormat == null) {
indices.Add(new Tuple<Line, string>(line, String.Join(" ", tile_index)));
} else{
indices.Add(new Tuple<Line, string>(line, String.Join("", tile_index)));
}
}
return new Tilemap(tilemap, raw, compressed, map_indices, indices, total, unique);
}
public Bitmap RenderTiles(List<Bitmap> tiles) {
Bitmap image = new Bitmap(Font.Width * 16, (int)Math.Ceiling(tiles.Count / 16.0) * Font.Height);
Graphics painter = Graphics.FromImage(image);
painter.Clear(Font.Index(Font.Map[" "].Item1).GetPixel(0, 0));
int row = 0, column = 0;
foreach (Bitmap tile in tiles) {
painter.DrawImage(tile, column, row);
if (column < (Font.Width * 15)) {
column += Font.Width;
} else {
column = 0;
row += Font.Height;
}
}
return image;
}
public void RenderTilesToFile(List<Bitmap> tiles, string filename) {
RenderTiles(tiles).Save(filename);
}
}
class Smeargle {
static int Main(string[] args) {
if (args.Length != 1) {
Console.WriteLine("Usage: Smeargle.exe game.toml");
return 1;
}
TomlTable table = null;
Game game;
using (StreamReader reader = new StreamReader(File.OpenRead(args[0]))) {
try {
table = TOML.Parse(reader);
} catch (TomlParseException ex) {
Console.WriteLine($"Error parsing {args[0]}:");
foreach (TomlSyntaxException synex in ex.SyntaxErrors) {
Console.WriteLine($"{synex.Line}:{synex.Column}: {synex.Message}");
}
return 1;
}
}
game = new Game(table);
foreach (Script script in game.Scripts) {
game.RenderScript(script);
}
return 0;
}
}
}