namespace Aeshnidae.Towns; /// /// Allegiance towns (Tom, 2026-09-29: "build me buyable towns" - something allegiances buy and /// build on, never attacked). Read from Settings.json in the deployed folder, written with these /// defaults on first run. Every number is a starting point to tune. /// /// Enabled the switch; off = /town refuses and no town routes anyone (the enclaves are /// taken down), but every charter, treasury and ledger row stays /// CharterPrice pyreals from the allegiance treasury to buy a town /// MinAccounts distinct ACCOUNTS in the allegiance (the monarch and everyone sworn under /// them) before it may buy - accounts, so one person's alts are not an allegiance /// CopyBase a town is copy CopyBase + its id of every landblock in it (InstancesNoDat /// enclaves); above the awakened worlds' range (100000-199999) /// TownCooldownMinutes between two uses of /town go by one character (0 = none); fractions are /// fine - 0.1667 is ten seconds, which is what live runs /// NoMonsters a town copy spawns no monsters (every monster generator held empty) /// Towns the menu: Kryst (an island - the sea is its edge) and every mainland town the /// Town Network links, Holtburg included since 2026-10-09 (a copy no longer /// replaces the real town, so owning the hub no longer hides it). A copy's /// landblocks join each other as one region, so you walk freely between them; /// walk off the edge of the set and you are in the real world, and you stay /// there until you /town go again /// public class Settings { public const string FileName = "Settings.json"; public bool Enabled { get; set; } = true; public long CharterPrice { get; set; } = 10_000_000; public int MinAccounts { get; set; } = 2; public int CopyBase { get; set; } = 200000; public double TownCooldownMinutes { get; set; } = 5; public bool NoMonsters { get; set; } = true; /// Where /town leave puts you: Holtburg square, where the Hub exit and retail's Portal to Holtburg land. public SpotSetting LeaveTo { get; set; } = new() { Cell = "0xA9B40019", X = 84, Y = 7.1f, Z = 94, W = 0.9969f, Qz = -0.0785f }; public List Towns { get; set; } = new() { new() { Key = "kryst", Name = "Kryst", Description = "the island village of Kryst and the Vesayen Isles around it - vendors, a town network portal, and the sea all round", // `town-admin scan E922` (2026-09-29): the land around the village, flood-filled - Kryst and // the Vesayen Isles are one archipelago, so the town is all of it (no walls on dry land). Landblocks = "E424 E51D E51E E523 E524 E61D E621 E622 E623 E624 E71B E71C E71D E71E E71F E720 E721 E722 E723 E724 E81C E81D E81E E81F E820 E821 E822 E91D E91E E920 E921 E922 EA21" .Split(' ').ToList(), Landing = new() { Cell = "0xE822002A", X = 132.7f, Y = 37.9f, Z = 20.1f, W = -0.866f, Qz = -0.5f }, }, // The mainland (Tom, 2026-09-29: "I want mainland towns to be purchasable"): every town the // Town Network links, landing where its portal there lands, the town being the 3x3 of // landblocks around that landing - walk off that 3x3 and you are in the real world. // Generated from the Town Network's portals (landblock 0007). // // Holtburg (2026-10-09) is on the menu now. It was left off while a bought town replaced // the real one for its members, because Holtburg is the server's hub - the Hub of // Awakening, the Trials and island portals, the buff bot, invasions - and an owner would // never have seen the real one again. A copy is opt-in now (`/town go` and nothing else), // so an owner walks the real Holtburg like everyone else and visits their own when they // want it. Its landing is the Town Network's own Holtburg portal (42820), which is also // where /town leave and the Hub's exit put you. new() { Key = "holtburg", Name = "Holtburg", Landblocks = "A8B3 A8B4 A8B5 A9B3 A9B4 A9B5 AAB3 AAB4 AAB5".Split(' ').ToList(), Landing = new() { Cell = "0xA9B40019", X = 84f, Y = 7.1f, Z = 94f, W = 0.9969f, Qz = -0.0785f } }, new() { Key = "ahurenga", Name = "Ahurenga", Landblocks = "0EB8 0EB9 0EBA 0FB8 0FB9 0FBA 10B8 10B9 10BA".Split(' ').ToList(), Landing = new() { Cell = "0x0FB90009", X = 43f, Y = 8.6f, Z = 0.005f, W = -0.9801f, Qz = -0.1985f } }, new() { Key = "al-jalima", Name = "Al-Jalima", Landblocks = "8487 8488 8489 8587 8588 8589 8687 8688 8689".Split(' ').ToList(), Landing = new() { Cell = "0x8588002C", X = 120.4f, Y = 95.47f, Z = 90.05f, W = 1f, Qz = 0f } }, new() { Key = "baishi", Name = "Baishi", Landblocks = "CD40 CD41 CD42 CE40 CE41 CE42 CF40 CF41 CF42".Split(' ').ToList(), Landing = new() { Cell = "0xCE410007", X = 12.6f, Y = 152.8f, Z = 55.1f, W = -0.5446f, Qz = -0.8387f } }, new() { Key = "timaru", Name = "Timaru", Landblocks = "1CB5 1CB6 1CB7 1DB5 1DB6 1DB7 1EB5 1EB6 1EB7".Split(' ').ToList(), Landing = new() { Cell = "0x1DB60016", X = 71.39f, Y = 134.3f, Z = 90f, W = 0.7071f, Qz = -0.7071f } }, new() { Key = "danbys-outpost", Name = "Danby's Outpost", Landblocks = "599B 599C 599D 5A9B 5A9C 5A9D 5B9B 5B9C 5B9D".Split(' ').ToList(), Landing = new() { Cell = "0x5A9C0004", X = 23.5f, Y = 77.1f, Z = 6f, W = 0f, Qz = -1f } }, new() { Key = "eastham", Name = "Eastham", Landblocks = "CD93 CD94 CD95 CE93 CE94 CE95 CF93 CF94 CF95".Split(' ').ToList(), Landing = new() { Cell = "0xCE940035", X = 151.1f, Y = 112.6f, Z = 17.42f, W = -0.9366f, Qz = -0.3505f } }, new() { Key = "fiun-outpost", Name = "Fiun Outpost", Landblocks = "37F6 37F7 37F8 38F6 38F7 38F8 39F6 39F7 39F8".Split(' ').ToList(), Landing = new() { Cell = "0x38F7001B", X = 91f, Y = 54f, Z = -0.095f, W = -0.4848f, Qz = -0.8746f } }, new() { Key = "fort-tethana", Name = "Fort Tethana", Landblocks = "2580 2581 2582 2680 2681 2682 2780 2781 2782".Split(' ').ToList(), Landing = new() { Cell = "0x2681001D", X = 77.7f, Y = 108.1f, Z = 240f, W = -0.5225f, Qz = -0.8526f } }, new() { Key = "freehold", Name = "Freehold", Landblocks = "F123 F124 F125 F223 F224 F225 F323 F324 F325".Split(' ').ToList(), Landing = new() { Cell = "0xF224001A", X = 81.8f, Y = 33f, Z = 0f, W = 0.2411f, Qz = -0.9705f } }, new() { Key = "glenden-wood", Name = "Glenden Wood", Landblocks = "9FA3 9FA4 9FA5 A0A3 A0A4 A0A5 A1A3 A1A4 A1A5".Split(' ').ToList(), Landing = new() { Cell = "0xA0A40025", X = 96.3f, Y = 119.8f, Z = 59.95f, W = 0.7071f, Qz = -0.7071f } }, new() { Key = "lin", Name = "Lin", Landblocks = "DB3B DB3C DB3D DC3B DC3C DC3D DD3B DD3C DD3D".Split(' ').ToList(), Landing = new() { Cell = "0xDC3C0011", X = 59.72f, Y = 10.77f, Z = 18.96f, W = -0.3584f, Qz = -0.9336f } }, new() { Key = "lytelthorpe", Name = "Lytelthorpe", Landblocks = "BF7F BF80 BF81 C07F C080 C081 C17F C180 C181".Split(' ').ToList(), Landing = new() { Cell = "0xC0800007", X = 11.72f, Y = 155.6f, Z = 33.03f, W = -0.4024f, Qz = -0.9155f } }, new() { Key = "mayoi", Name = "Mayoi", Landblocks = "E531 E532 E533 E631 E632 E633 E731 E732 E733".Split(' ').ToList(), Landing = new() { Cell = "0xE6320021", X = 107.4f, Y = 10.76f, Z = 29.91f, W = -0.6428f, Qz = -0.766f } }, new() { Key = "oolutangas-refuge", Name = "Oolutanga's Refuge", Landblocks = "F581 F582 F583 F681 F682 F683 F781 F782 F783".Split(' ').ToList(), Landing = new() { Cell = "0xF6820033", X = 145.7f, Y = 49.85f, Z = 58.01f, W = -0.4675f, Qz = -0.884f } }, new() { Key = "plateau-village", Name = "Plateau Village", Landblocks = "48B6 48B7 48B8 49B6 49B7 49B8 4AB6 4AB7 4AB8".Split(' ').ToList(), Landing = new() { Cell = "0x49B70021", X = 100.1f, Y = 20.8f, Z = 238.6f, W = -0.5878f, Qz = -0.809f } }, new() { Key = "al-arqas", Name = "Al-Arqas", Landblocks = "8E57 8E58 8E59 8F57 8F58 8F59 9057 9058 9059".Split(' ').ToList(), Landing = new() { Cell = "0x8F58003B", X = 183.9f, Y = 60.18f, Z = 9.326f, W = 0.7071f, Qz = -0.7071f } }, new() { Key = "arwic", Name = "Arwic", Landblocks = "C5A8 C5A9 C5AA C6A8 C6A9 C6AA C7A8 C7A9 C7AA".Split(' ').ToList(), Landing = new() { Cell = "0xC6A90009", X = 46.8f, Y = 4.219f, Z = 42.01f, W = 1f, Qz = 0f } }, new() { Key = "bluespire", Name = "Bluespire", Landblocks = "20AF 20B0 20B1 21AF 21B0 21B1 22AF 22B0 22B1".Split(' ').ToList(), Landing = new() { Cell = "0x21B00017", X = 48.19f, Y = 165.9f, Z = 0.005f, W = -0.08362f, Qz = -0.9965f } }, new() { Key = "cragstone", Name = "Cragstone", Landblocks = "BA9E BA9F BAA0 BB9E BB9F BBA0 BC9E BC9F BCA0".Split(' ').ToList(), Landing = new() { Cell = "0xBB9F0040", X = 169.4f, Y = 168.3f, Z = 54.01f, W = 0.5787f, Qz = -0.8156f } }, new() { Key = "dryreach", Name = "Dryreach", Landblocks = "D974 D975 D976 DA74 DA75 DA76 DB74 DB75 DB76".Split(' ').ToList(), Landing = new() { Cell = "0xDA75002B", X = 132f, Y = 60f, Z = 18f, W = 1f, Qz = 0f } }, new() { Key = "eastwatch", Name = "Eastwatch", Landblocks = "48EF 48F0 48F1 49EF 49F0 49F1 4AEF 4AF0 4AF1".Split(' ').ToList(), Landing = new() { Cell = "0x49F00013", X = 70f, Y = 70f, Z = 170f, W = 0.6756f, Qz = -0.7373f } }, new() { Key = "greenspire", Name = "Greenspire", Landblocks = "2AB4 2AB5 2AB6 2BB4 2BB5 2BB6 2CB4 2CB5 2CB6".Split(' ').ToList(), Landing = new() { Cell = "0x2BB5003C", X = 179f, Y = 86.57f, Z = 0.005f, W = 0.3523f, Qz = -0.9359f } }, new() { Key = "hebian-to", Name = "Hebian-To", Landblocks = "E54D E54E E54F E64D E64E E64F E74D E74E E74F".Split(' ').ToList(), Landing = new() { Cell = "0xE64E002F", X = 138.3f, Y = 161.9f, Z = 20.04f, W = 0.9239f, Qz = -0.3827f } }, new() { Key = "kara", Name = "Kara", Landblocks = "B916 B917 B918 BA16 BA17 BA18 BB16 BB17 BB18".Split(' ').ToList(), Landing = new() { Cell = "0xBA170039", X = 181.2f, Y = 3.2f, Z = 167.7f, W = -0.848f, Qz = -0.5299f } }, new() { Key = "khayyaban", Name = "Khayyaban", Landblocks = "9E43 9E44 9E45 9F43 9F44 9F45 A043 A044 A045".Split(' ').ToList(), Landing = new() { Cell = "0x9F440012", X = 90f, Y = 24.55f, Z = 31.89f, W = -0.7826f, Qz = -0.6225f } }, new() { Key = "linvak-tukal", Name = "Linvak Tukal", Landblocks = "A11D A11E A11F A21D A21E A21F A31D A31E A31F".Split(' ').ToList(), Landing = new() { Cell = "0xA21E001A", X = 83f, Y = 38f, Z = 560.4f, W = 1f, Qz = 0f } }, new() { Key = "nanto", Name = "Nanto", Landblocks = "E53D E53E E53F E63D E63E E63F E73D E73E E73F".Split(' ').ToList(), Landing = new() { Cell = "0xE63E0022", X = 96.96f, Y = 37.72f, Z = 74.54f, W = 0f, Qz = -1f } }, new() { Key = "neydisa-castle", Name = "Neydisa Castle", Landblocks = "94D5 94D6 94D7 95D5 95D6 95D7 96D5 96D6 96D7".Split(' ').ToList(), Landing = new() { Cell = "0x95D60033", X = 146.9f, Y = 71.3f, Z = 99.8f, W = -0.7314f, Qz = -0.682f } }, new() { Key = "redspire", Name = "Redspire", Landblocks = "16B1 16B2 16B3 17B1 17B2 17B3 18B1 18B2 18B3".Split(' ').ToList(), Landing = new() { Cell = "0x17B2002A", X = 132.6f, Y = 25.81f, Z = 44.01f, W = 0.9985f, Qz = -0.05506f } }, new() { Key = "rithwic", Name = "Rithwic", Landblocks = "C88B C88C C88D C98B C98C C98D CA8B CA8C CA8D".Split(' ').ToList(), Landing = new() { Cell = "0xC98C0028", X = 113.7f, Y = 190.3f, Z = 22f, W = -0.7071f, Qz = -0.7071f } }, new() { Key = "samsur", Name = "Samsur", Landblocks = "967A 967B 967C 977A 977B 977C 987A 987B 987C".Split(' ').ToList(), Landing = new() { Cell = "0x977B000C", X = 25.81f, Y = 73.85f, Z = 0.005f, W = 0.93f, Qz = -0.3677f } }, new() { Key = "sanamar", Name = "Sanamar", Landblocks = "32D8 32D9 32DA 33D8 33D9 33DA 34D8 34D9 34DA".Split(' ').ToList(), Landing = new() { Cell = "0x33D90015", X = 59.1f, Y = 100.3f, Z = 52.01f, W = 0f, Qz = -1f } }, new() { Key = "shoushi", Name = "Shoushi", Landblocks = "D954 D955 D956 DA54 DA55 DA56 DB54 DB55 DB56".Split(' ').ToList(), Landing = new() { Cell = "0xDA55001D", X = 84.8f, Y = 99f, Z = 20f, W = 1f, Qz = 0f } }, new() { Key = "silyun", Name = "Silyun", Landblocks = "25EB 25EC 25ED 26EB 26EC 26ED 27EB 27EC 27ED".Split(' ').ToList(), Landing = new() { Cell = "0x26EC003D", X = 175.9f, Y = 110.3f, Z = 80f, W = 0.674f, Qz = -0.7387f } }, new() { Key = "tou-tou", Name = "Tou-Tou", Landblocks = "F458 F459 F45A F558 F559 F55A F658 F659 F65A".Split(' ').ToList(), Landing = new() { Cell = "0xF5590034", X = 152.6f, Y = 80.8f, Z = 20f, W = 0.9239f, Qz = -0.3827f } }, new() { Key = "westwatch", Name = "Westwatch", Landblocks = "22D9 22DA 22DB 23D9 23DA 23DB 24D9 24DA 24DB".Split(' ').ToList(), Landing = new() { Cell = "0x23DA002C", X = 140.9f, Y = 86f, Z = 2.005f, W = -0.4067f, Qz = -0.9135f } }, new() { Key = "xarabydun", Name = "Xarabydun", Landblocks = "924A 924B 924C 934A 934B 934C 944A 944B 944C".Split(' ').ToList(), Landing = new() { Cell = "0x934B0021", X = 108.3f, Y = 6.1f, Z = 18.1f, W = -0.9646f, Qz = -0.2639f } }, new() { Key = "yanshi", Name = "Yanshi", Landblocks = "B36E B36F B370 B46E B46F B470 B56E B56F B570".Split(' ').ToList(), Landing = new() { Cell = "0xB46F001E", X = 75.2f, Y = 124.1f, Z = 34.69f, W = 1f, Qz = 0f } }, new() { Key = "yaraq", Name = "Yaraq", Landblocks = "7C63 7C64 7C65 7D63 7D64 7D65 7E63 7E64 7E65".Split(' ').ToList(), Landing = new() { Cell = "0x7D64000D", X = 31.9f, Y = 104.6f, Z = 11.9f, W = 0.5771f, Qz = -0.8166f } }, new() { Key = "zaikhal", Name = "Zaikhal", Landblocks = "7F8F 7F90 7F91 808F 8090 8091 818F 8190 8191".Split(' ').ToList(), Landing = new() { Cell = "0x80900013", X = 64.86f, Y = 55.69f, Z = 124f, W = -0.9299f, Qz = -0.3679f } }, new() { Key = "qalabar", Name = "Qalaba'r", Landblocks = "9621 9622 9623 9721 9722 9723 9821 9822 9823".Split(' ').ToList(), Landing = new() { Cell = "0x9722003A", X = 168.4f, Y = 24.62f, Z = 102f, W = -0.9228f, Qz = -0.3853f } }, new() { Key = "sawato", Name = "Sawato", Landblocks = "C85A C85B C85C C95A C95B C95C CA5A CA5B CA5C".Split(' ').ToList(), Landing = new() { Cell = "0xC95B0001", X = 14.8f, Y = 0.3f, Z = 12f, W = 0.9304f, Qz = -0.3665f } }, new() { Key = "stonehold", Name = "Stonehold", Landblocks = "63D4 63D5 63D6 64D4 64D5 64D6 65D4 65D5 65D6".Split(' ').ToList(), Landing = new() { Cell = "0x64D5000B", X = 30f, Y = 50f, Z = 78f, W = 0.8434f, Qz = -0.5373f } }, new() { Key = "tufa", Name = "Tufa", Landblocks = "866B 866C 866D 876B 876C 876D 886B 886C 886D".Split(' ').ToList(), Landing = new() { Cell = "0x876C0008", X = 2f, Y = 186.9f, Z = 18f, W = -0.7071f, Qz = -0.7071f } }, new() { Key = "uziz", Name = "Uziz", Landblocks = "A15F A160 A161 A25F A260 A261 A35F A360 A361".Split(' ').ToList(), Landing = new() { Cell = "0xA260003C", X = 182.9f, Y = 87.93f, Z = 20f, W = -0.3635f, Qz = -0.9316f } }, }; private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, }; 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}"); 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); } } public TownSetting? Town(string key) => Towns?.FirstOrDefault(t => string.Equals(t.Key, key, StringComparison.OrdinalIgnoreCase) || string.Equals(t.Name, key, StringComparison.OrdinalIgnoreCase)); /// The town on the menu whose landblocks cover this one, or null - what /town answers "where am I" with. public TownSetting? TownAt(ushort landblock) => Towns?.FirstOrDefault(t => t.LandblockIds().Contains(landblock)); } public class TownSetting { /// What /town buy takes and the database records: "kryst". public string Key { get; set; } = ""; public string Name { get; set; } = ""; public string Description { get; set; } = ""; /// Hex landblock ids, the whole island. public List Landblocks { get; set; } = new(); /// Where /town go puts a member: the town's own arrival point. public SpotSetting Landing { get; set; } = new(); public List LandblockIds() => (Landblocks ?? new List()) .Select(t => ushort.TryParse((t ?? "").Trim().Replace("0x", "", StringComparison.OrdinalIgnoreCase), System.Globalization.NumberStyles.HexNumber, null, out var lb) ? (ushort?)lb : null) .Where(lb => lb is not null).Select(lb => lb!.Value).Distinct().ToList(); } public class SpotSetting { public string Cell { get; set; } = ""; public float X { get; set; } public float Y { get; set; } public float Z { get; set; } public float W { get; set; } = 1; public float Qz { get; set; } public Position? ToPosition() => uint.TryParse((Cell ?? "").Trim().Replace("0x", "", StringComparison.OrdinalIgnoreCase), System.Globalization.NumberStyles.HexNumber, null, out var cell) ? new Position(cell, X, Y, Z, 0, 0, Qz, W) : null; }