Compare commits

..

15 Commits

Author SHA1 Message Date
Kwoth
fa12fcea58 change: .race will now have 82-94% payout rate based on the number of players playign (1-12, x0.01 per player). Any player over 12 won't increase payout 2024-11-05 04:27:27 +00:00
Kwoth
274219c40b docs: Upped version to 5.1.19, updated changelog
fix: Fixed timely on different shards
2024-11-05 02:57:01 +00:00
Kwoth
96c9b47da2 add: timely now has an option in gambling whether to use no protection, captcha, or button.
fix: grpc api fix for dashy
2024-11-04 14:35:59 +00:00
Kwoth
b5d1469df1 Merge branch 'v5' of https://gitlab.com/kwoth/nadekobot into v5 2024-11-04 12:28:52 +00:00
Kwoth
d7747bd25a fix: timely fixes 2024-11-04 12:28:42 +00:00
Kwoth
7d162d1f04 fix: timely fixes 2024-11-04 12:28:01 +00:00
Kwoth
704d061d46 fix: fixed pipeline, added missing strings 2024-11-04 10:58:44 +00:00
Kwoth
c39c9061fd add: added timely boost bonus to gambling.yml
change: .betstats renamed to .gamblestats/.gs
add: added .betstats, .betstats <game> and .betstats <user> <game?> command which shows you your stats for gambling commands
2024-11-04 10:42:05 +00:00
Kwoth
619ddba4f8 fix: fixed pagination numbers in xplb and xpglb 2024-11-04 02:03:53 +00:00
Kwoth
3acef04b32 change: strikeout slightly thinner to make password easier to read on plants 2024-11-03 13:45:12 +00:00
Kwoth
83a1d959b1 fix: Added nordic and ugro finnic languages to flag translate 2024-11-03 12:05:51 +00:00
Kwoth
a1632722bc fix: fix timely 2024-11-03 09:32:43 +00:00
Kwoth
ee0a28afab fix: revert patron migration temporarily as ef core is bugging out hard 2024-11-03 08:39:37 +00:00
Kwoth
2b301c0aab fix: possible fix for patron table 2024-11-03 08:31:23 +00:00
Kwoth
b6b6b4e19e fix: Fixed UserId patron table error
fix: Added au and kz countries as en and kz languages respectively
fix: Strikeout is thinner now on plants
2024-11-03 08:24:00 +00:00
32 changed files with 7777 additions and 192 deletions

View File

@@ -2,6 +2,30 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [5.1.19] - 04.11.2024
### Added
- Added `.betstats`
- See your own stats with .betstats
- Target someone else: .betstats @seraphe
- You can also specify a game .betstats lula
- Or both! .betstats seraphe br
- `.timely` can now have a server boost bonus
- Configure server ids and reward amount in data/gambling.yml
- anyone who boosts one of the sepcified servers gets the amount as base timely bonus
### Changed
- `.plant/pick` password font size will be slightly bigger
- `.race` will now have 82-94% payout rate based on the number of players playing (1-12, x0.01 per player).
- Any player over 12 won't increase payout
### Fixed
- `.xplb` and `.xpglb` now have proper ranks after page 1
- Fixed boost bonus on shards different than the specified servers' shard
## [5.1.18] - 02.11.2024 ## [5.1.18] - 02.11.2024
### Added ### Added
@@ -12,14 +36,16 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
language of that country language of that country
- 5 second cooldown per user - 5 second cooldown per user
- The message can only be translated once per language (counter resets every 24h) - The message can only be translated once per language (counter resets every 24h)
- `.timely` now has a button. Togglable via `.conf gambling` it's called pass because previously it was a captcha, but captchas are too annoying - `.timely` now has a button. Togglable via `.conf gambling` it's called pass because previously it was a captcha, but
captchas are too annoying
## Changed ## Changed
- [public bot] Patreon reward bonus for flowers reduced. Timely bonuses stay the same - [public bot] Patreon reward bonus for flowers reduced. Timely bonuses stay the same
- discriminators removed from the databases. All users who had ???? as discriminator have been renamed to ??username. - discriminators removed from the databases. All users who had ???? as discriminator have been renamed to ??username.
- all new unknown users will have ??Unknown as their name - all new unknown users will have ??Unknown as their name
- Flower currency generation will now have a strikeout to try combat the pickbots. This is the weakest but easiest protection to implement. There may be more options in the future - Flower currency generation will now have a strikeout to try combat the pickbots. This is the weakest but easiest
protection to implement. There may be more options in the future
## Fixed ## Fixed

