#nullable disable using System.Globalization; using LinqToDB; using LinqToDB.EntityFrameworkCore; using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.TypeReaders; using EllieBot.Db.Models; using EllieBot.Modules.Gambling.Common; using EllieBot.Modules.Gambling.Common.Connect4; using EllieBot.Modules.Games.Quests; using EllieBot.Modules.Patronage; namespace EllieBot.Modules.Gambling.Services; public class GamblingService : IEService, IReadyExecutor { public ConcurrentDictionary<(ulong, ulong), RollDuelGame> Duels { get; } = new(); public ConcurrentDictionary<ulong, Connect4Game> Connect4Games { get; } = new(); private readonly DbService _db; private readonly DiscordSocketClient _client; private readonly IBotCache _cache; private readonly GamblingConfigService _gcs; private readonly IPatronageService _ps; private readonly QuestService _quests; private readonly EllieRandom _rng; private static readonly TypedKey<long> _curDecayKey = new("currency:last_decay"); public GamblingService( DbService db, DiscordSocketClient client, IBotCache cache, GamblingConfigService gcs, IPatronageService ps, QuestService quests) { _db = db; _client = client; _cache = cache; _gcs = gcs; _ps = ps; _quests = quests; _rng = new EllieRandom(); } public Task OnReadyAsync() => Task.WhenAll(CurrencyDecayLoopAsync(), TransactionClearLoopAsync()); public string GeneratePassword() { var num = _rng.Next((int)Math.Pow(31, 2), (int)Math.Pow(32, 3)); return new kwum(num).ToString(); } private async Task TransactionClearLoopAsync() { if (_client.ShardId != 0) return; using var timer = new PeriodicTimer(TimeSpan.FromHours(1)); while (await timer.WaitForNextTickAsync()) { try { var lifetime = _gcs.Data.Currency.TransactionsLifetime; if (lifetime <= 0) continue; var now = DateTime.UtcNow; var days = TimeSpan.FromDays(lifetime); await using var uow = _db.GetDbContext(); await uow.Set<CurrencyTransaction>() .DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days); } catch (Exception ex) { Log.Warning(ex, "An unexpected error occurred in transactions cleanup loop: {ErrorMessage}", ex.Message); } } } private async Task CurrencyDecayLoopAsync() { if (_client.ShardId != 0) return; using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5)); while (await timer.WaitForNextTickAsync()) { try { var config = _gcs.Data; var maxDecay = config.Decay.MaxDecay; if (config.Decay.Percent is <= 0 or > 1 || maxDecay < 0) continue; var now = DateTime.UtcNow; await using var uow = _db.GetDbContext(); var result = await _cache.GetAsync(_curDecayKey); if (result.TryPickT0(out var bin, out _) && (now - DateTime.FromBinary(bin) < TimeSpan.FromHours(config.Decay.HourInterval))) { continue; } Log.Information(""" --- Decaying users' currency --- | decay: {ConfigDecayPercent}% | max: {MaxDecay} | threshold: {DecayMinTreshold} """, config.Decay.Percent * 100, maxDecay, config.Decay.MinThreshold); if (maxDecay == 0) maxDecay = int.MaxValue; var decay = (double)config.Decay.Percent; await uow.Set<DiscordUser>() .Where(x => x.CurrencyAmount > config.Decay.MinThreshold && x.UserId != _client.CurrentUser.Id) .UpdateAsync(old => new() { CurrencyAmount = maxDecay > Sql.Round((old.CurrencyAmount * decay) - 0.5) ? (long)(old.CurrencyAmount - Sql.Round((old.CurrencyAmount * decay) - 0.5)) : old.CurrencyAmount - maxDecay }); await uow.SaveChangesAsync(); await _cache.AddAsync(_curDecayKey, now.ToBinary()); } catch (Exception ex) { Log.Warning(ex, "An unexpected error occurred in currency decay loop: {ErrorMessage}", ex.Message); } } } private static readonly TypedKey<EconomyResult> _ecoKey = new("ellie:economy"); private static readonly SemaphoreSlim _timelyLock = new(1, 1); private static TypedKey<Dictionary<ulong, long>> _timelyKey = new("timely:claims"); public async Task<TimeSpan?> ClaimTimelyAsync(ulong userId, int period) { if (period == 0) return null; await _timelyLock.WaitAsync(); try { // get the dictionary from the cache or get a new one var dict = (await _cache.GetOrAddAsync(_timelyKey, () => Task.FromResult(new Dictionary<ulong, long>())))!; var now = DateTime.UtcNow; var nowB = now.ToBinary(); // try to get users last claim if (!dict.TryGetValue(userId, out var lastB)) lastB = dict[userId] = now.ToBinary(); var diff = now - DateTime.FromBinary(lastB); // if its now, or too long ago => success if (lastB == nowB || diff > period.Hours()) { // update the cache dict[userId] = nowB; await _cache.AddAsync(_timelyKey, dict); return null; } else { // otherwise return the remaining time return period.Hours() - diff; } } finally { _timelyLock.Release(); } } public bool UserHasTimelyReminder(ulong userId) { var db = _db.GetDbContext(); return db.GetTable<Reminder>() .Any(x => x.UserId == userId && x.Type == ReminderType.Timely); } public async Task RemoveAllTimelyClaimsAsync() => await _cache.RemoveAsync(_timelyKey); private string N(long amount) => CurrencyHelper.N(amount, CultureInfo.InvariantCulture, _gcs.Data.Currency.Sign); public async Task<(long val, string msg)> GetAmountAndMessage(ulong userId, long baseAmount) { var totalAmount = baseAmount; var gcsData = _gcs.Data; var boostGuilds = gcsData.BoostBonus.GuildIds ?? []; var guildUsers = await boostGuilds .Select(async gid => { try { var guild = _client.GetGuild(gid) as IGuild ?? await _client.Rest.GetGuildAsync(gid, false); var user = await guild.GetUserAsync(gid) ?? await _client.Rest.GetGuildUserAsync(gid, userId); return (guild, user); } catch { return default; } }) .WhenAll(); var userInfo = guildUsers.FirstOrDefault(x => x.user?.PremiumSince is not null); var booster = userInfo != default; if (booster) totalAmount += gcsData.BoostBonus.BaseTimelyBonus; var hasCompletedDailies = await _quests.UserCompletedDailies(userId); if (hasCompletedDailies) totalAmount = (long)(1.5 * totalAmount); var patron = await _ps.GetPatronAsync(userId); var percentBonus = (_ps.PercentBonus(patron) / 100f); totalAmount += (long)(totalAmount * percentBonus); var msg = $"**{N(baseAmount)}** base reward\n\n"; if (boostGuilds.Count > 0) { if (booster) msg += $"✅ *+{N(gcsData.BoostBonus.BaseTimelyBonus)} bonus for boosting {userInfo.guild}!*\n"; else msg += $"❌ *+0 bonus for boosting {userInfo.guild}*\n"; } if (_ps.GetConfig().IsEnabled) { if (percentBonus > float.Epsilon) msg += $"✅ *+{percentBonus:P0} bonus for the [Patreon](https://patreon.com/elliebot) pledge! <:hart:746995901758832712>*\n"; else msg += $"❌ *+0 bonus for the [Patreon](https://patreon.com/elliebot) pledge*\n"; } if (hasCompletedDailies) { msg += $"✅ *+50% bonus for completing daily quests*\n"; } else { msg += $"❌ *+0 bonus for completing daily quests*\n"; } return (totalAmount, msg); } }