MultSqlString = CallHist counts every station the call history file lists, once per callsign, which is how N1MM dedups it. The file reaches the scoring code through ContestLog.History, and a QsoContext now carries it; without a file loaded nothing counts. BonusPoints2 = +50, calls.txt reads the callsigns from the support-files folder and adds 50 to the contact, *2 doubles it and a plain 50 scores 50 instead. The folder comes from the registry, which the app passes when it reads the .udc files. N1MM's other form, where the file is grids.txt and the bonus is looked up by grid square, is not read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
186 lines
6.2 KiB
C#
186 lines
6.2 KiB
C#
using Nonemm.Contests;
|
|
using Nonemm.Core;
|
|
using Nonemm.Core.Calls;
|
|
using Nonemm.Core.Country;
|
|
using Nonemm.Storage;
|
|
|
|
namespace Nonemm.Session;
|
|
|
|
/// The running contest: the log, the score and what happens when a contact is
|
|
/// logged. One of these per contest, however many radios the station has.
|
|
/// What the operator is typing lives in `OperatingPosition`, one per radio.
|
|
///
|
|
/// No UI framework is referenced here, so the behaviour an operator judges the
|
|
/// logger by is tested with plain unit tests.
|
|
public sealed class ContestSession
|
|
{
|
|
private readonly LogStore store;
|
|
|
|
public ContestSession(
|
|
LogStore store,
|
|
Contest contest,
|
|
ContestInstance instance,
|
|
StationInfo me,
|
|
CountryFile? countries,
|
|
CallHistory? history = null)
|
|
{
|
|
this.store = store;
|
|
Countries = countries;
|
|
History = history ?? CallHistory.Empty;
|
|
Contest = contest;
|
|
Instance = instance;
|
|
Me = me;
|
|
Log = new ContestLog(contest, me, countries, instance.Entry) { History = History };
|
|
Log.Restore(store.Qsos(instance.ContestNumber));
|
|
Editor = new QsoEditor(contest, me, countries);
|
|
SentNumber = NextSentNumber();
|
|
}
|
|
|
|
public Contest Contest { get; }
|
|
|
|
public ContestInstance Instance { get; }
|
|
|
|
public StationInfo Me { get; }
|
|
|
|
public ContestLog Log { get; }
|
|
|
|
public QsoEditor Editor { get; }
|
|
|
|
public CountryFile? Countries { get; }
|
|
|
|
/// What was published about the stations in this contest before it started.
|
|
public CallHistory History { get; }
|
|
|
|
/// Serial numbers count up across the station, not per radio.
|
|
public int SentNumber { get; private set; }
|
|
|
|
public string Operator { get; set; } = "";
|
|
|
|
public event EventHandler? Changed;
|
|
|
|
public event EventHandler<Qso>? Logged;
|
|
|
|
/// A contact that was edited, with the call and time it had before. The
|
|
/// other stations need the old pair to find their copy of it.
|
|
public event EventHandler<QsoChange>? Edited;
|
|
|
|
public event EventHandler<Qso>? Deleted;
|
|
|
|
/// Scores the contact, writes it and hands back what was stored: the
|
|
/// timestamp can move by a second when another contact with the same call
|
|
/// already holds it.
|
|
public Qso Add(Qso qso)
|
|
{
|
|
Qso scored = Log.Add(qso);
|
|
Qso stored = store.Add(scored);
|
|
if (stored.TimestampUtc != scored.TimestampUtc)
|
|
{
|
|
Log.Replace(stored);
|
|
}
|
|
SentNumber = NextSentNumber();
|
|
Logged?.Invoke(this, stored);
|
|
Changed?.Invoke(this, EventArgs.Empty);
|
|
return stored;
|
|
}
|
|
|
|
public void Delete(string id)
|
|
{
|
|
Qso? gone = Log.Qsos.FirstOrDefault(q => q.Id == id);
|
|
store.Delete(id);
|
|
Log.Remove(id);
|
|
if (gone is not null)
|
|
{
|
|
Deleted?.Invoke(this, gone);
|
|
}
|
|
Changed?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
/// Changes one field of a logged contact. A refused edit leaves the log
|
|
/// alone and the caller gets the reason back.
|
|
public QsoEdit Edit(string id, QsoField field, string text) =>
|
|
EditWith(id, before => Editor.Apply(before, field, text));
|
|
|
|
/// Replaces a logged contact with one the caller has already built and
|
|
/// checked. The edit contact dialog changes many fields at once and comes
|
|
/// in this way.
|
|
public void Update(Qso qso) => EditWith(qso.Id, _ => QsoEdit.Accept(qso));
|
|
|
|
public void NotifyChanged() => Changed?.Invoke(this, EventArgs.Empty);
|
|
|
|
/// A contact worked by another station of a multi-operator entry. It is
|
|
/// scored here from the rules rather than trusting what the sender worked
|
|
/// out, and it raises no `Logged`, so it does not go back out to the other
|
|
/// stations. False when we already hold it.
|
|
public bool AddFromNetwork(Qso qso)
|
|
{
|
|
if (Log.Qsos.Any(q => q.Id == qso.Id))
|
|
{
|
|
return false;
|
|
}
|
|
Log.Insert(store.Add(FromNetwork(qso)));
|
|
SentNumber = NextSentNumber();
|
|
Changed?.Invoke(this, EventArgs.Empty);
|
|
return true;
|
|
}
|
|
|
|
/// An edit made at another station. False when we do not hold the contact
|
|
/// it changes.
|
|
public bool ReplaceFromNetwork(Qso qso, string oldCall, DateTime oldTimestampUtc)
|
|
{
|
|
if (Find(qso.Id, oldCall, oldTimestampUtc) is not { } old)
|
|
{
|
|
return false;
|
|
}
|
|
Qso replacement = FromNetwork(qso) with { Id = old.Id };
|
|
store.Update(replacement);
|
|
Log.Replace(replacement);
|
|
Changed?.Invoke(this, EventArgs.Empty);
|
|
return true;
|
|
}
|
|
|
|
/// A contact deleted at another station. False when we do not hold it.
|
|
public bool DeleteFromNetwork(string id, string call, DateTime timestampUtc)
|
|
{
|
|
if (Find(id, call, timestampUtc) is not { } gone)
|
|
{
|
|
return false;
|
|
}
|
|
store.Delete(gone.Id);
|
|
Log.Remove(gone.Id);
|
|
Changed?.Invoke(this, EventArgs.Empty);
|
|
return true;
|
|
}
|
|
|
|
private Qso FromNetwork(Qso qso) => qso with
|
|
{
|
|
ContestNumber = Instance.ContestNumber,
|
|
IsOriginal = false,
|
|
};
|
|
|
|
/// N1MM keys a contact on its call and time, so a message from N1MM carries
|
|
/// no id we would recognise. Fall back to that pair when the id misses.
|
|
private Qso? Find(string id, string call, DateTime timestampUtc) =>
|
|
Log.Qsos.FirstOrDefault(q => id.Length > 0 && q.Id == id)
|
|
?? Log.Qsos.FirstOrDefault(q =>
|
|
string.Equals(q.Call.Text, call, StringComparison.OrdinalIgnoreCase)
|
|
&& q.TimestampUtc == timestampUtc);
|
|
|
|
private QsoEdit EditWith(string id, Func<Qso, QsoEdit> change)
|
|
{
|
|
Qso before = Log.Qsos.FirstOrDefault(q => q.Id == id)
|
|
?? throw new InvalidOperationException($"no contact with id {id} in the log");
|
|
QsoEdit edit = change(before);
|
|
if (edit.Result is not null)
|
|
{
|
|
store.Update(edit.Result);
|
|
Log.Replace(edit.Result);
|
|
Edited?.Invoke(this, new QsoChange(edit.Result, before.Call.Text, before.TimestampUtc));
|
|
Changed?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
return edit;
|
|
}
|
|
|
|
private int NextSentNumber() =>
|
|
Log.Qsos.Count == 0 ? 1 : Log.Qsos.Max(q => q.SentNumber) + 1;
|
|
}
|