Each station worked out its next number as the highest in its log plus one, so two operators typing at the same moment both sent the same number and one of the two contacts was wrong in the sponsor's log. SerialNumberPool holds the highest number taken per band, which is N1MM's QSONumbers array. A number is taken at the first character of the callsign, as N1MM takes it, and ReserveNr tells the other stations before the contact is worked. A number at or below one this station has used is answered with the whole pool, N1MM's QSONRS, and the station that is behind counts on from there. The pool also goes out when a contest is opened and when the numbers are started again. Contest.NumbersByBand says whether the numbers run per band or as one sequence for the station. CQ WPX runs one per band for a multi-two and a multi-multi; every other contest here runs one sequence. The band index in the message is N1MM's, so the array lines up with theirs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
130 lines
3.6 KiB
C#
130 lines
3.6 KiB
C#
using Nonemm.Contests;
|
|
|
|
namespace Nonemm.Session;
|
|
|
|
/// What the operator has typed: the callsign, a value per exchange box, and
|
|
/// which box the cursor is in. The callsign box is field 0.
|
|
public sealed class EntryFields
|
|
{
|
|
private readonly List<string> values;
|
|
|
|
public EntryFields(IReadOnlyList<ExchangeField> exchange)
|
|
{
|
|
Exchange = exchange;
|
|
values = [.. Enumerable.Repeat("", exchange.Count)];
|
|
}
|
|
|
|
public IReadOnlyList<ExchangeField> Exchange { get; }
|
|
|
|
/// Raised when the callsign box goes from empty to holding something. That
|
|
/// is where N1MM takes the serial number for the contact, so the number is
|
|
/// out on the network before the contact is worked.
|
|
public event EventHandler? CallStarted;
|
|
|
|
public string Call
|
|
{
|
|
get;
|
|
set
|
|
{
|
|
bool wasEmpty = field.Trim().Length == 0;
|
|
field = value;
|
|
if (wasEmpty && field.Trim().Length > 0)
|
|
{
|
|
CallStarted?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
} = "";
|
|
|
|
/// 0 is the callsign box; 1 and up are the exchange boxes in order.
|
|
public int Focus { get; private set; }
|
|
|
|
public int FieldCount => Exchange.Count + 1;
|
|
|
|
public string this[int field]
|
|
{
|
|
get => field == 0 ? Call : values[field - 1];
|
|
set
|
|
{
|
|
if (field == 0)
|
|
{
|
|
Call = value;
|
|
}
|
|
else
|
|
{
|
|
values[field - 1] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
public string ValueOf(ExchangeSlot slot)
|
|
{
|
|
for (int at = 0; at < Exchange.Count; at++)
|
|
{
|
|
if (Exchange[at].Slot == slot)
|
|
{
|
|
return values[at];
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
public void Set(ExchangeSlot slot, string value)
|
|
{
|
|
for (int at = 0; at < Exchange.Count; at++)
|
|
{
|
|
if (Exchange[at].Slot == slot)
|
|
{
|
|
values[at] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void FocusOn(int field) => Focus = Math.Clamp(field, 0, FieldCount - 1);
|
|
|
|
/// What space and tab do: move to the next box the operator types in, and
|
|
/// round to the callsign again from the last one. The report boxes and the
|
|
/// sent serial are stepped over, because nobody types 599 or the number
|
|
/// they are sending — N1MM steps over the same three and fills them in.
|
|
public void Advance() => Focus = Next(1);
|
|
|
|
public void Retreat() => Focus = Next(-1);
|
|
|
|
private int Next(int by)
|
|
{
|
|
int at = Focus;
|
|
for (int step = 0; step < FieldCount; step++)
|
|
{
|
|
at = (at + by + FieldCount) % FieldCount;
|
|
if (at == 0 || !IsFilledIn(Exchange[at - 1]))
|
|
{
|
|
return at;
|
|
}
|
|
}
|
|
return Focus;
|
|
}
|
|
|
|
private static bool IsFilledIn(ExchangeField field) =>
|
|
field.IsSent || field.Kind == ExchangeFieldKind.Report;
|
|
|
|
/// True when the operator has typed nothing. The boxes holding what this
|
|
/// station sends are filled in by the program, so they do not count.
|
|
public bool IsEmpty =>
|
|
Call.Trim().Length == 0 &&
|
|
Exchange.Where((f, at) => !f.IsSent && values[at].Trim().Length > 0).Any() == false;
|
|
|
|
/// True when everything the contest asks for has been typed.
|
|
public bool IsComplete =>
|
|
Call.Trim().Length > 0 &&
|
|
Exchange.Where((f, at) => f.IsRequired && values[at].Trim().Length == 0).Any() == false;
|
|
|
|
public void Clear()
|
|
{
|
|
Call = "";
|
|
for (int at = 0; at < values.Count; at++)
|
|
{
|
|
values[at] = "";
|
|
}
|
|
Focus = 0;
|
|
}
|
|
}
|