using Nonemm.Contests.Rules; using Nonemm.Contests.Udc; using Nonemm.Core; namespace Nonemm.Contests; /// The contests on offer: the ones written into the program, plus whatever /// `.udc` files the operator has put in the user-defined contests folder. public sealed class ContestRegistry { private static readonly IReadOnlyList BuiltIn = [ new("CQWW", "CQ World Wide DX", [ModeCategory.Cw, ModeCategory.Phone, ModeCategory.Digital], m => new CqWorldWide(m)), new("CQWPX", "CQ WPX", [ModeCategory.Cw, ModeCategory.Phone, ModeCategory.Digital], m => new CqWpx(m)), new("ARRLDX", "ARRL International DX", [ModeCategory.Cw, ModeCategory.Phone], m => new ArrlDx(m)), new("IARU", "IARU HF World Championship", [ModeCategory.Cw, ModeCategory.Phone], _ => new IaruHf()), new("SS", "ARRL Sweepstakes", [ModeCategory.Cw, ModeCategory.Phone], m => new Sweepstakes(m)), new("WAE", "Worked All Europe DX", [ModeCategory.Cw, ModeCategory.Phone, ModeCategory.Digital], m => new Wae(m)), new("ARRLRTTY", "ARRL RTTY Roundup", [ModeCategory.Digital], _ => new RttyRoundup()), new("NAQP", "North American QSO Party", [ModeCategory.Cw, ModeCategory.Phone, ModeCategory.Digital], m => new NorthAmericanQsoParty(m)), new("OKOMDX", "Czech and Slovak Republics DX", [ModeCategory.Cw], _ => new OkOmDx()), new("YOTA", "YOTA Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new Yota()), new("IOTA", "RSGB Islands On The Air", [ModeCategory.Cw, ModeCategory.Phone], _ => new Iota()), new("EUDXC", "EU DX Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new EuDxContest()), new("CQ160", "CQ World-Wide 160 Metre", [ModeCategory.Cw, ModeCategory.Phone], m => new CqOneSixty(m)), new("XERTTY", "Mexico RTTY Contest", [ModeCategory.Digital], _ => new MexicoRtty()), new("REF", "French REF DX contest", [ModeCategory.Cw, ModeCategory.Phone], m => new FrenchRef(m)), new("UKRAINDX", "Ukrainian DX", [ModeCategory.Cw, ModeCategory.Phone], _ => new UkrainianDx()), new("BARTGRTTYS", "BARTG RTTY Sprint", [ModeCategory.Digital], _ => new BartgSprint()), new("ARRL10M", "ARRL 10 Metre Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new Arrl10Metre()), new("RUSDXRTTY", "Russian DX RTTY", [ModeCategory.Digital], _ => new RussianDxRtty()), new("RDAC", "Russian District Award Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new RussianDistrictAward()), new("YOHFDX", "YO DX HF", [ModeCategory.Cw, ModeCategory.Phone], _ => new YoDxHf()), new("OCEANIA", "Oceania DX", [ModeCategory.Cw, ModeCategory.Phone], m => new OceaniaDx(m)), new("FDREG1", "IARU Region 1 Field Day", [ModeCategory.Cw, ModeCategory.Phone], _ => new FieldDayRegionOne()), new("SARTGRTTY", "SARTG World Wide RTTY", [ModeCategory.Digital], _ => new SartgRtty()), new("ALLASIA", "All Asian DX", [ModeCategory.Cw, ModeCategory.Phone], m => new AllAsian(m)), new("DX", "General logging", [], _ => new GeneralLogging()), ]; private readonly Dictionary byName; private ContestRegistry(Dictionary byName) => this.byName = byName; public static ContestRegistry Create(IEnumerable? userDefined = null) { Dictionary byName = BuiltIn.ToDictionary( c => c.Name, StringComparer.OrdinalIgnoreCase); foreach (UdcFile file in userDefined ?? []) { UserDefinedContest contest = new(file); byName[contest.Name] = new ContestChoice( contest.Name, contest.DisplayName, contest.Modes, _ => new UserDefinedContest(file)); } return new ContestRegistry(byName); } /// Reads every `.udc` file in the folder. Published files come with the /// extension in either case, and a file that will not parse is reported /// rather than silently left out. public static ContestRegistry FromFolder(string folder, out IReadOnlyList problems) { List files = []; List failures = []; if (Directory.Exists(folder)) { IEnumerable found = Directory.EnumerateFiles(folder) .Where(f => Path.GetExtension(f).Equals(".udc", StringComparison.OrdinalIgnoreCase)); foreach (string path in found) { try { files.Add(UdcFile.Parse(File.ReadAllText(path))); } catch (Exception e) when (e is FormatException or IOException) { failures.Add($"{path}: {e.Message}"); } } } problems = failures; return Create(files); } public IReadOnlyList Choices => byName.Values.OrderBy(c => c.DisplayName, StringComparer.Ordinal).ToList(); public Contest Create(string name, ModeCategory mode) { ContestChoice choice = Find(name, ref mode) ?? throw new KeyNotFoundException($"no contest named '{name}'"); return choice.Create(mode); } public bool Has(string name) { ModeCategory ignored = ModeCategory.Cw; return Find(name, ref ignored) is not null; } /// A name N1MM wrote carries the mode, and that is better than whatever the /// log's mode column says, so it wins when the name resolves that way. private ContestChoice? Find(string name, ref ModeCategory mode) { if (byName.TryGetValue(name, out ContestChoice? choice)) { return choice; } if (N1mmContestNames.TryResolve(name, out string family, out ModeCategory named) && byName.TryGetValue(family, out ContestChoice? resolved)) { mode = named; return resolved; } return null; } }