using ACE.Database.SQLFormatters.World;
namespace Aeshnidae.DevKit;
///
/// Renders a weenie, or a landblock's instances, to SQL the same way ACE's own
/// /export-sql and /createinst do - but into memory, so the bytes can go to Discord,
/// into a submission, or anywhere else, without touching the content folder.
///
/// The writers are built once and kept. WeenieSQLWriter's constructor pulls every
/// weenie name and spell name from the database to label its output with comments,
/// which is the slow part of ACE's exporter too; here it is paid on the first export
/// only. (Ported from Aeshnidae.ContentTools, which this mod supersedes.)
///
internal static class Exporter
{
private static WeenieSQLWriter? _weenieWriter;
private static LandblockInstanceWriter? _landblockWriter;
private static readonly object _lock = new();
/// UTF-8 without a BOM - a BOM in front of "DELETE FROM" is a syntax error to mariadb.
public static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
public sealed record Result(WorldWeenie Weenie, string FileName, byte[] Sql, int Lines);
public sealed record LandblockResult(ushort Landblock, string FileName, byte[] Sql, int Instances, int Lines);
private static WeenieSQLWriter WeenieWriter
{
get
{
if (_weenieWriter is not null) return _weenieWriter;
lock (_lock)
{
_weenieWriter ??= new WeenieSQLWriter
{
WeenieNames = Names.Weenies,
SpellNames = Names.Spells,
TreasureDeath = DatabaseManager.World.GetAllTreasureDeath(),
TreasureWielded = DatabaseManager.World.GetAllTreasureWielded(),
PacketOpCodes = PacketOpCodeNames.Values,
};
}
return _weenieWriter;
}
}
private static LandblockInstanceWriter LandblockWriter
{
get
{
if (_landblockWriter is not null) return _landblockWriter;
lock (_lock) _landblockWriter ??= new LandblockInstanceWriter { WeenieNames = Names.Weenies };
return _landblockWriter;
}
}
/// Accepts a wcid or a class name, exactly as /export-sql does.
public static WorldWeenie? Find(string wcidOrName) =>
uint.TryParse(wcidOrName, out var wcid)
? DatabaseManager.World.GetWeenie(wcid)
: DatabaseManager.World.GetWeenie(wcidOrName);
public static Result? Export(string wcidOrName)
{
var weenie = Find(wcidOrName);
if (weenie is null)
return null;
var writer = WeenieWriter;
using var stream = new MemoryStream();
using (var sw = new StreamWriter(stream, Utf8NoBom, leaveOpen: true))
{
writer.CreateSQLDELETEStatement(weenie, sw);
sw.WriteLine();
writer.CreateSQLINSERTStatement(weenie, sw);
}
var bytes = stream.ToArray();
return new Result(weenie, writer.GetDefaultFileName(weenie), bytes, CountLines(bytes));
}
///
/// The landblock's instances as ACE's own DELETE + INSERT block, read fresh from
/// the database (the cache is dropped first so a /createinst a moment ago is in it).
/// Null when the landblock has no instances at all.
///
public static LandblockResult? ExportLandblock(ushort landblock)
{
DatabaseManager.World.ClearCachedInstancesByLandblock(landblock);
var instances = DatabaseManager.World.GetCachedInstancesByLandblock(landblock);
if (instances is null || instances.Count == 0)
return null;
var writer = LandblockWriter;
using var stream = new MemoryStream();
using (var sw = new StreamWriter(stream, Utf8NoBom, leaveOpen: true))
{
writer.CreateSQLDELETEStatement(instances, sw);
sw.WriteLine();
writer.CreateSQLINSERTStatement(instances, sw);
}
var bytes = stream.ToArray();
return new LandblockResult(landblock, $"{landblock:X4}.sql", bytes, instances.Count, CountLines(bytes));
}
private static int CountLines(byte[] bytes)
{
var lines = 0;
foreach (var b in bytes) if (b == (byte)'\n') lines++;
return lines;
}
/// Drop the cached writers so a reload picks up renamed weenies or spells.
public static void Reset()
{
lock (_lock)
{
_weenieWriter = null;
_landblockWriter = null;
}
}
}