added timely boost bonus to gambling.yml

.betstats renamed to .gamblestats/.gs
added .betstats, .betstats <game> and .betstats <user> <game?> command which shows you your stats for gambling commands
This commit is contained in:
Toastie 2024-11-05 16:11:05 +13:00
parent 39297c6f83
commit 7da8f2c403
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
21 changed files with 13690 additions and 6320 deletions

View file

@ -11,9 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Dockerfile = Dockerfile Dockerfile = Dockerfile
ellie-menu.ps1 = ellie-menu.ps1 ellie-menu.ps1 = ellie-menu.ps1
LICENSE = LICENSE LICENSE = LICENSE
migrate.ps1 = migrate.ps1
README.md = README.md README.md = README.md
remove-migrations.ps1 = remove-migrations.ps1
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot", "src\EllieBot\EllieBot.csproj", "{4D9001F7-B3E8-48FE-97AA-CFD36DA65A64}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot", "src\EllieBot\EllieBot.csproj", "{4D9001F7-B3E8-48FE-97AA-CFD36DA65A64}"
@ -30,7 +28,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Marmalade", "src\Elli
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Voice", "src\EllieBot.Voice\EllieBot.Voice.csproj", "{1D93CE3C-80B4-49C7-A9A2-99988920AAEC}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Voice", "src\EllieBot.Voice\EllieBot.Voice.csproj", "{1D93CE3C-80B4-49C7-A9A2-99988920AAEC}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EllieBot.GrpcApiBase", "src\EllieBot.GrpcApiBase\EllieBot.GrpcApiBase.csproj", "{3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.GrpcApiBase", "src\EllieBot.GrpcApiBase\EllieBot.GrpcApiBase.csproj", "{3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution

View file

@ -1,8 +0,0 @@
if ($args.Length -eq 0) {
Write-Host "Please provide a migration name." -ForegroundColor Red
}
else {
$migrationName = $args[0]
dotnet ef migrations add $migrationName -o Migrations/Mysql -c SqliteContext -p src/EllieBot/EllieBot.csproj
dotnet ef migrations add $migrationName -o Migrations/PostgreSql -c PostgreSqlContext -p src/EllieBot/EllieBot.csproj
}

View file

@ -62,6 +62,7 @@ public abstract class EllieContext : DbContext
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; } public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
public DbSet<HoneypotChannel> HoneyPotChannels { get; set; } public DbSet<HoneypotChannel> HoneyPotChannels { get; set; }
// public DbSet<GuildColors> GuildColors { get; set; } // public DbSet<GuildColors> GuildColors { get; set; }
@ -73,7 +74,16 @@ public abstract class EllieContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
#region Flag Translate #region UserBetStats
modelBuilder.Entity<UserBetStats>()
.HasIndex(x => new { x.UserId, x.Game })
.IsUnique();
#endregion
#region Flag Translate
modelBuilder.Entity<FlagTranslateChannel>() modelBuilder.Entity<FlagTranslateChannel>()
.HasIndex(x => new { x.GuildId, x.ChannelId }) .HasIndex(x => new { x.GuildId, x.ChannelId })
@ -85,12 +95,12 @@ public abstract class EllieContext : DbContext
modelBuilder.Entity<NCPixel>() modelBuilder.Entity<NCPixel>()
.HasAlternateKey(x => x.Position); .HasAlternateKey(x => x.Position);
modelBuilder.Entity<NCPixel>() modelBuilder.Entity<NCPixel>()
.HasIndex(x => x.OwnerId); .HasIndex(x => x.OwnerId);
#endregion #endregion
#region QUOTES #region QUOTES
var quoteEntity = modelBuilder.Entity<Quote>(); var quoteEntity = modelBuilder.Entity<Quote>();
@ -307,10 +317,10 @@ public abstract class EllieContext : DbContext
var selfassignableRolesEntity = modelBuilder.Entity<SelfAssignedRole>(); var selfassignableRolesEntity = modelBuilder.Entity<SelfAssignedRole>();
selfassignableRolesEntity.HasIndex(s => new selfassignableRolesEntity.HasIndex(s => new
{ {
s.GuildId, s.GuildId,
s.RoleId s.RoleId
}) })
.IsUnique(); .IsUnique();
selfassignableRolesEntity.Property(x => x.Group).HasDefaultValue(0); selfassignableRolesEntity.Property(x => x.Group).HasDefaultValue(0);
@ -384,10 +394,10 @@ public abstract class EllieContext : DbContext
var xps = modelBuilder.Entity<UserXpStats>(); var xps = modelBuilder.Entity<UserXpStats>();
xps.HasIndex(x => new xps.HasIndex(x => new
{ {
x.UserId, x.UserId,
x.GuildId x.GuildId
}) })
.IsUnique(); .IsUnique();
xps.HasIndex(x => x.UserId); xps.HasIndex(x => x.UserId);
@ -433,9 +443,9 @@ public abstract class EllieContext : DbContext
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
ci.HasIndex(x => new ci.HasIndex(x => new
{ {
x.Name x.Name
}) })
.IsUnique(); .IsUnique();
#endregion #endregion
@ -554,10 +564,10 @@ public abstract class EllieContext : DbContext
.IsUnique(false); .IsUnique(false);
rr2.HasIndex(x => new rr2.HasIndex(x => new
{ {
x.MessageId, x.MessageId,
x.Emote x.Emote
}) })
.IsUnique(); .IsUnique();
}); });
@ -632,11 +642,11 @@ public abstract class EllieContext : DbContext
{ {
// user can own only one of each item // user can own only one of each item
x.HasIndex(model => new x.HasIndex(model => new
{ {
model.UserId, model.UserId,
model.ItemType, model.ItemType,
model.ItemKey model.ItemKey
}) })
.IsUnique(); .IsUnique();
}); });
@ -661,10 +671,10 @@ public abstract class EllieContext : DbContext
#region Sticky Roles #region Sticky Roles
modelBuilder.Entity<StickyRole>(sr => sr.HasIndex(x => new modelBuilder.Entity<StickyRole>(sr => sr.HasIndex(x => new
{ {
x.GuildId, x.GuildId,
x.UserId x.UserId
}) })
.IsUnique()); .IsUnique());
#endregion #endregion
@ -709,10 +719,10 @@ public abstract class EllieContext : DbContext
modelBuilder modelBuilder
.Entity<GreetSettings>(gs => gs.HasIndex(x => new .Entity<GreetSettings>(gs => gs.HasIndex(x => new
{ {
x.GuildId, x.GuildId,
x.GreetType x.GreetType
}) })
.IsUnique()); .IsUnique());
modelBuilder.Entity<GreetSettings>(gs => modelBuilder.Entity<GreetSettings>(gs =>
@ -720,7 +730,7 @@ public abstract class EllieContext : DbContext
gs gs
.Property(x => x.IsEnabled) .Property(x => x.IsEnabled)
.HasDefaultValue(false); .HasDefaultValue(false);
gs gs
.Property(x => x.AutoDeleteTimer) .Property(x => x.AutoDeleteTimer)
.HasDefaultValue(0); .HasDefaultValue(0);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace EllieBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class betstats : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "userbetstats",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
game = table.Column<int>(type: "integer", nullable: false),
wincount = table.Column<long>(type: "bigint", nullable: false),
losecount = table.Column<long>(type: "bigint", nullable: false),
totalbet = table.Column<decimal>(type: "numeric", nullable: false),
paidout = table.Column<decimal>(type: "numeric", nullable: false),
maxwin = table.Column<long>(type: "bigint", nullable: false),
maxbet = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_userbetstats", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_userbetstats_userid_game",
table: "userbetstats",
columns: new[] { "userid", "game" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "userbetstats");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations
{
/// <inheritdoc />
public partial class betstats : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "UserBetStats",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
Game = table.Column<int>(type: "INTEGER", nullable: false),
WinCount = table.Column<long>(type: "INTEGER", nullable: false),
LoseCount = table.Column<long>(type: "INTEGER", nullable: false),
TotalBet = table.Column<decimal>(type: "TEXT", nullable: false),
PaidOut = table.Column<decimal>(type: "TEXT", nullable: false),
MaxWin = table.Column<long>(type: "INTEGER", nullable: false),
MaxBet = table.Column<long>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserBetStats", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_UserBetStats_UserId_Game",
table: "UserBetStats",
columns: new[] { "UserId", "Game" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "UserBetStats");
}
}
}

