Compare commits

...

2 Commits

Author SHA1 Message Date
barelyprofessional
7981f57a34 - Moved the Kasino shop models to their own file
- Added investments as a derivative of assets
- Added profile state flags which can retain basic states like IsSponsored
- Added profile state data using EF Core's JSON functionality so it should automatically serialize / deserialize the accompanying model for convenience (OnModelCreating code commented out due to the models not yet having a DbSet as I won't bake them in until KasinoShop is ready)
2026-04-26 20:39:49 -05:00
barelyprofessional
e725ca5864 Moved to Lazy<T> and a static class for handling Redis connections with some methods to make it easier to work with JSON. Completely untested. 2026-04-26 20:30:56 -05:00
11 changed files with 444 additions and 142 deletions

View File

@@ -12,8 +12,8 @@ public class ApplicationDbContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
base.OnModelCreating(modelBuilder); //modelBuilder.Entity<KasinoShopProfileDbModel>()
// .OwnsOne(p => p.StateData, b => b.ToJson());
} }
public DbSet<UserDbModel> Users { get; set; } public DbSet<UserDbModel> Users { get; set; }

View File

@@ -61,8 +61,7 @@ public class RouletteCommand : ICommand
var settings = await SettingsProvider.GetMultipleValuesAsync([ var settings = await SettingsProvider.GetMultipleValuesAsync([
BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay, BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay,
BuiltIn.Keys.KasinoRouletteEnabled, BuiltIn.Keys.KasinoRouletteEnabled,
BuiltIn.Keys.KasinoRouletteCountdownDuration, BuiltIn.Keys.KasinoRouletteCountdownDuration
BuiltIn.Keys.BotRedisConnectionString
]); ]);
// Check if roulette is enabled // Check if roulette is enabled
@@ -84,8 +83,7 @@ public class RouletteCommand : ICommand
return; return;
} }
var redis = await ConnectionMultiplexer.ConnectAsync(settings[BuiltIn.Keys.BotRedisConnectionString].Value!); _redisDb = Redis.Multiplexer.GetDatabase();
_redisDb = redis.GetDatabase();
var countdownDuration = TimeSpan.FromSeconds( var countdownDuration = TimeSpan.FromSeconds(
settings[BuiltIn.Keys.KasinoRouletteCountdownDuration].ToType<int>()); settings[BuiltIn.Keys.KasinoRouletteCountdownDuration].ToType<int>());

View File

