Abstract away cache. 2 implementations: redis and memory

This commit is contained in:
Kwoth
2022-06-23 13:07:45 +00:00
parent 1716c69132
commit 210da263ad
75 changed files with 11525 additions and 1547 deletions

View File

@@ -19,8 +19,8 @@ public partial class Gambling
private static readonly char[] _fateRolls = { '-', ' ', '+' };
private readonly IImageCache _images;
public DiceRollCommands(IDataCache data)
=> _images = data.LocalImages;
public DiceRollCommands(ImageCache images)
=> _images = images;
[Cmd]
public async partial Task Roll()
@@ -31,10 +31,10 @@ public partial class Gambling
var num1 = gen / 10;
var num2 = gen % 10;
using var img1 = GetDice(num1);
using var img2 = GetDice(num2);
using var img1 = await GetDiceAsync(num1);
using var img2 = await GetDiceAsync(num2);
using var img = new[] { img1, img2 }.Merge(out var format);
await using var ms = img.ToStream(format);
await using var ms = await img.ToStreamAsync(format);
await ctx.Channel.SendFileAsync(ms,
$"dice.{format.FileExtensions.First()}",
Format.Bold(ctx.User.ToString()) + " " + GetText(strs.dice_rolled(Format.Code(gen.ToString()))));
@@ -96,7 +96,7 @@ public partial class Gambling
else
toInsert = dice.Count;
dice.Insert(toInsert, GetDice(randomNumber));
dice.Insert(toInsert, await GetDiceAsync(randomNumber));
values.Insert(toInsert, randomNumber);
}
@@ -195,20 +195,19 @@ public partial class Gambling
await ReplyConfirmLocalizedAsync(strs.dice_rolled(Format.Bold(rolled.ToString())));
}
private Image<Rgba32> GetDice(int num)
private async Task<Image<Rgba32>> GetDiceAsync(int num)
{
if (num is < 0 or > 10)
throw new ArgumentOutOfRangeException(nameof(num));
if (num == 10)
{
var images = _images.Dice;
using var imgOne = Image.Load(images[1]);
using var imgZero = Image.Load(images[0]);
using var imgOne = Image.Load(await _images.GetDiceAsync(1));
using var imgZero = Image.Load(await _images.GetDiceAsync(0));
return new[] { imgOne, imgZero }.Merge();
}
return Image.Load(_images.Dice[num]);
return Image.Load(await _images.GetDiceAsync(num));
}
}
}

View File

@@ -14,8 +14,8 @@ public partial class Gambling
private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new();
private readonly IImageCache _images;
public DrawCommands(IDataCache data)
=> _images = data.LocalImages;
public DrawCommands(IImageCache images)
=> _images = images;
private async Task<(Stream ImageStream, string ToSend)> InternalDraw(int num, ulong? guildId = null)
{
@@ -43,7 +43,8 @@ public partial class Gambling
var currentCard = cards.Draw();
cardObjects.Add(currentCard);
images.Add(Image.Load(_images.GetCard(currentCard.ToString().ToLowerInvariant().Replace(' ', '_'))));
var cardName = currentCard.ToString().ToLowerInvariant().Replace(' ', '_');
images.Add(Image.Load(await File.ReadAllBytesAsync($"data/images/cards/{cardName}.png")));
}
using var img = images.Merge();

View File