View File

@@ -1,3 +0,0 @@
dotnet ef migrations remove -c SqliteContext -f -p src/NadekoBot/NadekoBot.csproj
dotnet ef migrations remove -c PostgreSqlContext -f -p src/NadekoBot/NadekoBot.csproj

View File

@@ -15,6 +15,17 @@ service GrpcXp {
rpc AddReward(AddRewardRequest) returns (AddRewardReply); rpc AddReward(AddRewardRequest) returns (AddRewardReply);
rpc DeleteReward(DeleteRewardRequest) returns (DeleteRewardReply); rpc DeleteReward(DeleteRewardRequest) returns (DeleteRewardReply);
rpc SetServerExclusion(SetServerExclusionRequest) returns (SetServerExclusionReply);
}
message SetServerExclusionRequest {
uint64 guildId = 1;
bool serverExcluded = 2;
}
message SetServerExclusionReply {
bool success = 1;
} }
message GetXpLbRequest { message GetXpLbRequest {
@@ -32,7 +43,8 @@ message XpLbUserReply {
string username = 2; string username = 2;
int64 xp = 3; int64 xp = 3;
int64 level = 4; int64 level = 4;
string avatar = 5; int64 levelPercent = 5;
string avatar = 6;
} }
message ResetUserXpRequest { message ResetUserXpRequest {

View File

@@ -1,8 +1,12 @@
#nullable disable #nullable disable
using System.ComponentModel.DataAnnotations;
namespace NadekoBot.Db.Models; namespace NadekoBot.Db.Models;
public class PatronUser public class PatronUser
{ {
// [Key]
// public int Id { get; set; }
public string UniquePlatformUserId { get; set; } public string UniquePlatformUserId { get; set; }
public ulong UserId { get; set; } public ulong UserId { get; set; }
public int AmountCents { get; set; } public int AmountCents { get; set; }

View File

@@ -62,6 +62,7 @@ public abstract class NadekoContext : 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,6 +74,15 @@ public abstract class NadekoContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
#region UserBetStats
modelBuilder.Entity<UserBetStats>()
.HasIndex(x => new { x.UserId, x.Game })
.IsUnique();
#endregion
#region Flag Translate #region Flag Translate
modelBuilder.Entity<FlagTranslateChannel>() modelBuilder.Entity<FlagTranslateChannel>()

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 NadekoBot.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");
}
}
}

View File

@@ -3227,6 +3227,57 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("greetsettings", (string)null); b.ToTable("greetsettings", (string)null);
}); });
modelBuilder.Entity("NadekoBot.Services.UserBetStats", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Game")
.HasColumnType("integer")
.HasColumnName("game");
b.Property<long>("LoseCount")
.HasColumnType("bigint")
.HasColumnName("losecount");
b.Property<long>("MaxBet")
.HasColumnType("bigint")
.HasColumnName("maxbet");
b.Property<long>("MaxWin")
.HasColumnType("bigint")
.HasColumnName("maxwin");
b.Property<decimal>("PaidOut")
.HasColumnType("numeric")
.HasColumnName("paidout");
b.Property<decimal>("TotalBet")
.HasColumnType("numeric")
.HasColumnName("totalbet");
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.Property<long>("WinCount")
.HasColumnType("bigint")
.HasColumnName("wincount");
b.HasKey("Id")
.HasName("pk_userbetstats");
b.HasIndex("UserId", "Game")
.IsUnique()
.HasDatabaseName("ix_userbetstats_userid_game");
b.ToTable("userbetstats", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.AntiAltSetting", b => modelBuilder.Entity("NadekoBot.Db.Models.AntiAltSetting", b =>
{ {
b.HasOne("NadekoBot.Db.Models.GuildConfig", null) b.HasOne("NadekoBot.Db.Models.GuildConfig", null)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.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

@@ -2399,6 +2399,44 @@ namespace NadekoBot.Migrations
b.ToTable("GreetSettings"); b.ToTable("GreetSettings");
}); });
modelBuilder.Entity("NadekoBot.Services.UserBetStats", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Game")
.HasColumnType("INTEGER");
b.Property<long>("LoseCount")
.HasColumnType("INTEGER");
b.Property<long>("MaxBet")
.HasColumnType("INTEGER");
b.Property<long>("MaxWin")
.HasColumnType("INTEGER");
b.Property<decimal>("PaidOut")
.HasColumnType("TEXT");
b.Property<decimal>("TotalBet")
.HasColumnType("TEXT");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<long>("WinCount")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId", "Game")
.IsUnique();
b.ToTable("UserBetStats");
});
modelBuilder.Entity("NadekoBot.Db.Models.AntiAltSetting", b => modelBuilder.Entity("NadekoBot.Db.Models.AntiAltSetting", b =>
{ {
b.HasOne("NadekoBot.Db.Models.GuildConfig", null) b.HasOne("NadekoBot.Db.Models.GuildConfig", null)

View File

@@ -6,6 +6,10 @@ namespace NadekoBot.Modules.Gambling.Common.AnimalRacing;
public sealed class AnimalRace : IDisposable public sealed class AnimalRace : IDisposable
{ {
public const double BASE_MULTIPLIER = 0.82;
public const double MAX_MULTIPLIER = 0.94;
public const double MULTI_PER_USER = 0.01;
public enum Phase public enum Phase
{ {
WaitingForPlayers, WaitingForPlayers,
@@ -100,7 +104,7 @@ public sealed class AnimalRace : IDisposable
foreach (var user in _users) foreach (var user in _users)
{ {
if (user.Bet > 0) if (user.Bet > 0)
await _currency.AddAsync(user.UserId, user.Bet, new("animalrace", "refund")); await _currency.AddAsync(user.UserId, (long)(user.Bet * BASE_MULTIPLIER), new("animalrace", "refund"));
} }
_ = OnStartingFailed?.Invoke(this); _ = OnStartingFailed?.Invoke(this);
@@ -131,8 +135,10 @@ public sealed class AnimalRace : IDisposable
if (FinishedUsers[0].Bet > 0) if (FinishedUsers[0].Bet > 0)
{ {
Multi = FinishedUsers.Count
* Math.Min(MAX_MULTIPLIER, BASE_MULTIPLIER + (MULTI_PER_USER * FinishedUsers.Count));
await _currency.AddAsync(FinishedUsers[0].UserId, await _currency.AddAsync(FinishedUsers[0].UserId,
FinishedUsers[0].Bet * (_users.Count - 1), (long)(FinishedUsers[0].Bet * Multi),
new("animalrace", "win")); new("animalrace", "win"));
} }
@@ -140,6 +146,8 @@ public sealed class AnimalRace : IDisposable
}); });
} }
public double Multi { get; set; } = BASE_MULTIPLIER;
public void Dispose() public void Dispose()
{ {
CurrentPhase = Phase.Ended; CurrentPhase = Phase.Ended;

View File

@@ -74,10 +74,14 @@ public partial class Gambling
if (race.FinishedUsers[0].Bet > 0) if (race.FinishedUsers[0].Bet > 0)
{ {
return Response() return Response()
.Confirm(GetText(strs.animal_race), .Embed(_sender.CreateEmbed()
GetText(strs.animal_race_won_money(Format.Bold(winner.Username), .WithOkColor()
winner.Animal.Icon, .WithTitle(GetText(strs.animal_race))
(race.FinishedUsers[0].Bet * (race.Users.Count - 1)) + CurrencySign))) .WithDescription(GetText(strs.animal_race_won_money(
Format.Bold(winner.Username),
winner.Animal.Icon,
N(race.FinishedUsers[0].Bet * race.Multi))))
.WithFooter($"x{race.Multi:F2}"))
.SendAsync(); .SendAsync();
} }
@@ -113,14 +117,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;
@@ -129,10 +133,10 @@ public partial class Gambling
else else
{ {
await msg.ModifyAsync(x => x.Embed = _sender.CreateEmbed() await msg.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
.WithTitle(GetText(strs.animal_race)) .WithTitle(GetText(strs.animal_race))
.WithDescription(text) .WithDescription(text)
.WithOkColor() .WithOkColor()
.Build()); .Build());
} }
} }

View File

@@ -14,6 +14,13 @@ using System.Text;
using NadekoBot.Modules.Gambling.Rps; using NadekoBot.Modules.Gambling.Rps;
using NadekoBot.Common.TypeReaders; using NadekoBot.Common.TypeReaders;
using NadekoBot.Modules.Patronage; using NadekoBot.Modules.Patronage;
using SixLabors.Fonts;
using SixLabors.Fonts.Unicode;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Color = SixLabors.ImageSharp.Color;
namespace NadekoBot.Modules.Gambling; namespace NadekoBot.Modules.Gambling;
@@ -71,7 +78,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 GambleStats()
{ {
var stats = await _gamblingTxTracker.GetAllAsync(); var stats = await _gamblingTxTracker.GetAllAsync();
@@ -153,10 +218,12 @@ public partial class Gambling : GamblingModule<GamblingService>
customId: "timely:" + _rng.Next(123456, 999999)), customId: "timely:" + _rng.Next(123456, 999999)),
async (smc) => async (smc) =>
{ {
await smc.DeferAsync();
await ClaimTimely(); await ClaimTimely();
}); });
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)]
public async Task Timely() public async Task Timely()
{ {
var val = Config.Timely.Amount; var val = Config.Timely.Amount;
@@ -167,62 +234,64 @@ public partial class Gambling : GamblingModule<GamblingService>
return; return;
} }
if (Config.Timely.RequirePassword) if (Config.Timely.ProtType == TimelyProt.Button)
{ {
// 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();
return; return;
} }
else if (Config.Timely.ProtType == TimelyProt.Captcha)
{
var password = _service.GeneratePassword();
var img = new Image<Rgba32>(70, 35);
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(35, 17),
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();
}
}
await ClaimTimely(); await ClaimTimely();
} }
@@ -249,6 +318,29 @@ public partial class Gambling : GamblingModule<GamblingService>
var val = Config.Timely.Amount; var val = Config.Timely.Amount;
var boostGuilds = Config.BoostBonus.GuildIds ?? new();
var guildUsers = await boostGuilds
.Select(async gid =>
{
try
{
var guild = await _client.Rest.GetGuildAsync(gid, false);
var user = await _client.Rest.GetGuildUserAsync(gid, ctx.User.Id);
return (guild, user);
}
catch
{
return default;
}
})
.WhenAll();
var userInfo = guildUsers.FirstOrDefault(x => x.user?.PremiumSince is not null);
var booster = userInfo != default;
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 +351,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 {userInfo.guild}!*";
await Response().Confirm(msg).Interaction(inter).SendAsync();
}
else
await Response().Confirm(strs.timely(N(val), period)).Interaction(inter).SendAsync();
} }
[Cmd] [Cmd]
@@ -370,8 +471,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 +729,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 +752,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 NadekoBot.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; } = 11;
[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
@@ -113,9 +119,17 @@ public partial class TimelyConfig
public int Cooldown { get; set; } = 24; public int Cooldown { get; set; } = 24;
[Comment(""" [Comment("""
Whether the users are required to type a password when they do timely. How will timely be protected?
None, Button (users have to click the button) or Captcha (users have to type the captcha from an image)
""")] """)]
public bool RequirePassword { get; set; } = true; public TimelyProt ProtType { get; set; } = TimelyProt.Button;
}
public enum TimelyProt
{
None,
Button,
Captcha
} }
[Cloneable] [Cloneable]
@@ -414,3 +428,14 @@ public sealed partial class BotCutConfig
""")] """)]
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; } = new();
[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,9 +144,9 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
ConfigPrinters.ToString, ConfigPrinters.ToString,
val => val >= 0); val => val >= 0);
AddParsedProp("timely.pass", AddParsedProp("timely.prot",
gs => gs.Timely.RequirePassword, gs => gs.Timely.ProtType,
bool.TryParse, ConfigParsers.InsensitiveEnum,
ConfigPrinters.ToString); ConfigPrinters.ToString);
Migrate(); Migrate();
@@ -189,11 +189,11 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
}); });
} }
if (data.Version < 9) if (data.Version < 11)
{ {
ModifyConfig(c => ModifyConfig(c =>
{ {
c.Version = 9; c.Version = 11;
}); });
} }
} }

