Reworked currency service. Some features are missing

This commit is contained in:
Kwoth
2022-01-20 13:22:38 +01:00
parent fa41c5a319
commit 0ef2da6f10
31 changed files with 433 additions and 325 deletions

View File

@@ -342,6 +342,7 @@ resharper_place_simple_embedded_statement_on_same_line = false
resharper_wrap_chained_binary_expressions = chop_if_long
resharper_wrap_chained_binary_patterns = chop_if_long
resharper_wrap_chained_method_calls = chop_if_long
resharper_wrap_object_and_collection_initializer_style = chop_always
resharper_csharp_wrap_before_first_type_parameter_constraint = true
resharper_csharp_place_type_constraints_on_same_line = false
@@ -349,4 +350,4 @@ resharper_csharp_wrap_before_extends_colon = true
resharper_csharp_place_constructor_initializer_on_same_line = false
resharper_force_attribute_style = separate
resharper_braces_for_ifelse = required_for_multiline
resharper_arrange_redundant_parentheses_highlighting = hint
resharper_arrange_redundant_parentheses_highlighting = hint

View File

@@ -75,7 +75,7 @@ public sealed class AnimalRace : IDisposable
if (CurrentPhase != Phase.WaitingForPlayers)
throw new AlreadyStartedException();
if (!await _currency.RemoveAsync(userId, "BetRace", bet))
if (!await _currency.RemoveAsync(userId, bet, new("animalrace", "bet")))
throw new NotEnoughFundsException();
if (_users.Contains(user))
@@ -100,7 +100,7 @@ public sealed class AnimalRace : IDisposable
{
foreach (var user in _users)
if (user.Bet > 0)
await _currency.AddAsync(user.UserId, "Race refund", user.Bet);
await _currency.AddAsync(user.UserId, user.Bet, new("animalrace", "refund"));
_ = OnStartingFailed?.Invoke(this);
CurrentPhase = Phase.Ended;
@@ -130,8 +130,8 @@ public sealed class AnimalRace : IDisposable
if (FinishedUsers[0].Bet > 0)
await _currency.AddAsync(FinishedUsers[0].UserId,
"Won a Race",
FinishedUsers[0].Bet * (_users.Count - 1));
FinishedUsers[0].Bet * (_users.Count - 1),
new("animalrace", "win"));
_ = OnEnded?.Invoke(this);
});

View File

@@ -120,7 +120,7 @@ public class Blackjack
if (Players.Count >= 5)
return false;
if (!await _cs.RemoveAsync(user, "BlackJack-gamble", bet, gamble: true)) return false;
if (!await _cs.RemoveAsync(user, bet, new("blackjack","gamble"))) return false;
Players.Add(new(user, bet));
_= PrintState();
@@ -210,7 +210,7 @@ public class Blackjack
foreach (var usr in Players)
if (usr.State is User.UserState.Won or User.UserState.Blackjack)
await _cs.AddAsync(usr.DiscordUser.Id, "BlackJack-win", usr.Bet * 2, true);
await _cs.AddAsync(usr.DiscordUser.Id, usr.Bet * 2, new("blackjack", "win"));
}
public async Task<bool> Double(IUser u)
@@ -234,7 +234,7 @@ public class Blackjack
if (CurrentUser != u)
return false;
if (!await _cs.RemoveAsync(u.DiscordUser.Id, "Blackjack-double", u.Bet))
if (!await _cs.RemoveAsync(u.DiscordUser.Id, u.Bet, new("blackjack", "double")))
return false;
u.Bet *= 2;

View File

@@ -98,7 +98,7 @@ public sealed class Connect4Game : IDisposable
{
var __ = OnGameFailedToStart?.Invoke(this);
CurrentPhase = Phase.Ended;
await _cs.AddAsync(_players[0].Value.UserId, "Connect4-refund", _options.Bet, true);
await _cs.AddAsync(_players[0].Value.UserId, _options.Bet, new("connect4", "refund"));
}
}
finally { _locker.Release(); }
@@ -119,7 +119,7 @@ public sealed class Connect4Game : IDisposable
if (bet != _options.Bet) // can't join if bet amount is not the same
return false;
if (!await _cs.RemoveAsync(userId, "Connect4-bet", bet, true)) // user doesn't have enough money to gamble
if (!await _cs.RemoveAsync(userId, bet, new("connect4", "bet"))) // user doesn't have enough money to gamble
return false;
if (_rng.Next(0, 2) == 0) //rolling from 0-1, if number is 0, join as first player
@@ -342,13 +342,13 @@ public sealed class Connect4Game : IDisposable
if (result == Result.Draw)
{
_cs.AddAsync(CurrentPlayer.UserId, "Connect4-draw", _options.Bet, true);
_cs.AddAsync(OtherPlayer.UserId, "Connect4-draw", _options.Bet, true);
_cs.AddAsync(CurrentPlayer.UserId, _options.Bet, new("connect4", "draw"));
_cs.AddAsync(OtherPlayer.UserId, _options.Bet, new("connect4", "draw"));
return;
}
if (winId is not null)
_cs.AddAsync(winId.Value, "Connnect4-win", (long)(_options.Bet * 1.98), true);
_cs.AddAsync(winId.Value, (long)(_options.Bet * 1.98), new("connect4", "win"));
}
private Field GetPlayerPiece(ulong userId)

View File

@@ -64,7 +64,7 @@ public partial class Gambling
}
if (options.Bet > 0)
if (!await _cs.RemoveAsync(ctx.User.Id, "Connect4-bet", options.Bet, true))
if (!await _cs.RemoveAsync(ctx.User.Id, options.Bet, new("connect4", "bet")))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
_service.Connect4Games.TryRemove(ctx.Channel.Id, out _);

