2024-09-21 00:44:21 +12:00
|
|
|
#nullable disable
|
2025-03-25 14:58:02 +13:00
|
|
|
using System.Globalization;
|
2024-09-21 00:44:21 +12:00
|
|
|
using LinqToDB;
|
|
|
|
using LinqToDB.EntityFrameworkCore;
|
|
|
|
using EllieBot.Common.ModuleBehaviors;
|
2025-04-05 19:44:45 +13:00
|
|
|
using EllieBot.Common.TypeReaders;
|
2024-09-21 00:44:21 +12:00
|
|
|
using EllieBot.Db.Models;
|
|
|
|
using EllieBot.Modules.Gambling.Common;
|
|
|
|
using EllieBot.Modules.Gambling.Common.Connect4;
|
2025-03-28 21:13:53 +13:00
|
|
|
using EllieBot.Modules.Games.Quests;
|
2025-03-25 14:58:02 +13:00
|
|
|
using EllieBot.Modules.Patronage;
|
2024-09-21 00:44:21 +12:00
|
|
|
|
|
|
|
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;
|
2025-03-25 14:58:02 +13:00
|
|
|
private readonly GamblingConfigService _gcs;
|
|
|
|
private readonly IPatronageService _ps;
|
2025-03-28 21:13:53 +13:00
|
|
|
private readonly QuestService _quests;
|
2024-11-02 01:31:06 +13:00
|
|
|
private readonly EllieRandom _rng;
|
2024-09-21 00:44:21 +12:00
|
|
|
|
|
|
|
private static readonly TypedKey<long> _curDecayKey = new("currency:last_decay");
|
|
|
|
|
|
|
|
public GamblingService(
|
|
|
|
DbService db,
|
|
|
|
DiscordSocketClient client,
|
|
|
|
IBotCache cache,
|
2025-03-25 14:58:02 +13:00
|
|
|
GamblingConfigService gcs,
|
2025-03-28 21:13:53 +13:00
|
|
|
IPatronageService ps,
|
|
|
|
QuestService quests)
|
2024-09-21 00:44:21 +12:00
|
|
|
{
|
|
|
|
_db = db;
|
|
|
|
_client = client;
|
|
|
|
_cache = cache;
|
2025-03-25 14:58:02 +13:00
|
|
|
_gcs = gcs;
|
|
|
|
_ps = ps;
|
2025-03-28 21:13:53 +13:00
|
|
|
_quests = quests;
|
2024-11-02 01:31:06 +13:00
|
|
|
_rng = new EllieRandom();
|
2024-09-21 00:44:21 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
public Task OnReadyAsync()
|
|
|
|
=> Task.WhenAll(CurrencyDecayLoopAsync(), TransactionClearLoopAsync());
|
|
|
|
|
2024-11-02 01:31:06 +13:00
|
|
|
|
|
|
|
public string GeneratePassword()
|
|
|
|
{
|
|
|
|
var num = _rng.Next((int)Math.Pow(31, 2), (int)Math.Pow(32, 3));
|
|
|
|
return new kwum(num).ToString();
|
|
|
|
}
|
|
|
|
|
2024-09-21 00:44:21 +12:00
|
|
|
private async Task TransactionClearLoopAsync()
|
|
|
|
{
|
|
|
|
if (_client.ShardId != 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
|
|
|
|
while (await timer.WaitForNextTickAsync())
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2025-03-25 14:58:02 +13:00
|
|
|
var lifetime = _gcs.Data.Currency.TransactionsLifetime;
|
2024-09-21 00:44:21 +12:00
|
|
|
if (lifetime <= 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
var days = TimeSpan.FromDays(lifetime);
|
|
|
|
await using var uow = _db.GetDbContext();
|
|
|
|
await uow.Set<CurrencyTransaction>()
|
2025-03-25 14:58:02 +13:00
|
|
|
.DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days);
|
2024-09-21 00:44:21 +12:00
|
|
|
}
|
|
|
|
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
|
|
|
|
{
|
2025-03-25 14:58:02 +13:00
|
|
|
var config = _gcs.Data;
|
2024-09-21 00:44:21 +12:00
|
|
|
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("""
|
2024-11-02 01:31:06 +13:00
|
|
|
--- Decaying users' currency ---
|
|
|
|
| decay: {ConfigDecayPercent}%
|
|
|
|
| max: {MaxDecay}
|
|
|
|
| threshold: {DecayMinTreshold}
|
|
|
|
""",
|
2024-09-21 00:44:21 +12:00
|
|
|
config.Decay.Percent * 100,
|
|
|
|
maxDecay,
|
|
|
|
config.Decay.MinThreshold);
|
|
|
|
|
|
|
|
if (maxDecay == 0)
|
|
|
|
maxDecay = int.MaxValue;
|
|
|
|
|
|
|
|
var decay = (double)config.Decay.Percent;
|
|
|
|
await uow.Set<DiscordUser>()
|
2025-03-25 14:58:02 +13:00
|
|
|
.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
|
|
|
|
});
|
2024-09-21 00:44:21 +12:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-26 12:07:52 +13:00
|
|
|
private static readonly TypedKey<EconomyResult> _ecoKey = new("ellie:economy");
|
2024-09-21 00:44:21 +12:00
|
|
|
|
|
|
|
private static readonly SemaphoreSlim _timelyLock = new(1, 1);
|
|
|
|
|
|
|
|
private static TypedKey<Dictionary<ulong, long>> _timelyKey
|
|
|
|
= new("timely:claims");
|
|
|
|
|
2025-03-25 14:58:02 +13:00
|
|
|
|
2024-09-21 00:44:21 +12:00
|
|
|
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();
|
2024-11-02 01:31:06 +13:00
|
|
|
return db.GetTable<Reminder>()
|
|
|
|
.Any(x => x.UserId == userId
|
|
|
|
&& x.Type == ReminderType.Timely);
|
2025-03-25 14:58:02 +13:00
|
|
|
}
|
2024-09-21 00:44:21 +12:00
|
|
|
|
|
|
|
public async Task RemoveAllTimelyClaimsAsync()
|
|
|
|
=> await _cache.RemoveAsync(_timelyKey);
|
2025-03-25 14:58:02 +13:00
|
|
|
|
|
|
|
private string N(long amount)
|
|
|
|
=> CurrencyHelper.N(amount, CultureInfo.InvariantCulture, _gcs.Data.Currency.Sign);
|
|
|
|
|
2025-04-05 19:44:45 +13:00
|
|
|
public async Task<(long val, string msg)> GetAmountAndMessage(ulong userId, long baseAmount)
|
2025-03-25 14:58:02 +13:00
|
|
|
{
|
2025-04-05 19:44:45 +13:00
|
|
|
var totalAmount = baseAmount;
|
2025-03-25 14:58:02 +13:00
|
|
|
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)
|
2025-04-05 19:44:45 +13:00
|
|
|
totalAmount += gcsData.BoostBonus.BaseTimelyBonus;
|
2025-03-25 14:58:02 +13:00
|
|
|
|
2025-03-28 21:13:53 +13:00
|
|
|
var hasCompletedDailies = await _quests.UserCompletedDailies(userId);
|
|
|
|
|
|
|
|
if (hasCompletedDailies)
|
2025-04-05 19:44:45 +13:00
|
|
|
totalAmount = (long)(1.5 * totalAmount);
|
2025-03-28 21:13:53 +13:00
|
|
|
|
2025-03-25 14:58:02 +13:00
|
|
|
var patron = await _ps.GetPatronAsync(userId);
|
|
|
|
var percentBonus = (_ps.PercentBonus(patron) / 100f);
|
|
|
|
|
2025-04-05 19:44:45 +13:00
|
|
|
totalAmount += (long)(totalAmount * percentBonus);
|
2025-03-25 14:58:02 +13:00
|
|
|
|
2025-04-05 19:44:45 +13:00
|
|
|
var msg = $"**{N(baseAmount)}** base reward\n\n";
|
2025-03-25 14:58:02 +13:00
|
|
|
if (boostGuilds.Count > 0)
|
|
|
|
{
|
|
|
|
if (booster)
|
2025-03-30 17:37:34 +13:00
|
|
|
msg += $"✅ *+{N(gcsData.BoostBonus.BaseTimelyBonus)} bonus for boosting {userInfo.guild}!*\n";
|
2025-03-25 14:58:02 +13:00
|
|
|
else
|
2025-03-30 17:37:34 +13:00
|
|
|
msg += $"❌ *+0 bonus for boosting {userInfo.guild}*\n";
|
2025-03-25 14:58:02 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
if (_ps.GetConfig().IsEnabled)
|
|
|
|
{
|
|
|
|
if (percentBonus > float.Epsilon)
|
|
|
|
msg +=
|
2025-03-30 17:37:34 +13:00
|
|
|
$"✅ *+{percentBonus:P0} bonus for the [Patreon](https://patreon.com/elliebot) pledge! <:hart:746995901758832712>*\n";
|
2025-03-25 14:58:02 +13:00
|
|
|
else
|
2025-03-30 17:37:34 +13:00
|
|
|
msg += $"❌ *+0 bonus for the [Patreon](https://patreon.com/elliebot) pledge*\n";
|
2025-03-25 14:58:02 +13:00
|
|
|
}
|
2025-04-05 19:44:45 +13:00
|
|
|
|
2025-03-28 21:13:53 +13:00
|
|
|
if (hasCompletedDailies)
|
|
|
|
{
|
2025-03-30 17:37:34 +13:00
|
|
|
msg += $"✅ *+50% bonus for completing daily quests*\n";
|
2025-03-28 21:13:53 +13:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2025-03-30 17:37:34 +13:00
|
|
|
msg += $"❌ *+0 bonus for completing daily quests*\n";
|
2025-03-28 21:13:53 +13:00
|
|
|
}
|
2025-03-25 14:58:02 +13:00
|
|
|
|
2025-04-05 19:44:45 +13:00
|
|
|
|
|
|
|
return (totalAmount, msg);
|
2025-03-25 14:58:02 +13:00
|
|
|
}
|
2024-09-21 00:44:21 +12:00
|
|
|
}
|