@@ -0,0 +1,305 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace KfChatDotNetBot.Models.DbModels;
public class KasinoShopProfileDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Shop profiles belong to a user, not their gambler ID
/// they persist even if the user abandons their profile
/// </summary>
public required UserDbModel User { get; set; }
/// <summary>
/// Assets held by this profile
/// </summary>
public required List<KasinoShopProfileAssetDbModel> Assets { get; set; }
/// <summary>
/// Loans taken out by this profile
/// </summary>
[InverseProperty(nameof(KasinoShopProfileLoanDbModel.Borrower))]
public required List<KasinoShopProfileLoanDbModel> LoansTaken { get; set; }
/// <summary>
/// Loans owed to this profile
/// </summary>
[InverseProperty(nameof(KasinoShopProfileLoanDbModel.Lender))]
public required List<KasinoShopProfileLoanDbModel> LoansOwed { get; set; }
/// <summary>
/// State of the profile
/// </summary>
public required KasinoShopProfileStateFlags State { get; set; } = KasinoShopProfileStateFlags.None;
/// <summary>
/// JSON object containing data related to the above states
/// </summary>
public required KasinoShopProfileStateDataModel StateData { get; set; }
/// <summary>
/// Profile balance in the "Krypto" currency
/// </summary>
public required decimal KryptoBalance { get; set; }
}
// Note this is serialized to JSON by Entity Framework so you can go wild shoving random bullshit in here
public class KasinoShopProfileStateDataModel
{
/// <summary>
/// Profile credit score for determining creditworthiness etc.
/// </summary>
// Actually considered making this uint but I like the idea of negative credit
public required int KreditScore { get; set; }
/// <summary>
/// Amount this user has wagered towards their sponsor requirement
/// </summary>
public decimal? SponsorWagerAmount { get; set; } = null;
/// <summary>
/// The sponsor's wager requirement
/// </summary>
public decimal? SponsorWagerRequirement { get; set; } = null;
/// <summary>
/// Modifier that alters the house edge for your gambler entity
/// </summary>
public required decimal HouseEdgeModifier { get; set; } = 1;
/// <summary>
/// How much crack you've smoked?
/// </summary>
public required int CrackCounter { get; set; } = 0;
/// <summary>
/// How many floor nugs you got embedded in the carpet
/// </summary>
public required int FloorNugs { get; set; } = 0;
/// <summary>
/// Time when your weed buff ends
/// </summary>
public required DateTimeOffset WeedBuffEnds { get; set; } = DateTimeOffset.UtcNow;
/// <summary>
/// Time when your crack buff ends
/// </summary>
public required DateTimeOffset CrackBuffEnds { get; set; } = DateTimeOffset.UtcNow;
/// <summary>
/// Dodgy stat tracking
/// </summary>
public required KasinoShopStatTrackerModel StatTracker { get; set; } = new();
}
public class KasinoShopStatTrackerModel
{
public decimal TotalDeposited { get; set; } = 0;
public decimal TotalWithdrawn { get; set; } = 0;
public decimal TotalLossback { get; set; } = 0;
/// <summary>
/// Track wager statistics by game
/// </summary>
public Dictionary<WagerGame, decimal> StatTracker { get; set; } = new();
}
public class KasinoShopProfileLoanDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Profile of the user who owns this loan
/// </summary>
public required KasinoShopProfileDbModel Borrower { get; set; }
/// <summary>
/// Profile of the user to whom this loan is owed/payable to
/// </summary>
public required KasinoShopProfileDbModel Lender { get; set; }
/// <summary>
/// Amount loaned
/// </summary>
public required decimal Amount { get; set; }
/// <summary>
/// Amount to be paid out to the loaner
/// </summary>
public required decimal PayoutAmount { get; set; }
/// <summary>
/// Date and time loan entry was created
/// </summary>
public required DateTimeOffset Created { get; set; }
/// <summary>
/// State of this loan
/// </summary>
public required LoanState State { get; set; }
}
public class KasinoShopProfileAssetDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Profile of the user who owns this asset
/// </summary>
public required KasinoShopProfileDbModel Profile { get; set; }
/// <summary>
/// Value of the item at the time of acquisition in Krypto
/// </summary>
public required decimal OriginalValue { get; set; }
/// <summary>
/// What the value of the item is right now
/// </summary>
public required decimal CurrentValue { get; set; }
/// <summary>
/// Asset name
/// </summary>
public required string Name { get; set; }
/// <summary>
/// Asset type
/// </summary>
public required AssetType AssetType { get; set; }
/// <summary>
/// Date and time the asset was acquired
/// </summary>
public required DateTimeOffset Acquired { get; set; }
/// <summary>
/// History of value changes (e.g. interest events)
/// </summary>
public required List<KasinoShopProfileAssetValueChangeDbModel> ValueChangeReports { get; set; }
/// <summary>
/// Use this to store enum values for assets that have a subtype (e.g. Car Type)
/// but were otherwise not special enough to have their own table (e.g. Car)
/// </summary>
public int? AssetSubType { get; set; } = null;
/// <summary>
/// Serialized JSON for extra information where the schema can't accommodate for you
/// </summary>
public string? Extra { get; set; } = null;
}
public class KasinoShopProfileAssetInvestmentDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Related asset for this investment
/// </summary>
public required KasinoShopProfileAssetDbModel Asset { get; set; }
/// <summary>
/// What type of investment it is
/// </summary>
public required InvestmentType InvestmentType { get; set; }
/// <summary>
/// Last time interest was calculated
/// </summary>
public required DateTimeOffset LastInterestCalculation { get; set; }
/// <summary>
/// Low point for interest calculations
/// </summary>
public required float InterestRangeMin { get; set; }
/// <summary>
/// High point for interest calculations
/// </summary>
public required float InterestRangeMax { get; set; }
/// <summary>
/// Use this to store enum values for investments that have a subtype (e.g. Shoe Brand)
/// but were otherwise not special enough to have their own table (e.g. Shoe)
/// </summary>
public int? InvestmentSubType { get; set; } = null;
/// <summary>
/// Serialized JSON for extra information where the schema can't accommodate for you
/// </summary>
public string? Extra { get; set; } = null;
}
public class KasinoShopProfileAssetValueChangeDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Related asset
/// </summary>
public required KasinoShopProfileAssetDbModel Asset { get; set; }
/// <summary>
/// Effect of the change
/// </summary>
public required decimal ValueChangeEffect { get; set; }
/// <summary>
/// Change percent as a decimal fraction?
/// </summary>
public required decimal ValueChangePercent { get; set; }
/// <summary>
/// Descriptive text for the value change (like the source of it)
/// </summary>
public required string Description { get; set; }
}
[Flags]
public enum KasinoShopProfileStateFlags : ulong
{
None,
IsSponsored,
IsWeeded,
IsCracked,
IsInWithdrawal,
IsLoanable
}
[Flags]
public enum KasinoShopProfileAssetState : ulong
{
None,
/// <summary>
/// Only applicable to smashable objects (e.g. PC peripherals)
/// </summary>
IsSmashed
}
public enum AssetType
{
Investment,
Smashable,
Car,
Random
}
public enum InvestmentType
{
Shoes,
Stake,
Gold,
Silver,
Skin,
House,
Random
}
public enum LoanState
{
/// <summary>
/// Loan not fully paid but borrower still in good standing
/// </summary>
Active,
/// <summary>
/// Past due but not yet a serious violation of terms
/// </summary>
Delinquent,
/// <summary>
/// Loan terms violated, time to collect
/// </summary>
Default,
/// <summary>
/// Loan settled by agreement to amended terms (e.g. paid off less than the full amount)
/// </summary>
Settled,
/// <summary>
/// Loan fully repaid for the total amount and closed out
/// </summary>
Repaid,
/// <summary>
/// Written off debt due to being unable to collect
/// </summary>
Uncollectible,
/// <summary>
/// Administrative state for loans canceled due to serious malfeasance
/// </summary>
Canceled
}

