namespace Aeshnidae.Trials;
///
/// The weekly trophy. At the award moment (Settings.AwardDay / AwardHour, checked on
/// the world tick once a minute) the fastest recorded run of the week that just
/// ended, per Trial, is written to `aeshnidae_trial_award` - one row per (week,
/// landblock), which is what makes the check idempotent - and then delivered: a
/// trophy item named and inscribed for the win, and a lump of experience. A winner
/// who is offline gets both at their next login (Patches.PostEnterWorld ->
/// ); a winner whose pack is full keeps the row pending and
/// is told. The winners are posted to Discord. The week last processed is kept in
/// lastaward.txt beside the dll so a restart in the award hour does not re-run it -
/// the unique key would refuse anyway, but the file saves the queries.
///
public static class Awards
{
private static string StampPath => Path.Combine(Mod.ModPath, "lastaward.txt");
private static DateTime _lastCheckUtc = DateTime.MinValue;
private static string? _lastAwarded;
public static string LastResult { get; private set; } = "never run";
/// Once a minute: has an award moment passed since the last week processed?
public static void MaybeAward()
{
var now = DateTime.UtcNow;
if ((now - _lastCheckUtc).TotalSeconds < 60)
return;
_lastCheckUtc = now;
if (!Mod.Settings.Enabled || !TrialDb.Ready)
return;
var week = Weeks.PreviousKey(DateTime.Now, Mod.Settings);
_lastAwarded ??= ReadStamp();
if (_lastAwarded == week)
return;
var lines = AwardWeek(week, "the clock");
WriteStamp(week);
foreach (var line in lines)
ModManager.Log($"[{Mod.Name}] {line}");
}
private static string ReadStamp()
{
try { return File.Exists(StampPath) ? File.ReadAllText(StampPath).Trim() : ""; }
catch { return ""; }
}
private static void WriteStamp(string week)
{
_lastAwarded = week;
try { File.WriteAllText(StampPath, week); }
catch (Exception ex) { ModManager.Log($"[{Mod.Name}] could not write {StampPath}: {ex.Message}", ModManager.LogLevel.Warn); }
}
///
/// Awards one week: every Trial's fastest complete run of that week that has not
/// been awarded yet. Returns one line per Trial saying what happened. Safe to run
/// again for the same week - already-awarded Trials are skipped.
///
public static List AwardWeek(string week, string by)
{
var s = Mod.Settings;
var lines = new List();
var posts = new List();
foreach (var (landblock, trial) in Mod.Trials.OrderBy(kv => kv.Key))
{
var name = Mod.TrialName(landblock);
try
{
if (TrialDb.WeekAwarded(week, landblock))
{
lines.Add($"{name}: week {week} already awarded");
continue;
}
var best = TrialDb.WeekBest(week, landblock);
if (best is null)
{
lines.Add($"{name}: no recorded run in week {week}");
continue;
}
var award = new AwardRow
{
Week = week,
Landblock = landblock,
CharacterId = best.CharacterId,
CharacterName = best.CharacterName,
Tier = best.Tier,
Seconds = best.Seconds,
RunId = best.Id,
Xp = s.WeeklyAwardXp,
AwardedAt = DateTime.UtcNow,
};
if (!TrialDb.InsertAward(award))
{
lines.Add($"{name}: week {week} already awarded (raced)");
continue;
}
var online = PlayerManager.GetOnlinePlayer(best.CharacterId);
var delivered = online is not null && Deliver(online, award);
lines.Add($"{name}: week {week} trophy to {best.CharacterName} - {best.Time} at tier {best.Tier} - " +
(delivered ? "delivered" : online is null ? "pending (offline)" : "pending (could not take it)"));
posts.Add($"**{name}** - {best.CharacterName}, {best.Time} (tier {best.Tier}{(best.Kills > 0 ? $", {best.Kills} kills" : "")})");
}
catch (Exception ex)
{
lines.Add($"{name}: award failed: {ex.Message}");
ModManager.Log($"[{Mod.Name}] award for {name} week {week} failed: {ex}", ModManager.LogLevel.Error);
}
}
LastResult = $"{week} by {by} at {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC: {posts.Count} trophy(ies)";
if (posts.Count > 0 && s.AnnounceAwards)
{
var text = new StringBuilder();
text.AppendLine($"**Trial trophies - week of {Weeks.Describe(week, s)}** ({week}). The fastest clear of each dungeon this week, any tier: a trophy and {s.WeeklyAwardXp:N0} experience.");
foreach (var p in posts)
text.AppendLine(p);
text.Append("`/trial` in game for the boards.");
Discord.Post(text.ToString());
}
try { Feed.Write(); } catch { /* logged inside */ }
return lines;
}
/// Hands a trophy and its experience to a player in the world. False leaves the row pending.
public static bool Deliver(Player player, AwardRow award)
{
var s = Mod.Settings;
var name = Mod.TrialName(award.Landblock);
WorldObject? trophy = null;
try
{
trophy = WorldObjectFactory.CreateNewWorldObject(s.TrophyWcid);
if (trophy is null)
{
ModManager.Log($"[{Mod.Name}] trophy weenie {s.TrophyWcid} does not exist - is ledger 2026-09-21_001 applied? {award.CharacterName}'s trophy stays pending",
ModManager.LogLevel.Error);
return false;
}
trophy.Name = $"Trial Trophy: {name}";
trophy.Inscription = $"Cleared in {award.Time}, week of {Weeks.Describe(award.Week, s)}, tier {award.Tier}";
trophy.ScribeName = "Aelrynth";
trophy.LongDesc = $"The week's fastest clear of {name}: {award.CharacterName}, {award.Time} at tier {award.Tier}, the week of {Weeks.Describe(award.Week, s)}.";
if (!player.TryCreateInInventoryWithNetworking(trophy))
{
trophy.Destroy();
player.SendMessage($"You have won the week's Trial trophy for {name} ({award.Time}) but cannot carry it. Make room and log in again to receive it.");
return false;
}
}
catch (Exception ex)
{
try { trophy?.Destroy(); } catch { }
ModManager.Log($"[{Mod.Name}] could not create {award.CharacterName}'s trophy: {ex}", ModManager.LogLevel.Error);
return false;
}
try
{
if (award.Xp > 0)
player.GrantXP(award.Xp, XpType.Quest, ShareType.None);
TrialDb.MarkClaimed(award.Id, DateTime.UtcNow);
award.ClaimedAt = DateTime.UtcNow;
player.SendMessage($"Trial trophy: the week's fastest clear of {name} was yours - {award.Time} at tier {award.Tier}. " +
$"A trophy is in your pack{(award.Xp > 0 ? $" and {award.Xp:N0} experience is yours" : "")}.");
ModManager.Log($"[{Mod.Name}] delivered week {award.Week} trophy for {name} to {player.Name}");
return true;
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] delivering {award.CharacterName}'s award failed after the trophy: {ex}", ModManager.LogLevel.Error);
return false;
}
}
/// Everything this character has won and not yet received.
public static void ClaimPending(Player player)
{
try
{
if (player?.Session is null || !TrialDb.Ready)
return;
foreach (var award in TrialDb.PendingFor(player.Guid.Full))
{
if (!Deliver(player, award))
break; // a full pack: leave the rest for next time
}
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] pending claim for {player?.Name} failed: {ex.Message}", ModManager.LogLevel.Warn);
}
}
}