View File

@@ -147,7 +147,7 @@ public class PlantPickService : INService, IExecNoCommand
pass = pass.TrimTo(10, true).ToLowerInvariant(); pass = pass.TrimTo(10, true).ToLowerInvariant();
using var img = Image.Load<Rgba32>(curImg); using var img = Image.Load<Rgba32>(curImg);
// choose font size based on the image height, so that it's visible // choose font size based on the image height, so that it's visible
var font = _fonts.NotoSans.CreateFont(img.Height / 12.0f, FontStyle.Bold); var font = _fonts.NotoSans.CreateFont(img.Height / 11.0f, FontStyle.Bold);
img.Mutate(x => img.Mutate(x =>
{ {
// measure the size of the text to be drawing // measure the size of the text to be drawing
@@ -159,7 +159,7 @@ public class PlantPickService : INService, IExecNoCommand
// fill the background with black, add 5 pixels on each side to make it look better // fill the background with black, add 5 pixels on each side to make it look better
x.FillPolygon(Color.ParseHex("00000080"), x.FillPolygon(Color.ParseHex("00000080"),
new PointF(0, 0), new PointF(1, 1),
new PointF(size.Width + 5, 0), new PointF(size.Width + 5, 0),
new PointF(size.Width + 5, size.Height + 10), new PointF(size.Width + 5, size.Height + 10),
new PointF(0, size.Height + 10)); new PointF(0, size.Height + 10));
@@ -169,7 +169,7 @@ public class PlantPickService : INService, IExecNoCommand
Start = 0, Start = 0,
End = pass.GetGraphemeCount(), End = pass.GetGraphemeCount(),
Font = font, Font = font,
StrikeoutPen = new SolidPen(Color.White, 5), StrikeoutPen = new SolidPen(Color.White, 2),
TextDecorations = TextDecorations.Strikeout TextDecorations = TextDecorations.Strikeout
}; };

View File

@@ -37,7 +37,7 @@ public sealed class NewGamblingService : IGamblingService, INService
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, INService
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

@@ -69,5 +69,13 @@ public partial class FlagTranslateService
YE ar YE ar
AL sq AL sq
AE ar AE ar
AU en
NZ en
KZ kz
NO no
SE sv
DK da
FI fi
HU hu
"""; """;
} }

