Files
2026-10-14 06:59:51 +02:00

75 lines
3.5 KiB
C#

using System;
using System.Collections.Generic;
using Campus_live_rewrite.api.server.Classes;
namespace Campus_live_rewrite.api.server
{
/// <summary>
/// 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.
/// </summary>
public static class GameRewardGate
{
private static readonly object _lock = new();
private static readonly Dictionary<ulong, DateTime> _lastComplete = new();
private static readonly Dictionary<ulong, DateTime> _dailyGrace = new();
/// <summary>Call when a FirstActivityOfDay claim was ACCEPTED (it was the day's first).</summary>
public static void NoteDailyAccepted(ulong playerId)
{
lock (_lock) _dailyGrace[playerId] = DateTime.UtcNow;
}
/// <summary>
/// Whether a PostGameActivity may be paid now. Marks the clock when it may.
/// <paramref name="why"/> explains a refusal.
/// </summary>
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;
}
}
/// <summary>The gate for a request of <paramref name="type"/>; only game completes are limited here.</summary>
public static bool Allow(ulong playerId, GameRewardType type, out string why)
{
why = "";
return type != GameRewardType.PostGameActivity || TryGameComplete(playerId, out why);
}
}
}