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); } } }