View File

@@ -225,7 +225,7 @@ public partial class Xp : NadekoModule<XpService>
else if (userXpData.AwardedXp < 0) else if (userXpData.AwardedXp < 0)
awardStr = $"({userXpData.AwardedXp})"; awardStr = $"({userXpData.AwardedXp})";
embed.AddField($"#{i + 1 + (curPage * 9)} {user?.ToString() ?? users[i].UserId.ToString()}", embed.AddField($"#{i + 1 + (curPage * 10)} {user?.ToString() ?? users[i].UserId.ToString()}",
$"{GetText(strs.level_x(levelStats.Level))} - {levelStats.TotalXp}xp {awardStr}"); $"{GetText(strs.level_x(levelStats.Level))} - {levelStats.TotalXp}xp {awardStr}");
} }
@@ -278,7 +278,7 @@ public partial class Xp : NadekoModule<XpService>
for (var i = 0; i < users.Count; i++) for (var i = 0; i < users.Count; i++)
{ {
var user = users[i]; var user = users[i];
embed.AddField($"#{i + 1 + (curPage * 9)} {user}", embed.AddField($"#{i + 1 + (curPage * 10)} {user}",
$"{GetText(strs.level_x(new LevelStats(users[i].TotalXp).Level))} - {users[i].TotalXp}xp"); $"{GetText(strs.level_x(new LevelStats(users[i].TotalXp).Level))} - {users[i].TotalXp}xp");
} }