View File

@@ -76,9 +76,9 @@ public class GameStatusEvent : ICurrencyEvent
try
{
await _cs.AddBulkAsync(toAward,
toAward.Select(_ => "GameStatus Event"),
toAward.Select(_ => _amount),
true);
_amount,
new("event", "gamestatus")
);
if (_isPotLimited)
await msg.ModifyAsync(m =>

View File

@@ -71,7 +71,7 @@ public class ReactionEvent : ICurrencyEvent
try
{
await _cs.AddBulkAsync(toAward, toAward.Select(_ => "Reaction Event"), toAward.Select(_ => _amount), true);
await _cs.AddBulkAsync(toAward, _amount, new("event", "reaction"));
if (_isPotLimited)
await msg.ModifyAsync(m =>

View File

@@ -80,7 +80,7 @@ public partial class Gambling
if (!await CheckBetMandatory(amount) || amount == 1)
return;
var removed = await _cs.RemoveAsync(ctx.User, "Betflip Gamble", amount, false, true);
var removed = await _cs.RemoveAsync(ctx.User, amount, new("betflip", "bet"));
if (!removed)
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
@@ -106,7 +106,7 @@ public partial class Gambling
{
var toWin = (long)(amount * Config.BetFlip.Multiplier);
str = Format.Bold(ctx.User.ToString()) + " " + GetText(strs.flip_guess(toWin + CurrencySign));
await _cs.AddAsync(ctx.User, "Betflip Gamble", toWin, false, true);
await _cs.AddAsync(ctx.User, toWin, new("betflip", "win"));
}
else
{

View File

@@ -3,12 +3,14 @@ using NadekoBot.Db;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Services.Currency;
using NadekoBot.Services.Database.Models;
using System.Globalization;
using System.Numerics;
namespace NadekoBot.Modules.Gambling;
// todo leave empty servers
public partial class Gambling : GamblingModule<GamblingService>
{
public enum RpsPick
@@ -67,10 +69,11 @@ public partial class Gambling : GamblingModule<GamblingService>
return cur.ToString("C0", flowersCi);
}
public string GetCurrency(ulong id)
public async Task<string> GetBalanceStringAsync(ulong userId)
{
using var uow = _db.GetDbContext();
return n(uow.DiscordUser.GetUserCurrency(id));
var wallet = await _cs.GetWalletAsync(userId);
var bal = await wallet.GetBalance();
return n(bal);
}
[Cmd]
@@ -78,9 +81,11 @@ public partial class Gambling : GamblingModule<GamblingService>
{
var ec = _service.GetEconomy();
decimal onePercent = 0;
// This stops the top 1% from owning more than 100% of the money
if (ec.Cash > 0)
onePercent =
ec.OnePercent / (ec.Cash - ec.Bot); // This stops the top 1% from owning more than 100% of the money
onePercent = ec.OnePercent / (ec.Cash - ec.Bot);
// [21:03] Bob Page: Kinda remids me of US economy
var embed = _eb.Create()
.WithTitle(GetText(strs.economy_state))
@@ -93,6 +98,7 @@ public partial class Gambling : GamblingModule<GamblingService>
.AddField(GetText(strs.total),
((BigInteger)(ec.Cash + ec.Planted + ec.Waifus)).ToString("N", Culture) + CurrencySign)
.WithOkColor();
// ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table
await ctx.Channel.EmbedAsync(embed);
}
@@ -114,7 +120,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
await _cs.AddAsync(ctx.User.Id, "Timely claim", val);
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
await ReplyConfirmLocalizedAsync(strs.timely(n(val), period));
}
@@ -225,14 +231,18 @@ public partial class Gambling : GamblingModule<GamblingService>
[Cmd]
[Priority(0)]
public async partial Task Cash(ulong userId)
=> await ReplyConfirmLocalizedAsync(strs.has(Format.Code(userId.ToString()), $"{GetCurrency(userId)}"));
{
var cur = await GetBalanceStringAsync(userId);
await ReplyConfirmLocalizedAsync(strs.has(Format.Code(userId.ToString()), cur));
}
[Cmd]
[Priority(1)]
public async partial Task Cash([Leftover] IUser user = null)
{
user ??= ctx.User;
await ConfirmLocalizedAsync(strs.has(Format.Bold(user.ToString()), $"{GetCurrency(user.Id)}"));
var cur = await GetBalanceStringAsync(user.Id);
await ConfirmLocalizedAsync(strs.has(Format.Bold(user.ToString()), cur));
}
[Cmd]
@@ -242,15 +252,13 @@ public partial class Gambling : GamblingModule<GamblingService>
{
if (amount <= 0 || ctx.User.Id == receiver.Id || receiver.IsBot)
return;
var success =
await _cs.RemoveAsync((IGuildUser)ctx.User, $"Gift to {receiver.Username} ({receiver.Id}).", amount);
if (!success)
if (!await _cs.TransferAsync(ctx.User.Id, receiver.Id, amount, msg))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
}
await _cs.AddAsync(receiver, $"Gift from {ctx.User.Username} ({ctx.User.Id}) - {msg}.", amount, true);
await ReplyConfirmLocalizedAsync(strs.gifted(n(amount), Format.Bold(receiver.ToString())));
}
@@ -290,10 +298,10 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
await _cs.AddAsync(usr,
$"Awarded by bot owner. ({ctx.User.Username}/{ctx.User.Id}) {msg ?? ""}",
await _cs.AddAsync(usr.Id,
amount,
gamble: ctx.Client.CurrentUser.Id != usrId);
new Extra("owner", "award", $"Awarded by bot owner. ({ctx.User.Username}/{ctx.User.Id}) {msg ?? ""}")
);
await ReplyConfirmLocalizedAsync(strs.awarded(n(amount), $"<@{usrId}>"));
}
@@ -305,10 +313,11 @@ public partial class Gambling : GamblingModule<GamblingService>
{
var users = (await ctx.Guild.GetUsersAsync()).Where(u => u.GetRoles().Contains(role)).ToList();
await _cs.AddBulkAsync(users.Select(x => x.Id),
users.Select(_ => $"Awarded by bot owner to **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"),
users.Select(_ => amount),
true);
await _cs.AddBulkAsync(users.Select(x => x.Id).ToList(),
amount,
new("owner",
"award",
$"Awarded by bot owner to **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"));
await ReplyConfirmLocalizedAsync(strs.mass_award(n(amount),
Format.Bold(users.Count.ToString()),
@@ -323,10 +332,9 @@ public partial class Gambling : GamblingModule<GamblingService>
{
var users = (await role.GetMembersAsync()).ToList();
await _cs.RemoveBulkAsync(users.Select(x => x.Id),
users.Select(_ => $"Taken by bot owner from **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"),
users.Select(_ => amount),
true);
await _cs.RemoveBulkAsync(users.Select(x => x.Id).ToList(),
amount,
new("owner", "take", $"Taken by bot owner from **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"));
await ReplyConfirmLocalizedAsync(strs.mass_take(n(amount),
Format.Bold(users.Count.ToString()),
@@ -342,10 +350,9 @@ public partial class Gambling : GamblingModule<GamblingService>
if (amount <= 0)
return;
if (await _cs.RemoveAsync(user,
$"Taken by bot owner.({ctx.User.Username}/{ctx.User.Id})",
if (await _cs.RemoveAsync(user.Id,
amount,
gamble: ctx.Client.CurrentUser.Id != user.Id))
new("owner", "take", $"Taken by bot owner. ({ctx.User.Username}/{ctx.User.Id})")))
await ReplyConfirmLocalizedAsync(strs.take(n(amount), Format.Bold(user.ToString())));
else
await ReplyErrorLocalizedAsync(strs.take_fail(n(amount), Format.Bold(user.ToString()), CurrencySign));
@@ -360,9 +367,8 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
if (await _cs.RemoveAsync(usrId,
$"Taken by bot owner.({ctx.User.Username}/{ctx.User.Id})",
amount,
ctx.Client.CurrentUser.Id != usrId))
new("owner", "take", $"Taken by bot owner. ({ctx.User.Username}/{ctx.User.Id})")))
await ReplyConfirmLocalizedAsync(strs.take(n(amount), $"<@{usrId}>"));
else
await ReplyErrorLocalizedAsync(strs.take_fail(n(amount), Format.Code(usrId.ToString()), CurrencySign));
@@ -467,7 +473,7 @@ public partial class Gambling : GamblingModule<GamblingService>
if (!await CheckBetMandatory(amount))
return;
if (!await _cs.RemoveAsync(ctx.User, "Betroll Gamble", amount, false, true))
if (!await _cs.RemoveAsync(ctx.User, amount, new("betroll", "bet")))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
@@ -483,7 +489,7 @@ public partial class Gambling : GamblingModule<GamblingService>
{
var win = (long)(amount * result.Multiplier);
str += GetText(strs.br_win(n(win), result.Threshold + (result.Roll == 100 ? " 👑" : "")));
await _cs.AddAsync(ctx.User, "Betroll Gamble", win, false, true);
await _cs.AddAsync(ctx.User, win, new("betroll", "win"));
}
else
{
@@ -600,16 +606,20 @@ public partial class Gambling : GamblingModule<GamblingService>
var nadekoPick = (RpsPick)new NadekoRandom().Next(0, 3);
if (amount > 0)
if (!await _cs.RemoveAsync(ctx.User.Id, "Rps-bet", amount, true))
{
if (!await _cs.RemoveAsync(ctx.User.Id,
amount,
new("rps", "bet", "")))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
}
}
string msg;
if (pick == nadekoPick)
{
await _cs.AddAsync(ctx.User.Id, "Rps-draw", amount, true);
await _cs.AddAsync(ctx.User.Id, amount, new("rps", "draw"));
embed.WithOkColor();
msg = GetText(strs.rps_draw(GetRpsPick(pick)));
}
@@ -618,7 +628,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|| (pick == RpsPick.Scissors && nadekoPick == RpsPick.Paper))
{
amount = (long)(amount * Config.BetFlip.Multiplier);
await _cs.AddAsync(ctx.User.Id, "Rps-win", amount, true);
await _cs.AddAsync(ctx.User.Id, amount, new("rps", "win"));
embed.WithOkColor();
embed.AddField(GetText(strs.won), n(amount));
msg = GetText(strs.rps_win(ctx.User.Mention, GetRpsPick(pick), GetRpsPick(nadekoPick)));

View File

@@ -83,7 +83,7 @@ WHERE CurrencyAmount > {config.Decay.MinThreshold} AND UserId!={_client.CurrentU
public async Task<SlotResponse> SlotAsync(ulong userId, long amount)
{
var takeRes = await _cs.RemoveAsync(userId, "Slot Machine", amount, true);
var takeRes = await _cs.RemoveAsync(userId, amount, new("slot", "bet"));
if (!takeRes)
return new() { Error = GamblingError.NotEnough };
@@ -96,7 +96,7 @@ WHERE CurrencyAmount > {config.Decay.MinThreshold} AND UserId!={_client.CurrentU
{
won = (long)(result.Multiplier * amount);
await _cs.AddAsync(userId, $"Slot Machine x{result.Multiplier}", won, true);
await _cs.AddAsync(userId, won, new("slot", "win", $"Slot Machine x{result.Multiplier}"));
}
var toReturn = new SlotResponse { Multiplier = result.Multiplier, Won = won };

View File

@@ -270,7 +270,7 @@ public class PlantPickService : INService
if (amount > 0)
// give the picked currency to the user
await _cs.AddAsync(uid, "Picked currency", amount);
await _cs.AddAsync(uid, amount, new("currency", "collect"));
uow.SaveChanges();
}
@@ -337,14 +337,14 @@ public class PlantPickService : INService
return false;
// remove currency from the user who's planting
if (await _cs.RemoveAsync(uid, "Planted currency", amount))
if (await _cs.RemoveAsync(uid, amount, new("put/collect", "put")))
{
// try to send the message with the currency image
var msgId = await SendPlantMessageAsync(gid, ch, user, amount, pass);
if (msgId is null)
{
// if it fails it will return null, if it returns null, refund
await _cs.AddAsync(uid, "Planted currency refund", amount);
await _cs.AddAsync(uid, amount, new("put/collect", "refund"));
return false;
}

View File

@@ -38,7 +38,7 @@ public class CurrencyRaffleService : INService
//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))
if (!await _cs.RemoveAsync(user.Id, amount, new("raffle", "join")))
{
if (newGame)
Games.Remove(channelId);
@@ -47,7 +47,7 @@ public class CurrencyRaffleService : INService
if (!crg.AddUser(user, amount))
{
await _cs.AddAsync(user.Id, "Curency Raffle Refund", amount);
await _cs.AddAsync(user.Id, amount, new("raffle", "refund"));
return (null, JoinErrorType.AlreadyJoinedOrInvalidAmount);
}
@@ -62,7 +62,7 @@ public class CurrencyRaffleService : INService
var winner = crg.GetWinner();
var won = crg.Users.Sum(x => x.Amount);
await _cs.AddAsync(winner.DiscordUser.Id, "Currency Raffle Win", won);
await _cs.AddAsync(winner.DiscordUser.Id, won, new("raffle", "win"));
Games.Remove(channelId, out _);
_ = onEnded(winner.DiscordUser, won);
}

View File

@@ -115,7 +115,7 @@ public partial class Gambling
return;
}
if (await _cs.RemoveAsync(ctx.User.Id, $"Shop purchase - {entry.Type}", entry.Price))
if (await _cs.RemoveAsync(ctx.User.Id, entry.Price, new("shop", "buy", entry.Type.ToString())))
{
try
{
@@ -124,14 +124,14 @@ public partial class Gambling
catch (Exception ex)
{
Log.Warning(ex, "Error adding shop role");
await _cs.AddAsync(ctx.User.Id, "Shop error refund", entry.Price);
await _cs.AddAsync(ctx.User.Id, entry.Price, new("shop", "error-refund"));
await ReplyErrorLocalizedAsync(strs.shop_role_purchase_error);
return;
}
var profit = GetProfitAmount(entry.Price);
await _cs.AddAsync(entry.AuthorId, $"Shop sell item - {entry.Type}", profit);
await _cs.AddAsync(ctx.Client.CurrentUser.Id, "Shop sell item - cut", entry.Price - profit);
await _cs.AddAsync(entry.AuthorId, profit, new("shop", "sell", $"Shop sell item - {entry.Type}"));
await _cs.AddAsync(ctx.Client.CurrentUser.Id, entry.Price - profit, new("shop", "cut"));
await ReplyConfirmLocalizedAsync(strs.shop_role_purchase(Format.Bold(role.Name)));
return;
}
@@ -150,7 +150,7 @@ public partial class Gambling
var item = entry.Items.ToArray()[new NadekoRandom().Next(0, entry.Items.Count)];
if (await _cs.RemoveAsync(ctx.User.Id, $"Shop purchase - {entry.Type}", entry.Price))
if (await _cs.RemoveAsync(ctx.User.Id, entry.Price, new("shop", "buy", entry.Type.ToString())))
{
await using (var uow = _db.GetDbContext())
{
@@ -168,12 +168,12 @@ public partial class Gambling
.AddField(GetText(strs.name), entry.Name, true));
await _cs.AddAsync(entry.AuthorId,
$"Shop sell item - {entry.Name}",
GetProfitAmount(entry.Price));
GetProfitAmount(entry.Price),
new("shop", "sell", entry.Name));
}
catch
{
await _cs.AddAsync(ctx.User.Id, $"Shop error refund - {entry.Name}", entry.Price);
await _cs.AddAsync(ctx.User.Id, entry.Price, new("shop", "error-refund", entry.Name));
await using (var uow = _db.GetDbContext())
{
var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigsForId(ctx.Guild.Id,
@@ -411,6 +411,7 @@ public partial class Gambling
var embed = _eb.Create().WithOkColor();
if (entry.Type == ShopEntryType.Role)
{
return embed
.AddField(GetText(strs.name),
GetText(strs.shop_role(Format.Bold(ctx.Guild.GetRole(entry.RoleId)?.Name
@@ -418,10 +419,15 @@ public partial class Gambling
true)
.AddField(GetText(strs.price), entry.Price.ToString(), true)
.AddField(GetText(strs.type), entry.Type.ToString(), true);
}
if (entry.Type == ShopEntryType.List)
{
return embed.AddField(GetText(strs.name), entry.Name, true)
.AddField(GetText(strs.price), entry.Price.ToString(), true)
.AddField(GetText(strs.type), GetText(strs.random_unique_item), true);
}
//else if (entry.Type == ShopEntryType.Infinite_List)
// return embed.AddField(GetText(strs.name), GetText(strs.shop_role(Format.Bold(entry.RoleName)), true))
// .AddField(GetText(strs.price), entry.Price.ToString(), true)

View File

@@ -61,9 +61,8 @@ public class VoteRewardService : INService, IReadyExecutor
var ids = data.Select(x => x.UserId).ToList();
await _currencyService.AddBulkAsync(ids,
data.Select(_ => "top.gg vote reward"),
data.Select(_ => _gamb.Data.VoteReward),
true);
_gamb.Data.VoteReward,
new("vote", "top.gg", "top.gg vote reward"));
Log.Information("Rewarding {Count} top.gg voters", ids.Count());
}
@@ -90,9 +89,8 @@ public class VoteRewardService : INService, IReadyExecutor
var ids = data.Select(x => x.UserId).ToList();
await _currencyService.AddBulkAsync(ids,
data.Select(_ => "discords.com vote reward"),
data.Select(_ => _gamb.Data.VoteReward),
true);
_gamb.Data.VoteReward,
new("vote", "discords", "discords.com vote reward"));
Log.Information("Rewarding {Count} discords.com voters", ids.Count());
}

View File

@@ -45,7 +45,7 @@ public class WaifuService : INService
// 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))
if (!await _cs.RemoveAsync(owner.Id, (int)(waifu.Price * 0.6), new("waifu", "affinity-penalty")))
// unable to pay 60% penalty
return false;
@@ -55,7 +55,7 @@ public class WaifuService : INService
}
else // if not, pay 10% fee
{
if (!await _cs.RemoveAsync(owner.Id, "Waifu Transfer", waifu.Price / 10, true)) return false;
if (!await _cs.RemoveAsync(owner.Id, waifu.Price / 10, new("waifu", "transfer"))) return false;
waifu.Price = (int)(waifu.Price * 0.95); // half of 10% = 5% price reduction
if (waifu.Price < settings.Waifu.MinPrice)
@@ -97,7 +97,7 @@ public class WaifuService : INService
{
await using var uow = _db.GetDbContext();
var price = GetResetPrice(user);
if (!await _cs.RemoveAsync(user.Id, "Waifu Reset", price, true))
if (!await _cs.RemoveAsync(user.Id, price, new("waifu", "reset")))
return false;
var affs = uow.WaifuUpdates.AsQueryable()
@@ -144,7 +144,7 @@ public class WaifuService : INService
{
var claimer = uow.GetOrCreateUser(user);
var waifu = uow.GetOrCreateUser(target);
if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, true))
if (!await _cs.RemoveAsync(user.Id, amount, new("waifu", "claim")))
{
result = WaifuClaimResult.NotEnoughFunds;
}
@@ -160,7 +160,7 @@ public class WaifuService : INService
}
else if (isAffinity && amount > w.Price * settings.Waifu.Multipliers.CrushClaim)
{
if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, true))
if (!await _cs.RemoveAsync(user.Id, amount, new("waifu", "claim")))
{
result = WaifuClaimResult.NotEnoughFunds;
}
@@ -179,7 +179,7 @@ public class WaifuService : INService
}
else if (amount >= w.Price * settings.Waifu.Multipliers.NormalClaim) // if no affinity
{
if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, true))
if (!await _cs.RemoveAsync(user.Id, amount, new("waifu", "claim")))
{
result = WaifuClaimResult.NotEnoughFunds;
}
@@ -288,13 +288,13 @@ public class WaifuService : INService
if (w.Affinity?.UserId == user.Id)
{
await _cs.AddAsync(w.Waifu.UserId, "Waifu Compensation", amount, true);
await _cs.AddAsync(w.Waifu.UserId, amount, new("waifu", "compensation"));
w.Price = (int)Math.Floor(w.Price * _gss.Data.Waifu.Multipliers.DivorceNewValue);
result = DivorceResult.SucessWithPenalty;
}
else
{
await _cs.AddAsync(user.Id, "Waifu Refund", amount, true);
await _cs.AddAsync(user.Id, amount, new("waifu", "refund"));
result = DivorceResult.Success;
}
@@ -316,7 +316,7 @@ public class WaifuService : INService
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;
if (!await _cs.RemoveAsync(from, itemObj.Price, new("waifu", "item"))) return false;
await using var uow = _db.GetDbContext();
var w = uow.WaifuInfo.ByWaifuUserId(giftedWaifu.Id, set => set.Include(x => x.Items).Include(x => x.Claimer));

View File

@@ -29,7 +29,7 @@ public class WheelOfFortuneGame
var amount = (long)(_bet * _config.WheelOfFortune.Multipliers[result]);
if (amount > 0)
await _cs.AddAsync(_userId, "Wheel Of Fortune - won", amount, true);
await _cs.AddAsync(_userId, amount, new("wheel", "win"));
return new() { Index = result, Amount = amount };
}

View File

@@ -29,7 +29,7 @@ public partial class Gambling
if (!await CheckBetMandatory(amount))
return;
if (!await _cs.RemoveAsync(ctx.User.Id, "Wheel Of Fortune - bet", amount, true))
if (!await _cs.RemoveAsync(ctx.User.Id, amount, new("wheel", "bet")))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;

View File

@@ -86,16 +86,16 @@ public class RollDuelGame
_locker.Release();
}
if (!await _cs.RemoveAsync(P1, "Roll Duel", Amount))
if (!await _cs.RemoveAsync(P1, Amount, new("rollduel", "bet")))
{
await OnEnded?.Invoke(this, Reason.NoFunds);
CurrentState = State.Ended;
return;
}
if (!await _cs.RemoveAsync(P2, "Roll Duel", Amount))
if (!await _cs.RemoveAsync(P2, Amount, new("rollduel", "bet")))
{
await _cs.AddAsync(P1, "Roll Duel - refund", Amount);
await _cs.AddAsync(P1, Amount, new("rollduel", "refund"));
await OnEnded?.Invoke(this, Reason.NoFunds);
CurrentState = State.Ended;
return;
@@ -114,9 +114,9 @@ public class RollDuelGame
else
Winner = P2;
var won = (long)(Amount * 2 * 0.98f);
await _cs.AddAsync(Winner, "Roll Duel win", won);
await _cs.AddAsync(Winner, won, new("rollduel", "win"));
await _cs.AddAsync(_botId, "Roll Duel fee", (Amount * 2) - won);
await _cs.AddAsync(_botId, (Amount * 2) - won, new("rollduel", "fee"));
}
try { await OnGameTick?.Invoke(this); }

View File

@@ -96,7 +96,7 @@ public sealed class HangmanService : IHangmanService, ILateExecutor
}
if (rew > 0)
await _cs.AddAsync(msg.Author, "hangman win", rew, gamble: true);
await _cs.AddAsync(msg.Author, rew, new("hangman", "win"));
await SendState((ITextChannel)msg.Channel, msg.Author, msg.Content, state);
}

View File

@@ -251,7 +251,7 @@ public class TriviaGame
var reward = _config.Trivia.CurrencyReward;
if (reward > 0)
await _cs.AddAsync(guildUser, "Won trivia", reward, true);
await _cs.AddAsync(guildUser, reward, new("trivia", "win"));
return;
}