@@ -25,11 +25,17 @@ public partial class Gambling
private static readonly NadekoRandom _rng = new();
private readonly IImageCache _images;
private readonly ICurrencyService _cs;
private readonly ImagesConfig _ic;
public FlipCoinCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gss)
public FlipCoinCommands(
IImageCache images,
ImagesConfig ic,
ICurrencyService cs,
GamblingConfigService gss)
: base(gss)
{
_images = data.LocalImages;
_ic = ic;
_images = images;
_cs = cs;
}
@@ -47,8 +53,8 @@ public partial class Gambling
var imgs = new Image<Rgba32>[count];
for (var i = 0; i < count; i++)
{
var headsArr = _images.Heads[_rng.Next(0, _images.Heads.Count)];
var tailsArr = _images.Tails[_rng.Next(0, _images.Tails.Count)];
var headsArr = await _images.GetHeadsImageAsync();
var tailsArr = await _images.GetTailsImageAsync();
if (_rng.Next(0, 10) < 5)
{
imgs[i] = Image.Load(headsArr);
@@ -94,7 +100,7 @@ public partial class Gambling
BetFlipGuess result;
Uri imageToSend;
var coins = _images.ImageUrls.Coins;
var coins = _ic.Data.Coins;
if (_rng.Next(0, 1000) <= 499)
{
imageToSend = coins.Heads[_rng.Next(0, coins.Heads.Length)];

View File

@@ -38,7 +38,6 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly DbService _db;
private readonly ICurrencyService _cs;
private readonly IDataCache _cache;
private readonly DiscordSocketClient _client;
private readonly NumberFormatInfo _enUsCulture;
private readonly DownloadTracker _tracker;
@@ -51,7 +50,6 @@ public partial class Gambling : GamblingModule<GamblingService>
public Gambling(
DbService db,
ICurrencyService currency,
IDataCache cache,
DiscordSocketClient client,
DownloadTracker tracker,
GamblingConfigService configService,
@@ -61,7 +59,6 @@ public partial class Gambling : GamblingModule<GamblingService>
{
_db = db;
_cs = currency;
_cache = cache;
_client = client;
_bank = bank;
_ps = ps;
@@ -124,7 +121,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
if (_cache.AddTimelyClaim(ctx.User.Id, period) is { } rem)
if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } rem)
{
var now = DateTime.UtcNow;
var relativeTag = TimestampTag.FromDateTime(now.Add(rem), TimestampTagStyles.Relative);
@@ -145,7 +142,7 @@ public partial class Gambling : GamblingModule<GamblingService>
[OwnerOnly]
public async partial Task TimelyReset()
{
_cache.RemoveAllTimelyClaims();
await _service.RemoveAllTimelyClaimsAsync();
await ReplyConfirmLocalizedAsync(strs.timely_reset);
}

View File

@@ -1,16 +1,13 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using NadekoBot.Db.Models;
using NadekoBot.Migrations;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Common.Connect4;
using NadekoBot.Modules.Gambling.Common.Slot;
using NadekoBot.Modules.Gambling.Common.WheelOfFortune;
using Newtonsoft.Json;
namespace NadekoBot.Modules.Gambling.Services;
@@ -22,7 +19,7 @@ public class GamblingService : INService, IReadyExecutor
private readonly ICurrencyService _cs;
private readonly Bot _bot;
private readonly DiscordSocketClient _client;
private readonly IDataCache _cache;
private readonly IBotCache _cache;
private readonly GamblingConfigService _gss;
public GamblingService(
@@ -30,7 +27,7 @@ public class GamblingService : INService, IReadyExecutor
Bot bot,
ICurrencyService cs,
DiscordSocketClient client,
IDataCache cache,
IBotCache cache,
GamblingConfigService gss)
{
_db = db;
@@ -73,6 +70,7 @@ public class GamblingService : INService, IReadyExecutor
}
}
private static readonly TypedKey<long> _curDecayKey = new("currency:last_decay");
private async Task CurrencyDecayLoopAsync()
{
if (_bot.Client.ShardId != 0)
@@ -88,11 +86,16 @@ public class GamblingService : INService, IReadyExecutor
if (config.Decay.Percent is <= 0 or > 1 || maxDecay < 0)
continue;
var now = DateTime.UtcNow;
await using var uow = _db.GetDbContext();
var lastCurrencyDecay = _cache.GetLastCurrencyDecay();
var result = await _cache.GetAsync(_curDecayKey);
if (DateTime.UtcNow - lastCurrencyDecay < TimeSpan.FromHours(config.Decay.HourInterval))
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}
@@ -115,8 +118,9 @@ public class GamblingService : INService, IReadyExecutor
: old.CurrencyAmount - maxDecay
});
_cache.SetLastCurrencyDecay();
await uow.SaveChangesAsync();
await _cache.AddAsync(_curDecayKey, now.ToBinary());
}
catch (Exception ex)
{
@@ -161,60 +165,100 @@ public class GamblingService : INService, IReadyExecutor
return toReturn;
}
private static readonly TypedKey<EconomyResult> _ecoKey = new("nadeko:economy");
public async Task<EconomyResult> GetEconomyAsync()
{
if (_cache.TryGetEconomy(out var data))
{
try
var data = await _cache.GetOrAddAsync(_ecoKey,
async () =>
{
return JsonConvert.DeserializeObject<EconomyResult>(data);
}
catch { }
}
await using var uow = _db.GetDbContext();
var cash = uow.DiscordUser.GetTotalCurrency();
var onePercent = uow.DiscordUser.GetTopOnePercentCurrency(_client.CurrentUser.Id);
decimal planted = uow.PlantedCurrency.AsQueryable().Sum(x => x.Amount);
var waifus = uow.WaifuInfo.GetTotalValue();
var bot = uow.DiscordUser.GetUserCurrency(_client.CurrentUser.Id);
decimal bank = await uow.GetTable<BankUser>()
.SumAsyncLinqToDB(x => x.Balance);
decimal cash;
decimal onePercent;
decimal planted;
decimal waifus;
decimal bank;
long bot;
var result = new EconomyResult
{
Cash = cash,
Planted = planted,
Bot = bot,
Waifus = waifus,
OnePercent = onePercent,
Bank = bank
};
using (var uow = _db.GetDbContext())
{
cash = uow.DiscordUser.GetTotalCurrency();
onePercent = uow.DiscordUser.GetTopOnePercentCurrency(_client.CurrentUser.Id);
planted = uow.PlantedCurrency.AsQueryable().Sum(x => x.Amount);
waifus = uow.WaifuInfo.GetTotalValue();
bot = uow.DiscordUser.GetUserCurrency(_client.CurrentUser.Id);
bank = await uow.GetTable<BankUser>()
.SumAsyncLinqToDB(x => x.Balance);
}
return result;
},
TimeSpan.FromMinutes(3));
var result = new EconomyResult
{
Cash = cash,
Planted = planted,
Bot = bot,
Waifus = waifus,
OnePercent = onePercent,
Bank = bank
};
_cache.SetEconomy(JsonConvert.SerializeObject(result));
return result;
return data;
}
public Task<WheelOfFortuneGame.Result> WheelOfFortuneSpinAsync(ulong userId, long bet)
=> new WheelOfFortuneGame(userId, bet, _gss.Data, _cs).SpinAsync();
public struct EconomyResult
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)
{
public decimal Cash { get; set; }
public decimal Planted { get; set; }
public decimal Waifus { get; set; }
public decimal OnePercent { get; set; }
public decimal Bank { get; set; }
public long Bot { get; set; }
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
{
await _timelyLock.WaitAsync();
}
}
public async Task RemoveAllTimelyClaimsAsync()
=> await _cache.RemoveAsync(_timelyKey);
public readonly struct EconomyResult
{
public decimal Cash { get; init; }
public decimal Planted { get; init; }
public decimal Waifus { get; init; }
public decimal OnePercent { get; init; }
public decimal Bank { get; init; }
public long Bot { get; init; }
}
}