View File

@@ -4,7 +4,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.1.18</Version> <Version>5.1.19</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>

View File

@@ -17,10 +17,13 @@ public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, IGrpcSvc, INService
public ServerServiceDefinition Bind() public ServerServiceDefinition Bind()
=> GrpcGreet.BindService(this); => GrpcGreet.BindService(this);
private static GrpcGreetSettings ToConf(GreetSettings? conf) private static GrpcGreetSettings ToConf(GreetSettings? conf, GreetType type)
{ {
if (conf is null) if (conf is null)
return new GrpcGreetSettings(); return new GrpcGreetSettings()
{
Type = (GrpcGreetType)type
};
return new GrpcGreetSettings() return new GrpcGreetSettings()
{ {
@@ -35,9 +38,10 @@ public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, IGrpcSvc, INService
{ {
var guildId = request.GuildId; var guildId = request.GuildId;
var conf = await _gs.GetGreetSettingsAsync(guildId, (GreetType)request.Type); var type = (GreetType)request.Type;
var conf = await _gs.GetGreetSettingsAsync(guildId, type);
return ToConf(conf); return ToConf(conf, type);
} }
public override async Task<UpdateGreetReply> UpdateGreet(UpdateGreetRequest request, ServerCallContext context) public override async Task<UpdateGreetReply> UpdateGreet(UpdateGreetRequest request, ServerCallContext context)

View File

@@ -67,12 +67,12 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, INService
}); });
rews = rews.Concat(roleRews.Select(x => new RewItemReply() rews = rews.Concat(roleRews.Select(x => new RewItemReply()
{ {
Level = x.Level, Level = x.Level,
Type = "Role", Type = x.Remove ? "RemoveRole" : "AddRole",
Value = guild.GetRole(x.RoleId)?.ToString() ?? x.RoleId.ToString() Value = guild.GetRole(x.RoleId)?.ToString() ?? x.RoleId.ToString()
})) }))
.OrderBy(x => x.Level); .OrderBy(x => x.Level);
reply.Rewards.AddRange(rews); reply.Rewards.AddRange(rews);
@@ -207,15 +207,15 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, INService
public override async Task<GetXpLbReply> GetXpLb(GetXpLbRequest request, ServerCallContext context) public override async Task<GetXpLbReply> GetXpLb(GetXpLbRequest request, ServerCallContext context)
{ {
if (request.Page < 0) if (request.Page < 1)
throw new RpcException(new Status(StatusCode.InvalidArgument, "Page must be greater than or equal to 0")); throw new RpcException(new Status(StatusCode.InvalidArgument, "Page must be greater than or equal to 1"));
var guild = _client.GetGuild(request.GuildId); var guild = _client.GetGuild(request.GuildId);
if (guild is null) if (guild is null)
throw new RpcException(new Status(StatusCode.NotFound, "Guild not found")); throw new RpcException(new Status(StatusCode.NotFound, "Guild not found"));
var data = await _xp.GetGuildUserXps(request.GuildId, request.Page); var data = await _xp.GetGuildUserXps(request.GuildId, request.Page - 1);
var total = await _xp.GetTotalGuildUsers(request.GuildId); var total = await _xp.GetTotalGuildUsers(request.GuildId);
var reply = new GetXpLbReply var reply = new GetXpLbReply
@@ -223,45 +223,60 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, INService
Total = total Total = total
}; };
reply.Users.AddRange(await data var users = await data
.Select(async x => .Select(async x =>
{ {
var user = guild.GetUser(x.UserId); var user = guild.GetUser(x.UserId);
if (user is null) if (user is null)
{ {
var du = await _duSvc.GetUserAsync(x.UserId); var du = await _duSvc.GetUserAsync(x.UserId);
if (du is null) if (du is null)
return new XpLbUserReply return new XpLbUserReply
{ {
UserId = x.UserId, UserId = x.UserId,
Avatar = string.Empty, Avatar = string.Empty,
Username = string.Empty, Username = string.Empty,
Xp = x.Xp, Xp = x.Xp,
Level = new LevelStats(x.Xp).Level Level = new LevelStats(x.Xp).Level
}; };
return new XpLbUserReply() return new XpLbUserReply()
{ {
UserId = x.UserId, UserId = x.UserId,
Avatar = du.RealAvatarUrl()?.ToString() ?? string.Empty, Avatar = du.RealAvatarUrl()?.ToString() ?? string.Empty,
Username = du.ToString() ?? string.Empty, Username = du.ToString() ?? string.Empty,
Xp = x.Xp, Xp = x.Xp,
Level = new LevelStats(x.Xp).Level Level = new LevelStats(x.Xp).Level
}; };
} }
return new XpLbUserReply return new XpLbUserReply
{ {
UserId = x.UserId, UserId = x.UserId,
Avatar = user?.GetAvatarUrl() ?? string.Empty, Avatar = user?.GetAvatarUrl() ?? string.Empty,
Username = user?.ToString() ?? string.Empty, Username = user?.ToString() ?? string.Empty,
Xp = x.Xp, Xp = x.Xp,
Level = new LevelStats(x.Xp).Level Level = new LevelStats(x.Xp).Level
}; };
}) })
.WhenAll()); .WhenAll();
reply.Users.AddRange(users);
return reply; return reply;
} }
public override async Task<SetServerExclusionReply> SetServerExclusion(
SetServerExclusionRequest request,
ServerCallContext context)
{
await Task.Yield();
var newValue = _xp.ToggleExcludeServer(request.GuildId);
return new()
{
Success = newValue
};
}
} }