View File

@@ -234,7 +234,7 @@ public class PatreonRewardsService : INService, IReadyExecutor
await uow.SaveChangesAsync();
await _currency.AddAsync(userId, "Patreon reward - new", eligibleFor, true);
await _currency.AddAsync(userId, eligibleFor, new("patreon", "new"));
Log.Information("Sending new currency reward to {UserId}", userId);
await SendMessageToUser(userId,
@@ -249,7 +249,7 @@ public class PatreonRewardsService : INService, IReadyExecutor
await uow.SaveChangesAsync();
await _currency.AddAsync(userId, "Patreon reward - recurring", eligibleFor, true);
await _currency.AddAsync(userId, eligibleFor, new("patreon", "recurring"));
Log.Information("Sending recurring currency reward to {UserId}", userId);
await SendMessageToUser(userId,
@@ -267,7 +267,7 @@ public class PatreonRewardsService : INService, IReadyExecutor
usr.AmountRewardedThisMonth = toAward;
await uow.SaveChangesAsync();
await _currency.AddAsync(userId, "Patreon reward - update", toAward, true);
await _currency.AddAsync(userId, toAward, new("patreon", "update"));
Log.Information("Sending updated currency reward to {UserId}", userId);
await SendMessageToUser(userId,

View File

@@ -210,7 +210,7 @@ public class XpService : INService
var crew = crews.FirstOrDefault(x => x.Level == i);
if (crew is not null)
//give the user the reward if it exists
await _cs.AddAsync(item.Key.User.Id, "Level-up Reward", crew.Amount);
await _cs.AddAsync(item.Key.User.Id, crew.Amount, new("xp", "level-up"));
}
}
}