View file

@ -113,14 +113,14 @@ public partial class Gambling
private async Task Ar_OnStateUpdate(AnimalRace race) private async Task Ar_OnStateUpdate(AnimalRace race)
{ {
var text = $@"|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚| var text = $@"|🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁🔚|
{string.Join("\n", race.Users.Select(p => {string.Join("\n", race.Users.Select(p =>
{ {
var index = race.FinishedUsers.IndexOf(p); var index = race.FinishedUsers.IndexOf(p);
var extra = index == -1 ? "" : $"#{index + 1} {(index == 0 ? "🏆" : "")}"; var extra = index == -1 ? "" : $"#{index + 1} {(index == 0 ? "🏆" : "")}";
return $"{(int)(p.Progress / 60f * 100),-2}%|{new string('‣', p.Progress) + p.Animal.Icon + extra}"; return $"{(int)(p.Progress / 60f * 100),-2}%|{new string('‣', p.Progress) + p.Animal.Icon + extra}";
}))} }))}
|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚|"; |🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁🔚|";
var msg = raceMessage; var msg = raceMessage;

View file

@ -71,7 +71,65 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
[Cmd] [Cmd]
[Priority(3)]
public async Task BetStats() public async Task BetStats()
=> await BetStats(ctx.User, null);
[Cmd]
[Priority(2)]
public async Task BetStats(GamblingGame game)
=> await BetStats(ctx.User, game);
[Cmd]
[Priority(1)]
public async Task BetStats([Leftover] IUser user)
=> await BetStats(user, null);
[Cmd]
[Priority(0)]
public async Task BetStats(IUser user, GamblingGame? game)
{
var stats = await _gamblingTxTracker.GetUserStatsAsync(user.Id, game);
if (stats.Count == 0)
stats = new()
{
new()
{
TotalBet = 1
}
};
var eb = _sender.CreateEmbed()
.WithOkColor()
.WithAuthor(user)
.AddField("Total Won", N(stats.Sum(x => x.PaidOut)), true)
.AddField("Biggest Win", N(stats.Max(x => x.MaxWin)), true)
.AddField("Biggest Bet", N(stats.Max(x => x.MaxBet)), true)
.AddField("# Bets", stats.Sum(x => x.WinCount + x.LoseCount), true)
.AddField("Payout",
(stats.Sum(x => x.PaidOut) / stats.Sum(x => x.TotalBet)).ToString("P2", Culture),
true);
if (game == null)
{
var favGame = stats.MaxBy(x => x.WinCount + x.LoseCount);
eb.AddField("Favorite Game",
favGame.Game + "\n" + Format.Italics((favGame.WinCount + favGame.LoseCount) + " plays"),
true);
}
else
{
eb.WithDescription(game.ToString())
.AddField("# Wins", stats.Sum(x => x.WinCount), true);
}
await Response()
.Embed(eb)
.SendAsync();
}
[Cmd]
public async Task GamblingStats()
{ {
var stats = await _gamblingTxTracker.GetAllAsync(); var stats = await _gamblingTxTracker.GetAllAsync();
@ -167,57 +225,8 @@ public partial class Gambling : GamblingModule<GamblingService>
return; return;
} }
if (Config.Timely.RequirePassword) if (Config.Timely.HasButton)
{ {
// var password = _service.GeneratePassword();
//
// var img = new Image<Rgba32>(100, 40);
//
// var font = _fonts.NotoSans.CreateFont(30);
// var outlinePen = new SolidPen(Color.Black, 1f);
// var strikeoutRun = new RichTextRun
// {
// Start = 0,
// End = password.GetGraphemeCount(),
// Font = font,
// StrikeoutPen = new SolidPen(Color.White, 3),
// TextDecorations = TextDecorations.Strikeout
// };
// // draw password on the image
// img.Mutate(x =>
// {
// x.DrawText(new RichTextOptions(font)
// {
// HorizontalAlignment = HorizontalAlignment.Center,
// VerticalAlignment = VerticalAlignment.Center,
// FallbackFontFamilies = _fonts.FallBackFonts,
// Origin = new(50, 20),
// TextRuns = [strikeoutRun]
// },
// password,
// Brushes.Solid(Color.White),
// outlinePen);
// });
// using var stream = await img.ToStreamAsync();
// var captcha = await Response()
// .Embed(_sender.CreateEmbed()
// .WithOkColor()
// .WithImageUrl("attachment://timely.png"))
// .File(stream, "timely.png")
// .SendAsync();
// try
// {
// var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
// if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
// {
// return;
// }
// }
// finally
// {
// _ = captcha.DeleteAsync();
// }
var interaction = CreateTimelyInteraction(); var interaction = CreateTimelyInteraction();
var msg = await Response().Pending(strs.timely_button).Interaction(interaction).SendAsync(); var msg = await Response().Pending(strs.timely_button).Interaction(interaction).SendAsync();
await msg.DeleteAsync(); await msg.DeleteAsync();
@ -249,6 +258,19 @@ public partial class Gambling : GamblingModule<GamblingService>
var val = Config.Timely.Amount; var val = Config.Timely.Amount;
var guildUsers = await (Config.BoostBonus
.GuildIds
?? new())
.Select(x => ((IGuild)_client.GetGuild(x))?.GetUserAsync(ctx.User.Id))
.WhenAll();
var boostGuildUser = guildUsers.FirstOrDefault(x => x?.PremiumSince is not null);
var booster = boostGuildUser is not null;
if (booster)
val += Config.BoostBonus.BaseTimelyBonus;
var patron = await _ps.GetPatronAsync(ctx.User.Id); var patron = await _ps.GetPatronAsync(ctx.User.Id);
var percentBonus = (_ps.PercentBonus(patron) / 100f); var percentBonus = (_ps.PercentBonus(patron) / 100f);
@ -259,7 +281,16 @@ public partial class Gambling : GamblingModule<GamblingService>
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim")); await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
await Response().Confirm(strs.timely(N(val), period)).Interaction(inter).SendAsync(); if (booster)
{
var msg = GetText(strs.timely(N(val), period))
+ "\n\n"
+ $"*+{N(Config.BoostBonus.BaseTimelyBonus)} bonus for boosting {boostGuildUser.Guild}!*";
await Response().Confirm(msg).Interaction(inter).SendAsync();
}
else
await Response().Confirm(strs.timely(N(val), period)).Interaction(inter).SendAsync();
} }
[Cmd] [Cmd]
@ -370,8 +401,9 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
var embed = _sender.CreateEmbed() var embed = _sender.CreateEmbed()
.WithTitle(GetText(strs.transactions(((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString() .WithTitle(GetText(strs.transactions(
?? $"{userId}"))) ((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
?? $"{userId}")))
.WithOkColor(); .WithOkColor();
var sb = new StringBuilder(); var sb = new StringBuilder();
@ -627,7 +659,9 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
else else
{ {
await Response().Error(strs.take_fail(N(amount), Format.Bold(user.ToString()), CurrencySign)).SendAsync(); await Response()
.Error(strs.take_fail(N(amount), Format.Bold(user.ToString()), CurrencySign))
.SendAsync();
} }
} }
@ -648,7 +682,9 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
else else
{ {
await Response().Error(strs.take_fail(N(amount), Format.Code(usrId.ToString()), CurrencySign)).SendAsync(); await Response()
.Error(strs.take_fail(N(amount), Format.Code(usrId.ToString()), CurrencySign))
.SendAsync();
} }
} }

View file

@ -11,7 +11,7 @@ namespace EllieBot.Modules.Gambling.Common;
public sealed partial class GamblingConfig : ICloneable<GamblingConfig> public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
{ {
[Comment("""DO NOT CHANGE""")] [Comment("""DO NOT CHANGE""")]
public int Version { get; set; } = 9; public int Version { get; set; } = 10;
[Comment("""Currency settings""")] [Comment("""Currency settings""")]
public CurrencyConfig Currency { get; set; } public CurrencyConfig Currency { get; set; }
@ -67,6 +67,11 @@ public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
[Comment("""Slot config""")] [Comment("""Slot config""")]
public SlotsConfig Slots { get; set; } public SlotsConfig Slots { get; set; }
[Comment("""
Bonus config for server boosts
""")]
public BoostBonusConfig BoostBonus { get; set; }
public GamblingConfig() public GamblingConfig()
{ {
BetRoll = new(); BetRoll = new();
@ -79,6 +84,7 @@ public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
Slots = new(); Slots = new();
LuckyLadder = new(); LuckyLadder = new();
BotCuts = new(); BotCuts = new();
BoostBonus = new();
} }
} }
@ -104,7 +110,7 @@ public partial class TimelyConfig
How much currency will the users get every time they run .timely command How much currency will the users get every time they run .timely command
setting to 0 or less will disable this feature setting to 0 or less will disable this feature
""")] """)]
public int Amount { get; set; } = 0; public long Amount { get; set; } = 0;
[Comment(""" [Comment("""
How often (in hours) can users claim currency with .timely command How often (in hours) can users claim currency with .timely command
@ -115,7 +121,7 @@ public partial class TimelyConfig
[Comment(""" [Comment("""
Whether the users are required to type a password when they do timely. Whether the users are required to type a password when they do timely.
""")] """)]
public bool RequirePassword { get; set; } = true; public bool HasButton { get; set; } = true;
} }
[Cloneable] [Cloneable]
@ -413,4 +419,18 @@ public sealed partial class BotCutConfig
Default 0.1 (10%). Default 0.1 (10%).
""")] """)]
public decimal ShopSaleCut { get; set; } = 0.1m; public decimal ShopSaleCut { get; set; } = 0.1m;
}
[Cloneable]
public sealed partial class BoostBonusConfig
{
[Comment("Users will receive a bonus if they boost any of these servers")]
public List<ulong> GuildIds { get; set; } =
[
117523346618318850
];
[Comment("This bonus will be added before any other multiplier is applied to the .timely command")]
public long BaseTimelyBonus { get; set; } = 50;
} }

