using System; using System.Collections.Generic; using Campus_live_rewrite.api.server.Classes; namespace Campus_live_rewrite.api.server { /// /// Rate limit on POST /api/gamerewards/v1/request - the whole request, not just the reward /// box it may offer. /// /// The client asks for a reward twice around the end of a game: rewardType=PostGameActivity /// (the game complete) and, once a day, rewardType=FirstActivityOfDay. Before this the /// currency and XP were paid on EVERY PostGameActivity - only the three-choice box sat behind /// a cooldown - so a client replaying the call farmed tokens and levels. /// /// The rule: /// - FirstActivityOfDay is once per day (PlayerDB.TryClaimDailyReward, unchanged) and is /// always let through when it is the day's first. /// - PostGameActivity is at most ONE per ConfigCampus.GameRewardCompleteCooldownSeconds per /// player ... EXCEPT when the player's daily FirstActivityOfDay was accepted within the /// last GameRewardDailyGraceSeconds: the two arrive together at the end of the day's /// first game, and that game complete must not be eaten by a cooldown the previous game /// started. The grace is consumed by the one game complete it lets through. /// A refused request is answered null (nothing offered) and pays nothing. /// public static class GameRewardGate { private static readonly object _lock = new(); private static readonly Dictionary _lastComplete = new(); private static readonly Dictionary _dailyGrace = new(); /// Call when a FirstActivityOfDay claim was ACCEPTED (it was the day's first). public static void NoteDailyAccepted(ulong playerId) { lock (_lock) _dailyGrace[playerId] = DateTime.UtcNow; } /// /// Whether a PostGameActivity may be paid now. Marks the clock when it may. /// explains a refusal. /// public static bool TryGameComplete(ulong playerId, out string why) { why = ""; int cd = ConfigCampus.GameRewardCompleteCooldownSeconds; var now = DateTime.UtcNow; lock (_lock) { bool graced = _dailyGrace.TryGetValue(playerId, out var daily) && (now - daily).TotalSeconds <= ConfigCampus.GameRewardDailyGraceSeconds; if (graced) { _dailyGrace.Remove(playerId); // one game complete rides on the daily bonus _lastComplete[playerId] = now; return true; } if (cd > 0 && _lastComplete.TryGetValue(playerId, out var last) && (now - last).TotalSeconds < cd) { why = $"game complete {Math.Round((now - last).TotalSeconds)}s after the last one (limit one per {cd}s)"; return false; } _lastComplete[playerId] = now; return true; } } /// The gate for a request of ; only game completes are limited here. public static bool Allow(ulong playerId, GameRewardType type, out string why) { why = ""; return type != GameRewardType.PostGameActivity || TryGameComplete(playerId, out why); } } }