diff --git a/src/NadekoBot/Bot.cs b/src/NadekoBot/Bot.cs index cfb8518e1..7d92cc5ac 100644 --- a/src/NadekoBot/Bot.cs +++ b/src/NadekoBot/Bot.cs @@ -145,7 +145,8 @@ namespace NadekoBot svcs.Scan(scan => scan .FromAssemblyOf() - .AddClasses(classes => classes.AssignableToAny( + .AddClasses(classes => classes + .AssignableToAny( // services typeof(INService), diff --git a/src/NadekoBot/Common/ImageUrls.cs b/src/NadekoBot/Common/ImageUrls.cs index 72061cfde..f10f2acaf 100644 --- a/src/NadekoBot/Common/ImageUrls.cs +++ b/src/NadekoBot/Common/ImageUrls.cs @@ -1,9 +1,11 @@ using System; +using NadekoBot.Common.Yml; namespace NadekoBot.Common { public class ImageUrls { + [Comment("DO NOT CHANGE")] public int Version { get; set; } = 2; public CoinData Coins { get; set; } diff --git a/src/NadekoBot/Common/OldImageUrls.cs b/src/NadekoBot/Common/OldImageUrls.cs new file mode 100644 index 000000000..b98224df5 --- /dev/null +++ b/src/NadekoBot/Common/OldImageUrls.cs @@ -0,0 +1,49 @@ +using System; + +namespace NadekoBot.Common +{ + public class OldImageUrls + { + public int Version { get; set; } = 2; + + public CoinData Coins { get; set; } + public Uri[] Currency { get; set; } + public Uri[] Dice { get; set; } + public RategirlData Rategirl { get; set; } + public XpData Xp { get; set; } + + //new + public RipData Rip { get; set; } + public SlotData Slots { get; set; } + + public class RipData + { + public Uri Bg { get; set; } + public Uri Overlay { get; set; } + } + + public class SlotData + { + public Uri[] Emojis { get; set; } + public Uri[] Numbers { get; set; } + public Uri Bg { get; set; } + } + + public class CoinData + { + public Uri[] Heads { get; set; } + public Uri[] Tails { get; set; } + } + + public class RategirlData + { + public Uri Matrix { get; set; } + public Uri Dot { get; set; } + } + + public class XpData + { + public Uri Bg { get; set; } + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index e9a23903d..dfda73b75 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -149,7 +149,7 @@ namespace NadekoBot.Modules.CustomReactions await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() .WithDescription($"#{id}") .AddField(GetText(strs.trigger), found.Trigger.TrimTo(1024)) - .AddField(GetText(strs.response), (found.Response + "\n```css\n" + found.Response).TrimTo(1020) + "```") + .AddField(GetText(strs.response), found.Response.TrimTo(1000).Replace("](", "]\\(")) ).ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Gambling/Common/GamblingError.cs b/src/NadekoBot/Modules/Gambling/Common/GamblingError.cs new file mode 100644 index 000000000..4732cb67e --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/Common/GamblingError.cs @@ -0,0 +1,8 @@ +namespace NadekoBot.Modules.Gambling +{ + public enum GamblingError + { + None, + NotEnough + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/Slot/SlotGame.cs b/src/NadekoBot/Modules/Gambling/Common/Slot/SlotGame.cs new file mode 100644 index 000000000..2140b640b --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/Common/Slot/SlotGame.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using NadekoBot.Common; + +namespace NadekoBot.Modules.Gambling.Common.Slot +{ + public class SlotGame + { + public class Result + { + public float Multiplier { get; } + public int[] Rolls { get; } + + public Result(float multiplier, int[] rolls) + { + Multiplier = multiplier; + Rolls = rolls; + } + } + + private static readonly Random _rng = new NadekoRandom(); + + public SlotGame() + { + } + + public Result Spin() + { + var rolls = new int[] { _rng.Next(0, 6), _rng.Next(0, 6), _rng.Next(0, 6) }; + var multi = 0; + + if (rolls.All(x => x == 5)) + multi = 30; + else if (rolls.All(x => x == rolls[0])) + multi = 10; + else if (rolls.Count(x => x == 5) == 2) + multi = 4; + else if (rolls.Any(x => x == 5)) + multi = 1; + + return new Result(multi, rolls); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/SlotResponse.cs b/src/NadekoBot/Modules/Gambling/Common/SlotResponse.cs new file mode 100644 index 000000000..a9ecfaafe --- /dev/null +++ b/src/NadekoBot/Modules/Gambling/Common/SlotResponse.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace NadekoBot.Modules.Gambling +{ + public class SlotResponse + { + public float Multiplier { get; set; } + public long Won { get; set; } + public List Rolls { get; set; } = new List(); + public GamblingError Error { get; set; } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs b/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs index 250722e64..28b39912d 100644 --- a/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs +++ b/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs @@ -22,15 +22,12 @@ namespace NadekoBot.Modules.Gambling { private readonly IImageCache _images; private readonly ICurrencyService _cs; - private readonly DbService _db; private static readonly NadekoRandom rng = new NadekoRandom(); - public FlipCoinCommands(IDataCache data, ICurrencyService cs, DbService db, - GamblingConfigService gss) : base(gss) + public FlipCoinCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gss) : base(gss) { _images = data.LocalImages; _cs = cs; - _db = db; } [NadekoCommand, Aliases] diff --git a/src/NadekoBot/Modules/Gambling/Services/GamblingService.cs b/src/NadekoBot/Modules/Gambling/Services/GamblingService.cs index 7d62ea0b1..8286f3599 100644 --- a/src/NadekoBot/Modules/Gambling/Services/GamblingService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/GamblingService.cs @@ -6,12 +6,14 @@ using NadekoBot.Modules.Gambling.Common.WheelOfFortune; using Newtonsoft.Json; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using NadekoBot.Common; using NadekoBot.Db; +using NadekoBot.Modules.Gambling.Common.Slot; using NadekoBot.Modules.Gambling.Services; using Serilog; @@ -82,6 +84,41 @@ WHERE CurrencyAmount > {config.Decay.MinThreshold} AND UserId!={_client.CurrentU }, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); } } + + public async Task SlotAsync(ulong userId, long amount) + { + var takeRes = await _cs.RemoveAsync(userId, "Slot Machine", amount, true); + + if (!takeRes) + { + return new SlotResponse + { + Error = GamblingError.NotEnough + }; + } + + var game = new SlotGame(); + var result = game.Spin(); + long won = 0; + + if (result.Multiplier > 0) + { + won = (long)(result.Multiplier * amount); + + await _cs.AddAsync(userId, $"Slot Machine x{result.Multiplier}", won, true); + } + + var toReturn = new SlotResponse + { + Multiplier = result.Multiplier, + Won = won, + }; + + toReturn.Rolls.AddRange(result.Rolls); + + return toReturn; + } + public struct EconomyResult { diff --git a/src/NadekoBot/Modules/Gambling/SlotCommands.cs b/src/NadekoBot/Modules/Gambling/SlotCommands.cs index 4c0f0fde5..cf525f7c8 100644 --- a/src/NadekoBot/Modules/Gambling/SlotCommands.cs +++ b/src/NadekoBot/Modules/Gambling/SlotCommands.cs @@ -9,12 +9,16 @@ using System.Threading; using System.Threading.Tasks; using NadekoBot.Common; using NadekoBot.Common.Attributes; +using NadekoBot.Db.Models; using NadekoBot.Modules.Gambling.Services; using NadekoBot.Modules.Gambling.Common; using NadekoBot.Services; +using SixLabors.Fonts; using Image = SixLabors.ImageSharp.Image; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; namespace NadekoBot.Modules.Gambling { @@ -33,12 +37,16 @@ namespace NadekoBot.Modules.Gambling //thanks to judge for helping me with this private readonly IImageCache _images; - private readonly ICurrencyService _cs; + private FontProvider _fonts; + private readonly DbService _db; - public SlotCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gamb) : base(gamb) + public SlotCommands(IDataCache data, + FontProvider fonts, DbService db, + GamblingConfigService gamb) : base(gamb) { _images = data.LocalImages; - _cs = cs; + _fonts = fonts; + _db = db; } public sealed class SlotMachine @@ -140,92 +148,115 @@ namespace NadekoBot.Modules.Gambling [NadekoCommand, Aliases] public async Task Slot(ShmartNumber amount) { - if (!_runningUsers.Add(ctx.User.Id)) + if (!_runningUsers.Add(ctx.User.Id)) return; - try - { - if (!await CheckBetMandatory(amount).ConfigureAwait(false)) - return; - const int maxAmount = 9999; - if (amount > maxAmount) - { - await ReplyErrorLocalizedAsync(strs.max_bet_limit(maxAmount + CurrencySign)); - return; - } + + try + { + if (!await CheckBetMandatory(amount).ConfigureAwait(false)) + return; + + await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false); - if (!await _cs.RemoveAsync(ctx.User, "Slot Machine", amount, false, gamble: true).ConfigureAwait(false)) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - Interlocked.Add(ref _totalBet, amount.Value); - using (var bgImage = Image.Load(_images.SlotBackground)) - { - var result = SlotMachine.Pull(); - int[] numbers = result.Numbers; + var result = await _service.SlotAsync(ctx.User.Id, amount); - for (int i = 0; i < 3; i++) - { - using (var randomImage = Image.Load(_images.SlotEmojis[numbers[i]])) - { - bgImage.Mutate(x => x.DrawImage(randomImage, new Point(95 + 142 * i, 330), new GraphicsOptions())); - } - } + if (result.Error != GamblingError.None) + { + if (result.Error == GamblingError.NotEnough) + { + await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); + } - var won = amount * result.Multiplier; - var printWon = won; - var n = 0; - do - { - var digit = (int)(printWon % 10); - using (var img = Image.Load(_images.SlotNumbers[digit])) - { - bgImage.Mutate(x => x.DrawImage(img, new Point(230 - n * 16, 462), new GraphicsOptions())); - } - n++; - } while ((printWon /= 10) != 0); + return; + } - var printAmount = amount; - n = 0; - do - { - var digit = (int)(printAmount % 10); - using (var img = Image.Load(_images.SlotNumbers[digit])) - { - bgImage.Mutate(x => x.DrawImage(img, new Point(395 - n * 16, 462), new GraphicsOptions())); - } - n++; - } while ((printAmount /= 10) != 0); + Interlocked.Add(ref _totalBet, amount); + Interlocked.Add(ref _totalPaidOut, result.Won); - var msg = GetText(strs.better_luck); - if (result.Multiplier != 0) - { - await _cs.AddAsync(ctx.User, $"Slot Machine x{result.Multiplier}", amount * result.Multiplier, false, gamble: true).ConfigureAwait(false); - Interlocked.Add(ref _totalPaidOut, amount * result.Multiplier); - if (result.Multiplier == 1) - msg = GetText(strs.slot_single(CurrencySign, 1)); - else if (result.Multiplier == 4) - msg = GetText(strs.slot_two(CurrencySign, 4)); - else if (result.Multiplier == 10) - msg = GetText(strs.slot_three(10)); - else if (result.Multiplier == 30) - msg = GetText(strs.slot_jackpot(30)); - } + long ownedAmount; + using (var uow = _db.GetDbContext()) + { + ownedAmount = uow.Set() + .FirstOrDefault(x => x.UserId == ctx.User.Id) + ?.CurrencyAmount ?? 0; + } - using (var imgStream = bgImage.ToStream()) - { - await ctx.Channel.SendFileAsync(imgStream, "result.png", ctx.User.Mention + " " + msg + $"\n`{GetText(strs.slot_bet)}:`{amount} `{GetText(strs.won)}:` {amount * result.Multiplier}{CurrencySign}").ConfigureAwait(false); - } - } - } - finally - { - var _ = Task.Run(async () => - { - await Task.Delay(1000).ConfigureAwait(false); - _runningUsers.Remove(ctx.User.Id); - }); - } + using (var bgImage = Image.Load(_images.SlotBackground, out var format)) + { + var numbers = new int[3]; + result.Rolls.CopyTo(numbers, 0); + + bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions + { + TextOptions = new TextOptions() + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + WrapTextWidth = 140, + } + }, result.Won.ToString(), _fonts.DottyFont.CreateFont(65), SixLabors.ImageSharp.Color.Red, + new PointF(227, 92))); + + bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions + { + TextOptions = new TextOptions() + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + WrapTextWidth = 135, + } + }, amount.ToString(), _fonts.DottyFont.CreateFont(50), SixLabors.ImageSharp.Color.Red, + new PointF(129, 472))); + + bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions + { + TextOptions = new TextOptions() + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + WrapTextWidth = 135, + } + }, ownedAmount.ToString(), _fonts.DottyFont.CreateFont(50), SixLabors.ImageSharp.Color.Red, + new PointF(325, 472))); + //sw.PrintLap("drew red text"); + + for (var i = 0; i < 3; i++) + { + using (var img = Image.Load(_images.SlotEmojis[numbers[i]])) + { + bgImage.Mutate(x => x.DrawImage(img, new Point(148 + 105 * i, 217), 1f)); + } + } + + var msg = GetText(strs.better_luck); + if (result.Multiplier > 0) + { + if (result.Multiplier == 1f) + msg = GetText(strs.slot_single(CurrencySign, 1)); + else if (result.Multiplier == 4f) + msg = GetText(strs.slot_two(CurrencySign, 4)); + else if (result.Multiplier == 10f) + msg = GetText(strs.slot_three(10)); + else if (result.Multiplier == 30f) + msg = GetText(strs.slot_jackpot(30)); + } + + using (var imgStream = bgImage.ToStream()) + { + await ctx.Channel.SendFileAsync(imgStream, + filename: "result.png", + text: Format.Bold(ctx.User.ToString()) + " " + msg).ConfigureAwait(false); + } + } + } + finally + { + var _ = Task.Run(async () => + { + await Task.Delay(1000).ConfigureAwait(false); + _runningUsers.Remove(ctx.User.Id); + }); + } } } } diff --git a/src/NadekoBot/Modules/Utility/QuoteCommands.cs b/src/NadekoBot/Modules/Utility/QuoteCommands.cs index e2e2bcb79..2e256be2a 100644 --- a/src/NadekoBot/Modules/Utility/QuoteCommands.cs +++ b/src/NadekoBot/Modules/Utility/QuoteCommands.cs @@ -115,9 +115,7 @@ namespace NadekoBot.Modules.Utility .WithOkColor() .WithTitle(GetText(strs.quote_id($"#{data.Id}"))) .AddField(GetText(strs.trigger), data.Keyword) - .AddField(GetText(strs.response), data.Text.Length > 1000 - ? GetText(strs.redacted_too_long) - : Format.Sanitize(data.Text)) + .AddField(GetText(strs.response), Format.Sanitize(data.Text).Replace("](", "]\\(")) .WithFooter(GetText(strs.created_by($"{data.AuthorName} ({data.AuthorId})"))) ).ConfigureAwait(false); } diff --git a/src/NadekoBot/Services/Common/ImageLoader.cs b/src/NadekoBot/Services/Common/ImageLoader.cs deleted file mode 100644 index 44046b537..000000000 --- a/src/NadekoBot/Services/Common/ImageLoader.cs +++ /dev/null @@ -1,141 +0,0 @@ -using Newtonsoft.Json.Linq; -using StackExchange.Redis; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Serilog; - -namespace NadekoBot.Services.Common -{ - public class ImageLoader - { - private readonly HttpClient _http; - private readonly ConnectionMultiplexer _con; - - public Func GetKey { get; } - - private IDatabase _db => _con.GetDatabase(); - - private readonly List>> uriTasks = new List>>(); - - public ImageLoader(HttpClient http, ConnectionMultiplexer con, Func getKey) - { - _http = http; - _con = con; - GetKey = getKey; - } - - private async Task GetImageData(Uri uri) - { - if (uri.IsFile) - { - try - { - var bytes = await File.ReadAllBytesAsync(uri.LocalPath); - return bytes; - } - catch (Exception ex) - { - Log.Warning(ex, "Failed reading image bytes"); - return null; - } - } - else - { - return await _http.GetByteArrayAsync(uri); - } - } - - async Task HandleJArray(JArray arr, string key) - { - var tasks = arr.Where(x => x.Type == JTokenType.String) - .Select(async x => - { - try - { - return await GetImageData((Uri)x).ConfigureAwait(false); - } - catch - { - Log.Error("Error retreiving image for key {Key}: {Data}", key, x); - return null; - } - }); - - byte[][] vals = Array.Empty(); - vals = await Task.WhenAll(tasks).ConfigureAwait(false); - if (vals.Any(x => x is null)) - vals = vals.Where(x => x != null).ToArray(); - - await _db.KeyDeleteAsync(GetKey(key)).ConfigureAwait(false); - await _db.ListRightPushAsync(GetKey(key), - vals.Where(x => x != null) - .Select(x => (RedisValue)x) - .ToArray()).ConfigureAwait(false); - - if (arr.Count != vals.Length) - { - Log.Information("{2}/{1} URIs for the key '{0}' have been loaded. Some of the supplied URIs are either unavailable or invalid.", key, arr.Count, vals.Count()); - } - } - - async Task> HandleUri(Uri uri, string key) - { - try - { - RedisValue data = await GetImageData(uri).ConfigureAwait(false); - return new KeyValuePair(GetKey(key), data); - } - catch - { - Log.Information("Setting '{0}' image failed. The URI you provided is either unavailable or invalid.", key.ToLowerInvariant()); - return new KeyValuePair("", ""); - } - } - - Task HandleJObject(JObject obj, string parent = "") - { - string GetParentString() - { - if (string.IsNullOrWhiteSpace(parent)) - return ""; - else - return parent + "_"; - } - List tasks = new List(); - Task t; - // go through all of the kvps in the object - foreach (var kvp in obj) - { - // if it's a JArray, resole it using jarray method which will - // return task aka an array of all images' bytes - if (kvp.Value.Type == JTokenType.Array) - { - t = HandleJArray((JArray)kvp.Value, GetParentString() + kvp.Key); - tasks.Add(t); - } - else if (kvp.Value.Type == JTokenType.String) - { - var uriTask = HandleUri((Uri)kvp.Value, GetParentString() + kvp.Key); - uriTasks.Add(uriTask); - } - else if (kvp.Value.Type == JTokenType.Object) - { - t = HandleJObject((JObject)kvp.Value, GetParentString() + kvp.Key); - tasks.Add(t); - } - } - return Task.WhenAll(tasks); - } - - public async Task LoadAsync(JObject obj) - { - await HandleJObject(obj).ConfigureAwait(false); - var results = await Task.WhenAll(uriTasks).ConfigureAwait(false); - await _db.StringSetAsync(results.Where(x => x.Key != "").ToArray()).ConfigureAwait(false); - } - } -} diff --git a/src/NadekoBot/Services/Common/RedisImageArray.cs b/src/NadekoBot/Services/Common/RedisImageArray.cs deleted file mode 100644 index 5accb5008..000000000 --- a/src/NadekoBot/Services/Common/RedisImageArray.cs +++ /dev/null @@ -1,52 +0,0 @@ -using StackExchange.Redis; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace NadekoBot.Services.Common -{ - public sealed class RedisImageArray : IReadOnlyList - { - public byte[] this[int index] - { - get - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - - return _con.GetDatabase().ListGetByIndex(_key, index); - } - } - - public int Count => _data.IsValueCreated - ? _data.Value.Length - : (int)_con.GetDatabase().ListLength(_key); - - private readonly ConnectionMultiplexer _con; - private readonly string _key; - - private readonly Lazy _data; - - public RedisImageArray(string key, ConnectionMultiplexer con) - { - _con = con; - _key = key; - _data = new Lazy(() => _con.GetDatabase().ListRange(_key).Select(x => (byte[])x).ToArray(), true); - } - - public IEnumerator GetEnumerator() - { - var actualData = _data.Value; - for (int i = 0; i < actualData.Length; i++) - { - yield return actualData[i]; - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _data.Value.GetEnumerator(); - } - } -} diff --git a/src/NadekoBot/Services/Impl/FontProvider.cs b/src/NadekoBot/Services/Impl/FontProvider.cs index 1ef29a47a..f922e725b 100644 --- a/src/NadekoBot/Services/Impl/FontProvider.cs +++ b/src/NadekoBot/Services/Impl/FontProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; namespace NadekoBot.Services { @@ -48,8 +49,11 @@ namespace NadekoBot.Services } RipFont = NotoSans.CreateFont(20, FontStyle.Bold); + DottyFont = FallBackFonts.First(x => x.Name == "dotty"); } + public FontFamily DottyFont { get; } + public FontFamily UniSans { get; } public FontFamily NotoSans { get; } //public FontFamily Emojis { get; } diff --git a/src/NadekoBot/Services/Impl/ImagesService.cs b/src/NadekoBot/Services/Impl/ImagesService.cs deleted file mode 100644 index b438bbcb9..000000000 --- a/src/NadekoBot/Services/Impl/ImagesService.cs +++ /dev/null @@ -1,181 +0,0 @@ -using NadekoBot.Common; -using NadekoBot.Services.Common; -using NadekoBot.Extensions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StackExchange.Redis; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using NadekoBot.Common.ModuleBehaviors; -using Serilog; - -namespace NadekoBot.Services -{ - public sealed class RedisImagesCache : IImageCache, IReadyExecutor - { - private readonly ConnectionMultiplexer _con; - private readonly IBotCredentials _creds; - private readonly HttpClient _http; - - private IDatabase _db => _con.GetDatabase(); - - private const string _basePath = "data/"; - private const string _cardsPath = "data/images/cards"; - - public ImageUrls ImageUrls { get; private set; } - - public IReadOnlyList Heads => GetByteArrayData(ImageKey.Coins_Heads); - - public IReadOnlyList Tails => GetByteArrayData(ImageKey.Coins_Tails); - - public IReadOnlyList Dice => GetByteArrayData(ImageKey.Dice); - - public IReadOnlyList SlotEmojis => GetByteArrayData(ImageKey.Slots_Emojis); - - public IReadOnlyList SlotNumbers => GetByteArrayData(ImageKey.Slots_Numbers); - - public IReadOnlyList Currency => GetByteArrayData(ImageKey.Currency); - - public byte[] SlotBackground => GetByteData(ImageKey.Slots_Bg); - - public byte[] RategirlMatrix => GetByteData(ImageKey.Rategirl_Matrix); - - public byte[] RategirlDot => GetByteData(ImageKey.Rategirl_Dot); - - public byte[] XpBackground => GetByteData(ImageKey.Xp_Bg); - - public byte[] Rip => GetByteData(ImageKey.Rip_Bg); - - public byte[] RipOverlay => GetByteData(ImageKey.Rip_Overlay); - - public byte[] GetCard(string key) - { - return _con.GetDatabase().StringGet(GetKey("card_" + key)); - } - - public enum ImageKey - { - Coins_Heads, - Coins_Tails, - Dice, - Slots_Bg, - Slots_Numbers, - Slots_Emojis, - Rategirl_Matrix, - Rategirl_Dot, - Xp_Bg, - Rip_Bg, - Rip_Overlay, - Currency, - } - - public async Task OnReadyAsync() - { - if (await AllKeysExist()) - return; - - await Reload(); - } - - public RedisImagesCache(ConnectionMultiplexer con, IBotCredentials creds) - { - _con = con; - _creds = creds; - _http = new HttpClient(); - - ImageUrls = JsonConvert.DeserializeObject( - File.ReadAllText(Path.Combine(_basePath, "images.json"))); - } - - public async Task AllKeysExist() - { - try - { - var results = await Task.WhenAll(Enum.GetNames(typeof(ImageKey)) - .Select(x => x.ToLowerInvariant()) - .Select(x => _db.KeyExistsAsync(GetKey(x)))) - .ConfigureAwait(false); - - var cardsExist = await Task.WhenAll(GetAllCardNames() - .Select(x => "card_" + x) - .Select(x => _db.KeyExistsAsync(GetKey(x)))) - .ConfigureAwait(false); - - var num = results.Where(x => !x).Count(); - - return results.All(x => x) && cardsExist.All(x => x); - } - catch (Exception ex) - { - Log.Warning(ex, "Error checking for Image keys"); - return false; - } - } - - public async Task Reload() - { - try - { - var sw = Stopwatch.StartNew(); - var obj = JObject.Parse( - File.ReadAllText(Path.Combine(_basePath, "images.json"))); - - ImageUrls = obj.ToObject(); - var t = new ImageLoader(_http, _con, GetKey) - .LoadAsync(obj); - - var loadCards = Task.Run(async () => - { - await _db.StringSetAsync(Directory.GetFiles(_cardsPath) - .ToDictionary( - x => GetKey("card_" + Path.GetFileNameWithoutExtension(x)), - x => (RedisValue)File.ReadAllBytes(x)) // loads them and creates pairs to store in redis - .ToArray()) - .ConfigureAwait(false); - }); - - await Task.WhenAll(t, loadCards).ConfigureAwait(false); - - sw.Stop(); - Log.Information($"Images reloaded in {sw.Elapsed.TotalSeconds:F2}s"); - } - catch (Exception ex) - { - Log.Error(ex, "Error reloading image service"); - throw; - } - } - - private IEnumerable GetAllCardNames(bool showExtension = false) - { - return Directory.GetFiles(_cardsPath) // gets all cards from the cards folder - .Select(x => showExtension - ? Path.GetFileName(x) - : Path.GetFileNameWithoutExtension(x)); // gets their names - } - - public RedisKey GetKey(string key) - { - return $"{_creds.RedisKey()}_localimg_{key.ToLowerInvariant()}"; - } - - public byte[] GetByteData(string key) - { - return _db.StringGet(GetKey(key)); - } - - public byte[] GetByteData(ImageKey key) => GetByteData(key.ToString()); - - public RedisImageArray GetByteArrayData(string key) - { - return new RedisImageArray(GetKey(key), _con); - } - - public RedisImageArray GetByteArrayData(ImageKey key) => GetByteArrayData(key.ToString()); - } -} \ No newline at end of file diff --git a/src/NadekoBot/Services/Impl/RedisImageExtensions.cs b/src/NadekoBot/Services/Impl/RedisImageExtensions.cs new file mode 100644 index 000000000..952b01158 --- /dev/null +++ b/src/NadekoBot/Services/Impl/RedisImageExtensions.cs @@ -0,0 +1,13 @@ +using System; + +namespace NadekoBot.Services +{ + public static class RedisImageExtensions + { + private const string OldCdnUrl = "nadeko-pictures.nyc3.digitaloceanspaces.com"; + private const string NewCdnUrl = "cdn.nadeko.bot"; + + public static Uri ToNewCdn(this Uri uri) + => new(uri.ToString().Replace(OldCdnUrl, NewCdnUrl)); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Impl/RedisImagesCache.cs b/src/NadekoBot/Services/Impl/RedisImagesCache.cs new file mode 100644 index 000000000..d53037bda --- /dev/null +++ b/src/NadekoBot/Services/Impl/RedisImagesCache.cs @@ -0,0 +1,312 @@ +using NadekoBot.Common; +using NadekoBot.Extensions; +using Newtonsoft.Json; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using NadekoBot.Common.ModuleBehaviors; +using NadekoBot.Common.Yml; +using Serilog; + +namespace NadekoBot.Services +{ + public sealed class RedisImagesCache : IImageCache, IReadyExecutor + { + + private readonly ConnectionMultiplexer _con; + private readonly IBotCredentials _creds; + private readonly HttpClient _http; + private readonly string _imagesPath; + + private IDatabase _db => _con.GetDatabase(); + + private const string _basePath = "data/"; + private const string _cardsPath = "data/images/cards"; + + public ImageUrls ImageUrls { get; private set; } + + public enum ImageKeys + { + CoinHeads, + CoinTails, + Dice, + SlotBg, + SlotEmojis, + SlotNumbers, + Currency, + RategirlMatrix, + RategirlDot, + RipOverlay, + RipBg, + XpBg + } + + public IReadOnlyList Heads + => GetByteArrayData(ImageKeys.CoinHeads); + + public IReadOnlyList Tails + => GetByteArrayData(ImageKeys.CoinTails); + + public IReadOnlyList Dice + => GetByteArrayData(ImageKeys.Dice); + + public IReadOnlyList SlotEmojis + => GetByteArrayData(ImageKeys.SlotEmojis); + + public IReadOnlyList SlotNumbers + => GetByteArrayData(ImageKeys.SlotNumbers); + + public IReadOnlyList Currency + => GetByteArrayData(ImageKeys.Currency); + + public byte[] SlotBackground + => GetByteData(ImageKeys.SlotBg); + + public byte[] RategirlMatrix + => GetByteData(ImageKeys.RategirlMatrix); + + public byte[] RategirlDot + => GetByteData(ImageKeys.RategirlDot); + + public byte[] XpBackground + => GetByteData(ImageKeys.XpBg); + + public byte[] Rip + => GetByteData(ImageKeys.RipBg); + + public byte[] RipOverlay + => GetByteData(ImageKeys.RipOverlay); + + public byte[] GetCard(string key) + { + // since cards are always local for now, don't cache them + return File.ReadAllBytes(Path.Join(_cardsPath, key + ".jpg")); + + } + + public async Task OnReadyAsync() + { + if (await AllKeysExist()) + return; + + await Reload(); + } + + public RedisImagesCache(ConnectionMultiplexer con, IBotCredentials creds) + { + _con = con; + _creds = creds; + _http = new HttpClient(); + _imagesPath = Path.Combine(_basePath, "images.yml"); + + Migrate(); + + ImageUrls = Yaml.Deserializer.Deserialize(File.ReadAllText(_imagesPath)); + } + + private void Migrate() + { + // migrate to yml + if (File.Exists(Path.Combine(_basePath, "images.json"))) + { + var oldFilePath = Path.Combine(_basePath, "images.json"); + var backupFilePath = Path.Combine(_basePath, "images.json.backup"); + + var oldData = JsonConvert.DeserializeObject( + File.ReadAllText(oldFilePath)); + + if (oldData is not null) + { + var newData = new ImageUrls() + { + Coins = new ImageUrls.CoinData() + { + Heads = oldData.Coins.Heads.Length == 1 && + oldData.Coins.Heads[0].ToString() == "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/heads.png" + ? new[] { new Uri("https://cdn.nadeko.bot/coins/heads3.png") } + : oldData.Coins.Heads, + Tails = oldData.Coins.Tails.Length == 1 && + oldData.Coins.Tails[0].ToString() == "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/tails.png" + ? new[] { new Uri("https://cdn.nadeko.bot/coins/tails3.png") } + : oldData.Coins.Tails, + }, + Dice = oldData.Dice.Map(x => x.ToNewCdn()), + Currency = oldData.Currency.Map(x => x.ToNewCdn()), + Rategirl = new ImageUrls.RategirlData() + { + Dot = oldData.Rategirl.Dot.ToNewCdn(), + Matrix = oldData.Rategirl.Matrix.ToNewCdn() + }, + Rip = new ImageUrls.RipData() + { + Bg = oldData.Rip.Bg.ToNewCdn(), + Overlay = oldData.Rip.Overlay.ToNewCdn(), + }, + Slots = new ImageUrls.SlotData() + { + Bg = new Uri("https://cdn.nadeko.bot/slots/slots_bg.png"), + Emojis = new[] + { + "https://cdn.nadeko.bot/slots/0.png", + "https://cdn.nadeko.bot/slots/1.png", + "https://cdn.nadeko.bot/slots/2.png", + "https://cdn.nadeko.bot/slots/3.png", + "https://cdn.nadeko.bot/slots/4.png", + "https://cdn.nadeko.bot/slots/5.png" + }.Map(x => new Uri(x)), + Numbers = new[] + { + "https://cdn.nadeko.bot/other/slots/numbers/0.png", + "https://cdn.nadeko.bot/other/slots/numbers/1.png", + "https://cdn.nadeko.bot/other/slots/numbers/2.png", + "https://cdn.nadeko.bot/other/slots/numbers/3.png", + "https://cdn.nadeko.bot/other/slots/numbers/4.png", + "https://cdn.nadeko.bot/other/slots/numbers/5.png", + "https://cdn.nadeko.bot/other/slots/numbers/6.png", + "https://cdn.nadeko.bot/other/slots/numbers/7.png", + "https://cdn.nadeko.bot/other/slots/numbers/8.png", + "https://cdn.nadeko.bot/other/slots/numbers/9.png" + }.Map(x => new Uri(x)), + }, + Xp = new ImageUrls.XpData() + { + Bg = oldData.Xp.Bg.ToNewCdn(), + }, + Version = 2, + }; + + File.Move(oldFilePath, backupFilePath, true); + File.WriteAllText(_imagesPath, Yaml.Serializer.Serialize(newData)); + } + } + } + + public async Task Reload() + { + ImageUrls = Yaml.Deserializer.Deserialize(await File.ReadAllTextAsync(_imagesPath)); + foreach (var key in GetAllKeys()) + { + switch (key) + { + case ImageKeys.CoinHeads: + await Load(key, ImageUrls.Coins.Heads); + break; + case ImageKeys.CoinTails: + await Load(key, ImageUrls.Coins.Tails); + break; + case ImageKeys.Dice: + await Load(key, ImageUrls.Dice); + break; + case ImageKeys.SlotBg: + await Load(key, ImageUrls.Slots.Bg); + break; + case ImageKeys.SlotEmojis: + await Load(key, ImageUrls.Slots.Emojis); + break; + case ImageKeys.SlotNumbers: + await Load(key, ImageUrls.Slots.Numbers); + break; + case ImageKeys.Currency: + await Load(key, ImageUrls.Currency); + break; + case ImageKeys.RategirlMatrix: + await Load(key, ImageUrls.Rategirl.Matrix); + break; + case ImageKeys.RategirlDot: + await Load(key, ImageUrls.Rategirl.Dot); + break; + case ImageKeys.RipOverlay: + await Load(key, ImageUrls.Rip.Overlay); + break; + case ImageKeys.RipBg: + await Load(key, ImageUrls.Rip.Bg); + break; + case ImageKeys.XpBg: + await Load(key, ImageUrls.Xp.Bg); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + private async Task Load(ImageKeys key, Uri uri) + { + var data = await GetImageData(uri); + if (data is null) + return; + + await _db.StringSetAsync(GetRedisKey(key), data); + } + + private async Task Load(ImageKeys key, Uri[] uris) + { + await _db.KeyDeleteAsync(GetRedisKey(key)); + var imageData = await Task.WhenAll(uris.Select(GetImageData)); + var vals = imageData + .Where(x => x is not null) + .Select(x => (RedisValue)x) + .ToArray(); + + await _db.ListRightPushAsync(GetRedisKey(key), vals); + + if (uris.Length != vals.Length) + { + Log.Information("{Loaded}/{Max} URIs for the key '{ImageKey}' have been loaded.\n" + + "Some of the supplied URIs are either unavailable or invalid.", + vals.Length, uris.Length, key); + } + } + + private async Task GetImageData(Uri uri) + { + if (uri.IsFile) + { + try + { + var bytes = await File.ReadAllBytesAsync(uri.LocalPath); + return bytes; + } + catch (Exception ex) + { + Log.Warning(ex, "Failed reading image bytes from uri: {Uri}", uri.ToString()); + return null; + } + } + + try + { + return await _http.GetByteArrayAsync(uri); + } + catch (Exception ex) + { + Log.Warning(ex, "Image url you provided is not a valid image: {Uri}", uri.ToString()); + return null; + } + } + + private async Task AllKeysExist() + { + var tasks = await Task.WhenAll(GetAllKeys() + .Select(x => _db.KeyExistsAsync(GetRedisKey(x)))); + + return tasks.All(exist => exist); + } + + private IEnumerable GetAllKeys() => + Enum.GetValues(); + + private byte[][] GetByteArrayData(ImageKeys key) + => _db.ListRange(GetRedisKey(key)).Map(x => (byte[])x); + + private byte[] GetByteData(ImageKeys key) + => _db.StringGet(GetRedisKey(key)); + + private RedisKey GetRedisKey(ImageKeys key) + => _creds.RedisKey() + "_image_" + key; + } +} \ No newline at end of file diff --git a/src/NadekoBot/data/fonts/dotty.ttf b/src/NadekoBot/data/fonts/dotty.ttf new file mode 100644 index 000000000..4ce2814cf Binary files /dev/null and b/src/NadekoBot/data/fonts/dotty.ttf differ diff --git a/src/NadekoBot/data/images.json b/src/NadekoBot/data/images.json deleted file mode 100644 index 0d2043a7b..000000000 --- a/src/NadekoBot/data/images.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "Version": 3, - "Coins": { - "Heads": [ - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/heads.png" - ], - "Tails": [ - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/tails.png" - ] - }, - "Currency": [ - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/currency/0.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/currency/1.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/currency/2.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/currency/3.png" - ], - "Dice": [ - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/0.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/1.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/2.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/3.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/4.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/5.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/6.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/7.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/8.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/9.png" - ], - "Rategirl": { - "Matrix": "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rategirl/matrix.png", - "Dot": "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rategirl/dot.png" - }, - "Xp": { - "Bg": "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/xp/bg.png" - }, - "Rip": { - "Bg": "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rip/rip.png", - "Overlay": "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rip/overlay.png" - }, - "Slots": { - "Emojis": [ - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/emojis/0.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/emojis/1.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/emojis/2.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/emojis/3.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/emojis/4.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/emojis/5.png" - ], - "Numbers": [ - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/0.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/1.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/2.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/3.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/4.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/5.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/6.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/7.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/8.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/9.png" - ], - "Bg": "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/bg.png" - } -} \ No newline at end of file diff --git a/src/NadekoBot/data/images.yml b/src/NadekoBot/data/images.yml new file mode 100644 index 000000000..fd2b33150 --- /dev/null +++ b/src/NadekoBot/data/images.yml @@ -0,0 +1,50 @@ +# DO NOT CHANGE +version: 2 +coins: + heads: + - https://cdn.nadeko.bot/coins/heads3.png + tails: + - https://cdn.nadeko.bot/coins/tails3.png +currency: + - https://cdn.nadeko.bot/other/currency/0.jpg + - https://cdn.nadeko.bot/other/currency/1.jpg + - https://cdn.nadeko.bot/other/currency/2.jpg +dice: + - https://cdn.nadeko.bot/other/dice/0.png + - https://cdn.nadeko.bot/other/dice/1.png + - https://cdn.nadeko.bot/other/dice/2.png + - https://cdn.nadeko.bot/other/dice/3.png + - https://cdn.nadeko.bot/other/dice/4.png + - https://cdn.nadeko.bot/other/dice/5.png + - https://cdn.nadeko.bot/other/dice/6.png + - https://cdn.nadeko.bot/other/dice/7.png + - https://cdn.nadeko.bot/other/dice/8.png + - https://cdn.nadeko.bot/other/dice/9.png +rategirl: + matrix: https://cdn.nadeko.bot/other/rategirl/matrix.png + dot: https://cdn.nadeko.bot/other/rategirl/dot.png +xp: + bg: https://cdn.nadeko.bot/other/xp/bg.png +rip: + bg: https://cdn.nadeko.bot/other/rip/rip.png + overlay: https://cdn.nadeko.bot/other/rip/overlay.png +slots: + emojis: + - https://cdn.nadeko.bot/slots/0.png + - https://cdn.nadeko.bot/slots/1.png + - https://cdn.nadeko.bot/slots/2.png + - https://cdn.nadeko.bot/slots/3.png + - https://cdn.nadeko.bot/slots/4.png + - https://cdn.nadeko.bot/slots/5.png + numbers: + - https://cdn.nadeko.bot/other/slots/numbers/0.png + - https://cdn.nadeko.bot/other/slots/numbers/1.png + - https://cdn.nadeko.bot/other/slots/numbers/2.png + - https://cdn.nadeko.bot/other/slots/numbers/3.png + - https://cdn.nadeko.bot/other/slots/numbers/4.png + - https://cdn.nadeko.bot/other/slots/numbers/5.png + - https://cdn.nadeko.bot/other/slots/numbers/6.png + - https://cdn.nadeko.bot/other/slots/numbers/7.png + - https://cdn.nadeko.bot/other/slots/numbers/8.png + - https://cdn.nadeko.bot/other/slots/numbers/9.png + bg: https://cdn.nadeko.bot/slots/slots_bg.png diff --git a/src/NadekoBot/data/images_backup.json b/src/NadekoBot/data/images_backup.json deleted file mode 100644 index a3814eb85..000000000 --- a/src/NadekoBot/data/images_backup.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "Version": 1, - "Coins": { - "Heads": [ - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/heads.png" - ], - "Tails": [ - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/tails.png" - ] - }, - "Currency": [ - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/currency/0.jpg", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/currency/1.jpg", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/currency/2.jpg" - ], - "Dice": [ - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/0.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/1.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/2.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/3.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/4.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/5.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/6.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/7.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/8.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/dice/9.png" - ], - "Rategirl": { - "Matrix": "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rategirl/matrix.png", - "Dot": "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rategirl/dot.png" - }, - "Xp": { - "Bg": "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/xp/bg.png" - }, - "Rip": { - "Bg": "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rip/rip.png", - "Overlay": "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rip/overlay.png" - }, - "Slots": { - "Emojis": [ - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/emojis/0.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/emojis/1.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/emojis/2.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/emojis/3.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/emojis/4.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/emojis/5.png" - ], - "Numbers": [ - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/0.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/1.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/2.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/3.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/4.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/5.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/6.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/7.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/8.png", - "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/numbers/9.png" - ], - "Bg": "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/slots/bg.png" - } -} \ No newline at end of file