View file

@ -144,8 +144,8 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
ConfigPrinters.ToString, ConfigPrinters.ToString,
val => val >= 0); val => val >= 0);
AddParsedProp("timely.pass", AddParsedProp("timely.btn",
gs => gs.Timely.RequirePassword, gs => gs.Timely.HasButton,
bool.TryParse, bool.TryParse,
ConfigPrinters.ToString); ConfigPrinters.ToString);
@ -189,11 +189,11 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
}); });
} }
if (data.Version < 9) if (data.Version < 10)
{ {
ModifyConfig(c => ModifyConfig(c =>
{ {
c.Version = 9; c.Version = 10;
}); });
} }
} }

View file

@ -37,7 +37,7 @@ public sealed class NewGamblingService : IGamblingService, IEService
var won = (long)result.Won; var won = (long)result.Won;
if (won > 0) if (won > 0)
{ {
await _cs.AddAsync(userId, won, new("lula", "win")); await _cs.AddAsync(userId, won, new("lula", result.Multiplier >= 1 ? "win" : "lose"));
} }
return result; return result;
@ -155,7 +155,7 @@ public sealed class NewGamblingService : IGamblingService, IEService
var won = (long)result.Won; var won = (long)result.Won;
if (won > 0) if (won > 0)
{ {
await _cs.AddAsync(userId, won, new("slot", "won")); await _cs.AddAsync(userId, won, new("slot", "win"));
} }
return result; return result;

