namespace Aeshnidae.DevKit; /// /// A raw connection to the world database server, for the one thing ACE's own data /// layer cannot do: read a SECOND schema on the same server (the base copy) and /// compare it to the world. Connection details come from ACE's Config.js at runtime - /// no credentials in the mod, and the same user as the server, so if ACE can read it, /// this can. /// /// Everything here is read-only. The only write path in the mod goes through ACE's /// own importer (see Commands.HandleImport). /// public static class Db { private static string _connectionString = ""; public static bool Ready { get; private set; } /// Name of the schema ACE runs the world from, e.g. aeshnidae_world. public static string WorldSchema { get; private set; } = ""; public static void Initialize() { var cfg = ConfigManager.Config?.MySql?.World ?? throw new InvalidOperationException("world database is not configured"); WorldSchema = cfg.Database; _connectionString = new MySqlConnectionStringBuilder { Server = cfg.Host, Port = cfg.Port, Database = cfg.Database, UserID = cfg.Username, Password = cfg.Password, ConnectionTimeout = 10, }.ConnectionString; using var conn = Open(); Ready = true; } private static MySqlConnection Open() { var conn = new MySqlConnection(_connectionString); conn.Open(); return conn; } /// A row as column name to value; DBNull becomes null. public sealed class Row : Dictionary { public Row() : base(StringComparer.OrdinalIgnoreCase) { } } public static List Query(string sql, params (string name, object? value)[] args) { var rows = new List(); using var conn = Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = sql; foreach (var (name, value) in args) cmd.Parameters.AddWithValue(name, value ?? DBNull.Value); using var reader = cmd.ExecuteReader(); while (reader.Read()) { var row = new Row(); for (var i = 0; i < reader.FieldCount; i++) row[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i); rows.Add(row); } return rows; } public static object? Scalar(string sql, params (string name, object? value)[] args) { using var conn = Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = sql; foreach (var (name, value) in args) cmd.Parameters.AddWithValue(name, value ?? DBNull.Value); var v = cmd.ExecuteScalar(); return v is DBNull ? null : v; } public static bool SchemaExists(string schema) => Scalar("SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = @s", ("@s", schema)) is long n && n > 0; /// Column names of a table, in ordinal order, minus the ones passed to skip. public static List Columns(string schema, string table, params string[] skip) { var rows = Query( "SELECT column_name FROM information_schema.columns WHERE table_schema = @s AND table_name = @t ORDER BY ordinal_position", ("@s", schema), ("@t", table)); var skipSet = new HashSet(skip, StringComparer.OrdinalIgnoreCase); return rows.Select(r => (string)r["column_name"]!).Where(c => !skipSet.Contains(c)).ToList(); } /// /// Schema names are interpolated into SQL (they cannot be parameters), so they are /// checked against the only shape MariaDB identifiers take here. /// public static string SafeIdent(string ident) { if (string.IsNullOrEmpty(ident) || ident.Length > 64 || !ident.All(c => char.IsLetterOrDigit(c) || c == '_')) throw new ArgumentException($"'{ident}' is not a plain identifier"); return ident; } } /// /// Weenie and spell names for labelling output. Pulled once from ACE's own lookups /// (a full scan of the string table, the same one /export-sql pays for) and kept /// until the mod is reloaded. /// public static class Names { private static Dictionary? _weenies; private static Dictionary? _spells; private static readonly object _lock = new(); public static Dictionary Weenies { get { if (_weenies is not null) return _weenies; lock (_lock) _weenies ??= DatabaseManager.World.GetAllWeenieNames(); return _weenies; } } public static Dictionary Spells { get { if (_spells is not null) return _spells; lock (_lock) _spells ??= DatabaseManager.World.GetAllSpellNames(); return _spells; } } public static string Weenie(uint? wcid) => wcid is null ? "" : Weenies.TryGetValue(wcid.Value, out var n) ? n : $"wcid {wcid}"; public static string WeenieRef(uint? wcid) => wcid is null ? "" : $"{wcid} {Weenie(wcid)}"; public static string Spell(int? id) => id is null ? "" : Spells.TryGetValue((uint)id.Value, out var n) ? $"{n} ({id})" : $"spell {id}"; public static void Reset() { lock (_lock) { _weenies = null; _spells = null; } } }