mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-04-30 03:22:04 -04:00
Migrated away from extension methods for pretty much all the money stuff as it turns out it passes a copy of the object and not a reference. This was causing a lot of weird behavior probably due to EF change tracking.
Also added a lot more logging to the API itself.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="AngularNgOptimizedImage" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
||||
@@ -26,7 +26,7 @@ public class GetBalanceCommand : ICommand
|
||||
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
|
||||
CancellationToken ctx)
|
||||
{
|
||||
var gambler = await user.GetGamblerEntity(ct: ctx);
|
||||
var gambler = await Money.GetGamblerEntityAsync(user, ct: ctx);
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()}, your balance is {await gambler!.Balance.FormatKasinoCurrencyAsync()}", true);
|
||||
}
|
||||
@@ -45,8 +45,12 @@ public class GetExclusionCommand : ICommand
|
||||
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
|
||||
CancellationToken ctx)
|
||||
{
|
||||
var gambler = await user.GetGamblerEntity(ct: ctx);
|
||||
var exclusion = await gambler!.GetActiveExclusion(ct: ctx);
|
||||
var gambler = await Money.GetGamblerEntityAsync(user, ct: ctx);
|
||||
if (gambler == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Caught a null when retrieving {user.Id}'s gambler entity");
|
||||
}
|
||||
var exclusion = await Money.GetActiveExclusionAsync(gambler, ct: ctx);
|
||||
if (exclusion == null)
|
||||
{
|
||||
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, you are currently not excluded.", true);
|
||||
@@ -80,7 +84,7 @@ public class SendJuiceCommand : ICommand
|
||||
{
|
||||
var logger = LogManager.GetCurrentClassLogger();
|
||||
await using var db = new ApplicationDbContext();
|
||||
var gambler = await user.GetGamblerEntity(ct: ctx);
|
||||
var gambler = await Money.GetGamblerEntityAsync(user, ct: ctx);
|
||||
var targetUser = await db.Users.FirstOrDefaultAsync(u => u.KfId == int.Parse(arguments["user_id"].Value), ctx);
|
||||
var amount = decimal.Parse(arguments["amount"].Value);
|
||||
if (gambler == null)
|
||||
@@ -100,16 +104,16 @@ public class SendJuiceCommand : ICommand
|
||||
return;
|
||||
}
|
||||
|
||||
var targetGambler = await targetUser.GetGamblerEntity(ct: ctx);
|
||||
var targetGambler = await Money.GetGamblerEntityAsync(targetUser, ct: ctx);
|
||||
if (targetGambler == null)
|
||||
{
|
||||
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, you can't juice a banned user", true);
|
||||
return;
|
||||
}
|
||||
|
||||
await gambler.ModifyBalance(-amount, TransactionSourceEventType.Juicer,
|
||||
await Money.ModifyBalanceAsync(gambler, -amount, TransactionSourceEventType.Juicer,
|
||||
$"Juice sent to {targetUser.KfUsername}", ct: ctx);
|
||||
await targetGambler.ModifyBalance(amount, TransactionSourceEventType.Juicer, $"Juice from {user.KfUsername}",
|
||||
await Money.ModifyBalanceAsync(targetGambler, amount, TransactionSourceEventType.Juicer, $"Juice from {user.KfUsername}",
|
||||
gambler, ctx);
|
||||
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, {await amount.FormatKasinoCurrencyAsync()} has been sent to {targetUser.KfUsername}", true);
|
||||
}
|
||||
@@ -134,20 +138,23 @@ public class RakebackCommand : ICommand
|
||||
CancellationToken ctx)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
var gambler = await user.GetGamblerEntity(ct: ctx);
|
||||
db.Attach(gambler!);
|
||||
var gambler = await Money.GetGamblerEntityAsync(user, ct: ctx);
|
||||
if (gambler == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Caught a null when retrieving {user.Id}'s gambler entity");
|
||||
}
|
||||
var settings = await SettingsProvider.GetMultipleValuesAsync([
|
||||
BuiltIn.Keys.MoneyRakebackPercentage, BuiltIn.Keys.MoneyRakebackMinimumAmount
|
||||
]);
|
||||
var mostRecentRakeback = await db.Transactions.OrderBy(x => x.Id).LastOrDefaultAsync(tx =>
|
||||
tx.EventSource == TransactionSourceEventType.Rakeback && tx.Gambler == gambler, cancellationToken: ctx);
|
||||
tx.EventSource == TransactionSourceEventType.Rakeback && tx.Gambler.Id == gambler.Id, cancellationToken: ctx);
|
||||
long offset = 0;
|
||||
if (mostRecentRakeback != null)
|
||||
{
|
||||
offset = mostRecentRakeback.TimeUnixEpochSeconds;
|
||||
}
|
||||
|
||||
var wagers = await db.Wagers.Where(w => w.Gambler == gambler && w.TimeUnixEpochSeconds > offset).ToListAsync(ctx);
|
||||
var wagers = await db.Wagers.Where(w => w.Gambler.Id == gambler.Id && w.TimeUnixEpochSeconds > offset).ToListAsync(ctx);
|
||||
if (wagers.Count == 0)
|
||||
{
|
||||
await botInstance.SendChatMessageAsync(
|
||||
@@ -163,7 +170,7 @@ public class RakebackCommand : ICommand
|
||||
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, your rakeback payout of {await rakeback.FormatKasinoCurrencyAsync()} is below the minimum amount of {await minimumRakeback.FormatKasinoCurrencyAsync()}", true);
|
||||
return;
|
||||
}
|
||||
await gambler!.ModifyBalance(rakeback, TransactionSourceEventType.Rakeback, "Rakeback claimed by gambler",
|
||||
await Money.ModifyBalanceAsync(gambler, rakeback, TransactionSourceEventType.Rakeback, "Rakeback claimed by gambler",
|
||||
ct: ctx);
|
||||
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, the hostess has given you {await rakeback.FormatKasinoCurrencyAsync()} rakeback", true);
|
||||
}
|
||||
@@ -187,20 +194,23 @@ public class LossbackCommand : ICommand
|
||||
CancellationToken ctx)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
var gambler = await user.GetGamblerEntity(ct: ctx);
|
||||
db.Attach(gambler!);
|
||||
var gambler = await Money.GetGamblerEntityAsync(user, ct: ctx);
|
||||
if (gambler == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Caught a null when retrieving {user.Id}'s gambler entity");
|
||||
}
|
||||
var settings = await SettingsProvider.GetMultipleValuesAsync([
|
||||
BuiltIn.Keys.MoneyLossbackPercentage, BuiltIn.Keys.MoneyLossbackMinimumAmount
|
||||
]);
|
||||
var mostRecentLossback = await db.Transactions.OrderBy(x => x.Id).LastOrDefaultAsync(tx =>
|
||||
tx.EventSource == TransactionSourceEventType.Lossback && tx.Gambler == gambler, cancellationToken: ctx);
|
||||
tx.EventSource == TransactionSourceEventType.Lossback && tx.Gambler.Id == gambler.Id, cancellationToken: ctx);
|
||||
long offset = 0;
|
||||
if (mostRecentLossback != null)
|
||||
{
|
||||
offset = mostRecentLossback.TimeUnixEpochSeconds;
|
||||
}
|
||||
|
||||
var wagers = await db.Wagers.Where(w => w.Gambler == gambler && w.TimeUnixEpochSeconds > offset && w.Multiplier < 1).ToListAsync(ctx);
|
||||
var wagers = await db.Wagers.Where(w => w.Gambler.Id == gambler.Id && w.TimeUnixEpochSeconds > offset && w.Multiplier < 1).ToListAsync(ctx);
|
||||
if (wagers.Count == 0)
|
||||
{
|
||||
await botInstance.SendChatMessageAsync(
|
||||
@@ -216,7 +226,7 @@ public class LossbackCommand : ICommand
|
||||
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, your lossback payout of {await lossback.FormatKasinoCurrencyAsync()} is below the minimum amount of {await minimumLossback.FormatKasinoCurrencyAsync()}", true);
|
||||
return;
|
||||
}
|
||||
await gambler!.ModifyBalance(lossback, TransactionSourceEventType.Lossback, "Lossback claimed by gambler",
|
||||
await Money.ModifyBalanceAsync(gambler, lossback, TransactionSourceEventType.Lossback, "Lossback claimed by gambler",
|
||||
ct: ctx);
|
||||
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, the hostess has given you {await lossback.FormatKasinoCurrencyAsync()} lossback", true);
|
||||
}
|
||||
@@ -245,8 +255,12 @@ public class AbandonKasinoCommand : ICommand
|
||||
return;
|
||||
}
|
||||
await using var db = new ApplicationDbContext();
|
||||
var gambler = await user.GetGamblerEntity(ct: ctx);
|
||||
db.Attach(gambler!);
|
||||
var gambler = await Money.GetGamblerEntityAsync(user, ct: ctx);
|
||||
if (gambler == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Caught a null when retrieving {user.Id}'s gambler entity");
|
||||
}
|
||||
db.Attach(gambler);
|
||||
gambler!.State = GamblerState.Abandoned;
|
||||
await db.SaveChangesAsync(ctx);
|
||||
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, Kasino account with ID {gambler.Id} has been marked as abandoned.", true);
|
||||
|
||||
@@ -10,252 +10,6 @@ namespace KfChatDotNetBot.Extensions;
|
||||
|
||||
public static class MoneyExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve a gambler entity for a given user
|
||||
/// Returns null if createIfNoneExists is false and no gambler exists
|
||||
/// Also returns null if the user was permanently banned from gambling
|
||||
/// If there are multiple "active" gamblers, only the newest is returned
|
||||
/// </summary>
|
||||
/// <param name="user">User whose gambler entity you wish to retrieve</param>
|
||||
/// <param name="createIfNoneExists">Whether to create a gambler entity if none exists already</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<GamblerDbModel?> GetGamblerEntity(this UserDbModel user, bool createIfNoneExists = true, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
// Refetch user as I'm fairly certain some of the buggy behavior is coming from db.Attach being weird
|
||||
user = await db.Users.FirstAsync(u => u.Id == user.Id, cancellationToken: ct);
|
||||
var gambler =
|
||||
await db.Gamblers.OrderBy(x => x.Id).LastOrDefaultAsync(g => g.User == user && g.State != GamblerState.PermanentlyBanned,
|
||||
cancellationToken: ct);
|
||||
if (!createIfNoneExists) return gambler;
|
||||
var permaBanned = await db.Gamblers.AnyAsync(g => g.User == user && g.State == GamblerState.PermanentlyBanned, cancellationToken: ct);
|
||||
if (permaBanned) return null;
|
||||
var initialBalance = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.MoneyInitialBalance)).ToType<decimal>();
|
||||
await db.Gamblers.AddAsync(new GamblerDbModel
|
||||
{
|
||||
User = user,
|
||||
Balance = initialBalance,
|
||||
Created = DateTimeOffset.UtcNow,
|
||||
RandomSeed = Guid.NewGuid().ToString(),
|
||||
State = GamblerState.Active,
|
||||
TotalWagered = 0,
|
||||
NextVipLevelWagerRequirement = Money.VipLevels[0].BaseWagerRequirement
|
||||
}, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return await db.Gamblers.OrderBy(x => x.Id).LastOrDefaultAsync(g => g.User == user, cancellationToken: ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple check to see whether a user has been permanently banned from the kasino
|
||||
/// </summary>
|
||||
/// <param name="user">User to check for the permaban</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<bool> IsPermanentlyBanned(this UserDbModel user, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
return await db.Gamblers.AnyAsync(u => u.User.Id == user.Id && u.State == GamblerState.PermanentlyBanned,
|
||||
cancellationToken: ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modify a gambler's balance by a given +/- amount
|
||||
/// </summary>
|
||||
/// <param name="gambler">Gambler entity whose balance you wish to modify</param>
|
||||
/// <param name="effect">The 'effect' of this modification, as in how much to add or remove</param>
|
||||
/// <param name="eventSource">The event which initiated this balance modification</param>
|
||||
/// <param name="comment">Optional comment to provide for the transaction</param>
|
||||
/// <param name="from">If applicable, who sent the transaction (e.g. if a juicer)</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
public static async Task ModifyBalance(this GamblerDbModel gambler, decimal effect,
|
||||
TransactionSourceEventType eventSource, string? comment = null, GamblerDbModel? from = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
gambler = await db.Gamblers.FirstAsync(g => g.Id == gambler.Id, cancellationToken: ct);
|
||||
gambler.Balance += effect;
|
||||
await db.Transactions.AddAsync(new TransactionDbModel
|
||||
{
|
||||
Gambler = gambler,
|
||||
EventSource = eventSource,
|
||||
Effect = effect,
|
||||
Time = DateTimeOffset.UtcNow,
|
||||
Comment = comment,
|
||||
From = from,
|
||||
NewBalance = gambler.Balance,
|
||||
TimeUnixEpochSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
}, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a wager to the database
|
||||
/// Will also issue a balance update unless you explicitly disable autoModifyBalance
|
||||
/// </summary>
|
||||
/// <param name="gambler">Gambler who wagered</param>
|
||||
/// <param name="wagerAmount">The amount they wagered</param>
|
||||
/// <param name="wagerEffect">The effect of the wager on the gambler's balance.
|
||||
/// Please note this includes the wager itself. So for a bet of 500 that paid out 50, you pass in an effect of -450
|
||||
/// If instead they won 600 then the effect would be +100. the wagered amount is not factored into balance changes,
|
||||
/// it's just recorded for calculating bonuses and statistics</param>
|
||||
/// <param name="game">The game which was played as part of this wager</param>
|
||||
/// <param name="autoModifyBalance">Whether tu automatically update the user's balance according to the wager effect.
|
||||
/// Typically you should leave this on so as to ensure every wager has an associated transaction.</param>
|
||||
/// <param name="gameMeta">Optionally store metadata related to the wager, such as player choices, or game outcomes.
|
||||
/// Data will be serialized to JSON.</param>
|
||||
/// <param name="isComplete">Whether the game is 'complete'. Set to false for wagers with unknown outcomes.
|
||||
/// NOTE: wagerEffect will be ignored, instead value will be derived from the wagerAmount</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
public static async Task NewWager(this GamblerDbModel gambler, decimal wagerAmount, decimal wagerEffect,
|
||||
WagerGame game, bool autoModifyBalance = true, dynamic? gameMeta = null, bool isComplete = true,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var logger = LogManager.GetCurrentClassLogger();
|
||||
await using var db = new ApplicationDbContext();
|
||||
gambler = await db.Gamblers.FirstAsync(g => g.Id == gambler.Id, cancellationToken: ct);
|
||||
string? metaJson = null;
|
||||
if (gameMeta != null)
|
||||
{
|
||||
metaJson = JsonConvert.SerializeObject(gameMeta, Formatting.Indented);
|
||||
logger.Debug("Serialized metadata follows");
|
||||
logger.Debug(metaJson);
|
||||
}
|
||||
|
||||
if (!isComplete)
|
||||
{
|
||||
wagerEffect = -wagerAmount;
|
||||
logger.Debug($"isComplete is false, set wagerEffect to {wagerEffect}");
|
||||
}
|
||||
decimal multi = 0;
|
||||
if (isComplete && wagerAmount > 0 && wagerEffect > 0 && wagerAmount + wagerEffect > 0)
|
||||
{
|
||||
multi = (wagerAmount + wagerEffect) / wagerAmount;
|
||||
logger.Debug($"multi is {multi}");
|
||||
}
|
||||
|
||||
gambler.TotalWagered += wagerAmount;
|
||||
var wager = await db.Wagers.AddAsync(new WagerDbModel
|
||||
{
|
||||
Gambler = gambler,
|
||||
Time = DateTimeOffset.UtcNow,
|
||||
WagerAmount = wagerAmount,
|
||||
WagerEffect = wagerEffect,
|
||||
Multiplier = multi,
|
||||
Game = game,
|
||||
GameMeta = metaJson,
|
||||
IsComplete = isComplete,
|
||||
TimeUnixEpochSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
}, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
gambler.Balance += wagerEffect;
|
||||
if (!autoModifyBalance) return;
|
||||
|
||||
await db.Transactions.AddAsync(new TransactionDbModel
|
||||
{
|
||||
Gambler = gambler,
|
||||
EventSource = TransactionSourceEventType.Gambling,
|
||||
Time = DateTimeOffset.UtcNow,
|
||||
Effect = wagerEffect,
|
||||
Comment = $"Win from wager {wager.Entity.Id}",
|
||||
From = null,
|
||||
NewBalance = gambler.Balance,
|
||||
TimeUnixEpochSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
}, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an active exclusion, returns null if there's no active exclusion
|
||||
/// If there's somehow multiple exclusions, will just grab the most recent one
|
||||
/// </summary>
|
||||
/// <param name="gambler">Gambler entity to retrieve the exclusion for</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<GamblerExclusionDbModel?> GetActiveExclusion(this GamblerDbModel gambler, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
return (await db.Exclusions.Where(g => g.Gambler.Id == gambler.Id).ToListAsync(ct))
|
||||
.LastOrDefault(e => e.Expires <= DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get random number using the gambler's seed and a given number of iterations
|
||||
/// </summary>
|
||||
/// <param name="gambler">Gambler entity to reference their random seed</param>
|
||||
/// <param name="min">Minimum value for generating random</param>
|
||||
/// <param name="max">Maximum value for random, incremented by 1 if add1ToMaxParam is true
|
||||
/// so it's consistent with the behavior of min</param>
|
||||
/// <param name="iterations">Number of random number generator iterations to run before returning a result</param>
|
||||
/// <param name="incrementMaxParam">Increments the 'max' param by 1 as otherwise the value will never be returned by Random.Next()
|
||||
/// This is because the default behavior of .NET is unintuitive, min value can be returned but max is never by default</param>
|
||||
/// <returns>A random number based on the given parameters</returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public static int GetRandomNumber(this GamblerDbModel gambler, int min, int max, int iterations = 10,
|
||||
bool incrementMaxParam = true)
|
||||
{
|
||||
var random = new Random(gambler.RandomSeed.GetHashCode());
|
||||
var result = 0;
|
||||
var i = 0;
|
||||
if (incrementMaxParam) max++;
|
||||
if (iterations <= 0) throw new ArgumentException("Iterations cannot be 0 or lower");
|
||||
while (i < iterations)
|
||||
{
|
||||
i++;
|
||||
result = random.Next(min, max);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the user's current VIP level
|
||||
/// </summary>
|
||||
/// <param name="gambler">Gambler entity whose VIP level you want to get</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<GamblerPerkDbModel?> GetVipLevel(this GamblerDbModel gambler, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
var perk = await db.Perks.OrderBy(x => x.Id).LastOrDefaultAsync(
|
||||
p => p.Gambler.Id == gambler.Id && p.PerkType == GamblerPerkType.VipLevel, ct);
|
||||
return perk;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upgrade to the given VIP level. Grants a bonus as part of the level up.
|
||||
/// </summary>
|
||||
/// <param name="gambler">The gambler you wish to level up</param>
|
||||
/// <param name="nextVipLevel">VIP level to grant them</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns>The bonus they received</returns>
|
||||
public static async Task<decimal> UpgradeVipLevel(this GamblerDbModel gambler, NextVipLevelModel nextVipLevel,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
gambler = await db.Gamblers.FirstAsync(g => g.Id == gambler.Id, cancellationToken: ct);
|
||||
var payout = nextVipLevel.VipLevel.BonusPayout;
|
||||
if (nextVipLevel.Tier > 1)
|
||||
{
|
||||
payout = nextVipLevel.VipLevel.BonusPayout / (nextVipLevel.VipLevel.Tiers - 1);
|
||||
}
|
||||
|
||||
await db.Perks.AddAsync(new GamblerPerkDbModel
|
||||
{
|
||||
Gambler = gambler,
|
||||
PerkType = GamblerPerkType.VipLevel,
|
||||
PerkName = nextVipLevel.VipLevel.Name,
|
||||
PerkTier = nextVipLevel.Tier,
|
||||
Time = DateTimeOffset.UtcNow,
|
||||
Payout = payout,
|
||||
}, ct);
|
||||
gambler.NextVipLevelWagerRequirement = nextVipLevel.WagerRequirement;
|
||||
await db.SaveChangesAsync(ct);
|
||||
await gambler.ModifyBalance(payout, TransactionSourceEventType.Bonus,
|
||||
$"VIP Level '{nextVipLevel.VipLevel.Icon} {nextVipLevel.VipLevel.Name}' Tier {nextVipLevel.Tier} level up bonus", ct: ct);
|
||||
return payout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format an amount of money using configured symbols
|
||||
/// </summary>
|
||||
|
||||
@@ -74,7 +74,7 @@ internal class BotCommands
|
||||
if (!kasinoEnabled) return;
|
||||
}
|
||||
|
||||
if (kasinoCommand && user.IsPermanentlyBanned(_cancellationToken).Result)
|
||||
if (kasinoCommand && Money.IsPermanentlyBannedAsync(user, _cancellationToken).Result)
|
||||
{
|
||||
_bot.SendChatMessage($"@{message.Author.Username}, you've been permanently banned from the kasino. Contact support for more information.", true);
|
||||
return;
|
||||
@@ -84,8 +84,8 @@ internal class BotCommands
|
||||
{
|
||||
// GetGamblerEntity will only return null if the user is permanbanned
|
||||
// and we have a check further up the chain for that hence ignoring the null
|
||||
var exclusion = user.GetGamblerEntity(ct: _cancellationToken).Result
|
||||
!.GetActiveExclusion(ct: _cancellationToken).Result;
|
||||
var gambler = Money.GetGamblerEntityAsync(user, ct: _cancellationToken).Result;
|
||||
var exclusion = Money.GetActiveExclusionAsync(gambler!, ct: _cancellationToken).Result;
|
||||
if (exclusion != null)
|
||||
{
|
||||
_bot.SendChatMessage(
|
||||
@@ -140,14 +140,14 @@ internal class BotCommands
|
||||
if (!(await SettingsProvider.GetValueAsync(BuiltIn.Keys.MoneyEnabled)).ToBoolean()) return;
|
||||
var wagerCommand = HasAttribute<WagerCommand>(command);
|
||||
if (!wagerCommand) return;
|
||||
var gambler = await user.GetGamblerEntity(ct: _cancellationToken);
|
||||
var gambler = await Money.GetGamblerEntityAsync(user, ct: _cancellationToken);
|
||||
if (gambler == null) return;
|
||||
if (gambler.TotalWagered < gambler.NextVipLevelWagerRequirement) return;
|
||||
// The reason for doing this instead of passing in TotalWagered is that otherwise VIP levels might
|
||||
// get skipped if the user is a low VIP level but wagering very large amounts
|
||||
var newLevel = Money.GetNextVipLevel(gambler.NextVipLevelWagerRequirement);
|
||||
if (newLevel == null) return;
|
||||
var payout = await gambler.UpgradeVipLevel(newLevel, _cancellationToken);
|
||||
var payout = await Money.UpgradeVipLevelAsync(gambler, newLevel, _cancellationToken);
|
||||
await _bot.SendChatMessageAsync(
|
||||
$"🤑🤑 {user.FormatUsername()} has leveled up to to {newLevel.VipLevel.Icon} {newLevel.VipLevel.Name} Tier {newLevel.Tier} " +
|
||||
$"and received a bonus of {await payout.FormatKasinoCurrencyAsync()}", true);
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
using KfChatDotNetBot.Models;
|
||||
using Humanizer;
|
||||
using KfChatDotNetBot.Models;
|
||||
using KfChatDotNetBot.Models.DbModels;
|
||||
using KfChatDotNetBot.Settings;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
|
||||
namespace KfChatDotNetBot.Services;
|
||||
|
||||
public static class Money
|
||||
{
|
||||
private static Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
/// <summary>
|
||||
/// This is the list of available VIP levels for gamblers to ascend
|
||||
/// The order of this array is important, it begins with the loest level VIP, and ascends IN ORDER to the max level
|
||||
@@ -196,4 +203,276 @@ public static class Money
|
||||
WagerRequirement = nextTier
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a gambler entity for a given user
|
||||
/// Returns null if createIfNoneExists is false and no gambler exists
|
||||
/// Also returns null if the user was permanently banned from gambling
|
||||
/// If there are multiple "active" gamblers, only the newest is returned
|
||||
/// </summary>
|
||||
/// <param name="user">User whose gambler entity you wish to retrieve</param>
|
||||
/// <param name="createIfNoneExists">Whether to create a gambler entity if none exists already</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<GamblerDbModel?> GetGamblerEntityAsync(UserDbModel user, bool createIfNoneExists = true, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
var gambler =
|
||||
await db.Gamblers.OrderBy(x => x.Id).Include(x => x.User).LastOrDefaultAsync(g => g.User.Id == user.Id && g.State != GamblerState.PermanentlyBanned,
|
||||
cancellationToken: ct);
|
||||
_logger.Info($"Retrieved entity for {user.KfUsername}. Is Gambler Entity Null? => {gambler == null}");
|
||||
if (gambler != null)
|
||||
{
|
||||
_logger.Info($"Gambler entity details: {gambler.Id}, Created: {gambler.Created:o}");
|
||||
}
|
||||
if (!createIfNoneExists) return gambler;
|
||||
var permaBanned = await IsPermanentlyBannedAsync(user, ct);
|
||||
_logger.Info($"permaBanned => {permaBanned}");
|
||||
if (permaBanned) return null;
|
||||
var initialBalance = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.MoneyInitialBalance)).ToType<decimal>();
|
||||
await db.Gamblers.AddAsync(new GamblerDbModel
|
||||
{
|
||||
User = user,
|
||||
Balance = initialBalance,
|
||||
Created = DateTimeOffset.UtcNow,
|
||||
RandomSeed = Guid.NewGuid().ToString(),
|
||||
State = GamblerState.Active,
|
||||
TotalWagered = 0,
|
||||
NextVipLevelWagerRequirement = Money.VipLevels[0].BaseWagerRequirement
|
||||
}, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
var newEntity = await db.Gamblers.OrderBy(x => x.Id).Include(x => x.User)
|
||||
.LastOrDefaultAsync(g => g.User == user, cancellationToken: ct);
|
||||
if (newEntity == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Caught a null when trying to retrieve freshly created gambler entity for user {user.KfId}");
|
||||
}
|
||||
_logger.Info($"New gambler entity created for {user.KfUsername} with ID {newEntity.Id}");
|
||||
return newEntity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple check to see whether a user has been permanently banned from the kasino
|
||||
/// </summary>
|
||||
/// <param name="user">User to check for the permaban</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<bool> IsPermanentlyBannedAsync(UserDbModel user, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
return await db.Gamblers.AnyAsync(u => u.User.Id == user.Id && u.State == GamblerState.PermanentlyBanned,
|
||||
cancellationToken: ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modify a gambler's balance by a given +/- amount
|
||||
/// </summary>
|
||||
/// <param name="gambler">Gambler entity whose balance you wish to modify</param>
|
||||
/// <param name="effect">The 'effect' of this modification, as in how much to add or remove</param>
|
||||
/// <param name="eventSource">The event which initiated this balance modification</param>
|
||||
/// <param name="comment">Optional comment to provide for the transaction</param>
|
||||
/// <param name="from">If applicable, who sent the transaction (e.g. if a juicer)</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
public static async Task ModifyBalanceAsync(GamblerDbModel gambler, decimal effect,
|
||||
TransactionSourceEventType eventSource, string? comment = null, GamblerDbModel? from = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
db.Attach(gambler);
|
||||
_logger.Info($"Updating balance for {gambler.Id} with effect {effect:N}. Balance is currently {gambler.Balance:N}");
|
||||
gambler.Balance += effect;
|
||||
_logger.Info($"Balance is now {gambler.Balance:N}");
|
||||
await db.Transactions.AddAsync(new TransactionDbModel
|
||||
{
|
||||
Gambler = gambler,
|
||||
EventSource = eventSource,
|
||||
Effect = effect,
|
||||
Time = DateTimeOffset.UtcNow,
|
||||
Comment = comment,
|
||||
From = from,
|
||||
NewBalance = gambler.Balance,
|
||||
TimeUnixEpochSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
}, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a wager to the database
|
||||
/// Will also issue a balance update unless you explicitly disable autoModifyBalance
|
||||
/// </summary>
|
||||
/// <param name="gambler">Gambler who wagered</param>
|
||||
/// <param name="wagerAmount">The amount they wagered</param>
|
||||
/// <param name="wagerEffect">The effect of the wager on the gambler's balance.
|
||||
/// Please note this includes the wager itself. So for a bet of 500 that paid out 50, you pass in an effect of -450
|
||||
/// If instead they won 600 then the effect would be +100. the wagered amount is not factored into balance changes,
|
||||
/// it's just recorded for calculating bonuses and statistics</param>
|
||||
/// <param name="game">The game which was played as part of this wager</param>
|
||||
/// <param name="autoModifyBalance">Whether tu automatically update the user's balance according to the wager effect.
|
||||
/// Typically, you should leave this on so as to ensure every wager has an associated transaction.</param>
|
||||
/// <param name="gameMeta">Optionally store metadata related to the wager, such as player choices, or game outcomes.
|
||||
/// Data will be serialized to JSON.</param>
|
||||
/// <param name="isComplete">Whether the game is 'complete'. Set to false for wagers with unknown outcomes.
|
||||
/// NOTE: wagerEffect will be ignored, instead value will be derived from the wagerAmount</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
public static async Task NewWagerAsync(GamblerDbModel gambler, decimal wagerAmount, decimal wagerEffect,
|
||||
WagerGame game, bool autoModifyBalance = true, dynamic? gameMeta = null, bool isComplete = true,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
_logger.Info($"Adding a wager for {gambler.User.KfUsername}. wagerAmount => {wagerAmount:N}, " +
|
||||
$"wagerEffect => {wagerEffect:N}, game => {game.Humanize()}, autoModifyBalance => {autoModifyBalance}, " +
|
||||
$"isComplete => {isComplete}");
|
||||
await using var db = new ApplicationDbContext();
|
||||
db.Attach(gambler);
|
||||
string? metaJson = null;
|
||||
if (gameMeta != null)
|
||||
{
|
||||
metaJson = JsonConvert.SerializeObject(gameMeta, Formatting.Indented);
|
||||
_logger.Info("Serialized metadata follows");
|
||||
_logger.Info(metaJson);
|
||||
}
|
||||
|
||||
if (!isComplete)
|
||||
{
|
||||
wagerEffect = -wagerAmount;
|
||||
_logger.Info($"isComplete is false, set wagerEffect to {wagerEffect}");
|
||||
}
|
||||
decimal multi = 0;
|
||||
if (isComplete && wagerAmount > 0 && wagerEffect > 0 && wagerAmount + wagerEffect > 0)
|
||||
{
|
||||
multi = (wagerAmount + wagerEffect) / wagerAmount;
|
||||
_logger.Info($"multi is {multi}");
|
||||
}
|
||||
|
||||
gambler.TotalWagered += wagerAmount;
|
||||
_logger.Info($"Updated TotalWagered to {gambler.TotalWagered}");
|
||||
var wager = await db.Wagers.AddAsync(new WagerDbModel
|
||||
{
|
||||
Gambler = gambler,
|
||||
Time = DateTimeOffset.UtcNow,
|
||||
WagerAmount = wagerAmount,
|
||||
WagerEffect = wagerEffect,
|
||||
Multiplier = multi,
|
||||
Game = game,
|
||||
GameMeta = metaJson,
|
||||
IsComplete = isComplete,
|
||||
TimeUnixEpochSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
}, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
_logger.Info($"Added wager row with ID {wager.Entity.Id}");
|
||||
gambler.Balance += wagerEffect;
|
||||
if (!autoModifyBalance) return;
|
||||
|
||||
var txn = await db.Transactions.AddAsync(new TransactionDbModel
|
||||
{
|
||||
Gambler = gambler,
|
||||
EventSource = TransactionSourceEventType.Gambling,
|
||||
Time = DateTimeOffset.UtcNow,
|
||||
Effect = wagerEffect,
|
||||
Comment = $"Win from wager {wager.Entity.Id}",
|
||||
From = null,
|
||||
NewBalance = gambler.Balance,
|
||||
TimeUnixEpochSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
}, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
_logger.Info($"Added transaction with ID {txn.Entity.Id}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an active exclusion, returns null if there's no active exclusion
|
||||
/// If there's somehow multiple exclusions, will just grab the most recent one
|
||||
/// </summary>
|
||||
/// <param name="gambler">Gambler entity to retrieve the exclusion for</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<GamblerExclusionDbModel?> GetActiveExclusionAsync(GamblerDbModel gambler, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
return (await db.Exclusions.Where(g => g.Gambler.Id == gambler.Id).ToListAsync(ct))
|
||||
.LastOrDefault(e => e.Expires <= DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get random number using the gambler's seed and a given number of iterations
|
||||
/// </summary>
|
||||
/// <param name="gambler">Gambler entity to reference their random seed</param>
|
||||
/// <param name="min">Minimum value for generating random</param>
|
||||
/// <param name="max">Maximum value for random, incremented by 1 if add1ToMaxParam is true
|
||||
/// so it's consistent with the behavior of min</param>
|
||||
/// <param name="iterations">Number of random number generator iterations to run before returning a result</param>
|
||||
/// <param name="incrementMaxParam">Increments the 'max' param by 1 as otherwise the value will never be returned by Random.Next()
|
||||
/// This is because the default behavior of .NET is unintuitive, min value can be returned but max is never by default</param>
|
||||
/// <returns>A random number based on the given parameters</returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public static int GetRandomNumber(GamblerDbModel gambler, int min, int max, int iterations = 10,
|
||||
bool incrementMaxParam = true)
|
||||
{
|
||||
var random = new Random(gambler.RandomSeed.GetHashCode());
|
||||
var result = 0;
|
||||
var i = 0;
|
||||
if (incrementMaxParam) max++;
|
||||
if (iterations <= 0) throw new ArgumentException("Iterations cannot be 0 or lower");
|
||||
while (i < iterations)
|
||||
{
|
||||
i++;
|
||||
result = random.Next(min, max);
|
||||
}
|
||||
_logger.Info($"Generated random number {result} with min {min} and max {max} over {iterations} iterations");
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the user's current VIP level
|
||||
/// </summary>
|
||||
/// <param name="gambler">Gambler entity whose VIP level you want to get</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<GamblerPerkDbModel?> GetVipLevelAsync(GamblerDbModel gambler, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
var perk = await db.Perks.OrderBy(x => x.Id).LastOrDefaultAsync(
|
||||
p => p.Gambler.Id == gambler.Id && p.PerkType == GamblerPerkType.VipLevel, ct);
|
||||
_logger.Info($"User's VIP perk is null? {perk == null}");
|
||||
if (perk != null)
|
||||
{
|
||||
_logger.Info($"VIP perk found for {gambler.User.Id}, {perk.PerkName} tier {perk.PerkTier}");
|
||||
}
|
||||
return perk;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upgrade to the given VIP level. Grants a bonus as part of the level up.
|
||||
/// </summary>
|
||||
/// <param name="gambler">The gambler you wish to level up</param>
|
||||
/// <param name="nextVipLevel">VIP level to grant them</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns>The bonus they received</returns>
|
||||
public static async Task<decimal> UpgradeVipLevelAsync(GamblerDbModel gambler, NextVipLevelModel nextVipLevel,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await using var db = new ApplicationDbContext();
|
||||
db.Attach(gambler);
|
||||
var payout = nextVipLevel.VipLevel.BonusPayout;
|
||||
if (nextVipLevel.Tier > 1)
|
||||
{
|
||||
payout = nextVipLevel.VipLevel.BonusPayout / (nextVipLevel.VipLevel.Tiers - 1);
|
||||
}
|
||||
_logger.Info($"Calculated a VIP payout of {payout:N} for gambler ID {gambler.Id}");
|
||||
|
||||
await db.Perks.AddAsync(new GamblerPerkDbModel
|
||||
{
|
||||
Gambler = gambler,
|
||||
PerkType = GamblerPerkType.VipLevel,
|
||||
PerkName = nextVipLevel.VipLevel.Name,
|
||||
PerkTier = nextVipLevel.Tier,
|
||||
Time = DateTimeOffset.UtcNow,
|
||||
Payout = payout,
|
||||
}, ct);
|
||||
gambler.NextVipLevelWagerRequirement = nextVipLevel.WagerRequirement;
|
||||
await db.SaveChangesAsync(ct);
|
||||
await ModifyBalanceAsync(gambler, payout, TransactionSourceEventType.Bonus,
|
||||
$"VIP Level '{nextVipLevel.VipLevel.Icon} {nextVipLevel.VipLevel.Name}' Tier {nextVipLevel.Tier} level up bonus", ct: ct);
|
||||
return payout;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user