Killed history

This commit is contained in:
Kwoth
2021-09-06 21:29:22 +02:00
commit 7aca29ae8a
950 changed files with 366651 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
using System.Threading.Tasks;
using NadekoBot.Core.Services;
using System.Collections.Concurrent;
using NadekoBot.Modules.Gambling.Common.AnimalRacing;
namespace NadekoBot.Modules.Gambling.Services
{
public class AnimalRaceService : INService, IUnloadableService
{
public ConcurrentDictionary<ulong, AnimalRace> AnimalRaces { get; } = new ConcurrentDictionary<ulong, AnimalRace>();
public Task Unload()
{
foreach (var kvp in AnimalRaces)
{
try { kvp.Value.Dispose(); } catch { }
}
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,11 @@
using NadekoBot.Core.Modules.Gambling.Common.Blackjack;
using NadekoBot.Core.Services;
using System.Collections.Concurrent;
namespace NadekoBot.Core.Modules.Gambling.Services
{
public class BlackJackService : INService
{
public ConcurrentDictionary<ulong, Blackjack> Games { get; } = new ConcurrentDictionary<ulong, Blackjack>();
}
}

View File

@@ -0,0 +1,137 @@
using NadekoBot.Core.Services;
using NadekoBot.Core.Modules.Gambling.Common.Events;
using System.Collections.Concurrent;
using NadekoBot.Modules.Gambling.Common;
using Discord;
using Discord.WebSocket;
using System.Threading.Tasks;
using System;
using NadekoBot.Core.Services.Database.Models;
using System.Net.Http;
using Newtonsoft.Json;
using System.Linq;
using NadekoBot.Core.Modules.Gambling.Services;
using Serilog;
namespace NadekoBot.Modules.Gambling.Services
{
public class CurrencyEventsService : INService
{
public class VoteModel
{
public ulong User { get; set; }
public long Date { get; set; }
}
private readonly DiscordSocketClient _client;
private readonly ICurrencyService _cs;
private readonly IBotCredentials _creds;
private readonly IHttpClientFactory _http;
private readonly GamblingConfigService _configService;
private readonly ConcurrentDictionary<ulong, ICurrencyEvent> _events =
new ConcurrentDictionary<ulong, ICurrencyEvent>();
public CurrencyEventsService(DiscordSocketClient client,
IBotCredentials creds, ICurrencyService cs,
IHttpClientFactory http, GamblingConfigService configService)
{
_client = client;
_cs = cs;
_creds = creds;
_http = http;
_configService = configService;
if (_client.ShardId == 0)
{
Task t = BotlistUpvoteLoop();
}
}
private async Task BotlistUpvoteLoop()
{
if (string.IsNullOrWhiteSpace(_creds.VotesUrl))
return;
while (true)
{
await Task.Delay(TimeSpan.FromHours(1)).ConfigureAwait(false);
await TriggerVoteCheck().ConfigureAwait(false);
}
}
private async Task TriggerVoteCheck()
{
try
{
using (var req = new HttpRequestMessage(HttpMethod.Get, _creds.VotesUrl))
{
if (!string.IsNullOrWhiteSpace(_creds.VotesToken))
req.Headers.Add("Authorization", _creds.VotesToken);
using (var http = _http.CreateClient())
using (var res = await http.SendAsync(req).ConfigureAwait(false))
{
if (!res.IsSuccessStatusCode)
{
Log.Warning("Botlist API not reached.");
return;
}
var resStr = await res.Content.ReadAsStringAsync().ConfigureAwait(false);
var ids = JsonConvert.DeserializeObject<VoteModel[]>(resStr)
.Select(x => x.User)
.Distinct();
await _cs.AddBulkAsync(ids, ids.Select(x => "Voted - <https://discordbots.org/bot/nadeko/vote>"), ids.Select(x => 10L), true).ConfigureAwait(false);
}
}
}
catch (Exception ex)
{
Log.Warning(ex, "Error in TriggerVoteCheck");
}
}
public async Task<bool> TryCreateEventAsync(ulong guildId, ulong channelId, CurrencyEvent.Type type,
EventOptions opts, Func<CurrencyEvent.Type, EventOptions, long, EmbedBuilder> embed)
{
SocketGuild g = _client.GetGuild(guildId);
SocketTextChannel ch = g?.GetChannel(channelId) as SocketTextChannel;
if (ch == null)
return false;
ICurrencyEvent ce;
if (type == CurrencyEvent.Type.Reaction)
{
ce = new ReactionEvent(_client, _cs, g, ch, opts, _configService.Data, embed);
}
else if (type == CurrencyEvent.Type.GameStatus)
{
ce = new GameStatusEvent(_client, _cs, g, ch, opts, embed);
}
else
{
return false;
}
var added = _events.TryAdd(guildId, ce);
if (added)
{
try
{
ce.OnEnded += OnEventEnded;
await ce.StartEvent().ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Warning(ex, "Error starting event");
_events.TryRemove(guildId, out ce);
return false;
}
}
return added;
}
private Task OnEventEnded(ulong gid)
{
_events.TryRemove(gid, out _);
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,88 @@
using System.Threading.Tasks;
using NadekoBot.Core.Services;
using NadekoBot.Core.Modules.Gambling.Common;
using System.Threading;
using System.Linq;
using System.Collections.Generic;
using Discord;
using System;
namespace NadekoBot.Core.Modules.Gambling.Services
{
public class CurrencyRaffleService : INService
{
public enum JoinErrorType
{
NotEnoughCurrency,
AlreadyJoinedOrInvalidAmount
}
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
private readonly DbService _db;
private readonly ICurrencyService _cs;
public Dictionary<ulong, CurrencyRaffleGame> Games { get; } = new Dictionary<ulong, CurrencyRaffleGame>();
public CurrencyRaffleService(DbService db, ICurrencyService cs)
{
_db = db;
_cs = cs;
}
public async Task<(CurrencyRaffleGame, JoinErrorType?)> JoinOrCreateGame(ulong channelId, IUser user, long amount, bool mixed, Func<IUser, long, Task> onEnded)
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
var newGame = false;
if (!Games.TryGetValue(channelId, out var crg))
{
newGame = true;
crg = new CurrencyRaffleGame(mixed
? CurrencyRaffleGame.Type.Mixed
: CurrencyRaffleGame.Type.Normal);
Games.Add(channelId, crg);
}
//remove money, and stop the game if this
// user created it and doesn't have the money
if (!await _cs.RemoveAsync(user.Id, "Currency Raffle Join", amount).ConfigureAwait(false))
{
if (newGame)
Games.Remove(channelId);
return (null, JoinErrorType.NotEnoughCurrency);
}
if (!crg.AddUser(user, amount))
{
await _cs.AddAsync(user.Id, "Curency Raffle Refund", amount).ConfigureAwait(false);
return (null, JoinErrorType.AlreadyJoinedOrInvalidAmount);
}
if (newGame)
{
var _t = Task.Run(async () =>
{
await Task.Delay(60000).ConfigureAwait(false);
await _locker.WaitAsync().ConfigureAwait(false);
try
{
var winner = crg.GetWinner();
var won = crg.Users.Sum(x => x.Amount);
await _cs.AddAsync(winner.DiscordUser.Id, "Currency Raffle Win",
won).ConfigureAwait(false);
Games.Remove(channelId, out _);
var oe = onEnded(winner.DiscordUser, won);
}
catch { }
finally { _locker.Release(); }
});
}
return (crg, null);
}
finally
{
_locker.Release();
}
}
}
}

View File

@@ -0,0 +1,42 @@
using NadekoBot.Core.Common;
using NadekoBot.Core.Common.Configs;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Services;
namespace NadekoBot.Core.Modules.Gambling.Services
{
public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
{
public override string Name { get; } = "gambling";
private const string FilePath = "data/gambling.yml";
private static TypedKey<GamblingConfig> changeKey = new TypedKey<GamblingConfig>("config.gambling.updated");
public GamblingConfigService(IConfigSeria serializer, IPubSub pubSub)
: base(FilePath, serializer, pubSub, changeKey)
{
AddParsedProp("currency.name", gs => gs.Currency.Name, ConfigParsers.String, ConfigPrinters.ToString);
AddParsedProp("currency.sign", gs => gs.Currency.Sign, ConfigParsers.String, ConfigPrinters.ToString);
AddParsedProp("minbet", gs => gs.MinBet, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("maxbet", gs => gs.MaxBet, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("gen.min", gs => gs.Generation.MinAmount, int.TryParse, ConfigPrinters.ToString, val => val >= 1);
AddParsedProp("gen.max", gs => gs.Generation.MaxAmount, int.TryParse, ConfigPrinters.ToString, val => val >= 1);
AddParsedProp("gen.cd", gs => gs.Generation.GenCooldown, int.TryParse, ConfigPrinters.ToString, val => val > 0);
AddParsedProp("gen.chance", gs => gs.Generation.Chance, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0 && val <= 1);
AddParsedProp("gen.has_pw", gs => gs.Generation.HasPassword, bool.TryParse, ConfigPrinters.ToString);
AddParsedProp("bf.multi", gs => gs.BetFlip.Multiplier, decimal.TryParse, ConfigPrinters.ToString, val => val >= 1);
AddParsedProp("waifu.min_price", gs => gs.Waifu.MinPrice, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("waifu.multi.reset", gs => gs.Waifu.Multipliers.WaifuReset, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("waifu.multi.crush_claim", gs => gs.Waifu.Multipliers.CrushClaim, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("waifu.multi.normal_claim", gs => gs.Waifu.Multipliers.NormalClaim, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
AddParsedProp("waifu.multi.divorce_value", gs => gs.Waifu.Multipliers.DivorceNewValue, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
AddParsedProp("waifu.multi.all_gifts", gs => gs.Waifu.Multipliers.AllGiftPrices, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
AddParsedProp("waifu.multi.gift_effect", gs => gs.Waifu.Multipliers.GiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("decay.percent", gs => gs.Decay.Percent, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0 && val <= 1);
AddParsedProp("decay.maxdecay", gs => gs.Decay.MaxDecay, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("decay.threshold", gs => gs.Decay.MinThreshold, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
}
}
}

View File

@@ -0,0 +1,162 @@
using Discord.WebSocket;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Services;
using NadekoBot.Modules.Gambling.Common.Connect4;
using NadekoBot.Modules.Gambling.Common.WheelOfFortune;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Core.Modules.Gambling.Services;
using Serilog;
namespace NadekoBot.Modules.Gambling.Services
{
public class GamblingService : INService
{
private readonly DbService _db;
private readonly ICurrencyService _cs;
private readonly NadekoBot _bot;
private readonly DiscordSocketClient _client;
private readonly IDataCache _cache;
private readonly GamblingConfigService _gss;
public ConcurrentDictionary<(ulong, ulong), RollDuelGame> Duels { get; } = new ConcurrentDictionary<(ulong, ulong), RollDuelGame>();
public ConcurrentDictionary<ulong, Connect4Game> Connect4Games { get; } = new ConcurrentDictionary<ulong, Connect4Game>();
private readonly Timer _decayTimer;
public GamblingService(DbService db, NadekoBot bot, ICurrencyService cs,
DiscordSocketClient client, IDataCache cache, GamblingConfigService gss)
{
_db = db;
_cs = cs;
_bot = bot;
_client = client;
_cache = cache;
_gss = gss;
if (_bot.Client.ShardId == 0)
{
_decayTimer = new Timer(_ =>
{
var config = _gss.Data;
var maxDecay = config.Decay.MaxDecay;
if (config.Decay.Percent <= 0 || config.Decay.Percent > 1 || maxDecay < 0)
return;
using (var uow = _db.GetDbContext())
{
var lastCurrencyDecay = _cache.GetLastCurrencyDecay();
if (DateTime.UtcNow - lastCurrencyDecay < TimeSpan.FromHours(config.Decay.HourInterval))
return;
Log.Information($"Decaying users' currency - decay: {config.Decay.Percent * 100}% " +
$"| max: {maxDecay} " +
$"| threshold: {config.Decay.MinThreshold}");
if (maxDecay == 0)
maxDecay = int.MaxValue;
uow._context.Database.ExecuteSqlInterpolated($@"
UPDATE DiscordUser
SET CurrencyAmount=
CASE WHEN
{maxDecay} > ROUND(CurrencyAmount * {config.Decay.Percent} - 0.5)
THEN
CurrencyAmount - ROUND(CurrencyAmount * {config.Decay.Percent} - 0.5)
ELSE
CurrencyAmount - {maxDecay}
END
WHERE CurrencyAmount > {config.Decay.MinThreshold} AND UserId!={_client.CurrentUser.Id};");
_cache.SetLastCurrencyDecay();
uow.SaveChanges();
}
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
//using (var uow = _db.UnitOfWork)
//{
// //refund all of the currency users had at stake in gambling games
// //at the time bot was restarted
// var stakes = uow._context.Set<Stake>()
// .ToArray();
// var userIds = stakes.Select(x => x.UserId).ToArray();
// var reasons = stakes.Select(x => "Stake-" + x.Source).ToArray();
// var amounts = stakes.Select(x => x.Amount).ToArray();
// _cs.AddBulkAsync(userIds, reasons, amounts, gamble: true).ConfigureAwait(false);
// foreach (var s in stakes)
// {
// _cs.AddAsync(s.UserId, "Stake-" + s.Source, s.Amount, gamble: true)
// .GetAwaiter()
// .GetResult();
// }
// uow._context.Set<Stake>().RemoveRange(stakes);
// uow.Complete();
// Log.Information("Refunded {0} users' stakes.", stakes.Length);
//}
}
public struct EconomyResult
{
public decimal Cash { get; set; }
public decimal Planted { get; set; }
public decimal Waifus { get; set; }
public decimal OnePercent { get; set; }
public long Bot { get; set; }
}
public EconomyResult GetEconomy()
{
if (_cache.TryGetEconomy(out var data))
{
try
{
return JsonConvert.DeserializeObject<EconomyResult>(data);
}
catch { }
}
decimal cash;
decimal onePercent;
decimal planted;
decimal waifus;
long bot;
using (var uow = _db.GetDbContext())
{
cash = uow.DiscordUsers.GetTotalCurrency();
onePercent = uow.DiscordUsers.GetTopOnePercentCurrency(_client.CurrentUser.Id);
planted = uow.PlantedCurrency.GetTotalPlanted();
waifus = uow.Waifus.GetTotalValue();
bot = uow.DiscordUsers.GetUserCurrency(_client.CurrentUser.Id);
}
var result = new EconomyResult
{
Cash = cash,
Planted = planted,
Bot = bot,
Waifus = waifus,
OnePercent = onePercent,
};
_cache.SetEconomy(JsonConvert.SerializeObject(result));
return result;
}
public Task<WheelOfFortuneGame.Result> WheelOfFortuneSpinAsync(ulong userId, long bet)
{
return new WheelOfFortuneGame(userId, bet, _gss.Data, _cs).SpinAsync();
}
}
}

View File

@@ -0,0 +1,43 @@
using System.Threading.Tasks;
namespace NadekoBot.Core.Modules.Gambling.Services
{
public interface IShopService
{
/// <summary>
/// Changes the price of a shop item
/// </summary>
/// <param name="guildId">Id of the guild in which the shop is</param>
/// <param name="index">Index of the item</param>
/// <param name="newPrice">New item price</param>
/// <returns>Success status</returns>
Task<bool> ChangeEntryPriceAsync(ulong guildId, int index, int newPrice);
/// <summary>
/// Changes the name of a shop item
/// </summary>
/// <param name="guildId">Id of the guild in which the shop is</param>
/// <param name="index">Index of the item</param>
/// <param name="newName">New item name</param>
/// <returns>Success status</returns>
Task<bool> ChangeEntryNameAsync(ulong guildId, int index, string newName);
/// <summary>
/// Swaps indexes of 2 items in the shop
/// </summary>
/// <param name="guildId">Id of the guild in which the shop is</param>
/// <param name="index1">First entry's index</param>
/// <param name="index2">Second entry's index</param>
/// <returns>Whether swap was successful</returns>
Task<bool> SwapEntriesAsync(ulong guildId, int index1, int index2);
/// <summary>
/// Swaps indexes of 2 items in the shop
/// </summary>
/// <param name="guildId">Id of the guild in which the shop is</param>
/// <param name="fromIndex">Current index of the entry to move</param>
/// <param name="toIndex">Destination index of the entry</param>
/// <returns>Whether swap was successful</returns>
Task<bool> MoveEntryAsync(ulong guildId, int fromIndex, int toIndex);
}
}

View File

@@ -0,0 +1,106 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.Collections;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Extensions;
namespace NadekoBot.Core.Modules.Gambling.Services
{
public class ShopService : IShopService
{
private readonly DbService _db;
public ShopService(DbService db)
{
_db = db;
}
private IndexedCollection<ShopEntry> GetEntriesInternal(IUnitOfWork uow, ulong guildId) =>
uow.GuildConfigs.ForId(
guildId,
set => set.Include(x => x.ShopEntries).ThenInclude(x => x.Items)
)
.ShopEntries
.ToIndexed();
public async Task<bool> ChangeEntryPriceAsync(ulong guildId, int index, int newPrice)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
if (newPrice <= 0)
throw new ArgumentOutOfRangeException(nameof(newPrice));
using var uow = _db.GetDbContext();
var entries = GetEntriesInternal(uow, guildId);
if (index >= entries.Count)
return false;
entries[index].Price = newPrice;
await uow.SaveChangesAsync();
return true;
}
public async Task<bool> ChangeEntryNameAsync(ulong guildId, int index, string newName)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
if (string.IsNullOrWhiteSpace(newName))
throw new ArgumentNullException(nameof(newName));
using var uow = _db.GetDbContext();
var entries = GetEntriesInternal(uow, guildId);
if (index >= entries.Count)
return false;
entries[index].Name = newName.TrimTo(100);
await uow.SaveChangesAsync();
return true;
}
public async Task<bool> SwapEntriesAsync(ulong guildId, int index1, int index2)
{
if (index1 < 0)
throw new ArgumentOutOfRangeException(nameof(index1));
if (index2 < 0)
throw new ArgumentOutOfRangeException(nameof(index2));
using var uow = _db.GetDbContext();
var entries = GetEntriesInternal(uow, guildId);
if (index1 >= entries.Count || index2 >= entries.Count || index1 == index2)
return false;
entries[index1].Index = index2;
entries[index2].Index = index1;
await uow.SaveChangesAsync();
return true;
}
public async Task<bool> MoveEntryAsync(ulong guildId, int fromIndex, int toIndex)
{
if (fromIndex < 0)
throw new ArgumentOutOfRangeException(nameof(fromIndex));
if (toIndex < 0)
throw new ArgumentOutOfRangeException(nameof(toIndex));
using var uow = _db.GetDbContext();
var entries = GetEntriesInternal(uow, guildId);
if (fromIndex >= entries.Count || toIndex >= entries.Count || fromIndex == toIndex)
return false;
var entry = entries[fromIndex];
entries.RemoveAt(fromIndex);
entries.Insert(toIndex, entry);
await uow.SaveChangesAsync();
return true;
}
}
}

View File

@@ -0,0 +1,376 @@
using Discord;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Common.Collections;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Core.Services.Database.Repositories;
using NadekoBot.Core.Services.Impl;
using NadekoBot.Extensions;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Core.Modules.Gambling.Services;
using Image = SixLabors.ImageSharp.Image;
using Color = SixLabors.ImageSharp.Color;
namespace NadekoBot.Modules.Gambling.Services
{
public class PlantPickService : INService
{
private readonly DbService _db;
private readonly IBotStrings _strings;
private readonly IImageCache _images;
private readonly FontProvider _fonts;
private readonly ICurrencyService _cs;
private readonly CommandHandler _cmdHandler;
private readonly NadekoRandom _rng;
private readonly DiscordSocketClient _client;
private readonly GamblingConfigService _gss;
public readonly ConcurrentHashSet<ulong> _generationChannels = new ConcurrentHashSet<ulong>();
//channelId/last generation
public ConcurrentDictionary<ulong, DateTime> LastGenerations { get; } = new ConcurrentDictionary<ulong, DateTime>();
private readonly SemaphoreSlim pickLock = new SemaphoreSlim(1, 1);
public PlantPickService(DbService db, CommandHandler cmd, IBotStrings strings,
IDataCache cache, FontProvider fonts, ICurrencyService cs,
CommandHandler cmdHandler, DiscordSocketClient client, GamblingConfigService gss)
{
_db = db;
_strings = strings;
_images = cache.LocalImages;
_fonts = fonts;
_cs = cs;
_cmdHandler = cmdHandler;
_rng = new NadekoRandom();
_client = client;
_gss = gss;
cmd.OnMessageNoTrigger += PotentialFlowerGeneration;
using (var uow = db.GetDbContext())
{
var guildIds = client.Guilds.Select(x => x.Id).ToList();
var configs = uow._context.Set<GuildConfig>()
.AsQueryable()
.Include(x => x.GenerateCurrencyChannelIds)
.Where(x => guildIds.Contains(x.GuildId))
.ToList();
_generationChannels = new ConcurrentHashSet<ulong>(configs
.SelectMany(c => c.GenerateCurrencyChannelIds.Select(obj => obj.ChannelId)));
}
}
private string GetText(ulong gid, string key, params object[] rep)
=> _strings.GetText(key, gid, rep);
public bool ToggleCurrencyGeneration(ulong gid, ulong cid)
{
bool enabled;
using (var uow = _db.GetDbContext())
{
var guildConfig = uow.GuildConfigs.ForId(gid, set => set.Include(gc => gc.GenerateCurrencyChannelIds));
var toAdd = new GCChannelId() { ChannelId = cid };
if (!guildConfig.GenerateCurrencyChannelIds.Contains(toAdd))
{
guildConfig.GenerateCurrencyChannelIds.Add(toAdd);
_generationChannels.Add(cid);
enabled = true;
}
else
{
var toDelete = guildConfig.GenerateCurrencyChannelIds.FirstOrDefault(x => x.Equals(toAdd));
if (toDelete != null)
{
uow._context.Remove(toDelete);
}
_generationChannels.TryRemove(cid);
enabled = false;
}
uow.SaveChanges();
}
return enabled;
}
public IEnumerable<GeneratingChannel> GetAllGeneratingChannels()
{
using (var uow = _db.GetDbContext())
{
var chs = uow.GuildConfigs.GetGeneratingChannels();
return chs;
}
}
/// <summary>
/// Get a random currency image stream, with an optional password sticked onto it.
/// </summary>
/// <param name="pass">Optional password to add to top left corner.</param>
/// <returns>Stream of the currency image</returns>
public Stream GetRandomCurrencyImage(string pass, out string extension)
{
// get a random currency image bytes
var rng = new NadekoRandom();
var curImg = _images.Currency[rng.Next(0, _images.Currency.Count)];
if (string.IsNullOrWhiteSpace(pass))
{
// determine the extension
using (var img = Image.Load(curImg, out var format))
{
extension = format.FileExtensions.FirstOrDefault() ?? "png";
}
// return the image
return curImg.ToStream();
}
// 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;
}
/// <summary>
/// Add a password to the image.
/// </summary>
/// <param name="curImg">Image to add password to.</param>
/// <param name="pass">Password to add to top left corner.</param>
/// <returns>Image with the password in the top left corner.</returns>
private (Stream, string) AddPassword(byte[] curImg, string pass)
{
// draw lower, it looks better
pass = pass.TrimTo(10, true).ToLowerInvariant();
using (var img = Image.Load<Rgba32>(curImg, out var format))
{
// choose font size based on the image height, so that it's visible
var font = _fonts.NotoSans.CreateFont(img.Height / 12, FontStyle.Bold);
img.Mutate(x =>
{
// measure the size of the text to be drawing
var size = TextMeasurer.Measure(pass, new RendererOptions(font, new PointF(0, 0)));
// fill the background with black, add 5 pixels on each side to make it look better
x.FillPolygon(Color.ParseHex("00000080"),
new PointF(0, 0),
new PointF(size.Width + 5, 0),
new PointF(size.Width + 5, size.Height + 10),
new PointF(0, size.Height + 10));
// draw the password over the background
x.DrawText(pass,
font,
SixLabors.ImageSharp.Color.White,
new PointF(0, 0));
});
// return image as a stream for easy sending
return (img.ToStream(format), format.FileExtensions.FirstOrDefault() ?? "png");
}
}
private Task PotentialFlowerGeneration(IUserMessage imsg)
{
var msg = imsg as SocketUserMessage;
if (msg == null || msg.Author.IsBot)
return Task.CompletedTask;
if (!(imsg.Channel is ITextChannel channel))
return Task.CompletedTask;
if (!_generationChannels.Contains(channel.Id))
return Task.CompletedTask;
var _ = Task.Run(async () =>
{
try
{
var config = _gss.Data;
var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue);
var rng = new NadekoRandom();
if (DateTime.UtcNow - TimeSpan.FromSeconds(config.Generation.GenCooldown) < lastGeneration) //recently generated in this channel, don't generate again
return;
var num = rng.Next(1, 101) + config.Generation.Chance * 100;
if (num > 100 && LastGenerations.TryUpdate(channel.Id, DateTime.UtcNow, lastGeneration))
{
var dropAmount = config.Generation.MinAmount;
var dropAmountMax = config.Generation.MaxAmount;
if (dropAmountMax > dropAmount)
dropAmount = new NadekoRandom().Next(dropAmount, dropAmountMax + 1);
if (dropAmount > 0)
{
var prefix = _cmdHandler.GetPrefix(channel.Guild.Id);
var toSend = dropAmount == 1
? GetText(channel.GuildId, "curgen_sn", config.Currency.Sign)
+ " " + GetText(channel.GuildId, "pick_sn", prefix)
: GetText(channel.GuildId, "curgen_pl", dropAmount, config.Currency.Sign)
+ " " + GetText(channel.GuildId, "pick_pl", prefix);
var pw = config.Generation.HasPassword ? GenerateCurrencyPassword().ToUpperInvariant() : null;
IUserMessage sent;
using (var stream = GetRandomCurrencyImage(pw, out var ext))
{
sent = await channel.SendFileAsync(stream, $"currency_image.{ext}", toSend).ConfigureAwait(false);
}
await AddPlantToDatabase(channel.GuildId,
channel.Id,
_client.CurrentUser.Id,
sent.Id,
dropAmount,
pw).ConfigureAwait(false);
}
}
}
catch
{
}
});
return Task.CompletedTask;
}
/// <summary>
/// Generate a hexadecimal string from 1000 to ffff.
/// </summary>
/// <returns>A hexadecimal string from 1000 to ffff</returns>
private string GenerateCurrencyPassword()
{
// generate a number from 1000 to ffff
var num = _rng.Next(4096, 65536);
// convert it to hexadecimal
return num.ToString("x4");
}
public async Task<long> PickAsync(ulong gid, ITextChannel ch, ulong uid, string pass)
{
await pickLock.WaitAsync();
try
{
long amount;
ulong[] ids;
using (var uow = _db.GetDbContext())
{
// this method will sum all plants with that password,
// remove them, and get messageids of the removed plants
(amount, ids) = uow.PlantedCurrency.RemoveSumAndGetMessageIdsFor(ch.Id, pass);
if (amount > 0)
{
// give the picked currency to the user
await _cs.AddAsync(uid, "Picked currency", amount, gamble: false);
}
uow.SaveChanges();
}
try
{
// delete all of the plant messages which have just been picked
var _ = ch.DeleteMessagesAsync(ids);
}
catch { }
// return the amount of currency the user picked
return amount;
}
finally
{
pickLock.Release();
}
}
public async Task<ulong?> SendPlantMessageAsync(ulong gid, IMessageChannel ch, string user, long amount, string pass)
{
try
{
// get the text
var prefix = _cmdHandler.GetPrefix(gid);
var msgToSend = GetText(gid,
"planted",
Format.Bold(user),
amount + _gss.Data.Currency.Sign,
prefix);
if (amount > 1)
msgToSend += " " + GetText(gid, "pick_pl", prefix);
else
msgToSend += " " + GetText(gid, "pick_sn", prefix);
//get the image
using (var stream = GetRandomCurrencyImage(pass, out var ext))
{
// send it
var msg = await ch.SendFileAsync(stream, $"img.{ext}", msgToSend).ConfigureAwait(false);
// return sent message's id (in order to be able to delete it when it's picked)
return msg.Id;
}
}
catch
{
// if sending fails, return null as message id
return null;
}
}
public async Task<bool> PlantAsync(ulong gid, IMessageChannel ch, ulong uid, string user, long amount, string pass)
{
// normalize it - no more than 10 chars, uppercase
pass = pass?.Trim().TrimTo(10, hideDots: true).ToUpperInvariant();
// has to be either null or alphanumeric
if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric())
return false;
// remove currency from the user who's planting
if (await _cs.RemoveAsync(uid, "Planted currency", amount, gamble: false))
{
// try to send the message with the currency image
var msgId = await SendPlantMessageAsync(gid, ch, user, amount, pass).ConfigureAwait(false);
if (msgId == null)
{
// if it fails it will return null, if it returns null, refund
await _cs.AddAsync(uid, "Planted currency refund", amount, gamble: false);
return false;
}
// if it doesn't fail, put the plant in the database for other people to pick
await AddPlantToDatabase(gid, ch.Id, uid, msgId.Value, amount, pass).ConfigureAwait(false);
return true;
}
// if user doesn't have enough currency, fail
return false;
}
private async Task AddPlantToDatabase(ulong gid, ulong cid, ulong uid, ulong mid, long amount, string pass)
{
using (var uow = _db.GetDbContext())
{
uow.PlantedCurrency.Add(new PlantedCurrency
{
Amount = amount,
GuildId = gid,
ChannelId = cid,
Password = pass,
UserId = uid,
MessageId = mid,
});
await uow.SaveChangesAsync();
}
}
}
}

View File

@@ -0,0 +1,519 @@
using Discord;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Core.Modules.Gambling.Common.Waifu;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Core.Services.Database.Repositories;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Core.Modules.Gambling.Common;
using NadekoBot.Core.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling.Services
{
public class WaifuService : INService
{
public class FullWaifuInfo
{
public WaifuInfo Waifu { get; set; }
public IEnumerable<string> Claims { get; set; }
public int Divorces { get; set; }
}
private readonly DbService _db;
private readonly ICurrencyService _cs;
private readonly IDataCache _cache;
private readonly GamblingConfigService _gss;
public WaifuService(DbService db, ICurrencyService cs, IDataCache cache,
GamblingConfigService gss)
{
_db = db;
_cs = cs;
_cache = cache;
_gss = gss;
}
public async Task<bool> WaifuTransfer(IUser owner, ulong waifuId, IUser newOwner)
{
if (owner.Id == newOwner.Id || waifuId == newOwner.Id)
return false;
var settings = _gss.Data;
using (var uow = _db.GetDbContext())
{
var waifu = uow.Waifus.ByWaifuUserId(waifuId);
var ownerUser = uow.DiscordUsers.GetOrCreate(owner);
// owner has to be the owner of the waifu
if (waifu == null || waifu.ClaimerId != ownerUser.Id)
return false;
// if waifu likes the person, gotta pay the penalty
if (waifu.AffinityId == ownerUser.Id)
{
if (!await _cs.RemoveAsync(owner.Id,
"Waifu Transfer - affinity penalty",
(int)(waifu.Price * 0.6),
true))
{
// unable to pay 60% penalty
return false;
}
waifu.Price = (int)(waifu.Price * 0.7); // half of 60% = 30% price reduction
if (waifu.Price < settings.Waifu.MinPrice)
waifu.Price = settings.Waifu.MinPrice;
}
else // if not, pay 10% fee
{
if (!await _cs.RemoveAsync(owner.Id, "Waifu Transfer", waifu.Price / 10, gamble: true))
{
return false;
}
waifu.Price = (int) (waifu.Price * 0.95); // half of 10% = 5% price reduction
if (waifu.Price < settings.Waifu.MinPrice)
waifu.Price = settings.Waifu.MinPrice;
}
//new claimerId is the id of the new owner
var newOwnerUser = uow.DiscordUsers.GetOrCreate(newOwner);
waifu.ClaimerId = newOwnerUser.Id;
await uow.SaveChangesAsync();
}
return true;
}
public int GetResetPrice(IUser user)
{
var settings = _gss.Data;
using (var uow = _db.GetDbContext())
{
var waifu = uow.Waifus.ByWaifuUserId(user.Id);
if (waifu == null)
return settings.Waifu.MinPrice;
var divorces = uow._context.WaifuUpdates.Count(x => x.Old != null &&
x.Old.UserId == user.Id &&
x.UpdateType == WaifuUpdateType.Claimed &&
x.New == null);
var affs = uow._context.WaifuUpdates
.AsQueryable()
.Where(w => w.User.UserId == user.Id && w.UpdateType == WaifuUpdateType.AffinityChanged &&
w.New != null)
.ToList()
.GroupBy(x => x.New)
.Count();
return (int) Math.Ceiling(waifu.Price * 1.25f) +
((divorces + affs + 2) * settings.Waifu.Multipliers.WaifuReset);
}
}
public async Task<bool> TryReset(IUser user)
{
using (var uow = _db.GetDbContext())
{
var price = GetResetPrice(user);
if (!await _cs.RemoveAsync(user.Id, "Waifu Reset", price, gamble: true))
return false;
var affs = uow._context.WaifuUpdates
.AsQueryable()
.Where(w => w.User.UserId == user.Id
&& w.UpdateType == WaifuUpdateType.AffinityChanged
&& w.New != null);
var divorces = uow._context.WaifuUpdates
.AsQueryable()
.Where(x => x.Old != null &&
x.Old.UserId == user.Id &&
x.UpdateType == WaifuUpdateType.Claimed &&
x.New == null);
//reset changes of heart to 0
uow._context.WaifuUpdates.RemoveRange(affs);
//reset divorces to 0
uow._context.WaifuUpdates.RemoveRange(divorces);
var waifu = uow.Waifus.ByWaifuUserId(user.Id);
//reset price, remove items
//remove owner, remove affinity
waifu.Price = 50;
waifu.Items.Clear();
waifu.ClaimerId = null;
waifu.AffinityId = null;
//wives stay though
uow.SaveChanges();
}
return true;
}
public async Task<(WaifuInfo, bool, WaifuClaimResult)> ClaimWaifuAsync(IUser user, IUser target, int amount)
{
var settings = _gss.Data;
WaifuClaimResult result;
WaifuInfo w;
bool isAffinity;
using (var uow = _db.GetDbContext())
{
w = uow.Waifus.ByWaifuUserId(target.Id);
isAffinity = (w?.Affinity?.UserId == user.Id);
if (w == null)
{
var claimer = uow.DiscordUsers.GetOrCreate(user);
var waifu = uow.DiscordUsers.GetOrCreate(target);
if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true))
{
result = WaifuClaimResult.NotEnoughFunds;
}
else
{
uow.Waifus.Add(w = new WaifuInfo()
{
Waifu = waifu,
Claimer = claimer,
Affinity = null,
Price = amount
});
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = waifu,
Old = null,
New = claimer,
UpdateType = WaifuUpdateType.Claimed
});
result = WaifuClaimResult.Success;
}
}
else if (isAffinity && amount > w.Price * settings.Waifu.Multipliers.CrushClaim)
{
if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true))
{
result = WaifuClaimResult.NotEnoughFunds;
}
else
{
var oldClaimer = w.Claimer;
w.Claimer = uow.DiscordUsers.GetOrCreate(user);
w.Price = amount + (amount / 4);
result = WaifuClaimResult.Success;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = w.Waifu,
Old = oldClaimer,
New = w.Claimer,
UpdateType = WaifuUpdateType.Claimed
});
}
}
else if (amount >= w.Price * settings.Waifu.Multipliers.NormalClaim) // if no affinity
{
if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true))
{
result = WaifuClaimResult.NotEnoughFunds;
}
else
{
var oldClaimer = w.Claimer;
w.Claimer = uow.DiscordUsers.GetOrCreate(user);
w.Price = amount;
result = WaifuClaimResult.Success;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = w.Waifu,
Old = oldClaimer,
New = w.Claimer,
UpdateType = WaifuUpdateType.Claimed
});
}
}
else
result = WaifuClaimResult.InsufficientAmount;
await uow.SaveChangesAsync();
}
return (w, isAffinity, result);
}
public async Task<(DiscordUser, bool, TimeSpan?)> ChangeAffinityAsync(IUser user, IGuildUser target)
{
DiscordUser oldAff = null;
var success = false;
TimeSpan? remaining = null;
using (var uow = _db.GetDbContext())
{
var w = uow.Waifus.ByWaifuUserId(user.Id);
var newAff = target == null ? null : uow.DiscordUsers.GetOrCreate(target);
if (w?.Affinity?.UserId == target?.Id)
{
}
else if (!_cache.TryAddAffinityCooldown(user.Id, out remaining))
{
}
else if (w == null)
{
var thisUser = uow.DiscordUsers.GetOrCreate(user);
uow.Waifus.Add(new WaifuInfo()
{
Affinity = newAff,
Waifu = thisUser,
Price = 1,
Claimer = null
});
success = true;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = thisUser,
Old = null,
New = newAff,
UpdateType = WaifuUpdateType.AffinityChanged
});
}
else
{
if (w.Affinity != null)
oldAff = w.Affinity;
w.Affinity = newAff;
success = true;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = w.Waifu,
Old = oldAff,
New = newAff,
UpdateType = WaifuUpdateType.AffinityChanged
});
}
await uow.SaveChangesAsync();
}
return (oldAff, success, remaining);
}
public IEnumerable<WaifuLbResult> GetTopWaifusAtPage(int page)
{
using (var uow = _db.GetDbContext())
{
return uow.Waifus.GetTop(9, page * 9);
}
}
public ulong GetWaifuUserId(ulong ownerId, string name)
{
using var uow = _db.GetDbContext();
return uow.Waifus.GetWaifuUserId(ownerId, name);
}
public async Task<(WaifuInfo, DivorceResult, long, TimeSpan?)> DivorceWaifuAsync(IUser user, ulong targetId)
{
DivorceResult result;
TimeSpan? remaining = null;
long amount = 0;
WaifuInfo w = null;
using (var uow = _db.GetDbContext())
{
w = uow.Waifus.ByWaifuUserId(targetId);
var now = DateTime.UtcNow;
if (w?.Claimer == null || w.Claimer.UserId != user.Id)
result = DivorceResult.NotYourWife;
else if (!_cache.TryAddDivorceCooldown(user.Id, out remaining))
{
result = DivorceResult.Cooldown;
}
else
{
amount = w.Price / 2;
if (w.Affinity?.UserId == user.Id)
{
await _cs.AddAsync(w.Waifu.UserId, "Waifu Compensation", amount, gamble: true);
w.Price = (int) Math.Floor(w.Price * _gss.Data.Waifu.Multipliers.DivorceNewValue);
result = DivorceResult.SucessWithPenalty;
}
else
{
await _cs.AddAsync(user.Id, "Waifu Refund", amount, gamble: true);
result = DivorceResult.Success;
}
var oldClaimer = w.Claimer;
w.Claimer = null;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = w.Waifu,
Old = oldClaimer,
New = null,
UpdateType = WaifuUpdateType.Claimed
});
}
await uow.SaveChangesAsync();
}
return (w, result, amount, remaining);
}
public async Task<bool> GiftWaifuAsync(IUser from, IUser giftedWaifu, WaifuItemModel itemObj)
{
if (!await _cs.RemoveAsync(from, "Bought waifu item", itemObj.Price, gamble: true))
{
return false;
}
using (var uow = _db.GetDbContext())
{
var w = uow.Waifus.ByWaifuUserId(giftedWaifu.Id,
set => set.Include(x => x.Items)
.Include(x => x.Claimer));
if (w == null)
{
uow.Waifus.Add(w = new WaifuInfo()
{
Affinity = null,
Claimer = null,
Price = 1,
Waifu = uow.DiscordUsers.GetOrCreate(giftedWaifu),
});
}
w.Items.Add(new WaifuItem()
{
Name = itemObj.Name.ToLowerInvariant(),
ItemEmoji = itemObj.ItemEmoji,
});
if (w.Claimer?.UserId == from.Id)
{
w.Price += (int) (itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect);
}
else
{
w.Price += itemObj.Price / 2;
}
await uow.SaveChangesAsync();
}
return true;
}
public WaifuInfoStats GetFullWaifuInfoAsync(ulong targetId)
{
using (var uow = _db.GetDbContext())
{
var wi = uow.Waifus.GetWaifuInfo(targetId);
if (wi is null)
{
wi = new WaifuInfoStats
{
AffinityCount = 0,
AffinityName = null,
ClaimCount = 0,
ClaimerName = null,
Claims = new List<string>(),
Fans = new List<string>(),
DivorceCount = 0,
FullName = null,
Items = new List<WaifuItem>(),
Price = 1
};
}
return wi;
}
}
public WaifuInfoStats GetFullWaifuInfoAsync(IGuildUser target)
{
using (var uow = _db.GetDbContext())
{
var du = uow.DiscordUsers.GetOrCreate(target);
return GetFullWaifuInfoAsync(target.Id);
}
}
public string GetClaimTitle(int count)
{
ClaimTitle title;
if (count == 0)
title = ClaimTitle.Lonely;
else if (count == 1)
title = ClaimTitle.Devoted;
else if (count < 3)
title = ClaimTitle.Rookie;
else if (count < 6)
title = ClaimTitle.Schemer;
else if (count < 10)
title = ClaimTitle.Dilettante;
else if (count < 17)
title = ClaimTitle.Intermediate;
else if (count < 25)
title = ClaimTitle.Seducer;
else if (count < 35)
title = ClaimTitle.Expert;
else if (count < 50)
title = ClaimTitle.Veteran;
else if (count < 75)
title = ClaimTitle.Incubis;
else if (count < 100)
title = ClaimTitle.Harem_King;
else
title = ClaimTitle.Harem_God;
return title.ToString().Replace('_', ' ');
}
public string GetAffinityTitle(int count)
{
AffinityTitle title;
if (count < 1)
title = AffinityTitle.Pure;
else if (count < 2)
title = AffinityTitle.Faithful;
else if (count < 4)
title = AffinityTitle.Playful;
else if (count < 8)
title = AffinityTitle.Cheater;
else if (count < 11)
title = AffinityTitle.Tainted;
else if (count < 15)
title = AffinityTitle.Corrupted;
else if (count < 20)
title = AffinityTitle.Lewd;
else if (count < 25)
title = AffinityTitle.Sloot;
else if (count < 35)
title = AffinityTitle.Depraved;
else
title = AffinityTitle.Harlot;
return title.ToString().Replace('_', ' ');
}
public IReadOnlyList<WaifuItemModel> GetWaifuItems()
{
var conf = _gss.Data;
return _gss.Data.Waifu.Items
.Select(x => new WaifuItemModel(x.ItemEmoji, (int)(x.Price * conf.Waifu.Multipliers.AllGiftPrices), x.Name))
.ToList();
}
}
}