Read the .udc CallHist multiplier and BonusPoints2
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
This commit is contained in:
@@ -49,7 +49,10 @@ public sealed class AppSession : IDisposable
|
||||
Countries = LoadCountryFile(paths.CountryFile);
|
||||
Calls = LoadCallDatabase(paths.CallDatabaseFile);
|
||||
History = LoadCallHistory(settings.CallHistoryFile);
|
||||
Registry = ContestRegistry.FromFolder(paths.UserDefinedContests, out IReadOnlyList<string> problems);
|
||||
Registry = ContestRegistry.FromFolder(
|
||||
paths.UserDefinedContests,
|
||||
out IReadOnlyList<string> problems,
|
||||
paths.SupportFiles);
|
||||
UserDefinedContestProblems = problems;
|
||||
BandPlan = settings.ToBandPlan();
|
||||
ApplySpotSettings();
|
||||
@@ -520,7 +523,7 @@ public sealed class AppSession : IDisposable
|
||||
Countries = LoadCountryFile(Paths.CountryFile);
|
||||
Calls = LoadCallDatabase(Paths.CallDatabaseFile);
|
||||
History = LoadCallHistory(Settings.CallHistoryFile);
|
||||
Registry = ContestRegistry.FromFolder(Paths.UserDefinedContests, out _);
|
||||
Registry = ContestRegistry.FromFolder(Paths.UserDefinedContests, out _, Paths.SupportFiles);
|
||||
ApplySpotSettings();
|
||||
if (Logging is not null)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Nonemm.Core;
|
||||
using Nonemm.Core.Calls;
|
||||
using Nonemm.Core.Country;
|
||||
|
||||
namespace Nonemm.Contests;
|
||||
@@ -31,6 +32,10 @@ public sealed class ContestLog
|
||||
/// change them while the contest is open, and the score follows.
|
||||
public ContestEntry Entry { get; set; }
|
||||
|
||||
/// The call history file, for a contest that counts a multiplier from it.
|
||||
/// The session sets it when the operator loads one.
|
||||
public CallHistory History { get; set; } = CallHistory.Empty;
|
||||
|
||||
public Contest Contest => contest;
|
||||
|
||||
public IReadOnlyList<Qso> Qsos => qsos;
|
||||
@@ -132,7 +137,10 @@ public sealed class ContestLog
|
||||
/// is signing from is looked up when the call as sent places nowhere. What
|
||||
/// that country counts for is the contest's decision.
|
||||
private QsoContext ContextFor(Qso qso) =>
|
||||
new(qso, countries?.Find(qso.Call) ?? countries?.Find(qso.Call.Station), me, Entry);
|
||||
new(qso, countries?.Find(qso.Call) ?? countries?.Find(qso.Call.Station), me, Entry)
|
||||
{
|
||||
History = History,
|
||||
};
|
||||
|
||||
private static Qso ApplyVerdict(Qso qso, Verdict verdict) =>
|
||||
ApplyMultiplierFlags(qso, verdict.NewMultipliers) with { Points = verdict.Points };
|
||||
|
||||
@@ -42,19 +42,23 @@ public sealed class ContestRegistry
|
||||
|
||||
private ContestRegistry(Dictionary<string, ContestChoice> byName) => this.byName = byName;
|
||||
|
||||
public static ContestRegistry Create(IEnumerable<UdcFile>? userDefined = null)
|
||||
/// `supportFolder` is where a user-defined contest reads the files its
|
||||
/// settings name, which is the folder the country file is in.
|
||||
public static ContestRegistry Create(
|
||||
IEnumerable<UdcFile>? userDefined = null,
|
||||
string? supportFolder = null)
|
||||
{
|
||||
Dictionary<string, ContestChoice> byName = BuiltIn.ToDictionary(
|
||||
c => c.Name,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
foreach (UdcFile file in userDefined ?? [])
|
||||
{
|
||||
UserDefinedContest contest = new(file);
|
||||
UserDefinedContest contest = new(file, supportFolder);
|
||||
byName[contest.Name] = new ContestChoice(
|
||||
contest.Name,
|
||||
contest.DisplayName,
|
||||
contest.Modes,
|
||||
_ => new UserDefinedContest(file));
|
||||
_ => new UserDefinedContest(file, supportFolder));
|
||||
}
|
||||
return new ContestRegistry(byName);
|
||||
}
|
||||
@@ -62,7 +66,10 @@ public sealed class ContestRegistry
|
||||
/// Reads every `.udc` file in the folder. Published files come with the
|
||||
/// extension in either case, and a file that will not parse is reported
|
||||
/// rather than silently left out.
|
||||
public static ContestRegistry FromFolder(string folder, out IReadOnlyList<string> problems)
|
||||
public static ContestRegistry FromFolder(
|
||||
string folder,
|
||||
out IReadOnlyList<string> problems,
|
||||
string? supportFolder = null)
|
||||
{
|
||||
List<UdcFile> files = [];
|
||||
List<string> failures = [];
|
||||
@@ -83,7 +90,7 @@ public sealed class ContestRegistry
|
||||
}
|
||||
}
|
||||
problems = failures;
|
||||
return Create(files);
|
||||
return Create(files, supportFolder);
|
||||
}
|
||||
|
||||
public IReadOnlyList<ContestChoice> Choices =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Nonemm.Core;
|
||||
using Nonemm.Core.Calls;
|
||||
using Nonemm.Core.Country;
|
||||
|
||||
namespace Nonemm.Contests;
|
||||
@@ -7,6 +8,10 @@ namespace Nonemm.Contests;
|
||||
/// score by country, zone or continent without looking anything up itself.
|
||||
public sealed record QsoContext(Qso Qso, CountryLookup? Country, StationInfo Me, ContestEntry Entry)
|
||||
{
|
||||
/// The call history file the operator loaded, which a contest can count a
|
||||
/// multiplier from. Empty when there is none.
|
||||
public CallHistory History { get; init; } = CallHistory.Empty;
|
||||
|
||||
public Band? Band => Qso.Band;
|
||||
|
||||
public ModeCategory ModeCategory => Qso.Mode.Category;
|
||||
|
||||
70
src/Nonemm.Contests/Udc/UdcBonusCalls.cs
Normal file
70
src/Nonemm.Contests/Udc/UdcBonusCalls.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
namespace Nonemm.Contests.Udc;
|
||||
|
||||
/// `BonusPoints2 = <value>, <file>`: a list of callsigns in a file in the
|
||||
/// support-files folder, and what a contact with one of those stations is
|
||||
/// worth. `+50` adds 50 to the points, `*2` doubles them, and a plain `50`
|
||||
/// scores 50 instead of them.
|
||||
///
|
||||
/// The file is N1MM's: one callsign per line, followed by a comma and whatever
|
||||
/// else the line holds. N1MM's other form, where the file is `grids.txt` and
|
||||
/// the bonus is looked up by grid square, is not read.
|
||||
public sealed class UdcBonusCalls
|
||||
{
|
||||
private readonly HashSet<string> calls = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly char how;
|
||||
private readonly int value;
|
||||
|
||||
public UdcBonusCalls(string setting, string? supportFolder)
|
||||
{
|
||||
string[] parts = setting.Split(',', StringSplitOptions.TrimEntries);
|
||||
if (parts.Length < 2 || parts[0].Length == 0 || supportFolder is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
string amount = parts[0];
|
||||
how = amount[0] is '+' or '*' ? amount[0] : ' ';
|
||||
if (!int.TryParse(how == ' ' ? amount : amount[1..], out value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (string line in Lines(Path.Combine(supportFolder, parts[1])))
|
||||
{
|
||||
string call = line.Split(',')[0].Trim();
|
||||
if (call.Length > 0)
|
||||
{
|
||||
calls.Add(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDefined => calls.Count > 0;
|
||||
|
||||
/// What the contact scores once the bonus is in. A station that is not on
|
||||
/// the list scores what it scored.
|
||||
public int Apply(int points, string call) =>
|
||||
calls.Contains(call.Trim()) switch
|
||||
{
|
||||
false => points,
|
||||
true => how switch
|
||||
{
|
||||
'+' => points + value,
|
||||
'*' => points * value,
|
||||
_ => value,
|
||||
},
|
||||
};
|
||||
|
||||
/// A file that is not there leaves the bonus off rather than stopping the
|
||||
/// contest from opening: the operator can put it in and reload the support
|
||||
/// files.
|
||||
private static IReadOnlyList<string> Lines(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.ReadAllLines(path);
|
||||
}
|
||||
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ namespace Nonemm.Contests.Udc;
|
||||
/// One of a user-defined contest's up-to-three multipliers: where its value
|
||||
/// comes from, which stations may bring it in, and how often it may be counted.
|
||||
///
|
||||
/// Left out: `CallHist`, which counts a value from the call history file, and
|
||||
/// needs that file loaded before a multiplier can be worked out.
|
||||
/// `CallHist` counts a station the call history file lists, so it needs that
|
||||
/// file loaded; without one nothing counts.
|
||||
public sealed class UdcMultiplier
|
||||
{
|
||||
private readonly int index;
|
||||
@@ -106,10 +106,16 @@ public sealed class UdcMultiplier
|
||||
"SGRID" => Truncate(qso.Qso.GridSquare, 6),
|
||||
"FIELD" => Truncate(qso.Qso.GridSquare, 2),
|
||||
"CONTINENT" => qso.Continent,
|
||||
"CALLHIST" => Listed(qso),
|
||||
"FIRSTQSO" => "first",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// `CallHist` makes every station in the call history file a multiplier,
|
||||
/// counted by callsign. N1MM dedups them on the callsign the same way.
|
||||
private static string? Listed(QsoContext qso) =>
|
||||
qso.History.Find(qso.Qso.Call.Text) is null ? null : qso.Qso.Call.Text.ToUpperInvariant();
|
||||
|
||||
private static string? CountryOnContinent(QsoContext qso, string continent) =>
|
||||
continent.Equals(qso.Continent, StringComparison.OrdinalIgnoreCase) ? qso.CountryPrefix : null;
|
||||
|
||||
|
||||
@@ -16,8 +16,12 @@ public sealed class UserDefinedContest : Contest
|
||||
private readonly IReadOnlyList<string> workableStations;
|
||||
private readonly UdcCabrillo cabrillo;
|
||||
private readonly UdcCabrilloFormat cabrilloFormat;
|
||||
private readonly UdcBonusCalls bonusCalls;
|
||||
|
||||
public UserDefinedContest(UdcFile file)
|
||||
/// `supportFolder` is where `BonusPoints2` reads its list of callsigns
|
||||
/// from, which is N1MM's support-files folder. Without it that setting is
|
||||
/// left off.
|
||||
public UserDefinedContest(UdcFile file, string? supportFolder = null)
|
||||
{
|
||||
this.file = file;
|
||||
points = new UdcPoints(file.Text("PointsPerContact", "1"));
|
||||
@@ -28,6 +32,7 @@ public sealed class UserDefinedContest : Contest
|
||||
workableStations = file.List("IsWorkable");
|
||||
cabrillo = new UdcCabrillo(file.Text("CabrilloString"));
|
||||
cabrilloFormat = new UdcCabrilloFormat(file.Number("CabrilloFormat", 1));
|
||||
bonusCalls = new UdcBonusCalls(file.Text("BonusPoints2"), supportFolder);
|
||||
}
|
||||
|
||||
public static UserDefinedContest Load(string path) =>
|
||||
@@ -97,7 +102,8 @@ public sealed class UserDefinedContest : Contest
|
||||
}
|
||||
string sent = SentExchange(qso);
|
||||
double scored = points.For(qso, sent) * pointsMultiplier.For(qso);
|
||||
return (int)Math.Round(scored) + bonusPoints.For(qso, sent);
|
||||
int total = (int)Math.Round(scored) + bonusPoints.For(qso, sent);
|
||||
return bonusCalls.Apply(total, qso.Qso.Call.Text);
|
||||
}
|
||||
|
||||
/// What this station sends: the exchange the operator set up for the
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed class ContestSession
|
||||
Contest = contest;
|
||||
Instance = instance;
|
||||
Me = me;
|
||||
Log = new ContestLog(contest, me, countries, instance.Entry);
|
||||
Log = new ContestLog(contest, me, countries, instance.Entry) { History = History };
|
||||
Log.Restore(store.Qsos(instance.ContestNumber));
|
||||
Editor = new QsoEditor(contest, me, countries);
|
||||
SentNumber = NextSentNumber();
|
||||
|
||||
Reference in New Issue
Block a user