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
129 lines
6.0 KiB
Markdown
129 lines
6.0 KiB
Markdown
# 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.
|