mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-05-02 12:32:03 -04:00
Experimental convoluted rain refactor to use Redis instead of semaphores
This commit is contained in:
@@ -529,6 +529,31 @@ public class ChatBot
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wait for a chat message to be successfully delivered or not
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Reference to the message you're waiting for</param>
|
||||||
|
/// <param name="patience">How long to wait</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
/// <returns>True if the message was echoed, false otherwise</returns>
|
||||||
|
public async Task<bool> WaitForChatMessageAsync(SentMessageTrackerModel message, TimeSpan? patience = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (patience == null)
|
||||||
|
{
|
||||||
|
patience = TimeSpan.FromSeconds(60);
|
||||||
|
}
|
||||||
|
|
||||||
|
var patienceEnds = DateTimeOffset.UtcNow.Add(patience.Value);
|
||||||
|
while (message.ChatMessageId == null)
|
||||||
|
{
|
||||||
|
if (DateTimeOffset.UtcNow > patienceEnds) return false;
|
||||||
|
if (message.Status is SentMessageTrackerStatus.Lost or SentMessageTrackerStatus.NotSending) return false;
|
||||||
|
await Task.Delay(100, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public class SentMessageNotFoundException : Exception;
|
public class SentMessageNotFoundException : Exception;
|
||||||
|
|
||||||
private void OnUsersJoined(object sender, List<UserModel> users, UsersJsonModel jsonPayload)
|
private void OnUsersJoined(object sender, List<UserModel> users, UsersJsonModel jsonPayload)
|
||||||
|
|||||||
@@ -1,166 +1,15 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using KfChatDotNetBot.Extensions;
|
using KfChatDotNetBot.Extensions;
|
||||||
using KfChatDotNetBot.Models;
|
using KfChatDotNetBot.Models;
|
||||||
using KfChatDotNetBot.Models.DbModels;
|
using KfChatDotNetBot.Models.DbModels;
|
||||||
using KfChatDotNetBot.Services;
|
using KfChatDotNetBot.Services;
|
||||||
using KfChatDotNetBot.Settings;
|
|
||||||
using KfChatDotNetWsClient.Models.Events;
|
using KfChatDotNetWsClient.Models.Events;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
namespace KfChatDotNetBot.Commands.Kasino;
|
namespace KfChatDotNetBot.Commands.Kasino;
|
||||||
|
|
||||||
|
[KasinoCommand]
|
||||||
|
[WagerCommand]
|
||||||
public class RainCommand : ICommand
|
public class RainCommand : ICommand
|
||||||
{
|
{
|
||||||
public static class RainManager
|
|
||||||
{
|
|
||||||
public static Dictionary<int,Rain> rainLobbies { get; } = new(); //dictionary of lobbies with the creators id and the lobby
|
|
||||||
public static readonly SemaphoreSlim _lock = new(1, 1);
|
|
||||||
public static ChatBot botInstance;
|
|
||||||
|
|
||||||
|
|
||||||
public static async Task<bool> AddParticipant(GamblerDbModel gambler, UserDbModel user)
|
|
||||||
{
|
|
||||||
await _lock.WaitAsync();
|
|
||||||
bool openLobby = false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var lobby in rainLobbies.Values)
|
|
||||||
{
|
|
||||||
if (lobby.open)
|
|
||||||
{
|
|
||||||
lobby.AddParticipant(gambler);
|
|
||||||
openLobby = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_lock.Release();
|
|
||||||
}
|
|
||||||
return openLobby;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<bool> AddRain(int gamblerId, Rain rain)
|
|
||||||
{
|
|
||||||
await _lock.WaitAsync();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!rainLobbies.TryGetValue(gamblerId, out var lobby))
|
|
||||||
{
|
|
||||||
rainLobbies.Add(gamblerId, lobby);
|
|
||||||
}
|
|
||||||
else //can't start multiple rains at the same time
|
|
||||||
{
|
|
||||||
await botInstance.SendChatMessageAsync(
|
|
||||||
$"{lobby.creator.FormatUsername()}, you can't start multiple rains at the same time.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_lock.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task PayoutRain(Rain rain)
|
|
||||||
{
|
|
||||||
if (rain.participants.Count == 0)
|
|
||||||
{
|
|
||||||
await botInstance.SendChatMessageAsync($"{rain.creator.FormatUsername()} made it rain on nobody.");
|
|
||||||
await Money.ModifyBalanceAsync(rain.creatorGambler.Id, -rain.rainAmount, TransactionSourceEventType.Rain);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
decimal payout = rain.rainAmount / rain.participants.Count;
|
|
||||||
string rainParticipants = "";
|
|
||||||
int counter = 0;
|
|
||||||
foreach (var participant in rain.participants)
|
|
||||||
{
|
|
||||||
rainParticipants += (rain.participants.Count > 1 && counter == rain.participants.Count - 1) ? $"and {participant.User.FormatUsername()}" : $"{participant.User.FormatUsername()}, ";
|
|
||||||
await Money.ModifyBalanceAsync(participant.Id, payout, TransactionSourceEventType.Rain);
|
|
||||||
counter++;
|
|
||||||
}
|
|
||||||
if (rain.participants.Count == 1) rainParticipants = rain.participants[0].User.FormatUsername();
|
|
||||||
await botInstance.SendChatMessageAsync(
|
|
||||||
$"{rain.creator.FormatUsername()} made it rain {payout.FormatKasinoCurrencyAsync()} on {rainParticipants}",
|
|
||||||
true, autoDeleteAfter: TimeSpan.FromSeconds(30));
|
|
||||||
await Money.ModifyBalanceAsync(rain.creatorGambler.Id, -rain.rainAmount, TransactionSourceEventType.Rain);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task RemoveRain(Rain rain)
|
|
||||||
{
|
|
||||||
await _lock.WaitAsync();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
rainLobbies.Remove(rain.creatorGambler.Id);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_lock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public class Rain
|
|
||||||
{
|
|
||||||
public List<GamblerDbModel> participants = new List<GamblerDbModel>();
|
|
||||||
public UserDbModel creator;
|
|
||||||
public GamblerDbModel creatorGambler;
|
|
||||||
public DateTime startTime { get; } = DateTime.UtcNow;
|
|
||||||
public bool open = true;
|
|
||||||
public decimal rainAmount;
|
|
||||||
|
|
||||||
public Rain(UserDbModel user, GamblerDbModel gambler, decimal wager)
|
|
||||||
{
|
|
||||||
creator = user;
|
|
||||||
creatorGambler = gambler;
|
|
||||||
rainAmount = wager;
|
|
||||||
_ = RunRain();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddParticipant(GamblerDbModel gambler)
|
|
||||||
{
|
|
||||||
participants.Add(gambler);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RunRain()
|
|
||||||
{
|
|
||||||
var chatTimer = TimeSpan.FromSeconds(5);
|
|
||||||
int rainTimer = 60;
|
|
||||||
var msgId = await botInstance.SendChatMessageAsync(
|
|
||||||
$"{creator.FormatUsername()} is making it rain with {rainAmount.FormatKasinoCurrencyAsync()}! You have {rainTimer} seconds left to join!", true,
|
|
||||||
autoDeleteAfter: TimeSpan.FromSeconds(rainTimer));
|
|
||||||
int num = 0;
|
|
||||||
while (msgId.ChatMessageId == null)
|
|
||||||
{
|
|
||||||
num++;
|
|
||||||
if (msgId.Status is SentMessageTrackerStatus.NotSending or SentMessageTrackerStatus.Lost) return;
|
|
||||||
if (num > 100) return;
|
|
||||||
await Task.Delay(100);
|
|
||||||
}
|
|
||||||
for (int i = 0; i < rainTimer / Convert.ToInt32(chatTimer); i++)
|
|
||||||
{
|
|
||||||
await Task.Delay(chatTimer);
|
|
||||||
await botInstance.KfClient.EditMessageAsync(msgId.ChatMessageId!.Value,
|
|
||||||
$"{creator.FormatUsername()} is making it rain with {rainAmount.FormatKasinoCurrencyAsync()}! You have {rainTimer} seconds left to join!");
|
|
||||||
}
|
|
||||||
open = false;
|
|
||||||
await PayoutRain(this);
|
|
||||||
await RemoveRain(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public List<Regex> Patterns => [
|
public List<Regex> Patterns => [
|
||||||
new Regex(@"^rain (?<amount>\d+)$", RegexOptions.IgnoreCase),
|
new Regex(@"^rain (?<amount>\d+)$", RegexOptions.IgnoreCase),
|
||||||
new Regex(@"^rain (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
|
new Regex(@"^rain (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
|
||||||
@@ -169,17 +18,21 @@ public class RainCommand : ICommand
|
|||||||
|
|
||||||
public string? HelpText => "!rain <amount> to start a rain, !rain to join all active rains";
|
public string? HelpText => "!rain <amount> to start a rain, !rain to join all active rains";
|
||||||
public UserRight RequiredRight => UserRight.Loser;
|
public UserRight RequiredRight => UserRight.Loser;
|
||||||
public TimeSpan Timeout => TimeSpan.FromSeconds(30);
|
public TimeSpan Timeout => TimeSpan.FromSeconds(60);
|
||||||
public RateLimitOptionsModel? RateLimitOptions => null;
|
public RateLimitOptionsModel? RateLimitOptions => null;
|
||||||
|
|
||||||
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
|
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
|
||||||
CancellationToken ctx)
|
CancellationToken ctx)
|
||||||
{
|
{
|
||||||
if (!RainManager.botInstance.Equals(botInstance))
|
|
||||||
{
|
|
||||||
RainManager.botInstance = botInstance;
|
|
||||||
}
|
|
||||||
var cleanupDelay = TimeSpan.FromSeconds(30);
|
var cleanupDelay = TimeSpan.FromSeconds(30);
|
||||||
|
if (botInstance.BotServices.KasinoRain == null || !botInstance.BotServices.KasinoRain.IsInitialized())
|
||||||
|
{
|
||||||
|
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, rain is not available at this time", true,
|
||||||
|
autoDeleteAfter: cleanupDelay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rain = await botInstance.BotServices.KasinoRain.GetRainState();
|
||||||
var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx);
|
var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx);
|
||||||
if (gambler == null)
|
if (gambler == null)
|
||||||
{
|
{
|
||||||
@@ -187,62 +40,75 @@ public class RainCommand : ICommand
|
|||||||
}
|
}
|
||||||
if (!arguments.TryGetValue("amount", out var amount)) //if you're trying to join a rain
|
if (!arguments.TryGetValue("amount", out var amount)) //if you're trying to join a rain
|
||||||
{
|
{
|
||||||
if (RainManager.rainLobbies.Count == 0) //if there are no lobbies
|
if (rain == null) //if there are no lobbies
|
||||||
{
|
{
|
||||||
botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
$"{user.FormatUsername()}, there are no rain lobbies currently running. !rain <amount> to start a new rain lobby",
|
$"{user.FormatUsername()}, there's no rain currently running. !rain <amount> to start a new rain",
|
||||||
true, autoDeleteAfter: cleanupDelay);
|
true, autoDeleteAfter: cleanupDelay);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (rain.Participants.Contains(user.Id))
|
||||||
{
|
{
|
||||||
bool success = await RainManager.AddParticipant(gambler, user);
|
await botInstance.SendChatMessageAsync(
|
||||||
if (!success)
|
$"{user.FormatUsername()}, you're already participating in this rain!", true,
|
||||||
{
|
autoDeleteAfter: cleanupDelay);
|
||||||
await botInstance.SendChatMessageAsync(
|
return;
|
||||||
$"{user.FormatUsername()}, there are no rain lobbies currently running. !rain <amount> to start a new rain lobby",
|
|
||||||
true, autoDeleteAfter: cleanupDelay);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
string rainCreators = "";
|
|
||||||
await RainManager._lock.WaitAsync();
|
|
||||||
int lobbyCount = 0;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var lobby in RainManager.rainLobbies.Values)
|
|
||||||
{
|
|
||||||
rainCreators += (RainManager.rainLobbies.Count > 1 && lobbyCount == RainManager.rainLobbies.Count - 1) ? $"and {lobby.creator.FormatUsername()}": $"{lobby.creator.FormatUsername()}, ";
|
|
||||||
lobbyCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
RainManager._lock.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
string areIs = RainManager.rainLobbies.Count > 1 ? "are" : "is";
|
|
||||||
await botInstance.SendChatMessageAsync(
|
|
||||||
$"{rainCreators} {areIs} making it rain on {user.FormatUsername()}!",true, autoDeleteAfter: cleanupDelay);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await botInstance.BotServices.KasinoRain.AddParticipant(user.Id);
|
||||||
|
var pluralSuffix = string.Empty;
|
||||||
|
if (rain.Participants.Count > 0) pluralSuffix = "s";
|
||||||
|
await botInstance.SendChatMessageAsync(
|
||||||
|
$"LFG {user.FormatUsername()} is now a participant! There's now {rain.Participants.Count} participant{pluralSuffix}",
|
||||||
|
true, autoDeleteAfter: cleanupDelay);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
//if you're trying to start the rain
|
//if you're trying to start the rain
|
||||||
decimal decAmount = Convert.ToDecimal(amount.Value);
|
decimal decAmount = Convert.ToDecimal(amount.Value);
|
||||||
if (decAmount <= 0)
|
if (decAmount <= 0)
|
||||||
{
|
{
|
||||||
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, you can't make it rain with nothing.", true, autoDeleteAfter: cleanupDelay);
|
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, you can't make it rain with nothing.",
|
||||||
|
true, autoDeleteAfter: cleanupDelay);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (gambler.Balance < decAmount)
|
if (gambler.Balance < decAmount)
|
||||||
{
|
{
|
||||||
await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, your balance ${gambler.Balance} KKK is not enough to make it rain for ${decAmount} KKK.", true, autoDeleteAfter: cleanupDelay);
|
await botInstance.SendChatMessageAsync(
|
||||||
|
$"{user.FormatUsername()}, your balance {await gambler.Balance.FormatKasinoCurrencyAsync()} is not enough to make it rain for {await decAmount.FormatKasinoCurrencyAsync()}.",
|
||||||
|
true, autoDeleteAfter: cleanupDelay);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await RainManager.AddRain(gambler.Id,new RainManager.Rain(user, gambler, decAmount));
|
|
||||||
|
|
||||||
|
rain = new KasinoRainModel
|
||||||
|
{
|
||||||
|
Participants = [],
|
||||||
|
Creator = user.Id,
|
||||||
|
Started = DateTimeOffset.UtcNow,
|
||||||
|
RainAmount = decAmount,
|
||||||
|
PayoutWhen = DateTimeOffset.MaxValue
|
||||||
|
};
|
||||||
|
var timer = 60;
|
||||||
|
var msg = await botInstance.SendChatMessageAsync(
|
||||||
|
$"{user.FormatUsername()} is making it rain with {await decAmount.FormatKasinoCurrencyAsync()}! Type !rain in the next {timer} seconds to join.",
|
||||||
|
true);
|
||||||
|
var result = await botInstance.WaitForChatMessageAsync(msg, ct: ctx);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to send chat message for the rain. Not going to proceed with it");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait to set a real payout deadline only when chyat echoes the message out of fairness
|
||||||
|
// (and also so the timer doesn't overlap with the payout deadline)
|
||||||
|
rain.PayoutWhen = DateTimeOffset.UtcNow.AddSeconds(60);
|
||||||
|
await botInstance.BotServices.KasinoRain.SaveRainState(rain);
|
||||||
|
while (timer > 0)
|
||||||
|
{
|
||||||
|
timer--;
|
||||||
|
await botInstance.KfClient.EditMessageAsync(msg.ChatMessageId!.Value,
|
||||||
|
$"{user.FormatUsername()} is making it rain with {await decAmount.FormatKasinoCurrencyAsync()}! Type !rain in the next {timer} seconds to join.");
|
||||||
|
}
|
||||||
|
await botInstance.KfClient.DeleteMessageAsync(msg.ChatMessageId!.Value);
|
||||||
|
// At this point the timer should take care of things but truthfully it's
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,6 +40,7 @@ public class BotServices
|
|||||||
private Owncast? _owncastStatusCheck;
|
private Owncast? _owncastStatusCheck;
|
||||||
private ShuffleDotUs? _shuffleDotUs;
|
private ShuffleDotUs? _shuffleDotUs;
|
||||||
private YouTubePubSub? _youTubePubSub;
|
private YouTubePubSub? _youTubePubSub;
|
||||||
|
public KasinoRain? KasinoRain;
|
||||||
|
|
||||||
private Task? _websocketWatchdog;
|
private Task? _websocketWatchdog;
|
||||||
private Task? _howlggGetUserTimer;
|
private Task? _howlggGetUserTimer;
|
||||||
@@ -91,7 +92,8 @@ public class BotServices
|
|||||||
BuildPeerTubeLiveStatusCheck(),
|
BuildPeerTubeLiveStatusCheck(),
|
||||||
BuildOwncastLiveStatusCheck(),
|
BuildOwncastLiveStatusCheck(),
|
||||||
BuildShuffleDotUs(),
|
BuildShuffleDotUs(),
|
||||||
BuildYouTubePubSub()
|
BuildYouTubePubSub(),
|
||||||
|
BuildKasinoRain()
|
||||||
];
|
];
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -108,6 +110,12 @@ public class BotServices
|
|||||||
_howlggGetUserTimer = HowlggGetUserTimer();
|
_howlggGetUserTimer = HowlggGetUserTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task BuildKasinoRain()
|
||||||
|
{
|
||||||
|
_logger.Debug("Building the Kasino Rain thingy");
|
||||||
|
KasinoRain = new KasinoRain(_chatBot, _cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task BuildShuffle()
|
private async Task BuildShuffle()
|
||||||
{
|
{
|
||||||
_logger.Debug("Building Shuffle");
|
_logger.Debug("Building Shuffle");
|
||||||
|
|||||||
145
KfChatDotNetBot/Services/KasinoRain.cs
Normal file
145
KfChatDotNetBot/Services/KasinoRain.cs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using KfChatDotNetBot.Extensions;
|
||||||
|
using KfChatDotNetBot.Models.DbModels;
|
||||||
|
using KfChatDotNetBot.Settings;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NLog;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
|
||||||
|
namespace KfChatDotNetBot.Services;
|
||||||
|
|
||||||
|
public class KasinoRain : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||||
|
private Task? _rainTimerTask;
|
||||||
|
private IDatabase? _redisDb;
|
||||||
|
private ChatBot _kfChatBot;
|
||||||
|
private CancellationToken _ct;
|
||||||
|
private CancellationTokenSource _rainCts = new();
|
||||||
|
|
||||||
|
public KasinoRain(ChatBot kfChatBot, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
_kfChatBot = kfChatBot;
|
||||||
|
_ct = ct;
|
||||||
|
var connectionString = SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRedisConnectionString).Result;
|
||||||
|
if (string.IsNullOrEmpty(connectionString.Value))
|
||||||
|
{
|
||||||
|
_logger.Error($"Can't initialize the Kasino Rain service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var redis = ConnectionMultiplexer.Connect(connectionString.Value);
|
||||||
|
_redisDb = redis.GetDatabase();
|
||||||
|
_rainTimerTask = Task.Run(RainTimerTask, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsInitialized()
|
||||||
|
{
|
||||||
|
return _redisDb != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddParticipant(int userId)
|
||||||
|
{
|
||||||
|
var data = await GetRainState();
|
||||||
|
if (data == null) throw new InvalidOperationException("Failed to retrieve state or no rain is in progress");
|
||||||
|
if (data.Participants.Contains(userId)) return;
|
||||||
|
data.Participants.Add(userId);
|
||||||
|
await SaveRainState(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveRainState()
|
||||||
|
{
|
||||||
|
if (_redisDb == null) throw new InvalidOperationException("Kasino Rain service isn't initialized");
|
||||||
|
await _redisDb.KeyDeleteAsync("Rain.State");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<KasinoRainModel?> GetRainState()
|
||||||
|
{
|
||||||
|
if (_redisDb == null) throw new InvalidOperationException("Kasino Rain service isn't initialized");
|
||||||
|
var json = await _redisDb.StringGetAsync("Rain.State");
|
||||||
|
if (string.IsNullOrEmpty(json)) return null;
|
||||||
|
var data = JsonSerializer.Deserialize<KasinoRainModel>(json.ToString());
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveRainState(KasinoRainModel rain)
|
||||||
|
{
|
||||||
|
if (_redisDb == null) throw new InvalidOperationException("Kasino Rain service isn't initialized");
|
||||||
|
var json = JsonSerializer.Serialize(rain);
|
||||||
|
await _redisDb.StringSetAsync("Rain.State", json, null, When.Always);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RainTimerTask()
|
||||||
|
{
|
||||||
|
var interval = TimeSpan.FromSeconds(1);
|
||||||
|
using var timer = new PeriodicTimer(interval);
|
||||||
|
while (await timer.WaitForNextTickAsync(_ct))
|
||||||
|
{
|
||||||
|
var rain = await GetRainState();
|
||||||
|
if (rain == null) continue;
|
||||||
|
if (DateTimeOffset.UtcNow < rain.PayoutWhen) continue;
|
||||||
|
var creator = await Money.GetGamblerEntityAsync(rain.Creator, ct: _ct);
|
||||||
|
if (creator == null)
|
||||||
|
{
|
||||||
|
_logger.Error($"Somehow this rain was created by a non-existent (or banned) gambler with user ID {rain.Creator}? Wiping this fucked up state");
|
||||||
|
await RemoveRainState();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rain.Participants.Count == 0)
|
||||||
|
{
|
||||||
|
await _kfChatBot.SendChatMessageAsync(
|
||||||
|
$"Nobody participated in {creator.User.FormatUsername()}'s rain!",
|
||||||
|
true, autoDeleteAfter: TimeSpan.FromSeconds(30));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rain.RainAmount > creator.Balance)
|
||||||
|
{
|
||||||
|
await _kfChatBot.SendChatMessageAsync(
|
||||||
|
$"{creator.User.FormatUsername()} lost it all before he could bless everyone! The giveaway is canceled! :lossmanjack:",
|
||||||
|
true, autoDeleteAfter: TimeSpan.FromSeconds(30));
|
||||||
|
await RemoveRainState();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> participantNames = [];
|
||||||
|
var payout = rain.RainAmount / rain.Participants.Count;
|
||||||
|
decimal failedPayoutAmount = 0;
|
||||||
|
foreach (var participant in rain.Participants)
|
||||||
|
{
|
||||||
|
var gambler = await Money.GetGamblerEntityAsync(participant, ct: _ct);
|
||||||
|
if (gambler == null)
|
||||||
|
{
|
||||||
|
_logger.Error($"Somehow this participant ({participant}) does not have a gambler entity or has been banned");
|
||||||
|
failedPayoutAmount += payout;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
participantNames.Add(gambler.User.FormatUsername());
|
||||||
|
await Money.ModifyBalanceAsync(gambler.Id, payout, TransactionSourceEventType.Rain,
|
||||||
|
"Payout from rain event", creator.Id, _ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Money.ModifyBalanceAsync(creator.Id, -rain.RainAmount + failedPayoutAmount, TransactionSourceEventType.Rain,
|
||||||
|
$"Rained on {participantNames.Count} people", ct: _ct);
|
||||||
|
await _kfChatBot.SendChatMessageAsync(
|
||||||
|
$"{creator.User.FormatUsername()} made it rain {await payout.FormatKasinoCurrencyAsync()} on {string.Join(' ', participantNames)}",
|
||||||
|
true, autoDeleteAfter: TimeSpan.FromSeconds(30));
|
||||||
|
await RemoveRainState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_rainTimerTask?.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class KasinoRainModel
|
||||||
|
{
|
||||||
|
public required List<int> Participants { get; set; } = [];
|
||||||
|
public required int Creator { get; set; }
|
||||||
|
public required DateTimeOffset Started { get; set; }
|
||||||
|
public required decimal RainAmount { get; set; }
|
||||||
|
public required DateTimeOffset PayoutWhen { get; set; }
|
||||||
|
}
|
||||||
@@ -504,6 +504,8 @@ public static class BuiltIn
|
|||||||
public static string KasinoPlinkoEnabled = "Kasino.Plinko.Enabled";
|
public static string KasinoPlinkoEnabled = "Kasino.Plinko.Enabled";
|
||||||
[BuiltInSetting("Whether Xeet posting is enabled", SettingValueType.Boolean, "true", BooleanRegex)]
|
[BuiltInSetting("Whether Xeet posting is enabled", SettingValueType.Boolean, "true", BooleanRegex)]
|
||||||
public static string XeetEnabled = "Xeet.Enabled";
|
public static string XeetEnabled = "Xeet.Enabled";
|
||||||
|
[BuiltInSetting("Connection string for bot's Redis", SettingValueType.Text)]
|
||||||
|
public static string BotRedisConnectionString = "Bot.RedisConnectionString";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user