View file

@ -4,6 +4,6 @@ namespace EllieBot.Services;
public interface ITxTracker public interface ITxTracker
{ {
Task TrackAdd(long amount, TxData? txData); Task TrackAdd(ulong userId, long amount, TxData? txData);
Task TrackRemove(long amount, TxData? txData); Task TrackRemove(ulong userId, long amount, TxData? txData);
} }

View file

@ -77,7 +77,7 @@ public sealed class CurrencyService : ICurrencyService, IEService
{ {
var wallet = await GetWalletAsync(userId); var wallet = await GetWalletAsync(userId);
await wallet.Add(amount, txData); await wallet.Add(amount, txData);
await _txTracker.TrackAdd(amount, txData); await _txTracker.TrackAdd(userId, amount, txData);
} }
public async Task AddAsync( public async Task AddAsync(
@ -97,7 +97,7 @@ public sealed class CurrencyService : ICurrencyService, IEService
var wallet = await GetWalletAsync(userId); var wallet = await GetWalletAsync(userId);
var result = await wallet.Take(amount, txData); var result = await wallet.Take(amount, txData);
if (result) if (result)
await _txTracker.TrackRemove(amount, txData); await _txTracker.TrackRemove(userId, amount, txData);
return result; return result;
} }

View file

@ -1,8 +1,10 @@
using LinqToDB; using LinqToDB;
using LinqToDB.Data;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.ModuleBehaviors;
using EllieBot.Services.Currency; using EllieBot.Services.Currency;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using System.Collections.Concurrent;
namespace EllieBot.Services; namespace EllieBot.Services;
@ -10,15 +12,11 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
{ {
private static readonly IReadOnlySet<string> _gamblingTypes = new HashSet<string>(new[] private static readonly IReadOnlySet<string> _gamblingTypes = new HashSet<string>(new[]
{ {
"lula", "lula", "betroll", "betflip", "blackjack", "betdraw", "slot",
"betroll",
"betflip",
"blackjack",
"betdraw",
"slot",
}); });
private ConcurrentDictionary<string, (decimal Bet, decimal PaidOut)> _stats = new(); private NonBlocking.ConcurrentDictionary<string, (decimal Bet, decimal PaidOut)> globalStats = new();
private ConcurrentBag<UserBetStats> userStats = new();
private readonly DbService _db; private readonly DbService _db;
@ -28,83 +26,283 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
} }
public async Task OnReadyAsync() public async Task OnReadyAsync()
=> await Task.WhenAll(RunUserStatsCollector(), RunBetStatsCollector());
public async Task RunBetStatsCollector()
{ {
using var timer = new PeriodicTimer(TimeSpan.FromHours(1)); using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
while (await timer.WaitForNextTickAsync()) while (await timer.WaitForNextTickAsync())
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
await using var trans = await ctx.Database.BeginTransactionAsync();
try try
{ {
var keys = _stats.Keys; // update betstats
var keys = globalStats.Keys;
foreach (var key in keys) foreach (var key in keys)
{ {
if (_stats.TryRemove(key, out var stat)) if (globalStats.TryRemove(key, out var stat))
{ {
await ctx.GetTable<GamblingStats>() await ctx.GetTable<GamblingStats>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
Feature = key, Feature = key,
Bet = stat.Bet, Bet = stat.Bet,
PaidOut = stat.PaidOut, PaidOut = stat.PaidOut,
DateAdded = DateTime.UtcNow DateAdded = DateTime.UtcNow
}, old => new() },
{ old => new()
Bet = old.Bet + stat.Bet, {
PaidOut = old.PaidOut + stat.PaidOut, Bet = old.Bet + stat.Bet,
}, () => new() PaidOut = old.PaidOut + stat.PaidOut,
{ },
Feature = key () => new()
}); {
Feature = key
});
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "An error occurred in gambling tx tracker"); Log.Error(ex, "An error occurred in betstats gambling tx tracker");
}
finally
{
await trans.CommitAsync();
} }
} }
} }
public Task TrackAdd(long amount, TxData? txData) private async Task RunUserStatsCollector()
{
var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync())
{
try
{
if (userStats.Count == 0)
continue;
var users = new List<UserBetStats>(userStats.Count + 5);
while (userStats.TryTake(out var s))
users.Add(s);
if (users.Count == 0)
continue;
foreach (var (k, x) in users.GroupBy(x => (x.UserId, x.Game))
.ToDictionary(x => x.Key,
x => x.Aggregate((a, b) => new()
{
WinCount = a.WinCount + b.WinCount,
LoseCount = a.LoseCount + b.LoseCount,
TotalBet = a.TotalBet + b.TotalBet,
PaidOut = a.PaidOut + b.PaidOut,
MaxBet = Math.Max(a.MaxBet, b.MaxBet),
MaxWin = Math.Max(a.MaxWin, b.MaxWin),
})))
{
// bulk upsert in the future
await using var uow = _db.GetDbContext();
await uow.GetTable<UserBetStats>()
.InsertOrUpdateAsync(() => new()
{
UserId = k.UserId,
Game = k.Game,
WinCount = x.WinCount,
LoseCount = Math.Max(0, x.LoseCount),
TotalBet = x.TotalBet,
PaidOut = x.PaidOut,
MaxBet = x.MaxBet,
MaxWin = x.MaxWin
},
o => new()
{
WinCount = o.WinCount + x.WinCount,
LoseCount = Math.Max(0, o.LoseCount + x.LoseCount),
TotalBet = o.TotalBet + x.TotalBet,
PaidOut = o.PaidOut + x.PaidOut,
MaxBet = Math.Max(o.MaxBet, x.MaxBet),
MaxWin = Math.Max(o.MaxWin, x.MaxWin),
},
() => new()
{
UserId = k.UserId,
Game = k.Game
});
}
}
catch (Exception ex)
{
Log.Error(ex, "An error occurred in UserBetStats gambling tx tracker");
}
}
}
public Task TrackAdd(ulong userId, long amount, TxData? txData)
{ {
if (txData is null) if (txData is null)
return Task.CompletedTask; return Task.CompletedTask;
if (_gamblingTypes.Contains(txData.Type)) if (_gamblingTypes.Contains(txData.Type))
{ {
_stats.AddOrUpdate(txData.Type, globalStats.AddOrUpdate(txData.Type,
_ => (0, amount), _ => (0, amount),
(_, old) => (old.Bet, old.PaidOut + amount)); (_, old) => (old.Bet, old.PaidOut + amount));
} }
var mType = GetGameType(txData.Type);
if (mType is not { } type)
return Task.CompletedTask;
if (txData.Type == "lula")
{
if (txData.Extra == "lose")
{
userStats.Add(new()
{
UserId = userId,
Game = type,
WinCount = 0,
LoseCount = 0,
TotalBet = 0,
PaidOut = amount,
MaxBet = 0,
MaxWin = amount,
});
return Task.CompletedTask;
}
}
else if (txData.Type == "animalrace")
{
if (txData.Extra == "refund")
{
userStats.Add(new()
{
UserId = userId,
Game = type,
WinCount = 0,
LoseCount = -1,
TotalBet = -amount,
PaidOut = 0,
MaxBet = 0,
MaxWin = 0,
});
return Task.CompletedTask;
}
}
userStats.Add(new UserBetStats()
{
UserId = userId,
Game = type,
WinCount = 1,
LoseCount = -1,
TotalBet = 0,
PaidOut = amount,
MaxBet = 0,
MaxWin = amount,
});
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task TrackRemove(long amount, TxData? txData) public Task TrackRemove(ulong userId, long amount, TxData? txData)
{ {
if (txData is null) if (txData is null)
return Task.CompletedTask; return Task.CompletedTask;
if (_gamblingTypes.Contains(txData.Type)) if (_gamblingTypes.Contains(txData.Type))
{ {
_stats.AddOrUpdate(txData.Type, globalStats.AddOrUpdate(txData.Type,
_ => (amount, 0), _ => (amount, 0),
(_, old) => (old.Bet + amount, old.PaidOut)); (_, old) => (old.Bet + amount, old.PaidOut));
} }
var mType = GetGameType(txData.Type);
if (mType is not { } type)
return Task.CompletedTask;
userStats.Add(new UserBetStats()
{
UserId = userId,
Game = type,
WinCount = 0,
LoseCount = 1,
TotalBet = amount,
PaidOut = 0,
MaxBet = amount,
MaxWin = 0
});
return Task.CompletedTask; return Task.CompletedTask;
} }
private static GamblingGame? GetGameType(string game)
=> game switch
{
"lula" => GamblingGame.Lula,
"betroll" => GamblingGame.Betroll,
"betflip" => GamblingGame.Betflip,
"blackjack" => GamblingGame.Blackjack,
"betdraw" => GamblingGame.Betdraw,
"slot" => GamblingGame.Slots,
"animalrace" => GamblingGame.Race,
_ => null
};
public async Task<IReadOnlyCollection<GamblingStats>> GetAllAsync() public async Task<IReadOnlyCollection<GamblingStats>> GetAllAsync()
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
return await ctx.Set<GamblingStats>() return await ctx.Set<GamblingStats>()
.ToListAsyncEF(); .ToListAsyncEF();
} }
public async Task<List<UserBetStats>> GetUserStatsAsync(ulong userId, GamblingGame? game = null)
{
await using var ctx = _db.GetDbContext();
if (game is null)
return await ctx
.GetTable<UserBetStats>()
.Where(x => x.UserId == userId)
.ToListAsync();
return await ctx
.GetTable<UserBetStats>()
.Where(x => x.UserId == userId && x.Game == game)
.ToListAsync();
}
}
public sealed class UserBetStats
{
public int Id { get; set; }
public ulong UserId { get; set; }
public GamblingGame Game { get; set; }
public long WinCount { get; set; }
public long LoseCount { get; set; }
public decimal TotalBet { get; set; }
public decimal PaidOut { get; set; }
public long MaxWin { get; set; }
public long MaxBet { get; set; }
}
public enum GamblingGame
{
Betflip = 0,
Bf = 0,
Betroll = 1,
Br = 1,
Betdraw = 2,
Bd = 2,
Slots = 3,
Slot = 3,
Blackjack = 4,
Bj = 4,
Lula = 5,
Race = 6,
AnimalRace = 6
} }