View File

@@ -0,0 +1,123 @@
#nullable disable
using LinqToDB;
using NadekoBot.Services.Currency;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Services;
public class CurrencyService : ICurrencyService, INService
{
private readonly DbService _db;
public CurrencyService(DbService db)
=> _db = db;
public Task<IWallet> GetWalletAsync(ulong userId, CurrencyType type = CurrencyType.Default)
{
if (type == CurrencyType.Default)
{
return Task.FromResult<IWallet>(new DefaultWallet(userId, _db.GetDbContext()));
}
throw new ArgumentOutOfRangeException(nameof(type));
}
public async Task AddBulkAsync(
IReadOnlyCollection<ulong> userIds,
long amount,
Extra extra,
CurrencyType type = CurrencyType.Default)
{
if (type == CurrencyType.Default)
{
await using var ctx = _db.GetDbContext();
foreach (var userId in userIds)
{
var wallet = new DefaultWallet(userId, ctx);
await wallet.Add(amount, extra);
}
await ctx.SaveChangesAsync();
return;
}
throw new ArgumentOutOfRangeException(nameof(type));
}
public async Task RemoveBulkAsync(
IReadOnlyCollection<ulong> userIds,
long amount,
Extra extra,
CurrencyType type = CurrencyType.Default)
{
if (type == CurrencyType.Default)
{
await using var ctx = _db.GetDbContext();
await ctx.DiscordUser
.Where(x => userIds.Contains(x.UserId))
.UpdateAsync(du => new()
{
CurrencyAmount = du.CurrencyAmount >= amount
? du.CurrencyAmount - amount
: 0
});
return;
}
throw new ArgumentOutOfRangeException(nameof(type));
}
private CurrencyTransaction GetCurrencyTransaction(ulong userId, string reason, long amount)
=> new() { Amount = amount, UserId = userId, Reason = reason ?? "-" };
public async Task AddAsync(
ulong userId,
long amount,
Extra extra)
{
await using var wallet = await GetWalletAsync(userId);
await wallet.Add(amount, extra);
}
public async Task AddAsync(
IUser user,
long amount,
Extra extra)
{
await using var wallet = await GetWalletAsync(user.Id);
await wallet.Add(amount, extra);
}
public async Task<bool> RemoveAsync(
ulong userId,
long amount,
Extra extra)
{
await using var wallet = await GetWalletAsync(userId);
return await wallet.Take(amount, extra);
}
public async Task<bool> RemoveAsync(
IUser user,
long amount,
Extra extra)
{
await using var wallet = await GetWalletAsync(user.Id);
return await wallet.Take(amount, extra);
}
public async Task<bool> TransferAsync(
ulong from,
ulong to,
long amount,
string note)
{
await using var fromWallet = await GetWalletAsync(@from);
await using var toWallet = await GetWalletAsync(to);
var extra = new Extra("transfer", "gift", note);
return await fromWallet.Transfer(amount, toWallet, extra);
}
}

