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; } } }