From 393b73f87aa4056d60d113a345d621c16cca9af7 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Sat, 24 Oct 2026 03:47:29 +0000 Subject: [PATCH] Write N1MM's own Contest table rows for the built-in contests Co-Authored-By: Claude Sonnet 5.5 --- docs/n1mm-interop.md | 11 +++ src/Nonemm.App/AppSession.Broadcast.cs | 2 +- src/Nonemm.Contests/N1mmContestRows.cs | 74 +++++++++++++++++++ src/Nonemm.Contests/N1mmMultiplierNames.cs | 62 ---------------- src/Nonemm.Session/ContestDefinitions.cs | 22 +++++- .../ContestDefinitionsTests.cs | 39 +++++++++- 6 files changed, 143 insertions(+), 67 deletions(-) create mode 100644 src/Nonemm.Contests/N1mmContestRows.cs delete mode 100644 src/Nonemm.Contests/N1mmMultiplierNames.cs diff --git a/docs/n1mm-interop.md b/docs/n1mm-interop.md index c5ee1cf..1ff6418 100644 --- a/docs/n1mm-interop.md +++ b/docs/n1mm-interop.md @@ -135,3 +135,14 @@ a log has to score the same in both programs: - A maritime mobile counts for the country of the call it signs. CQ WW says it counts for the zone alone. - A callsign that places nowhere counts as a multiplier with an empty value. + +## The Contest table + +N1MM reads the `Contest` table when it opens a log. For the built-in contests +the row we write is N1MM's own (`N1mmContestRows`, taken from `om5m.s3db`): +display name, Cabrillo name, mode, dupe type, multiplier names, period, points +per contact and Cabrillo version. A contest that is not in that table, such as +a user-defined one, gets a row worked out from its rules. + +The Cabrillo name in that row is not always the one our Cabrillo export uses. +For WAE the table says `DARC-WAEDC-CW`, and the export writes `WAE-CW`. diff --git a/src/Nonemm.App/AppSession.Broadcast.cs b/src/Nonemm.App/AppSession.Broadcast.cs index 0f705bd..494f0e5 100644 --- a/src/Nonemm.App/AppSession.Broadcast.cs +++ b/src/Nonemm.App/AppSession.Broadcast.cs @@ -153,7 +153,7 @@ public sealed partial class AppSession scoreSent = DateTime.UtcNow; ContestInstance instance = Logging.Instance; StationInfo me = Settings.Station.ToStationInfo(); - IReadOnlyList multiplierNames = N1mmMultiplierNames.For(Logging.Contest, me); + IReadOnlyList multiplierNames = N1mmContestRows.MultiplierNames(Logging.Contest, me); List rows = ScoreBreakdown.For(Logging.Log.Qsos, Logging.Contest) .Select(r => new ScoreReportRow(r.Band, r.Mode, r.Qsos, r.Multipliers, r.Points)) .ToList(); diff --git a/src/Nonemm.Contests/N1mmContestRows.cs b/src/Nonemm.Contests/N1mmContestRows.cs new file mode 100644 index 0000000..a92ba9d --- /dev/null +++ b/src/Nonemm.Contests/N1mmContestRows.cs @@ -0,0 +1,74 @@ +using Nonemm.Core; + +namespace Nonemm.Contests; + +/// What N1MM keeps in its `Contest` table for one contest. `Multipliers` are +/// the internal names such as `ZN` and `Sect`, without the `N/A` entries. +public sealed record N1mmContestRow( + string DisplayName, + string CabrilloName, + string Mode, + int DupeType, + IReadOnlyList Multipliers, + int Period, + int PointsPerContact, + string CabrilloVersion); + +/// The `Contest` table rows of the built-in contests, taken from a log N1MM +/// wrote. N1MM reads this table when it opens a log, so a log written here +/// carries N1MM's values rather than ones derived from our rules. The score +/// broadcast names its multiplier types from the same rows. +public static class N1mmContestRows +{ + private static readonly Dictionary Known = + new(StringComparer.OrdinalIgnoreCase) + { + ["CQWWCW"] = new("CQ WorldWide CW", "CQ-WW-CW", "CW", 2, ["ZN", "CountryPrefix"], 2, 3, "3.0"), + ["CQWWSSB"] = new("CQ WorldWide SSB", "CQ-WW-SSB", "SSB", 2, ["ZN", "CountryPrefix"], 2, 3, "3.0"), + ["CQWWRTTY"] = new("CQ WorldWide RTTY", "CQ-WW-RTTY", "RTTY", 2, ["ZN", "CountryPrefix", "Sect"], 2, 3, "3.0"), + ["CQWPXCW"] = new("CQ WPX CW", "CQ-WPX-CW", "CW", 2, ["WPXPrefix"], 2, 2, "3.0"), + ["CQWPXSSB"] = new("CQ WPX SSB", "CQ-WPX-SSB", "SSB", 2, ["WPXPrefix"], 2, 2, "3.0"), + ["CQWPXRTTY"] = new("CQ WPX RTTY", "CQ-WPX-RTTY", "RTTY", 2, ["WPXPrefix"], 2, 2, "3.0"), + ["ARRLDXCW"] = new("ARRL DX CW", "ARRL-DX-CW", "CW", 2, ["Sect"], 2, 3, "3.0"), + ["ARRLDXSSB"] = new("ARRL DX SSB", "ARRL-DX-SSB", "SSB", 2, ["Sect"], 2, 3, "3.0"), + ["IARU"] = new("Internatl Amateur Radio Union HF Contest", "IARU-HF", "Both", 3, ["ZN", "HQ"], 2, 1, "3.0"), + ["SSCW"] = new("ARRL Sweepstakes CW (November)", "ARRL-SS-CW", "CW", 1, ["Sect"], 2, 2, "3.0"), + ["SSSSB"] = new("ARRL Sweepstakes SSB (November)", "ARRL-SS-SSB", "SSB", 1, ["Sect"], 2, 2, "3.0"), + ["WAECW"] = new("Worked All Europe - WAE CW", "DARC-WAEDC-CW", "CW", 3, ["CountryPrefix"], 1, 1, "3.0"), + ["WAESSB"] = new("Worked All Europe - WAE SSB", "DARC-WAEDC-SSB", "SSB", 3, ["CountryPrefix"], 1, 1, "3.0"), + ["WAERTTY"] = new("Worked All Europe - WAE RTTY", "DARC-WAEDC-RTTY", "RTTY", 3, ["CountryPrefix"], 1, 1, "3.0"), + ["ARRLRTTY"] = new("ARRL RTTY Roundup (January)", "ARRL-RTTY", "RTTY", 2, ["Sect", "CountryPrefix"], 1, 1, "3.0"), + ["NAQPCW"] = new("North American QSO Parties - CW", "NAQP-CW", "CW", 2, ["Sect", "NA"], 1, 1, "3.0"), + ["NAQPSSB"] = new("North American QSO Parties - SSB", "NAQP-SSB", "SSB", 2, ["Sect", "NA"], 1, 1, "3.0"), + ["NAQPRTTY"] = new("North American QSO Parties - RTTY", "NAQP-RTTY", "RTTY", 2, ["Sect", "NA"], 1, 1, "3.0"), + ["OKOMDX"] = new("Czech and Slovak Republics DX", "OK-OM-DX", "CW", 2, ["Sect"], 1, 1, "3.0"), + ["YOTA"] = new("YOTA Contest", "YOTA", "BOTH", 3, ["Exchange"], 2, 0, "3.0"), + ["IOTA"] = new("RSGB Islands On The Air Contest", "RSGB-IOTA", "Both", 3, ["Sect"], 1, 1, "3.0"), + ["EUDXC"] = new("EU DX Contest (not UDC)", "EUDXC", "Both", 3, ["Sect", "CountryPrefix"], 1, 1, "3.0"), + ["CQ160CW"] = new("CQ World-Wide 160 Meter DX Contest - CW", "CQ-160-CW", "CW", 1, ["StateProvince", "CountryPrefix"], 2, 1, "3.0"), + ["CQ160SSB"] = new("CQ World-Wide 160 Meter DX Contest - Phone", "CQ-160-SSB", "SSB", 1, ["StateProvince", "CountryPrefix"], 2, 1, "3.0"), + ["XERTTY"] = new("Mexico RTTY Contest", "XE-RTTY", "RTTY", 2, ["Countryprefix", "Sect"], 1, 1, "3.0"), + ["REFCW"] = new("French REF DX contest - CW", "REF-CW", "CW", 2, ["Sect", "CountryPrefix"], 1, 1, "3.0"), + ["REFSSB"] = new("French REF DX contest - SSB", "REF-SSB", "SSB", 2, ["Sect", "CountryPrefix"], 1, 1, "3.0"), + ["UKRAINDX"] = new("UKRAINIAN DX", "UKRAINIAN-DX", "BOTH", 3, ["CountryPrefix", "Sect"], 1, 1, "3.0"), + ["BARTGRTTYS"] = new("BARTG RTTY Sprint contest (January)", "BARTG-SPRINT", "RTTY", 2, ["CountryPrefix", "Sect", "Continent"], 2, 1, "3.0"), + ["ARRL10M"] = new("ARRL 10 Meter Contest", "ARRL-10", "Both", 3, ["StateProvince", "CountryPrefix"], 2, 1, "3.0"), + ["RUSDXRTTY"] = new("Russian DX RTTY", "RADIO-WW-RTTY", "RTTY", 2, ["CountryPrefix", "Sect"], 1, 1, "3.0"), + ["RDAC"] = new("Russian District Award Contest", "RDAC", "Both", 3, ["CountryPrefix", "Sect"], 1, 1, "3.0"), + ["YOHFDX"] = new("Romanian Amateur Radio Federation - YO DX HF", "YO-DX-HF", "Both", 3, ["CountryPrefix", "Sect"], 1, 1, "3.0"), + ["OCEANIACW"] = new("Oceania DX CW", "Oceania-DX-CW", "CW", 2, ["WPXPrefix"], 2, 2, "3.0"), + ["OCEANIASSB"] = new("Oceania DX SSB", "Oceania-DX-SSB", "SSB", 2, ["WPXPrefix"], 2, 2, "3.0"), + ["FDREG1"] = new("Internatl Amateur Radio Union, Region 1 Field Day", "FIELDDAY-REGION-1", "Both", 2, ["CountryPrefix"], 1, 1, "3.0"), + ["SARTGRTTY"] = new("Scandinavian Amateur Radio, WorldWide RTTY", "SARTG-RTTY", "RTTY", 2, ["CountryPrefix", "Sect"], 2, 1, "3.0"), + ["ALLASIACW"] = new("All Asian DX contest CW", "AADX-CW", "CW", 2, ["WPXPrefix"], 2, 1, "3.0"), + ["ALLASIASSB"] = new("All Asian DX contest SSB", "AADX-SSB", "SSB", 2, ["WPXPrefix"], 2, 1, "3.0"), + ["DX"] = new("General Logging", "DX", "Both", 3, ["CountryPrefix"], 32000, 1, "3.0"), + }; + + public static N1mmContestRow? Find(string name) => + Known.TryGetValue(name, out N1mmContestRow? row) ? row : null; + + /// A contest not in the table gets the names its rules give. + public static IReadOnlyList MultiplierNames(Contest contest, StationInfo me) => + Find(contest.Name)?.Multipliers ?? contest.MultiplierNamesFor(me); +} diff --git a/src/Nonemm.Contests/N1mmMultiplierNames.cs b/src/Nonemm.Contests/N1mmMultiplierNames.cs deleted file mode 100644 index 6ee9e47..0000000 --- a/src/Nonemm.Contests/N1mmMultiplierNames.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Nonemm.Core; - -namespace Nonemm.Contests; - -/// The multiplier names N1MM keeps in the `Contest` table, such as `ZN`, -/// `CountryPrefix` and `Sect`. The score broadcast names its multiplier types -/// from them, and a contest's own `MultiplierNamesFor` holds the names the -/// score window shows instead (`Zones`, `Countries`). Taken from a log N1MM -/// wrote, without the `N/A` entries. -public static class N1mmMultiplierNames -{ - private static readonly Dictionary> Known = - new(StringComparer.OrdinalIgnoreCase) - { - ["CQWWCW"] = ["ZN", "CountryPrefix"], - ["CQWWSSB"] = ["ZN", "CountryPrefix"], - ["CQWWRTTY"] = ["ZN", "CountryPrefix", "Sect"], - ["CQWPXCW"] = ["WPXPrefix"], - ["CQWPXSSB"] = ["WPXPrefix"], - ["CQWPXRTTY"] = ["WPXPrefix"], - ["ARRLDXCW"] = ["Sect"], - ["ARRLDXSSB"] = ["Sect"], - ["IARU"] = ["ZN", "HQ"], - ["SSCW"] = ["Sect"], - ["SSSSB"] = ["Sect"], - ["WAECW"] = ["CountryPrefix"], - ["WAESSB"] = ["CountryPrefix"], - ["WAERTTY"] = ["CountryPrefix"], - ["ARRLRTTY"] = ["Sect", "CountryPrefix"], - ["NAQPCW"] = ["Sect", "NA"], - ["NAQPSSB"] = ["Sect", "NA"], - ["NAQPRTTY"] = ["Sect", "NA"], - ["OKOMDX"] = ["Sect"], - ["YOTA"] = ["Exchange"], - ["IOTA"] = ["Sect"], - ["EUDXC"] = ["Sect", "CountryPrefix"], - ["CQ160CW"] = ["StateProvince", "CountryPrefix"], - ["CQ160SSB"] = ["StateProvince", "CountryPrefix"], - ["XERTTY"] = ["Countryprefix", "Sect"], - ["REFCW"] = ["Sect", "CountryPrefix"], - ["REFSSB"] = ["Sect", "CountryPrefix"], - ["UKRAINDX"] = ["CountryPrefix", "Sect"], - ["BARTGRTTYS"] = ["CountryPrefix", "Sect", "Continent"], - ["ARRL10M"] = ["StateProvince", "CountryPrefix"], - ["RUSDXRTTY"] = ["CountryPrefix", "Sect"], - ["RDAC"] = ["CountryPrefix", "Sect"], - ["YOHFDX"] = ["CountryPrefix", "Sect"], - ["OCEANIACW"] = ["WPXPrefix"], - ["OCEANIASSB"] = ["WPXPrefix"], - ["FDREG1"] = ["CountryPrefix"], - ["SARTGRTTY"] = ["CountryPrefix", "Sect"], - ["ALLASIACW"] = ["WPXPrefix"], - ["ALLASIASSB"] = ["WPXPrefix"], - ["DX"] = ["CountryPrefix"], - }; - - /// A contest not in the table gets the names its rules give. - public static IReadOnlyList For(Contest contest, StationInfo me) => - Known.TryGetValue(contest.Name, out IReadOnlyList? names) - ? names - : contest.MultiplierNamesFor(me); -} diff --git a/src/Nonemm.Session/ContestDefinitions.cs b/src/Nonemm.Session/ContestDefinitions.cs index 365e270..e111c0f 100644 --- a/src/Nonemm.Session/ContestDefinitions.cs +++ b/src/Nonemm.Session/ContestDefinitions.cs @@ -5,12 +5,30 @@ using Nonemm.Storage; namespace Nonemm.Session; /// Turns a contest's rules into the definition row N1MM keeps in the `Contest` -/// table, which it reads when it opens a log. +/// table, which it reads when it opens a log. A built-in contest takes N1MM's +/// own row; any other is worked out from its rules. public static class ContestDefinitions { public static ContestDefinition For(Contest contest, StationInfo me) { - IReadOnlyList mults = N1mmMultiplierNames.For(contest, me); + if (N1mmContestRows.Find(contest.Name) is { } row) + { + return new ContestDefinition + { + Name = contest.Name, + DisplayName = row.DisplayName, + CabrilloName = row.CabrilloName, + Mode = row.Mode, + DupeType = row.DupeType, + Multiplier1Name = NameAt(row.Multipliers, 0), + Multiplier2Name = NameAt(row.Multipliers, 1), + Multiplier3Name = NameAt(row.Multipliers, 2), + Period = row.Period, + PointsPerContact = row.PointsPerContact, + CabrilloVersion = row.CabrilloVersion, + }; + } + IReadOnlyList mults = contest.MultiplierNamesFor(me); return new ContestDefinition { Name = contest.Name, diff --git a/tests/Nonemm.Session.Tests/ContestDefinitionsTests.cs b/tests/Nonemm.Session.Tests/ContestDefinitionsTests.cs index 7f0bcea..2b722a1 100644 --- a/tests/Nonemm.Session.Tests/ContestDefinitionsTests.cs +++ b/tests/Nonemm.Session.Tests/ContestDefinitionsTests.cs @@ -1,4 +1,5 @@ using Nonemm.Contests.Rules; +using Nonemm.Contests.Udc; using Nonemm.Core; using Nonemm.Storage; @@ -53,6 +54,40 @@ public class ContestDefinitionsTests } [Fact] - public void ContestWithSeveralModesIsBoth() => - Assert.Equal("BOTH", ContestDefinitions.For(new IaruHf(), Germany).Mode); + public void ContestWithSeveralModesIsBothAsN1mmWritesIt() => + Assert.Equal("Both", ContestDefinitions.For(new IaruHf(), Germany).Mode); + + [Fact] + public void ABuiltInContestTakesTheRowN1mmWrote() + { + ContestDefinition definition = ContestDefinitions.For(new Wae(ModeCategory.Cw), Germany); + + Assert.Equal("DARC-WAEDC-CW", definition.CabrilloName); + Assert.Equal("Worked All Europe - WAE CW", definition.DisplayName); + Assert.Equal((3, 1, 1), (definition.DupeType, definition.Period, definition.PointsPerContact)); + } + + [Fact] + public void AUserDefinedContestIsWorkedOutFromItsFile() + { + UserDefinedContest contest = new(UdcFile.Parse(""" + [Contest] + Name=SAMPLETEST + DisplayName=Sample Test + CabrilloName=SAMPLE-TEST + Mode=CW + DupeType=2 + Multiplier1Name=Countries + MultSqlString=CountryPrefix + IsMultPer=1 + PointsPerContact=1 + EntryWindowInfo=RCVText, 500, Exchange1Text, 500 + FrameText=RcvRST Exch + DefaultContestExchange=599 + """)); + + ContestDefinition definition = ContestDefinitions.For(contest, Germany); + + Assert.Equal(("Sample Test", "SAMPLE-TEST", "Countries"), (definition.DisplayName, definition.CabrilloName, definition.Multiplier1Name)); + } }