Files
Nonemm/src/Nonemm.Contests/ContestRegistry.cs
ericek111 38e9010802 Let the published .udc files score the contests they are written for
I wrote SA 10 and OK/OM DX SSB in code after two failed attempts at the
download. The download works — the page posts a nonce with the form — and the
files say I got both contests wrong.

SA 10 is deleted from the code. Its file scores the station's log to the
contact, where the code had the wrong Cabrillo name and counted its multipliers
once in the contest rather than once per mode.

OK/OM DX now covers the CW running only, which is what N1MM's class is for and
what N1MM's own documentation says: the SSB running has different rules and a
pair of .udc files, one for each side. The CW rules go back to the class, so
Czechia and Slovakia are one side of the contest again. With the SSB files in
place both of the station's SSB logs score to the contact.

Reading those files turned up four things in the .udc reader: a file whose
extension is upper case was passed over, `Country` was not read as a
multiplier source, `OtherCountry` was not a points condition, and a section
multiplier counted the serial numbers the other side of a contest sends.

All Asian stays in code — N1MM has a class for it after all — with the Cabrillo
name and the band table it uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 10:00:21 +00:00

122 lines
5.9 KiB
C#

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<ContestChoice> 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<string, ContestChoice> byName;
private ContestRegistry(Dictionary<string, ContestChoice> byName) => this.byName = byName;
public static ContestRegistry Create(IEnumerable<UdcFile>? userDefined = null)
{
Dictionary<string, ContestChoice> 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<string> problems)
{
List<UdcFile> files = [];
List<string> failures = [];
if (Directory.Exists(folder))
{
IEnumerable<string> 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<ContestChoice> 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;
}
}