View File

@@ -34,7 +34,7 @@ public class PlantPickService : INService, IExecNoCommand
DbService db,
CommandHandler cmd,
IBotStrings strings,
IDataCache cache,
IImageCache images,
FontProvider fonts,
ICurrencyService cs,
CommandHandler cmdHandler,
@@ -43,7 +43,7 @@ public class PlantPickService : INService, IExecNoCommand
{
_db = db;
_strings = strings;
_images = cache.LocalImages;
_images = images;
_fonts = fonts;
_cs = cs;
_cmdHandler = cmdHandler;
@@ -110,30 +110,21 @@ public class PlantPickService : INService, IExecNoCommand
/// <param name="pass">Optional password to add to top left corner.</param>
/// <param name="extension">Extension of the file, defaults to png</param>
/// <returns>Stream of the currency image</returns>
public Stream GetRandomCurrencyImage(string pass, out string extension)
public async Task<(Stream, string)> GetRandomCurrencyImageAsync(string pass)
{
// get a random currency image bytes
var rng = new NadekoRandom();
var curImg = _images.Currency[rng.Next(0, _images.Currency.Count)];
var curImg = await _images.GetCurrencyImageAsync();
if (string.IsNullOrWhiteSpace(pass))
{
// determine the extension
using (_ = Image.Load(curImg, out var format))
{
extension = format.FileExtensions.FirstOrDefault() ?? "png";
}
using var load = _ = Image.Load(curImg, out var format);
// return the image
return curImg.ToStream();
return (curImg.ToStream(), format.FileExtensions.FirstOrDefault() ?? "png");
}
// get the image stream and extension
var (s, ext) = AddPassword(curImg, pass);
// set the out extension parameter to the extension we've got
extension = ext;
// return the image
return s;
return AddPassword(curImg, pass);
}
/// <summary>
@@ -214,10 +205,10 @@ public class PlantPickService : INService, IExecNoCommand
var pw = config.Generation.HasPassword ? GenerateCurrencyPassword().ToUpperInvariant() : null;
IUserMessage sent;
await using (var stream = GetRandomCurrencyImage(pw, out var ext))
{
var (stream, ext) = await GetRandomCurrencyImageAsync(pw);
await using (stream)
sent = await channel.SendFileAsync(stream, $"currency_image.{ext}", toSend);
}
await AddPlantToDatabase(channel.GuildId,
channel.Id,
@@ -278,7 +269,7 @@ public class PlantPickService : INService, IExecNoCommand
if (amount > 0)
// give the picked currency to the user
await _cs.AddAsync(uid, amount, new("currency", "collect"));
uow.SaveChanges();
await uow.SaveChangesAsync();
}
try
@@ -316,11 +307,14 @@ public class PlantPickService : INService, IExecNoCommand
msgToSend += " " + GetText(gid, strs.pick_sn(prefix));
//get the image
await using var stream = GetRandomCurrencyImage(pass, out var ext);
var (stream, ext) = await GetRandomCurrencyImageAsync(pass);
// send it
var msg = await ch.SendFileAsync(stream, $"img.{ext}", msgToSend);
// return sent message's id (in order to be able to delete it when it's picked)
return msg.Id;
await using (stream)
{
var msg = await ch.SendFileAsync(stream, $"img.{ext}", msgToSend);
// return sent message's id (in order to be able to delete it when it's picked)
return msg.Id;
}
}
catch
{

View File

@@ -32,13 +32,13 @@ public partial class Gambling
private readonly DbService _db;
public SlotCommands(
IDataCache data,
ImageCache images,
FontProvider fonts,
DbService db,
GamblingConfigService gamb)
: base(gamb)
{
_images = data.LocalImages;
_images = images;
_fonts = fonts;
_db = db;
}
@@ -130,7 +130,8 @@ public partial class Gambling
?? 0;
}
using (var bgImage = Image.Load<Rgba32>(_images.SlotBackground, out _))
var slotBg = await _images.GetSlotBgAsync();
using (var bgImage = Image.Load<Rgba32>(slotBg, out _))
{
var numbers = new int[3];
result.Rolls.CopyTo(numbers, 0);
@@ -184,7 +185,7 @@ public partial class Gambling
for (var i = 0; i < 3; i++)
{
using var img = Image.Load(_images.SlotEmojis[numbers[i]]);
using var img = Image.Load(await _images.GetSlotEmojiAsync(numbers[i]));
bgImage.Mutate(x => x.DrawImage(img, new Point(148 + (105 * i), 217), 1f));
}
@@ -201,7 +202,7 @@ public partial class Gambling
msg = GetText(strs.slot_jackpot(30));
}
await using (var imgStream = bgImage.ToStream())
await using (var imgStream = await bgImage.ToStreamAsync())
{
await ctx.Channel.SendFileAsync(imgStream,
"result.png",

View File

@@ -1,5 +1,6 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
@@ -14,7 +15,7 @@ public class WaifuService : INService, IReadyExecutor
{
private readonly DbService _db;
private readonly ICurrencyService _cs;
private readonly IDataCache _cache;
private readonly IBotCache _cache;
private readonly GamblingConfigService _gss;
private readonly IBotCredentials _creds;
private readonly DiscordSocketClient _client;
@@ -22,7 +23,7 @@ public class WaifuService : INService, IReadyExecutor
public WaifuService(
DbService db,
ICurrencyService cs,
IDataCache cache,
IBotCache cache,
GamblingConfigService gss,
IBotCredentials creds,
DiscordSocketClient client)
@@ -236,8 +237,13 @@ public class WaifuService : INService, IReadyExecutor
var newAff = target is null ? null : uow.GetOrCreateUser(target);
if (w?.Affinity?.UserId == target?.Id)
{
return (null, false, null);
}
else if (!_cache.TryAddAffinityCooldown(user.Id, out remaining))
remaining = await _cache.GetRatelimitAsync(GetAffinityKey(user.Id),
30.Minutes());
if (remaining is not null)
{
}
else if (w is null)
@@ -294,6 +300,12 @@ public class WaifuService : INService, IReadyExecutor
return uow.WaifuInfo.GetWaifuUserId(ownerId, name);
}
private static TypedKey<long> GetDivorceKey(ulong userId)
=> new($"waifu:divorce_cd:{userId}");
private static TypedKey<long> GetAffinityKey(ulong userId)
=> new($"waifu:affinity:{userId}");
public async Task<(WaifuInfo, DivorceResult, long, TimeSpan?)> DivorceWaifuAsync(IUser user, ulong targetId)
{
DivorceResult result;
@@ -305,10 +317,15 @@ public class WaifuService : INService, IReadyExecutor
w = uow.WaifuInfo.ByWaifuUserId(targetId);
if (w?.Claimer is null || w.Claimer.UserId != user.Id)
result = DivorceResult.NotYourWife;
else if (!_cache.TryAddDivorceCooldown(user.Id, out remaining))
result = DivorceResult.Cooldown;
else
{
remaining = await _cache.GetRatelimitAsync(GetDivorceKey(user.Id), 6.Hours());
if (remaining is TimeSpan rem)
{
result = DivorceResult.Cooldown;
return (w, result, amount, rem);
}
amount = w.Price / 2;
if (w.Affinity?.UserId == user.Id)
@@ -486,13 +503,13 @@ public class WaifuService : INService, IReadyExecutor
.ToList();
}
private static readonly TypedKey<long> _waifuDecayKey = $"waifu:last_decay";
public async Task OnReadyAsync()
{
// only decay waifu values from shard 0
if (_client.ShardId != 0)
return;
var redisKey = $"{_creds.RedisKey()}_last_waifu_decay";
while (true)
{
try
@@ -504,28 +521,31 @@ public class WaifuService : INService, IReadyExecutor
if (multi is < 0f or > 1f || decayInterval < 0)
continue;
var val = await _cache.Redis.GetDatabase().StringGetAsync(redisKey);
if (val != default)
var now = DateTime.UtcNow;
var nowB = now.ToBinary();
var result = await _cache.GetAsync(_waifuDecayKey);
if (result.TryGetValue(out var val))
{
var lastDecay = DateTime.FromBinary((long)val);
var lastDecay = DateTime.FromBinary(val);
var toWait = decayInterval.Hours() - (DateTime.UtcNow - lastDecay);
if (toWait > 0.Hours())
continue;
}
await _cache.Redis.GetDatabase().StringSetAsync(redisKey, DateTime.UtcNow.ToBinary());
await _cache.AddAsync(_waifuDecayKey, nowB);
await using var uow = _db.GetDbContext();
await uow.WaifuInfo
await uow.GetTable<WaifuInfo>()
.Where(x => x.Price > minPrice && x.ClaimerId == null)
.UpdateAsync(old => new()
{
Price = (long)(old.Price * multi)
});
await uow.SaveChangesAsync();
}
catch (Exception ex)
{