View File

@@ -0,0 +1,6 @@
namespace NadekoBot.Services.Currency;
public enum CurrencyType
{
Default,
}

View File

@@ -0,0 +1,104 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Services.Database;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Services.Currency;
public class DefaultWallet : IWallet
{
public ulong UserId { get; }
private readonly NadekoContext _ctx;
public DefaultWallet(ulong userId, NadekoContext ctx)
{
UserId = userId;
_ctx = ctx;
}
public Task<long> GetBalance()
=> _ctx.DiscordUser
.ToLinqToDBTable()
.Where(x => x.UserId == UserId)
.Select(x => x.CurrencyAmount)
.FirstOrDefaultAsync();
public async Task<bool> Take(long amount, Extra extra)
{
if (amount < 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Amount to take must be non negative.");
var changed = await _ctx.DiscordUser
.Where(x => x.UserId == UserId && x.CurrencyAmount >= amount)
.UpdateAsync(x => new()
{
CurrencyAmount = x.CurrencyAmount - amount
});
if (changed == 0)
return false;
// todo type, subtype
// todo from? by?
await _ctx.CreateLinqToDbContext()
.InsertAsync(new CurrencyTransaction()
{
Amount = -amount,
Reason = extra.Note,
UserId = UserId,
});
return true;
}
public async Task Add(long amount, Extra extra)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be greater than 0.");
await using (var tran = await _ctx.Database.BeginTransactionAsync())
{
var changed = await _ctx.DiscordUser
.UpdateAsync(x => new()
{
CurrencyAmount = x.CurrencyAmount + amount
});
if (changed == 0)
{
await _ctx.DiscordUser
.ToLinqToDBTable()
.Value(x => x.UserId, UserId)
.Value(x => x.Username, "Unknown")
.Value(x => x.Discriminator, "????")
.Value(x => x.CurrencyAmount, amount)
.InsertAsync();
}
await tran.CommitAsync();
}
var ct = new CurrencyTransaction()
{
Amount = amount,
Reason = extra.Note,
UserId = UserId,
};
await _ctx.CreateLinqToDbContext()
.InsertAsync(ct);
}
public void Dispose()
{
_ctx.SaveChanges();
_ctx.Dispose();
}
public async ValueTask DisposeAsync()
{
await _ctx.SaveChangesAsync();
await _ctx.DisposeAsync();
}
}

