namespace Aeshnidae.InstancesNoDat;
public class Settings
{
public const string FileName = "Settings.json";
///
/// Off by default, and it should stay off until the routing has been proven with
/// two players in two copies of one dungeon. The patches all decline to act when
/// no copy is open, so a loaded-but-unused mod is stock behaviour - but there is
/// no reason to have it loaded before it has been tested.
///
public bool Enabled { get; set; } = false;
// ---- THE TWO NUMBERS ----------------------------------------------------
//
// A copy is scaled by the enlightenment of whoever opened it, linearly: at 5.0
// percent each, ten enlightenments is +50%, not 1.05^10. Linear because these are
// meant to be readable from the settings file - if you want to know what EL 10
// feels like, it is ten times the number below, and nothing else.
//
// Difficulty moves monster skills, health and damage. Reward moves the experience
// and luminance their corpses pay out. Set them to the same value and a copy is as
// much richer as it is harder, which is the point.
/// Master switch. Off means copies are exactly stock difficulty.
public bool ScaleWithEnlightenment { get; set; } = true;
/// Percent harder, per enlightenment of the copy's owner.
public double DifficultyPercentPerEnlightenment { get; set; } = 5.0;
/// Percent richer, per enlightenment of the copy's owner.
public double RewardPercentPerEnlightenment { get; set; } = 5.0;
///
/// Stop counting enlightenments past this many. 0 is uncapped.
///
/// Worth setting to something while the curve is young. Skills and health scale
/// linearly and forever, and the first time somebody walks in at EL 40 you would
/// rather find out you had a ceiling than that you did not.
///
public int MaxEnlightenmentsCounted { get; set; } = 0;
// Which levers each percent actually moves. All on is the intended shape; these
// exist so a lever that turns out to be wrong can be switched off without
// abandoning the other four.
/// Monster attack and defence skills. The lever players feel as "it hits and dodges more".
public bool ScaleCreatureSkills { get; set; } = true;
/// Monster maximum health. The lever that makes fights longer.
public bool ScaleCreatureHealth { get; set; } = true;
///
/// Monster damage, via each creature's DamageRating.
///
/// Be aware this is the lever your players resist hardest: incoming damage runs
/// through 100 / (100 + their damage reduction rating), and an enlightened player
/// has more of that from the luminance auras they kept. Expect it to land softer
/// than the number suggests.
///
public bool ScaleCreatureDamage { get; set; } = true;
/// Experience from kills in a copy (the creature's XpOverride).
public bool ScaleExperience { get; set; } = true;
///
/// Luminance from kills in a copy (the creature's LuminanceAward).
///
/// The one that matters most on a server with a raised level cap, because
/// experience stops accruing entirely at max level - Player_Xp gates all of it
/// behind `if (Level != maxLevel)` - while luminance does not.
///
public bool ScaleLuminance { get; set; } = true;
// -------------------------------------------------------------------------
///
/// Highest copy number that may be created per landblock. Each copy is a full
/// Landblock with its own physics cells and its own spawns, so this is the memory
/// dial as much as the gameplay one.
///
public int MaxCopiesPerLandblock { get; set; } = 8;
///
/// Restrict copies to landblocks that have a dungeon interior.
///
/// ON, and this is a safety rail rather than a preference. Interiors are
/// self-contained: no adjacency, no terrain seams, no neighbouring landblocks to
/// keep consistent. Copying a surface landblock would need all of that reasoned
/// through, and none of it has been.
///
public bool InteriorsOnly { get; set; } = true;
///
/// Dungeons that hand out copies, and who shares each one.
///
/// This is the whole mechanism, and it needs no portal weenies and no world database
/// changes: name a dungeon here and EVERY route into it - a portal, a recall, a
/// summon, an admin teleport, a login - routes the arriving player to the copy their
/// group owns. Nothing else has to know instances exist, because entry is entry.
///
public List Instanced { get; set; } = new();
///
/// Close a copy once nobody is left in it. Off means copies persist until closed by
/// hand, which is what you want while testing and not what you want in play.
///
/// Note this is emptiness, not departure: an allegiance copy stays open while any
/// member is still inside, which is the behaviour a shared hall needs.
///
public bool CloseWhenEmpty { get; set; } = true;
///
/// How long a copy stays open after the last player leaves. Long enough to survive
/// a death and release, or a slow teleport, so nobody returns to a reset dungeon.
///
/// Worth raising a long way for allegiance scope - a hall that evaporates two
/// minutes after the last member logs off is not a hall.
///
public double EmptyGraceSeconds { get; set; } = 120;
///
/// Dying inside a copy costs no items and no coins.
///
/// ON, and this is a safety rail rather than a generosity. A copy is destroyed once
/// it empties, and its contents are destroyed with it rather than saved - which is
/// correct, because saving them would write instance leftovers against the real
/// landblock's id. But a death corpse IS contents: die, release to your lifestone,
/// and the copy empties and takes your gear with it. Unrecoverable, and through no
/// mistake of the player's.
///
/// Applied to every scope, not only personal. The hazard does not depend on who
/// shares the copy - an allegiance hall is destroyed the same way, just later.
///
///
/// Logs every routing decision: who or what was sent to which copy, and why.
///
/// On by default, because the failures this mod can produce are all invisible ones -
/// a player quietly left on the shared landblock, an object spawned into the wrong
/// copy, a reload that touched nothing. Each looks like "the instance is broken"
/// and none of them logs anything on its own. Noisy is the correct trade while this
/// is young; turn it off once it is boring.
///
public bool LogRouting { get; set; } = true;
public bool NoItemLossInCopies { get; set; } = true;
/// landblock -> its configuration, for the lookups the hot paths do.
public Dictionary InstancedMap()
{
var map = new Dictionary();
foreach (var entry in Instanced ?? new List())
{
if (entry.TryParse(out var lb))
map[lb] = entry;
else
ModManager.Log($"[{Mod.Name}] '{entry.Landblock}' is not a hex landblock id",
ModManager.LogLevel.Warn);
}
return map;
}
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
// Scope reads as "Personal" / "Allegiance" rather than 0 / 2. This file is
// edited by hand far more often than it is written by code.
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
};
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();
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] could not read {FileName}, 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 write {FileName}: {ex.Message}", ModManager.LogLevel.Warn);
}
}
}