namespace Aeshnidae.Challenge;
///
/// Reading a variant out of Aeshnidae.InstancesNoDat's own Settings.json, for the console
/// test and nothing else.
///
/// Why this exists: InstancesNoDat picks a landblock's configured variant in
/// Variants.Choose, which is reached only from Scaling.Remember, which takes a Player. With
/// nobody logged in there is no way to open a configured copy at all - and the first time
/// challenge-admin open was pointed at the Hub of Awakening it got the stock retail
/// landblock, all 24 retail portals and none of ours, which looked exactly like the new door
/// having failed to place (2026-10-13).
///
/// The file is read straight off disk and handed back as JSON, which Variants.Supply reads
/// with InstancesNoDat's own reader. That keeps the arrangement the filling hook already
/// has: a string crosses the boundary, never a type. Nothing here is used in play - the
/// provider path does not touch it.
///
public static class Variants
{
///
/// The variant JSON for a landblock from InstancesNoDat's Settings.json: the one named,
/// else the entry's FixedVariant, else its first. Null with a reason when there is none.
///
public static string? FromSettingsFile(ushort landblock, string? name, out string why)
{
why = "";
var path = Path.Combine(ModManager.ModPath, "Aeshnidae.InstancesNoDat", "Settings.json");
try
{
if (!File.Exists(path))
{
why = $"there is no {path} to read a variant from.";
return null;
}
using var stream = File.OpenRead(path);
var settings = JsonSerializer.Deserialize(stream, Options);
var entries = (settings?.Instanced ?? new List())
.Concat(settings?.Outdoor ?? new List());
var entry = entries.FirstOrDefault(e => Matches(e.Landblock, landblock));
if (entry is null)
{
why = $"InstancesNoDat has no entry for {landblock:X4}.";
return null;
}
var variants = entry.Variants ?? new List();
if (variants.Count == 0)
{
why = $"InstancesNoDat's {landblock:X4} entry has no variants, so a copy of it opens stock anyway.";
return null;
}
var pick = !string.IsNullOrWhiteSpace(name)
? variants.FirstOrDefault(v => string.Equals(v.Name, name, StringComparison.OrdinalIgnoreCase))
: variants.FirstOrDefault(v => string.Equals(v.Name, entry.FixedVariant, StringComparison.OrdinalIgnoreCase))
?? variants[0];
if (pick is null)
{
why = $"{landblock:X4} has no variant called '{name}'. It has: {string.Join(", ", variants.Select(v => v.Name))}.";
return null;
}
return JsonSerializer.Serialize(pick, new JsonSerializerOptions { WriteIndented = false });
}
catch (Exception ex)
{
why = $"reading {path} failed: {ex.Message}";
return null;
}
}
private static bool Matches(string? text, ushort landblock) =>
ushort.TryParse((text ?? "").Trim().Replace("0x", "", StringComparison.OrdinalIgnoreCase),
System.Globalization.NumberStyles.HexNumber, null, out var lb) && lb == landblock;
private static readonly JsonSerializerOptions Options = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
};
/// Only the two lists this needs; everything else in that file is ignored.
private class InstancesFile
{
public List? Instanced { get; set; }
public List? Outdoor { get; set; }
}
private class InstancedEntry
{
public string? Landblock { get; set; }
public string? FixedVariant { get; set; }
public List? Variants { get; set; }
}
}