View File

@@ -4,6 +4,6 @@ namespace NadekoBot.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, INService
{ {
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, INService
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 NadekoBot.Common.ModuleBehaviors; using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Services.Currency; using NadekoBot.Services.Currency;
using NadekoBot.Db.Models; using NadekoBot.Db.Models;
using System.Collections.Concurrent;
namespace NadekoBot.Services; namespace NadekoBot.Services;
@@ -10,15 +12,11 @@ public sealed class GamblingTxTracker : ITxTracker, INService, 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, INService, 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

@@ -2758,11 +2758,29 @@
"Gambling": [ "Gambling": [
{ {
"Aliases": [ "Aliases": [
".betstats" ".betstats",
".bs"
],
"Description": "Shows the current bet stats for yourself, or the targetted user.\nYou may optionally specify the game to show stats for.\nSupported games right now are: bf, br, bd, lula, slot, race",
"Usage": [
".betstats",
".betstats @someone",
".betstats @someone lula",
".betstats bd"
],
"Submodule": "Gambling",
"Module": "Gambling",
"Options": null,
"Requirements": []
},
{
"Aliases": [
".gamblestats",
".gs"
], ],
"Description": "Shows the total stats of several gambling features.\nUpdates once an hour.", "Description": "Shows the total stats of several gambling features.\nUpdates once an hour.",
"Usage": [ "Usage": [
".betstats" ".gamblestats"
], ],
"Submodule": "Gambling", "Submodule": "Gambling",
"Module": "Gambling", "Module": "Gambling",
@@ -6160,6 +6178,24 @@
"Options": null, "Options": null,
"Requirements": [] "Requirements": []
}, },
{
"Aliases": [
".translateflags",
".trfl",
".fltr",
".transflags"
],
"Description": "Toggles translate flags on the current channel.\nReacting with a country flag will translate the message to that country's language.",
"Usage": [
".translateflags"
],
"Submodule": "TranslateCommands",
"Module": "Searches",
"Options": null,
"Requirements": [
"ManageChannels Channel Permission"
]
},
{ {
"Aliases": [ "Aliases": [
".xkcd" ".xkcd"

View File

@@ -1,5 +1,5 @@
# DO NOT CHANGE # DO NOT CHANGE
version: 9 version: 11
# Currency settings # Currency settings
currency: currency:
# What is the emoji/character which represents the currency # What is the emoji/character which represents the currency
@@ -56,8 +56,9 @@ timely:
# How often (in hours) can users claim currency with .timely command # How often (in hours) can users claim currency with .timely command
# 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. # How will timely be protected?
requirePassword: true # None, Button (users have to click the button) or Captcha (users have to type the captcha from an image)
protType: Captcha
# 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 +274,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

View File

@@ -2703,7 +2703,7 @@ eventstart:
desc: "The type of event being started." desc: "The type of event being started."
options: options:
desc: "The optional option flags for the event." desc: "The optional option flags for the event."
betstats: gamblestats:
desc: |- desc: |-
Shows the total stats of several gambling features. Shows the total stats of several gambling features.
Updates once an hour. Updates once an hour.
@@ -4642,3 +4642,23 @@ translateflags:
- '' - ''
params: params:
- { } - { }
betstats:
desc: |-
Shows the current bet stats for yourself, or the targetted user.
You may optionally specify the game to show stats for.
Supported games right now are: bf, br, bd, lula, slot, race
ex:
- ''
- '@someone'
- '@someone lula'
- 'bd'
params:
- {}
- user:
desc: 'The user for who to show the betstats for.'
- user:
desc: 'The user for who to show the betstats for.'
game:
desc: 'The game to show betstats for. Omit to show betstats for all games combined'
- game:
desc: 'The game to show betstats for. Omit to show betstats for all games combined'

View File

@@ -3,7 +3,7 @@ if ($args.Length -eq 0) {
} }
else { else {
$migrationName = $args[0] $migrationName = $args[0]
dotnet ef migrations add $migrationName -c SqliteContext -p src/NadekoBot/NadekoBot.csproj dotnet ef migrations add $migrationName -c SqliteContext -p NadekoBot.csproj
dotnet ef migrations add $migrationName -c PostgreSqlContext -p src/NadekoBot/NadekoBot.csproj dotnet ef migrations add $migrationName -c PostgreSqlContext -p NadekoBot.csproj
} }

View File

@@ -0,0 +1,3 @@
dotnet ef migrations remove -c SqliteContext -f -p NadekoBot.csproj
dotnet ef migrations remove -c PostgreSqlContext -f -p NadekoBot.csproj