View File

@@ -202,113 +202,6 @@ public class GamblerPerkDbModel
public decimal? Payout { get; set; } public decimal? Payout { get; set; }
} }
public class KasinoShopProfileDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Shop profiles belong to a user, not their gambler ID
/// they persist even if the user abandons their profile
/// </summary>
public required UserDbModel User { get; set; }
public required List<KasinoShopProfileAssetDbModel> Assets { get; set; }
public required List<KasinoShopProfileLoanDbModel> Loans { get; set; }
}
public class KasinoShopProfileLoanDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
// Foreign key that the powers that be told me I need for the fancy navigation property
public int ProfileId { get; set; }
/// <summary>
/// Profile of the user who owns this loan
/// </summary>
public required KasinoShopProfileDbModel Profile { get; set; }
// Foreign key that the powers that be told me I need for the fancy navigation property
public int PayableToId { get; set; }
/// <summary>
/// Profile of the user to whom this loan is owed/payable to
/// </summary>
public required KasinoShopProfileDbModel PayableTo { get; set; }
/// <summary>
/// Amount loaned
/// </summary>
public required decimal Amount { get; set; }
/// <summary>
/// Amount to be paid out to the loaner
/// </summary>
public required decimal PayoutAmount { get; set; }
/// <summary>
/// Date and time loan entry was created
/// </summary>
public required DateTimeOffset Created { get; set; }
}
public class KasinoShopProfileAssetDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Profile of the user who owns this asset
/// </summary>
public required KasinoShopProfileDbModel Profile { get; set; }
/// <summary>
/// Value of the item at the time of acquisition in Krypto
/// </summary>
public required decimal OriginalValue { get; set; }
/// <summary>
/// Asset name
/// </summary>
public required string Name { get; set; }
/// <summary>
/// Asset type
/// </summary>
public required AssetType AssetType { get; set; }
/// <summary>
/// Date and time the asset was acquired
/// </summary>
public required DateTimeOffset Acquired { get; set; }
/// <summary>
/// History of value changes (e.g. interest events)
/// </summary>
public required List<KasinoShopProfileAssetValueChangeDbModel> ValueChangeReports { get; set; }
/// <summary>
/// Serialized JSON for extra information useful for certain assets (e.g. car model)
/// </summary>
public string? Extra { get; set; } = null;
}
public class KasinoShopProfileAssetValueChangeDbModel
{
/// <summary>
/// ID for the database row
/// </summary>
public int Id { get; set; }
/// <summary>
/// Related asset
/// </summary>
public required KasinoShopProfileAssetDbModel Asset { get; set; }
/// <summary>
/// Effect of the change
/// </summary>
public required decimal ValueChangeEffect { get; set; }
/// <summary>
/// Change percent as a decimal fraction?
/// </summary>
public required decimal ValueChangePercent { get; set; }
/// <summary>
/// Descriptive text for the value change (like the source of it)
/// </summary>
public required string Description { get; set; }
}
public enum GamblerPerkType public enum GamblerPerkType
{ {
/// <summary> /// <summary>

View File

@@ -0,0 +1,9 @@
namespace KfChatDotNetBot.Models;
/// <summary>
/// Holds state information for the kasino shop
/// </summary>
public class KasinoShopStateModel
{
public required decimal DefaultHouseEdgeModifier { get; set; } = 0;
}

View File

@@ -37,15 +37,14 @@ public class ConversationContextManager
public ConversationContextManager() public ConversationContextManager()
{ {
var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result; if (!Redis.IsAvailable)
if (string.IsNullOrEmpty(connectionString.Value))
{ {
Logger.Error($"Can't initialize the Nora ConversationContextManager service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}"); Logger.Error($"Can't initialize the Nora ConversationContextManager service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " +
$"or the Redis client failed to connect");
throw new InvalidOperationException("Redis isn't configured"); throw new InvalidOperationException("Redis isn't configured");
} }
var redis = ConnectionMultiplexer.Connect(connectionString.Value); _redisDb = Redis.Multiplexer.GetDatabase();
_redisDb = redis.GetDatabase();
} }
public static string GetContextKeyAsync(string mode, int userId, int roomId) public static string GetContextKeyAsync(string mode, int userId, int roomId)

View File

@@ -23,15 +23,14 @@ public class KasinoKrash : IDisposable
{ {
_kfChatBot = kfChatBot; _kfChatBot = kfChatBot;
_ct = ct; _ct = ct;
var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result; if (!Redis.IsAvailable)
if (string.IsNullOrEmpty(connectionString.Value))
{ {
_logger.Error($"Can't initialize the Kasino Krash service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}"); _logger.Error($"Can't initialize the Kasino Krash service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " +
$"or the Redis service failed to connect");
return; return;
} }
var redis = ConnectionMultiplexer.Connect(connectionString.Value); _redisDb = Redis.Multiplexer.GetDatabase();
_redisDb = redis.GetDatabase();
//attempt to pull a game from the db in case the bot crashed while a game was ongoing. if so it will restart the run //attempt to pull a game from the db in case the bot crashed while a game was ongoing. if so it will restart the run
TheGame = GetKrashState().Result; TheGame = GetKrashState().Result;
if (TheGame != null) _ = RunGame(); if (TheGame != null) _ = RunGame();

View File

@@ -241,15 +241,14 @@ public class KasinoMines
public KasinoMines(ChatBot kfChatBot, int gamblerId) public KasinoMines(ChatBot kfChatBot, int gamblerId)
{ {
_kfChatBot = kfChatBot; _kfChatBot = kfChatBot;
var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result; if (!Redis.IsAvailable)
if (string.IsNullOrEmpty(connectionString.Value))
{ {
_logger.Error($"Can't initialize the Kasino Mines service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}"); _logger.Error($"Can't initialize the Kasino Mines service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " +
$"or the Redis service failed to connect");
return; return;
} }
var redis = ConnectionMultiplexer.Connect(connectionString.Value); _redisDb = Redis.Multiplexer.GetDatabase();
_redisDb = redis.GetDatabase();
GetSavedGames(gamblerId).Wait(); GetSavedGames(gamblerId).Wait();
} }

View File

@@ -22,15 +22,14 @@ public class KasinoRain : IDisposable
{ {
_kfChatBot = kfChatBot; _kfChatBot = kfChatBot;
_ct = ct; _ct = ct;
var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result; if (!Redis.IsAvailable)
if (string.IsNullOrEmpty(connectionString.Value))
{ {
_logger.Error($"Can't initialize the Kasino Rain service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}"); _logger.Error($"Can't initialize the Kasino Rain service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " +
$"or the Redis service failed to connect");
return; return;
} }
var redis = ConnectionMultiplexer.Connect(connectionString.Value); _redisDb = Redis.Multiplexer.GetDatabase();
_redisDb = redis.GetDatabase();
_rainTimerTask = Task.Run(RainTimerTask, ct); _rainTimerTask = Task.Run(RainTimerTask, ct);
} }

View File

@@ -1,18 +1,9 @@
using System.Text.Json; using System.Text.Json;
using KfChatDotNetBot.Extensions; using KfChatDotNetBot.Extensions;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels; using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Settings; using KfChatDotNetBot.Settings;
using NLog; using NLog;
using StackExchange.Redis; using StackExchange.Redis;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using KfChatDotNetBot.Extensions;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Services;
using KfChatDotNetBot.Settings;
using KfChatDotNetWsClient.Models.Events;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RandN; using RandN;
using RandN.Compat; using RandN.Compat;

View File

@@ -0,0 +1,110 @@
using KfChatDotNetBot.Settings;
using System.Text.Json;
using NLog;
using StackExchange.Redis;
namespace KfChatDotNetBot.Services;
public static class Redis
{
public static bool IsAvailable => LazyMultiplexer.IsValueCreated;
// Claude told me this will act like a singleton ConnectionMultiplexer
// while keeping things nice and convenient with static methods
// FYI the exception will be thrown once, cached for the lifetime of the application
// If you configure a Redis connection string, you MUST restart the application
// https://learn.microsoft.com/en-us/dotnet/api/system.lazy-1?view=net-10.0#:~:text=Exception%20caching,-When
private static readonly Lazy<ConnectionMultiplexer> LazyMultiplexer =
new(() =>
{
var logger = LogManager.GetCurrentClassLogger();
var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result.Value;
if (string.IsNullOrEmpty(connectionString))
{
logger.Error($"Could not initiate the lazy connection multiplexer for the Redis service as the " +
$"connection string is not configured in {BuiltIn.Keys.BotRedisConnectionString}. " +
$"Redis won't be available to anything that relies on it. " +
$"If you do configure {BuiltIn.Keys.BotRedisConnectionString}, YOU MUST RESTART THE BOT");
throw new InvalidOperationException();
}
try
{
return ConnectionMultiplexer.Connect(
SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result.Value ??
throw new InvalidOperationException(
$"{BuiltIn.Keys.BotRedisConnectionString} not defined, cannot connect to Redis"));
}
catch (Exception e)
{
logger.Error($"Caught an exception when connecting to Redis at {connectionString}");
logger.Error(e);
throw;
}
});
// You can just grab this from wherever if you want a ready to go Redis connection
// ReSharper disable once MemberCanBePrivate.Global
public static ConnectionMultiplexer Multiplexer => LazyMultiplexer.Value;
private static IDatabase Db => Multiplexer.GetDatabase();
/// <summary>
/// Fetches a key from Redis asynchronously and deserializes its JSON value to T.
/// Returns default(T) if the key doesn't exist.
/// </summary>
/// <param name="key">Redis key</param>
public static async Task<T?> GetJsonAsync<T>(string key)
{
var value = await Db.StringGetAsync(key);
if (value.IsNullOrEmpty)
return default;
return JsonSerializer.Deserialize<T>(value.ToString());
}
/// <summary>
/// Fetches a key from Redis synchronously and deserializes its JSON value to T.
/// Returns default(T) if the key doesn't exist.
/// </summary>
/// <param name="key">Redis key</param>
public static T? GetJson<T>(string key)
{
var value = Db.StringGet(key);
if (value.IsNullOrEmpty)
return default;
return JsonSerializer.Deserialize<T>(value.ToString());
}
/// <summary>
/// Asynchronously set a key to a given object serialized using JSON
/// </summary>
/// <param name="key">Redis key</param>
/// <param name="value">Object that you wish to serialize</param>
/// <param name="expires">Expiration (null means never expires)</param>
/// <param name="when">Redis behavior whether the key has a value or not
/// When.Always = set the value regardless of whether the key has a value
/// When.Exists = only set the value if the key has a value already
/// When.NotExists = only set the value if the key has no value
/// </param>
public static async Task SetJsonAsync(string key, object value, TimeSpan? expires = null, When when = When.Always)
{
await Db.StringSetAsync(key, JsonSerializer.Serialize(value), expires, when);
}
/// <summary>
/// Synchronously set a key to a given object serialized using JSON
/// </summary>
/// <param name="key">Redis key</param>
/// <param name="value">Object that you wish to serialize</param>
/// <param name="expires">Expiration (null means never expires)</param>
/// <param name="when">Redis behavior whether the key has a value or not
/// When.Always = set the value regardless of whether the key has a value
/// When.Exists = only set the value if the key has a value already
/// When.NotExists = only set the value if the key has no value
/// </param>
public static void SetJson(string key, object value, TimeSpan? expires = null, When when = When.Always)
{
Db.StringSet(key, JsonSerializer.Serialize(value), expires, when);
}
}