namespace Aeshnidae.DevKit;
///
/// Per-mod configuration, read from Settings.json in the deployed mod folder.
///
/// The same dll runs on the live server and on staging; these settings are what make
/// the two behave differently. On live, everything that writes is off. On staging,
/// AllowImport is on and BaseSchema points at the pristine copy of live that
/// refresh-staging.sh leaves behind, which is what /wdiff and /submit compare against.
///
public class Settings
{
public const string FileName = "Settings.json";
// ------------------------------------------------------------------------
// Knobs. Two of these are the whole difference between live and staging.
// ------------------------------------------------------------------------
public bool Enabled { get; set; } = true;
///
/// Whether /wimport may write to this server's world database. FALSE on live, on
/// purpose: live changes go through the change ledger (push-content.sh) so they
/// are snapshotted and recorded. TRUE on staging, where the whole point is to try
/// things. Nothing else in this mod writes to the world database.
///
public bool AllowImport { get; set; } = false;
///
/// Schema holding the untouched copy of the live world database that this server's
/// world was cloned from ("aeshnidae_staging_base" on staging). /wdiff, /lbdiff and
/// /submit compare the world against it, so "what did I change" is answered exactly.
/// Empty on live - there is nothing to compare against, and those commands say so.
///
public string BaseSchema { get; set; } = "";
///
/// Where /submit writes its change file and patch note before posting them to
/// Discord, so a failed upload loses nothing. Empty means
/// <content_folder>/submitted (content_folder is ACE's own property; see
/// /modifystring content_folder).
///
public string SubmitDirectory { get; set; } = "";
///
/// Short label printed in command output and Discord posts so nobody mistakes a
/// staging result for a live one. Empty falls back to the server's WorldName.
///
public string ServerLabel { get; set; } = "";
///
/// Discord webhook for the channel exports and submissions are posted to (#content,
/// the developer channel). A credential: lives only in the deployed Settings.json,
/// which is gitignored, and every status command prints it masked.
///
public string WebhookUrl { get; set; } = "";
/// Name shown on the webhook post. Empty keeps the webhook's own.
public string Username { get; set; } = "Aelrynth DevKit";
///
/// Discord rejects attachments over its limit with a 413. 8 MB is the safe floor
/// for an unboosted server; every weenie ever exported here has been under 200 KB.
///
public int MaxAttachmentBytes { get; set; } = 8 * 1024 * 1024;
/// /wfind stops listing after this many matches; narrow the search instead.
public int MaxFindResults { get; set; } = 40;
///
/// /wdiff and /lbdiff print at most this many changed lines in chat. The full diff
/// always goes into the submitted change file, so the cap only limits chat spam.
///
public int MaxDiffLines { get; set; } = 80;
// ------------------------------------------------------------------------
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
};
public bool HasWebhook =>
!string.IsNullOrWhiteSpace(WebhookUrl) && WebhookUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
public bool HasBase => !string.IsNullOrWhiteSpace(BaseSchema);
public string Label => string.IsNullOrWhiteSpace(ServerLabel)
? (ConfigManager.Config?.Server?.WorldName ?? "server")
: ServerLabel;
/// The webhook with its token hidden, for status output.
public string MaskedWebhook
{
get
{
if (string.IsNullOrWhiteSpace(WebhookUrl)) return "(not set)";
var i = WebhookUrl.LastIndexOf('/');
return i > 0 && WebhookUrl.Length - i > 8 ? WebhookUrl[..(i + 5)] + "…" : "(set)";
}
}
public static Settings Load(string modPath)
{
var path = Path.Combine(modPath, FileName);
try
{
if (File.Exists(path))
return JsonSerializer.Deserialize(File.ReadAllText(path), JsonOptions) ?? new Settings();
var defaults = new Settings();
defaults.Save(modPath);
ModManager.Log($"[{Mod.Name}] wrote default settings to {path} - set WebhookUrl to enable exports; AllowImport and BaseSchema on staging only");
return defaults;
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] could not read {path}, using defaults: {ex.Message}", ModManager.LogLevel.Warn);
return new Settings();
}
}
public void Save(string modPath)
{
try
{
File.WriteAllText(Path.Combine(modPath, FileName), JsonSerializer.Serialize(this, JsonOptions));
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] could not save settings: {ex.Message}", ModManager.LogLevel.Error);
}
}
}