Files
Nonemm/docs/n1mm-interop.md
ericek111 04568d0ca3 Read the rest of what a .udc file says, and score what N1MM scores
Two pieces of work on the scoring engine.

The .udc reader now takes N1MM's published scoring vocabulary: the full
PointsPerContact condition set, the PointsMultBy family, PowerMult, BonusPoints,
the multiplier sources and the settings that say which stations bring a
multiplier in. MultMult is a weight rather than a switch, as in N1MM's
ComputeScore. The entry categories and the sent exchange reach the rules in a
new ContestEntry, so a contest can score by the power category it is entered in
or by what this station sends.

Then every contest in a real N1MM log was scored again from these rules and
compared with the points and multiplier flags N1MM wrote. That found five bugs:
ARRL DX and IARU read the exchange from the wrong column, a stored " " was read
as a value rather than as empty, CQ WW RTTY's own Canadian area names were not
counted, a maritime mobile scored nothing, and a contact in a mode the contest
does not run scored as if it were in the contest.

docs/n1mm-interop.md has the check and what still differs.

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

6.0 KiB

Opening a Nonemm log in N1MM

Checked on 2026-08-27 against N1MM Logger+ 1.0.11031 by writing a log here, copying it to a Windows machine and opening it there. N1MM read the contest, its categories and the contacts.

Three things had to be right, and none of them is obvious from the schema.

The Contest table needs a row for the contest

ContestInstance says which contest a log is; the Contest table says what that contest is. N1MM reads it in Contest.FromRow when it opens a log and throws InvalidOperationException: No current row if the row is missing, which reaches the operator as "A runtime error occurred".

So the definition row is written whenever a contest is opened, not only when it is created — a log made before this was understood gets its row the next time it is opened. ContestDefinitions.For builds the row from the contest's own rules: the display and Cabrillo names, the mode, the dupe type and the multiplier names.

The overlay category cannot be empty

An empty ContestInstance.OverlayCategory is answered with "Invalid Overlay Category:" and the log will not open. An entry with no overlay says N/A.

N1MM's list is not the Cabrillo specification's list. N1MM offers:

N/A, ROOKIE, BAND-LIMITED, TB-WIRES, OVER-50, HQ, NOVICE-TECH, EXPERT

so that is what the contest dialog offers.

The sent exchange omits the report

N1MM's contest dialog says "Omit RST: CQWW: 05". SentExchange for CQ WW is 14, not 599 14; for a serial number contest it is 001. The report is fixed for the whole contest and the entry window fills it in per contact.

Getting the schema in the first place

N1MM ships no template database. ham.s3db is built at run time by applying SQL migration files that N1MM writes out from string resources inside N1MMLogger.net.exe. To read them: decompile the executable, parse N1MMLogger.Net.Resources.resx, and take the untyped <data> entries named DXLogDDL_0001_initial_schema_and_data through DXLogDDL_0004_updates. The QSO table is DXLOG and the current PRAGMA user_version is 4.

src/Nonemm.Storage/Schema.sql is that schema, flattened to the state version 4 leaves behind.

One thing to avoid

Do not replace the database file under a running N1MM. It does not notice and throws on the next read.

A contact belongs to a ContestNR, not to the table key

ContestInstance has two numbers: ContestID, the table's primary key, and ContestNR. DXLOG.ContestNR refers to the second one. N1MM's ContestInstance.SQLWhereString reads:

" ContestNR = " + this.ContestNR + " "

In a fresh log the two numbers match, so keying on either works. In a log N1MM has been using for a while they drift apart. In one real log of 56284 contacts, joining DXLOG.ContestNR to ContestInstance.ContestID disagreed with the contact's own ContestName 52029 times; joining it to ContestInstance.ContestNR disagreed 109 times.

So ContestInstance.ContestNumber here is ContestNR, and ContestID is allocated separately when a contest is created.

N1MM names a contest per mode

CQ WW is CQWWCW, CQWWSSB or CQWWRTTY, never plain CQWW. The full list lives in the Contest table of any N1MM database. Nonemm writes the same 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 whatever ModeCategory says.

Older N1MM versions wrote the CW running of CQ WW as plain CQWW. That name is still in the registry, so those logs open.

Which column an exchange lands in

N1MM keeps the received exchange of ARRL DX, IARU and WAE in Sect, not in Exchange1, whatever the entry window calls the box. A contest that reads the wrong column scores every contact as if the operator had typed nothing, so the rule reads the column N1MM writes and the entry window points its box at the same place.

N1MM also writes a single space into a text column it has nothing for, so Continent comes back as " " rather than empty. Reading it as a value rather than as empty makes every contact look like another continent. The store trims what it reads.

Scoring checked against a real log

om5m.s3db in the repo root is a station's own N1MM log: 78 contest instances from 2019 to 2026, 57,000 contacts, 28 contest names. Replaying it is how the scoring rules are checked — every contact is scored again from our rules and compared with the points and multiplier flags N1MM wrote.

26 of the 37 instances whose contest we have rules for now agree on every contact and on the totals. What the check found and fixed:

What Was
ARRL DX and IARU read the wrong column no multipliers at all, and IARU scored every contact 1 point
a stored " " was read as a value 181 contacts in one log scored as another continent
the CQ WW RTTY area list NF and LB are that contest's own names for the Canadian areas, and were not counted
ARRL DX province spellings QB, NF and LB count as Quebec and as Newfoundland and Labrador
a maritime mobile scored nothing it counts for no country and still scores by continent
a contact in a mode the contest does not run scored as if it were in the contest; N1MM scores it zero and keeps its multiplier

What still differs, and why:

  • The country file of the day. A 2019 log holds calls that today's wl_cty.dat places elsewhere, and the multiplier moves to another contact with it. This is most of the difference in the 2019 CQ WW SSB log.
  • N1MM counts a call it cannot place as a multiplier with an empty value, and we do not. Same as the WAE difference already written down.
  • N1MM counts NF and LB as two ARRL DX multipliers. They are one province, and we count one.
  • A maritime mobile brings no country multiplier here, which is the published CQ WW rule; N1MM counts one for it.
  • Stale cached points. N1MM caches points in the row and does not always work them out again after a callsign is corrected, so a handful of rows hold a number that its own rules do not produce.