View File

@@ -0,0 +1,3 @@
namespace NadekoBot.Services.Currency;
public record class Extra(string Type, string Subtype, string Note = "", ulong OtherId = 0);

View File

@@ -0,0 +1,47 @@
using NadekoBot.Services.Currency;
#nullable disable
namespace NadekoBot.Services;
public interface ICurrencyService
{
Task<IWallet> GetWalletAsync(ulong userId, CurrencyType type = CurrencyType.Default);
Task AddBulkAsync(
IReadOnlyCollection<ulong> userIds,
long amount,
Extra extra,
CurrencyType type = CurrencyType.Default);
Task RemoveBulkAsync(
IReadOnlyCollection<ulong> userIds,
long amount,
Extra extra,
CurrencyType type = CurrencyType.Default);
Task AddAsync(
ulong userId,
long amount,
Extra extra);
Task AddAsync(
IUser user,
long amount,
Extra extra);
Task<bool> RemoveAsync(
ulong userId,
long amount,
Extra extra);
Task<bool> RemoveAsync(
IUser user,
long amount,
Extra extra);
Task<bool> TransferAsync(
ulong from,
ulong to,
long amount,
string note);
}

View File

@@ -0,0 +1,36 @@
namespace NadekoBot.Services.Currency;
public interface IWallet : IDisposable, IAsyncDisposable
{
public ulong UserId { get; }
public Task<long> GetBalance();
public Task<bool> Take(long amount, Extra extra);
public Task Add(long amount, Extra extra);
public async Task<bool> Transfer(
long amount,
IWallet to,
Extra extra)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be greater than 0.");
var succ = await Take(amount,
extra with
{
OtherId = to.UserId
});
if (!succ)
return false;
await to.Add(amount,
extra with
{
OtherId = UserId
});
return true;
}
}