View file

@ -848,6 +848,10 @@ eventstart:
- eventstart - eventstart
betstats: betstats:
- betstats - betstats
- bs
gamblestats:
- gamblestats
- gs
bettest: bettest:
- bettest - bettest
slot: slot:

View file

@ -1,5 +1,5 @@
# DO NOT CHANGE # DO NOT CHANGE
version: 9 version: 10
# Currency settings # Currency settings
currency: currency:
# What is the emoji/character which represents the currency # What is the emoji/character which represents the currency
@ -57,7 +57,7 @@ timely:
# setting to 0 or less will disable this feature # setting to 0 or less will disable this feature
cooldown: 12 cooldown: 12
# Whether the users are required to type a password when they do timely. # Whether the users are required to type a password when they do timely.
requirePassword: true hasButton: true
# How much will each user's owned currency decay over time. # How much will each user's owned currency decay over time.
decay: decay:
# Percentage of user's current currency which will be deducted every 24h. # Percentage of user's current currency which will be deducted every 24h.
@ -273,3 +273,10 @@ voteReward: 100
slots: slots:
# Hex value of the color which the numbers on the slot image will have. # Hex value of the color which the numbers on the slot image will have.
currencyFontColor: ff0000 currencyFontColor: ff0000
# Bonus config for server boosts
boostBonus:
# Users will receive a bonus if they boost any of these servers
guildIds:
- 117523346618318850
# This bonus will be added before any other multiplier is applied to the .timely command
baseTimelyBonus: 50

8
src/EllieBot/migrate.ps1 Normal file
View file

@ -0,0 +1,8 @@
if ($args.Length -eq 0) {
Write-Host "Please provide a migration name." -ForegroundColor Red
}
else {
$migrationName = $args[0]
dotnet ef migrations add $migrationName -c SqliteContext -p EllieBot.csproj
dotnet ef migrations add $migrationName -c PostgreSqlContext -p EllieBot.csproj
}