Three things stopped it. A contest is keyed by ContestNR, not by ContestID. ContestID is only the ContestInstance table's primary key; DXLOG.ContestNR refers to ContestNR, which is what N1MM's SQLWhereString matches on. The two agree in a fresh log and drift apart in one that has been used for a while, so we were reading one contest's header with another contest's contacts. In a real log of 56284 contacts, joining on ContestID disagreed with the contact's own ContestName 52029 times; joining on ContestNR disagreed 109 times. N1MM names a contest per mode: CQWWCW, CQWWSSB, CQWWRTTY, never plain CQWW. Our contests now use those names, and N1mmContestNames turns one back into a contest and a mode when a log is opened. The name carries the mode, so it beats the ModeCategory column. Plain CQWW still opens, because older versions wrote it that way. The log columns were sized by counting characters, which was too narrow for the headers: those are drawn in the theme's font, not the grid's monospace. The grid sizes them now, with the character count as a floor so a column of short values does not collapse. Bandmap spots now age from when they arrived rather than from the time written in them. A node with a wrong clock, or one replaying its backlog on connect, emptied the bandmap as fast as it filled it. Checked by opening a real 56284-contact N1MM log under Xvfb and reading the contest and contacts back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
76 lines
2.4 KiB
C#
76 lines
2.4 KiB
C#
using Nonemm.Core;
|
|
|
|
namespace Nonemm.Contests.Rules;
|
|
|
|
/// IARU HF World Championship. The exchange is an ITU zone or, for a society
|
|
/// headquarters station, its abbreviation; both count as multipliers per band.
|
|
public sealed class IaruHf : Contest
|
|
{
|
|
public string Name => "IARU";
|
|
|
|
public string DisplayName => "IARU HF World Championship";
|
|
|
|
public string CabrilloName => "IARU-HF";
|
|
|
|
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
|
[
|
|
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
|
new ExchangeField("Zone/HQ", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 6 },
|
|
];
|
|
|
|
public IReadOnlyList<string> MultiplierNames => ["Zones", "HQ"];
|
|
|
|
public DupeScope DupeScope => DupeScope.PerBandAndMode;
|
|
|
|
public bool HasSerialNumbers => false;
|
|
|
|
public IReadOnlyList<ModeCategory> Modes => [ModeCategory.Cw, ModeCategory.Phone];
|
|
|
|
public string SentExchangeFor(StationInfo me) => me.ItuZone.ToString();
|
|
|
|
public int PointsFor(QsoContext qso)
|
|
{
|
|
string exchange = Exchange(qso);
|
|
if (!IsZone(exchange))
|
|
{
|
|
return 1;
|
|
}
|
|
if (int.TryParse(exchange, out int zone) && zone == qso.Me.ItuZone)
|
|
{
|
|
return 1;
|
|
}
|
|
return qso.IsSameContinent ? 3 : 5;
|
|
}
|
|
|
|
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso)
|
|
{
|
|
string exchange = Exchange(qso);
|
|
if (exchange.Length == 0)
|
|
{
|
|
return [];
|
|
}
|
|
string band = qso.Band?.Name ?? "";
|
|
return [new Multiplier(IsZone(exchange) ? 1 : 2, exchange, band)];
|
|
}
|
|
|
|
public int TotalScore(ScoreTally tally) => tally.Points * tally.TotalMultipliers;
|
|
|
|
public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me) =>
|
|
new(
|
|
[
|
|
new CabrilloField(me.Callsign, 13),
|
|
new CabrilloField(qso.SentReport, 3),
|
|
new CabrilloField(me.ItuZone.ToString(), 6),
|
|
],
|
|
[
|
|
new CabrilloField(qso.Call.Text, 13),
|
|
new CabrilloField(qso.ReceivedReport, 3),
|
|
new CabrilloField(qso.Exchange1, 6),
|
|
]);
|
|
|
|
private static string Exchange(QsoContext qso) => qso.Qso.Exchange1.Trim().ToUpperInvariant();
|
|
|
|
private static bool IsZone(string exchange) =>
|
|
exchange.Length > 0 && exchange.All(char.IsAsciiDigit);
|
|
}
|