View File

@@ -1,43 +0,0 @@
#nullable disable
namespace NadekoBot.Services;
public interface ICurrencyService
{
Task AddAsync(
ulong userId,
string reason,
long amount,
bool gamble = false);
Task AddAsync(
IUser user,
string reason,
long amount,
bool sendMessage = false,
bool gamble = false);
Task AddBulkAsync(
IEnumerable<ulong> userIds,
IEnumerable<string> reasons,
IEnumerable<long> amounts,
bool gamble = false);
Task<bool> RemoveAsync(
ulong userId,
string reason,
long amount,
bool gamble = false);
Task<bool> RemoveAsync(
IUser userId,
string reason,
long amount,
bool sendMessage = false,
bool gamble = false);
Task RemoveBulkAsync(
IEnumerable<ulong> userIds,
IEnumerable<string> reasons,
IEnumerable<long> amounts,
bool gamble = false);
}

View File

@@ -1,3 +1,5 @@
using NadekoBot.Services.Currency;
#nullable disable
namespace NadekoBot.Services;

View File

@@ -1,185 +0,0 @@
#nullable disable
using NadekoBot.Db;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Services.Database;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Services;
public class CurrencyService : ICurrencyService, INService
{
private readonly DbService _db;
private readonly GamblingConfigService _gss;
private readonly IEmbedBuilderService _eb;
private readonly IUser _bot;
public CurrencyService(
DbService db,
DiscordSocketClient c,
GamblingConfigService gss,
IEmbedBuilderService eb)
{
_db = db;
_gss = gss;
_eb = eb;
_bot = c.CurrentUser;
}
private CurrencyTransaction GetCurrencyTransaction(ulong userId, string reason, long amount)
=> new() { Amount = amount, UserId = userId, Reason = reason ?? "-" };
private bool InternalChange(
ulong userId,
string userName,
string discrim,
string avatar,
string reason,
long amount,
bool gamble,
NadekoContext uow)
{
var result = uow.TryUpdateCurrencyState(userId, userName, discrim, avatar, amount);
if (result)
{
var t = GetCurrencyTransaction(userId, reason, amount);
uow.CurrencyTransactions.Add(t);
if (gamble)
{
var t2 = GetCurrencyTransaction(_bot.Id, reason, -amount);
uow.CurrencyTransactions.Add(t2);
uow.TryUpdateCurrencyState(_bot.Id, _bot.Username, _bot.Discriminator, _bot.AvatarId, -amount, true);
}
}
return result;
}
private async Task InternalAddAsync(
ulong userId,
string userName,
string discrim,
string avatar,
string reason,
long amount,
bool gamble)
{
if (amount < 0)
throw new ArgumentException("You can't add negative amounts. Use RemoveAsync method for that.",
nameof(amount));
await using var uow = _db.GetDbContext();
InternalChange(userId, userName, discrim, avatar, reason, amount, gamble, uow);
await uow.SaveChangesAsync();
}
public Task AddAsync(
ulong userId,
string reason,
long amount,
bool gamble = false)
=> InternalAddAsync(userId, null, null, null, reason, amount, gamble);
public async Task AddAsync(
IUser user,
string reason,
long amount,
bool sendMessage = false,
bool gamble = false)
{
await InternalAddAsync(user.Id, user.Username, user.Discriminator, user.AvatarId, reason, amount, gamble);
if (sendMessage)
try
{
var sign = _gss.Data.Currency.Sign;
await user.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle("Received Currency")
.AddField("Amount", amount + sign)
.AddField("Reason", reason));
}
catch
{
// ignored
}
}
public async Task AddBulkAsync(
IEnumerable<ulong> userIds,
IEnumerable<string> reasons,
IEnumerable<long> amounts,
bool gamble = false)
{
var idArray = userIds as ulong[] ?? userIds.ToArray();
var reasonArray = reasons as string[] ?? reasons.ToArray();
var amountArray = amounts as long[] ?? amounts.ToArray();
if (idArray.Length != reasonArray.Length || reasonArray.Length != amountArray.Length)
throw new ArgumentException("Cannot perform bulk operation. Arrays are not of equal length.");
var userIdHashSet = new HashSet<ulong>(idArray.Length);
await using var uow = _db.GetDbContext();
for (var i = 0; i < idArray.Length; i++)
// i have to prevent same user changing more than once as it will cause db error
if (userIdHashSet.Add(idArray[i]))
InternalChange(idArray[i], null, null, null, reasonArray[i], amountArray[i], gamble, uow);
await uow.SaveChangesAsync();
}
public async Task RemoveBulkAsync(
IEnumerable<ulong> userIds,
IEnumerable<string> reasons,
IEnumerable<long> amounts,
bool gamble = false)
{
var idArray = userIds as ulong[] ?? userIds.ToArray();
var reasonArray = reasons as string[] ?? reasons.ToArray();
var amountArray = amounts as long[] ?? amounts.ToArray();
if (idArray.Length != reasonArray.Length || reasonArray.Length != amountArray.Length)
throw new ArgumentException("Cannot perform bulk operation. Arrays are not of equal length.");
var userIdHashSet = new HashSet<ulong>(idArray.Length);
await using var uow = _db.GetDbContext();
for (var i = 0; i < idArray.Length; i++)
// i have to prevent same user changing more than once as it will cause db error
if (userIdHashSet.Add(idArray[i]))
InternalChange(idArray[i], null, null, null, reasonArray[i], -amountArray[i], gamble, uow);
await uow.SaveChangesAsync();
}
private async Task<bool> InternalRemoveAsync(
ulong userId,
string userName,
string userDiscrim,
string avatar,
string reason,
long amount,
bool gamble = false)
{
if (amount < 0)
throw new ArgumentException("You can't remove negative amounts. Use AddAsync method for that.",
nameof(amount));
bool result;
await using var uow = _db.GetDbContext();
result = InternalChange(userId, userName, userDiscrim, avatar, reason, -amount, gamble, uow);
await uow.SaveChangesAsync();
return result;
}
public Task<bool> RemoveAsync(
ulong userId,
string reason,
long amount,
bool gamble = false)
=> InternalRemoveAsync(userId, null, null, null, reason, amount, gamble);
public Task<bool> RemoveAsync(
IUser user,
string reason,
long amount,
bool sendMessage = false,
bool gamble = false)
=> InternalRemoveAsync(user.Id, user.Username, user.Discriminator, user.AvatarId, reason, amount, gamble);
}