Gambling moved to a separate project. Project builds

This commit is contained in:
Kwoth
2023-03-18 18:36:04 +01:00
parent db2328cdaf
commit 09171fb10a
361 changed files with 532 additions and 476 deletions

View File

@@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
using NadekoBot.Common.Configs;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using NadekoBot.Modules.NadekoExpressions;
using NadekoBot.Modules.Utility;
using NadekoBot.Services.Database.Models;
using Ninject;
@@ -15,15 +16,16 @@ using RunMode = Discord.Commands.RunMode;
namespace NadekoBot;
public sealed class Bot
public sealed class Bot : IBot
{
public event Func<GuildConfig, Task> JoinedGuild = delegate { return Task.CompletedTask; };
public DiscordSocketClient Client { get; }
public ImmutableArray<GuildConfig> AllGuildConfigs { get; private set; }
public IReadOnlyCollection<GuildConfig> AllGuildConfigs { get; private set; }
private IKernel Services { get; set; }
// todo remove
public string Mention { get; private set; }
public bool IsReady { get; private set; }
public int ShardId { get; set; }
@@ -33,6 +35,8 @@ public sealed class Bot
private readonly DbService _db;
private readonly IBotCredsProvider _credsProvider;
private readonly Assembly[] _moduleAssemblies;
// private readonly InteractionService _interactionService;
public Bot(int shardId, int? totalShards, string credPath = null)
@@ -83,10 +87,18 @@ public sealed class Bot
// _interactionService = new(Client.Rest);
Client.Log += Client_Log;
_moduleAssemblies = new[]
{
typeof(Bot).Assembly, // bot
typeof(Creds).Assembly, // bot.common
// modules
typeof(NadekoExpressions).Assembly,
};
}
public List<ulong> GetCurrentGuildIds()
public IReadOnlyList<ulong> GetCurrentGuildIds()
=> Client.Guilds.Select(x => x.Id).ToList();
private void AddServices()
@@ -117,14 +129,21 @@ public sealed class Bot
kernel.Bind<DiscordSocketClient>().ToConstant(Client).InSingletonScope();
kernel.Bind<CommandService>().ToConstant(_commandService).InSingletonScope();
kernel.Bind<Bot>().ToConstant(this).InSingletonScope();
kernel.Bind<IBot>().ToConstant(this).InSingletonScope();
kernel.Bind<ISeria>().To<JsonSeria>().InSingletonScope();
kernel.Bind<IConfigSeria>().To<YamlSeria>().InSingletonScope();
kernel.Bind<IMemoryCache>().ToConstant(new MemoryCache(new MemoryCacheOptions())).InSingletonScope();
kernel.AddConfigServices()
.AddConfigMigrators()
.AddMusic()
foreach (var a in _moduleAssemblies)
{
kernel.AddConfigServices(a)
.AddConfigMigrators(a)
.AddLifetimeServices(a);
}
kernel.AddMusic()
.AddCache(_creds)
.AddHttpClients();
@@ -137,8 +156,6 @@ public sealed class Bot
kernel.Bind<ICoordinator, IReadyExecutor>().To<RemoteGrpcCoordinator>().InSingletonScope();
}
kernel.AddLifetimeServices();
kernel.Bind<IServiceProvider>().ToConstant(kernel).InSingletonScope();
//initialize Services
@@ -149,7 +166,10 @@ public sealed class Bot
if (Client.ShardId == 0)
ApplyConfigMigrations();
LoadTypeReaders(typeof(Bot).Assembly);
foreach (var a in _moduleAssemblies)
{
LoadTypeReaders(a);
}
sw.Stop();
Log.Information("All services loaded in {ServiceLoadTime:F2}s", sw.Elapsed.TotalSeconds);
@@ -166,16 +186,16 @@ public sealed class Bot
private void LoadTypeReaders(Assembly assembly)
{
var filteredTypes = assembly.GetTypes()
.Where(x => x.IsSubclassOf(typeof(TypeReader))
&& x.BaseType?.GetGenericArguments().Length > 0
&& !x.IsAbstract);
.Where(x => x.IsSubclassOf(typeof(TypeReader))
&& x.BaseType?.GetGenericArguments().Length > 0
&& !x.IsAbstract);
foreach (var ft in filteredTypes)
{
var baseType = ft.BaseType;
if (baseType is null)
continue;
var typeReader = (TypeReader)ActivatorUtilities.CreateInstance(Services, ft);
var typeArgs = baseType.GetGenericArguments();
_commandService.AddTypeReader(typeArgs[0], typeReader);
@@ -280,7 +300,11 @@ public sealed class Bot
// start handling messages received in commandhandler
await commandHandler.StartHandling();
await _commandService.AddModulesAsync(typeof(Bot).Assembly, Services);
foreach (var a in _moduleAssemblies)
{
await _commandService.AddModulesAsync(a, Services);
}
// await _interactionService.AddModulesAsync(typeof(Bot).Assembly, Services);
IsReady = true;

View File

@@ -1,10 +0,0 @@
#nullable disable
namespace NadekoBot.Common;
public enum AddRemove
{
Add = int.MinValue,
Remove = int.MinValue + 1,
Rem = int.MinValue + 1,
Rm = int.MinValue + 1
}

View File

@@ -1,12 +0,0 @@
using System.Runtime.CompilerServices;
namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public sealed class AliasesAttribute : AliasAttribute
{
public AliasesAttribute([CallerMemberName] string memberName = "")
: base(CommandNameLoadHelper.GetAliasesFor(memberName))
{
}
}

View File

@@ -1,18 +0,0 @@
using System.Runtime.CompilerServices;
namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public sealed class CmdAttribute : CommandAttribute
{
public string MethodName { get; }
public CmdAttribute([CallerMemberName] string memberName = "")
: base(CommandNameLoadHelper.GetCommandNameFor(memberName))
{
MethodName = memberName.ToLowerInvariant();
Aliases = CommandNameLoadHelper.GetAliasesFor(memberName);
Remarks = memberName.ToLowerInvariant();
Summary = memberName.ToLowerInvariant();
}
}

View File

@@ -1,11 +0,0 @@
#nullable disable
namespace NadekoBot.Common;
/// <summary>
/// Classed marked with this attribute will not be added to the service provider
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class DIIgnoreAttribute : Attribute
{
}

View File

@@ -1,7 +0,0 @@
namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public sealed class NadekoOptionsAttribute<TOption> : Attribute
where TOption: INadekoCommandOptions
{
}

View File

@@ -1,21 +0,0 @@
#nullable disable
using System.Diagnostics.CodeAnalysis;
namespace NadekoBot.Common;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
public sealed class NoPublicBotAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
{
#if GLOBAL_NADEKO
return Task.FromResult(PreconditionResult.FromError("Not available on the public bot. To learn how to selfhost a private bot, click [here](https://nadekobot.readthedocs.io/en/latest/)."));
#else
return Task.FromResult(PreconditionResult.FromSuccess());
#endif
}
}

View File

@@ -1,21 +0,0 @@
#nullable disable
using System.Diagnostics.CodeAnalysis;
namespace NadekoBot.Common;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
public sealed class OnlyPublicBotAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
{
#if GLOBAL_NADEKO || DEBUG
return Task.FromResult(PreconditionResult.FromSuccess());
#else
return Task.FromResult(PreconditionResult.FromError("Only available on the public bot."));
#endif
}
}

View File

@@ -1,19 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class OwnerOnlyAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
{
var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();
return Task.FromResult(creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id
? PreconditionResult.FromSuccess()
: PreconditionResult.FromError("Not owner"));
}
}

View File

@@ -1,38 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public sealed class RatelimitAttribute : PreconditionAttribute
{
public int Seconds { get; }
public RatelimitAttribute(int seconds)
{
if (seconds <= 0)
throw new ArgumentOutOfRangeException(nameof(seconds));
Seconds = seconds;
}
public override async Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
{
if (Seconds == 0)
return PreconditionResult.FromSuccess();
var cache = services.GetRequiredService<IBotCache>();
var rem = await cache.GetRatelimitAsync(
new($"precondition:{context.User.Id}:{command.Name}"),
Seconds.Seconds());
if (rem is null)
return PreconditionResult.FromSuccess();
var msgContent = $"You can use this command again in {rem.Value.TotalSeconds:F1}s.";
return PreconditionResult.FromError(msgContent);
}
}

View File

@@ -1,30 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using NadekoBot.Modules.Administration.Services;
namespace Discord;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class UserPermAttribute : RequireUserPermissionAttribute
{
public UserPermAttribute(GuildPerm permission)
: base(permission)
{
}
public UserPermAttribute(ChannelPerm permission)
: base(permission)
{
}
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
{
var permService = services.GetRequiredService<DiscordPermOverrideService>();
if (permService.TryGetOverrides(context.Guild?.Id ?? 0, command.Name.ToUpperInvariant(), out _))
return Task.FromResult(PreconditionResult.FromSuccess());
return base.CheckPermissionsAsync(context, command, services);
}
}

View File

@@ -1,17 +0,0 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Common;
public class CmdStrings
{
public string[] Usages { get; }
public string Description { get; }
[JsonConstructor]
public CmdStrings([JsonProperty("args")] string[] usages, [JsonProperty("desc")] string description)
{
Usages = usages;
Description = description;
}
}

View File

@@ -1,9 +0,0 @@
#nullable disable
namespace NadekoBot.Common;
public class CommandData
{
public string Cmd { get; set; }
public string Desc { get; set; }
public string[] Usage { get; set; }
}

View File

@@ -1,31 +0,0 @@
using YamlDotNet.Serialization;
namespace NadekoBot.Common.Attributes;
public static class CommandNameLoadHelper
{
private static readonly IDeserializer _deserializer = new Deserializer();
private static readonly Lazy<Dictionary<string, string[]>> _lazyCommandAliases
= new(() => LoadAliases());
public static Dictionary<string, string[]> LoadAliases(string aliasesFilePath = "data/aliases.yml")
{
var text = File.ReadAllText(aliasesFilePath);
return _deserializer.Deserialize<Dictionary<string, string[]>>(text);
}
public static string[] GetAliasesFor(string methodName)
=> _lazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1
? aliases.Skip(1).ToArray()
: Array.Empty<string>();
public static string GetCommandNameFor(string methodName)
{
methodName = methodName.ToLowerInvariant();
var toReturn = _lazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0
? aliases[0]
: methodName;
return toReturn;
}
}

View File

@@ -1,275 +0,0 @@
#nullable disable
using NadekoBot.Common.Yml;
namespace NadekoBot.Common;
public sealed class Creds : IBotCredentials
{
[Comment("""DO NOT CHANGE""")]
public int Version { get; set; }
[Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")]
public string Token { get; set; }
[Comment("""
List of Ids of the users who have bot owner permissions
**DO NOT ADD PEOPLE YOU DON'T TRUST**
""")]
public ICollection<ulong> OwnerIds { get; set; }
[Comment("Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted")]
public bool UsePrivilegedIntents { get; set; }
[Comment("""
The number of shards that the bot will be running on.
Leave at 1 if you don't know what you're doing.
note: If you are planning to have more than one shard, then you must change botCache to 'redis'.
Also, in that case you should be using NadekoBot.Coordinator to start the bot, and it will correctly override this value.
""")]
public int TotalShards { get; set; }
[Comment(
"""
Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
Used only for Youtube Data Api (at the moment).
""")]
public string GoogleApiKey { get; set; }
[Comment(
"""
Create a new custom search here https://programmablesearchengine.google.com/cse/create/new
Enable SafeSearch
Remove all Sites to Search
Enable Search the entire web
Copy the 'Search Engine ID' to the SearchId field
Do all steps again but enable image search for the ImageSearchId
""")]
public IGoogleApiConfig Google { get; set; }
[Comment("""Settings for voting system for discordbots. Meant for use on global Nadeko.""")]
public IVotesSettings Votes { get; set; }
[Comment("""
Patreon auto reward system settings.
go to https://www.patreon.com/portal -> my clients -> create client
""")]
public IPatreonSettings Patreon { get; set; }
[Comment("""Api key for sending stats to DiscordBotList.""")]
public string BotListToken { get; set; }
[Comment("""Official cleverbot api key.""")]
public string CleverbotApiKey { get; set; }
[Comment(@"Official GPT-3 api key.")]
public string Gpt3ApiKey { get; set; }
[Comment("""
Which cache implementation should bot use.
'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset.
'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml
""")]
public BotCacheImplemenation BotCache { get; set; }
[Comment("""
Redis connection string. Don't change if you don't know what you're doing.
Only used if botCache is set to 'redis'
""")]
public string RedisOptions { get; set; }
[Comment("""Database options. Don't change if you don't know what you're doing. Leave null for default values""")]
public IDbOptions Db { get; set; }
[Comment("""
Address and port of the coordinator endpoint. Leave empty for default.
Change only if you've changed the coordinator address or port.
""")]
public string CoordinatorUrl { get; set; }
[Comment(
"""Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)""")]
public string RapidApiKey { get; set; }
[Comment("""
https://locationiq.com api key (register and you will receive the token in the email).
Used only for .time command.
""")]
public string LocationIqApiKey { get; set; }
[Comment("""
https://timezonedb.com api key (register and you will receive the token in the email).
Used only for .time command
""")]
public string TimezoneDbApiKey { get; set; }
[Comment("""
https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use.
Used for cryptocurrency related commands.
""")]
public string CoinmarketcapApiKey { get; set; }
// [Comment(@"https://polygon.io/dashboard/api-keys api key. Free plan allows for 5 queries per minute.
// Used for stocks related commands.")]
// public string PolygonIoApiKey { get; set; }
[Comment("""Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api""")]
public string OsuApiKey { get; set; }
[Comment("""
Optional Trovo client id.
You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors.
""")]
public string TrovoClientId { get; set; }
[Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")]
public string TwitchClientId { get; set; }
[Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")]
public string TwitchClientSecret { get; set; }
[Comment("""
Command and args which will be used to restart the bot.
Only used if bot is executed directly (NOT through the coordinator)
placeholders:
{0} -> shard id
{1} -> total shards
Linux default
cmd: dotnet
args: "NadekoBot.dll -- {0}"
Windows default
cmd: NadekoBot.exe
args: "{0}"
""")]
public IRestartConfig RestartCommand { get; set; }
public Creds()
{
Version = 7;
Token = string.Empty;
UsePrivilegedIntents = true;
OwnerIds = new List<ulong>();
TotalShards = 1;
GoogleApiKey = string.Empty;
Votes = new VotesSettings(string.Empty, string.Empty, string.Empty, string.Empty);
Patreon = new PatreonSettings(string.Empty, string.Empty, string.Empty, string.Empty);
BotListToken = string.Empty;
CleverbotApiKey = string.Empty;
Gpt3ApiKey = string.Empty;
BotCache = BotCacheImplemenation.Memory;
RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=";
Db = new DbOptions()
{
Type = "sqlite",
ConnectionString = "Data Source=data/NadekoBot.db"
};
CoordinatorUrl = "http://localhost:3442";
RestartCommand = new RestartConfig();
Google = new GoogleApiConfig();
}
public class DbOptions
: IDbOptions
{
[Comment("""
Database type. "sqlite", "mysql" and "postgresql" are supported.
Default is "sqlite"
""")]
public string Type { get; set; }
[Comment("""
Database connection string.
You MUST change this if you're not using "sqlite" type.
Default is "Data Source=data/NadekoBot.db"
Example for mysql: "Server=localhost;Port=3306;Uid=root;Pwd=my_super_secret_mysql_password;Database=nadeko"
Example for postgresql: "Server=localhost;Port=5432;User Id=postgres;Password=my_super_secret_postgres_password;Database=nadeko;"
""")]
public string ConnectionString { get; set; }
}
public sealed record PatreonSettings : IPatreonSettings
{
public string ClientId { get; set; }
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public string ClientSecret { get; set; }
[Comment(
"""Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type "prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);" in the console. (ctrl + shift + i)""")]
public string CampaignId { get; set; }
public PatreonSettings(
string accessToken,
string refreshToken,
string clientSecret,
string campaignId)
{
AccessToken = accessToken;
RefreshToken = refreshToken;
ClientSecret = clientSecret;
CampaignId = campaignId;
}
public PatreonSettings()
{
}
}
public sealed record VotesSettings : IVotesSettings
{
[Comment("""
top.gg votes service url
This is the url of your instance of the NadekoBot.Votes api
Example: https://votes.my.cool.bot.com
""")]
public string TopggServiceUrl { get; set; }
[Comment("""
Authorization header value sent to the TopGG service url with each request
This should be equivalent to the TopggKey in your NadekoBot.Votes api appsettings.json file
""")]
public string TopggKey { get; set; }
[Comment("""
discords.com votes service url
This is the url of your instance of the NadekoBot.Votes api
Example: https://votes.my.cool.bot.com
""")]
public string DiscordsServiceUrl { get; set; }
[Comment("""
Authorization header value sent to the Discords service url with each request
This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsettings.json file
""")]
public string DiscordsKey { get; set; }
public VotesSettings()
{
}
public VotesSettings(
string topggServiceUrl,
string topggKey,
string discordsServiceUrl,
string discordsKey)
{
TopggServiceUrl = topggServiceUrl;
TopggKey = topggKey;
DiscordsServiceUrl = discordsServiceUrl;
DiscordsKey = discordsKey;
}
}
}
public class GoogleApiConfig : IGoogleApiConfig
{
public string SearchId { get; init; }
public string ImageSearchId { get; init; }
}

View File

@@ -1,38 +0,0 @@
#nullable disable
namespace NadekoBot.Common;
public class DownloadTracker : INService
{
private ConcurrentDictionary<ulong, DateTime> LastDownloads { get; } = new();
private readonly SemaphoreSlim _downloadUsersSemaphore = new(1, 1);
/// <summary>
/// Ensures all users on the specified guild were downloaded within the last hour.
/// </summary>
/// <param name="guild">Guild to check and potentially download users from</param>
/// <returns>Task representing download state</returns>
public async Task EnsureUsersDownloadedAsync(IGuild guild)
{
#if GLOBAL_NADEKO
return;
#endif
await _downloadUsersSemaphore.WaitAsync();
try
{
var now = DateTime.UtcNow;
// download once per hour at most
var added = LastDownloads.AddOrUpdate(guild.Id,
now,
(_, old) => now - old > TimeSpan.FromHours(1) ? now : old);
// means that this entry was just added - download the users
if (added == now)
await guild.DownloadUsersAsync();
}
finally
{
_downloadUsersSemaphore.Release();
}
}
}

View File

@@ -1,13 +0,0 @@
#nullable disable
namespace NadekoBot.Common;
public static class Helpers
{
public static void ReadErrorAndExit(int exitCode)
{
if (!Console.IsInputRedirected)
Console.ReadKey();
Environment.Exit(exitCode);
}
}

View File

@@ -1,7 +0,0 @@
#nullable disable
namespace NadekoBot.Common;
public interface INadekoCommandOptions
{
void NormalizeOptions();
}

View File

@@ -1,51 +0,0 @@
#nullable disable
using NadekoBot.Common.Yml;
using Cloneable;
namespace NadekoBot.Common;
[Cloneable]
public partial class ImageUrls : ICloneable<ImageUrls>
{
[Comment("DO NOT CHANGE")]
public int Version { get; set; } = 3;
public CoinData Coins { get; set; }
public Uri[] Currency { get; set; }
public Uri[] Dice { get; set; }
public RategirlData Rategirl { get; set; }
public XpData Xp { get; set; }
//new
public RipData Rip { get; set; }
public SlotData Slots { get; set; }
public class RipData
{
public Uri Bg { get; set; }
public Uri Overlay { get; set; }
}
public class SlotData
{
public Uri[] Emojis { get; set; }
public Uri Bg { get; set; }
}
public class CoinData
{
public Uri[] Heads { get; set; }
public Uri[] Tails { get; set; }
}
public class RategirlData
{
public Uri Matrix { get; set; }
public Uri Dot { get; set; }
}
public class XpData
{
public Uri Bg { get; set; }
}
}

View File

@@ -1,14 +0,0 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace NadekoBot.Common.JsonConverters;
public class CultureInfoConverter : JsonConverter<CultureInfo>
{
public override CultureInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> new(reader.GetString() ?? "en-US");
public override void Write(Utf8JsonWriter writer, CultureInfo value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.Name);
}

View File

@@ -1,14 +0,0 @@
using SixLabors.ImageSharp.PixelFormats;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace NadekoBot.Common.JsonConverters;
public class Rgba32Converter : JsonConverter<Rgba32>
{
public override Rgba32 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> Rgba32.ParseHex(reader.GetString());
public override void Write(Utf8JsonWriter writer, Rgba32 value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToHex());
}

View File

@@ -1,14 +0,0 @@
#nullable disable
using CommandLine;
namespace NadekoBot.Common;
public class LbOpts : INadekoCommandOptions
{
[Option('c', "clean", Default = false, HelpText = "Only show users who are on the server.")]
public bool Clean { get; set; }
public void NormalizeOptions()
{
}
}

View File

@@ -1,16 +0,0 @@
#nullable disable
using LinqToDB;
using System.Linq.Expressions;
namespace NadekoBot.Common;
public static class Linq2DbExpressions
{
[ExpressionMethod(nameof(GuildOnShardExpression))]
public static bool GuildOnShard(ulong guildId, int totalShards, int shardId)
=> throw new NotSupportedException();
private static Expression<Func<ulong, int, int, bool>> GuildOnShardExpression()
=> (guildId, totalShards, shardId)
=> guildId / 4194304 % (ulong)totalShards == (ulong)shardId;
}

View File

@@ -1,52 +0,0 @@
#nullable disable
using System.Net;
using System.Runtime.CompilerServices;
namespace NadekoBot.Common;
public class LoginErrorHandler
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Handle(Exception ex)
=> Log.Fatal(ex, "A fatal error has occurred while attempting to connect to Discord");
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Handle(HttpException ex)
{
switch (ex.HttpCode)
{
case HttpStatusCode.Unauthorized:
Log.Error("Your bot token is wrong.\n"
+ "You can find the bot token under the Bot tab in the developer page.\n"
+ "Fix your token in the credentials file and restart the bot");
break;
case HttpStatusCode.BadRequest:
Log.Error("Something has been incorrectly formatted in your credentials file.\n"
+ "Use the JSON Guide as reference to fix it and restart the bot");
Log.Error("If you are on Linux, make sure Redis is installed and running");
break;
case HttpStatusCode.RequestTimeout:
Log.Error("The request timed out. Make sure you have no external program blocking the bot "
+ "from connecting to the internet");
break;
case HttpStatusCode.ServiceUnavailable:
case HttpStatusCode.InternalServerError:
Log.Error("Discord is having internal issues. Please, try again later");
break;
case HttpStatusCode.TooManyRequests:
Log.Error("Your bot has been ratelimited by Discord. Please, try again later.\n"
+ "Global ratelimits usually last for an hour");
break;
default:
Log.Warning("An error occurred while attempting to connect to Discord");
break;
}
Log.Fatal(ex, "Fatal error occurred while loading credentials");
}
}

View File

@@ -1,46 +0,0 @@
#nullable disable
namespace NadekoBot.Common;
public class OldCreds
{
public string Token { get; set; } = string.Empty;
public ulong[] OwnerIds { get; set; } = new ulong[1];
public string LoLApiKey { get; set; } = string.Empty;
public string GoogleApiKey { get; set; } = string.Empty;
public string MashapeKey { get; set; } = string.Empty;
public string OsuApiKey { get; set; } = string.Empty;
public string SoundCloudClientId { get; set; } = string.Empty;
public string CleverbotApiKey { get; set; } = string.Empty;
public string CarbonKey { get; set; } = string.Empty;
public int TotalShards { get; set; } = 1;
public string PatreonAccessToken { get; set; } = string.Empty;
public string PatreonCampaignId { get; set; } = "334038";
public RestartConfig RestartCommand { get; set; }
public string ShardRunCommand { get; set; } = string.Empty;
public string ShardRunArguments { get; set; } = string.Empty;
public int? ShardRunPort { get; set; }
public string MiningProxyUrl { get; set; } = string.Empty;
public string MiningProxyCreds { get; set; } = string.Empty;
public string BotListToken { get; set; } = string.Empty;
public string TwitchClientId { get; set; } = string.Empty;
public string VotesToken { get; set; } = string.Empty;
public string VotesUrl { get; set; } = string.Empty;
public string RedisOptions { get; set; } = string.Empty;
public string LocationIqApiKey { get; set; } = string.Empty;
public string TimezoneDbApiKey { get; set; } = string.Empty;
public string CoinmarketcapApiKey { get; set; } = string.Empty;
public class RestartConfig
{
public string Cmd { get; set; }
public string Args { get; set; }
public RestartConfig(string cmd, string args)
{
Cmd = cmd;
Args = args;
}
}
}

View File

@@ -1,23 +0,0 @@
using CommandLine;
namespace NadekoBot.Common;
public static class OptionsParser
{
public static T ParseFrom<T>(string[]? args)
where T : INadekoCommandOptions, new()
=> ParseFrom(new T(), args).Item1;
public static (T, bool) ParseFrom<T>(T options, string[]? args)
where T : INadekoCommandOptions
{
using var p = new Parser(x =>
{
x.HelpWriter = null;
});
var res = p.ParseArguments<T>(args);
var output = res.MapResult(x => x, _ => options);
output.NormalizeOptions();
return (output, res.Tag == ParserResultType.Parsed);
}
}

View File

@@ -1,9 +0,0 @@
#nullable disable
namespace NadekoBot.Common;
public class OsuMapData
{
public string Title { get; set; }
public string Artist { get; set; }
public string Version { get; set; }
}

View File

@@ -1,58 +0,0 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Common;
public class OsuUserBests
{
[JsonProperty("beatmap_id")]
public string BeatmapId { get; set; }
[JsonProperty("score_id")]
public string ScoreId { get; set; }
[JsonProperty("score")]
public string Score { get; set; }
[JsonProperty("maxcombo")]
public string Maxcombo { get; set; }
[JsonProperty("count50")]
public double Count50 { get; set; }
[JsonProperty("count100")]
public double Count100 { get; set; }
[JsonProperty("count300")]
public double Count300 { get; set; }
[JsonProperty("countmiss")]
public int Countmiss { get; set; }
[JsonProperty("countkatu")]
public double Countkatu { get; set; }
[JsonProperty("countgeki")]
public double Countgeki { get; set; }
[JsonProperty("perfect")]
public string Perfect { get; set; }
[JsonProperty("enabled_mods")]
public int EnabledMods { get; set; }
[JsonProperty("user_id")]
public string UserId { get; set; }
[JsonProperty("date")]
public string Date { get; set; }
[JsonProperty("rank")]
public string Rank { get; set; }
[JsonProperty("pp")]
public double Pp { get; set; }
[JsonProperty("replay_available")]
public string ReplayAvailable { get; set; }
}

View File

@@ -1,8 +0,0 @@
#nullable disable
namespace NadekoBot.Common.Pokemon;
public class PokemonNameId
{
public int Id { get; set; }
public string Name { get; set; }
}

View File

@@ -1,42 +0,0 @@
#nullable disable
using System.Text.Json.Serialization;
namespace NadekoBot.Common.Pokemon;
public class SearchPokemon
{
[JsonPropertyName("num")]
public int Id { get; set; }
public string Species { get; set; }
public string[] Types { get; set; }
public GenderRatioClass GenderRatio { get; set; }
public BaseStatsClass BaseStats { get; set; }
public Dictionary<string, string> Abilities { get; set; }
public float HeightM { get; set; }
public float WeightKg { get; set; }
public string Color { get; set; }
public string[] Evos { get; set; }
public string[] EggGroups { get; set; }
public class GenderRatioClass
{
public float M { get; set; }
public float F { get; set; }
}
public class BaseStatsClass
{
public int Hp { get; set; }
public int Atk { get; set; }
public int Def { get; set; }
public int Spa { get; set; }
public int Spd { get; set; }
public int Spe { get; set; }
public override string ToString()
=> $@"💚**HP:** {Hp,-4} ⚔**ATK:** {Atk,-4} 🛡**DEF:** {Def,-4}
✨**SPA:** {Spa,-4} 🎇**SPD:** {Spd,-4} 💨**SPE:** {Spe,-4}";
}
}

View File

@@ -1,10 +0,0 @@
#nullable disable
namespace NadekoBot.Common.Pokemon;
public class SearchPokemonAbility
{
public string Desc { get; set; }
public string ShortDesc { get; set; }
public string Name { get; set; }
public float Rating { get; set; }
}

View File

@@ -1,15 +0,0 @@
#nullable disable
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace NadekoBot.Common;
public class RequireObjectPropertiesContractResolver : DefaultContractResolver
{
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
var contract = base.CreateObjectContract(objectType);
contract.ItemRequired = Required.DisallowNull;
return contract;
}
}

View File

@@ -34,11 +34,11 @@ public static class ServiceCollectionExtensions
return kernel;
}
public static IKernel AddConfigServices(this IKernel kernel)
public static IKernel AddConfigServices(this IKernel kernel, Assembly a)
{
kernel.Bind(x =>
{
var configs = x.FromThisAssembly()
var configs = x.From(a)
.SelectAllClasses()
.Where(f => f.IsAssignableToGenericType(typeof(ConfigServiceBase<>)));
@@ -49,8 +49,8 @@ public static class ServiceCollectionExtensions
return kernel;
}
public static IKernel AddConfigMigrators(this IKernel kernel)
=> kernel.AddSealedSubclassesOf(typeof(IConfigMigrator));
public static IKernel AddConfigMigrators(this IKernel kernel, Assembly a)
=> kernel.AddSealedSubclassesOf(typeof(IConfigMigrator), a);
public static IKernel AddMusic(this IKernel kernel)
{
@@ -69,11 +69,11 @@ public static class ServiceCollectionExtensions
return kernel;
}
public static IKernel AddSealedSubclassesOf(this IKernel kernel, Type baseType)
public static IKernel AddSealedSubclassesOf(this IKernel kernel, Type baseType, Assembly a)
{
kernel.Bind(x =>
{
var classes = x.FromThisAssembly()
var classes = x.From(a)
.SelectAllClasses()
.Where(c => c.IsPublic && c.IsNested && baseType.IsAssignableFrom(baseType));
@@ -128,11 +128,11 @@ public static class ServiceCollectionExtensions
public static IConfigureSyntax BindToSelfWithInterfaces(this IJoinExcludeIncludeBindSyntax matcher)
=> matcher.BindSelection((type, types) => types.Append(type));
public static IKernel AddLifetimeServices(this IKernel kernel)
public static IKernel AddLifetimeServices(this IKernel kernel, Assembly a)
{
kernel.Bind(scan =>
{
scan.FromThisAssembly()
scan.From(a)
.SelectAllClasses()
.Where(c => (c.IsAssignableTo(typeof(INService))
|| c.IsAssignableTo(typeof(IExecOnMessage))

View File

@@ -1,30 +0,0 @@
#nullable disable
namespace NadekoBot.Common.TypeReaders;
public sealed class CommandTypeReader : NadekoTypeReader<CommandInfo>
{
private readonly CommandService _cmds;
private readonly CommandHandler _handler;
public CommandTypeReader(CommandHandler handler, CommandService cmds)
{
_handler = handler;
_cmds = cmds;
}
public override ValueTask<TypeReaderResult<CommandInfo>> ReadAsync(ICommandContext ctx, string input)
{
input = input.ToUpperInvariant();
var prefix = _handler.GetPrefix(ctx.Guild);
if (!input.StartsWith(prefix.ToUpperInvariant(), StringComparison.InvariantCulture))
return new(TypeReaderResult.FromError<CommandInfo>(CommandError.ParseFailed, "No such command found."));
input = input[prefix.Length..];
var cmd = _cmds.Commands.FirstOrDefault(c => c.Aliases.Select(a => a.ToUpperInvariant()).Contains(input));
if (cmd is null)
return new(TypeReaderResult.FromError<CommandInfo>(CommandError.ParseFailed, "No such command found."));
return new(TypeReaderResult.FromSuccess(cmd));
}
}

View File

@@ -1,13 +0,0 @@
#nullable disable
namespace NadekoBot.Common.TypeReaders;
public sealed class EmoteTypeReader : NadekoTypeReader<Emote>
{
public override ValueTask<TypeReaderResult<Emote>> ReadAsync(ICommandContext ctx, string input)
{
if (!Emote.TryParse(input, out var emote))
return new(TypeReaderResult.FromError<Emote>(CommandError.ParseFailed, "Input is not a valid emote"));
return new(TypeReaderResult.FromSuccess(emote));
}
}

View File

@@ -1,24 +0,0 @@
#nullable disable
namespace NadekoBot.Common.TypeReaders;
public sealed class GuildTypeReader : NadekoTypeReader<IGuild>
{
private readonly DiscordSocketClient _client;
public GuildTypeReader(DiscordSocketClient client)
=> _client = client;
public override ValueTask<TypeReaderResult<IGuild>> ReadAsync(ICommandContext context, string input)
{
input = input.Trim().ToUpperInvariant();
var guilds = _client.Guilds;
IGuild guild = guilds.FirstOrDefault(g => g.Id.ToString().Trim().ToUpperInvariant() == input) //by id
?? guilds.FirstOrDefault(g => g.Name.Trim().ToUpperInvariant() == input); //by name
if (guild is not null)
return new(TypeReaderResult.FromSuccess(guild));
return new(
TypeReaderResult.FromError<IGuild>(CommandError.ParseFailed, "No guild by that name or Id found"));
}
}

View File

@@ -1,33 +0,0 @@
namespace NadekoBot.Common.TypeReaders;
public sealed class GuildUserTypeReader : NadekoTypeReader<IGuildUser>
{
public override async ValueTask<TypeReaderResult<IGuildUser>> ReadAsync(ICommandContext ctx, string input)
{
if (ctx.Guild is null)
return TypeReaderResult.FromError<IGuildUser>(CommandError.Unsuccessful, "Must be in a guild.");
input = input.Trim();
IGuildUser? user = null;
if (MentionUtils.TryParseUser(input, out var id))
user = await ctx.Guild.GetUserAsync(id, CacheMode.AllowDownload);
if (ulong.TryParse(input, out id))
user = await ctx.Guild.GetUserAsync(id, CacheMode.AllowDownload);
if (user is null)
{
var users = await ctx.Guild.GetUsersAsync(CacheMode.CacheOnly);
user = users.FirstOrDefault(x => x.Username == input)
?? users.FirstOrDefault(x =>
string.Equals(x.ToString(), input, StringComparison.InvariantCultureIgnoreCase))
?? users.FirstOrDefault(x =>
string.Equals(x.Username, input, StringComparison.InvariantCultureIgnoreCase));
}
if (user is null)
return TypeReaderResult.FromError<IGuildUser>(CommandError.ObjectNotFound, "User not found.");
return TypeReaderResult.FromSuccess(user);
}
}

View File

@@ -1,21 +0,0 @@
#nullable disable
using Nadeko.Common;
namespace NadekoBot.Common.TypeReaders;
public sealed class KwumTypeReader : NadekoTypeReader<kwum>
{
public override ValueTask<TypeReaderResult<kwum>> ReadAsync(ICommandContext context, string input)
{
if (kwum.TryParse(input, out var val))
return new(TypeReaderResult.FromSuccess(val));
return new(TypeReaderResult.FromError<kwum>(CommandError.ParseFailed, "Input is not a valid kwum"));
}
}
public sealed class SmartTextTypeReader : NadekoTypeReader<SmartText>
{
public override ValueTask<TypeReaderResult<SmartText>> ReadAsync(ICommandContext ctx, string input)
=> new(TypeReaderResult.FromSuccess(SmartText.CreateFrom(input)));
}

View File

@@ -1,27 +0,0 @@
#nullable disable
namespace NadekoBot.Common.TypeReaders.Models;
public class PermissionAction
{
public static PermissionAction Enable
=> new(true);
public static PermissionAction Disable
=> new(false);
public bool Value { get; }
public PermissionAction(bool value)
=> Value = value;
public override bool Equals(object obj)
{
if (obj is null || GetType() != obj.GetType())
return false;
return Value == ((PermissionAction)obj).Value;
}
public override int GetHashCode()
=> Value.GetHashCode();
}

View File

@@ -1,55 +0,0 @@
#nullable disable
using System.Text.RegularExpressions;
namespace NadekoBot.Common.TypeReaders.Models;
public class StoopidTime
{
private static readonly Regex _regex = new(
@"^(?:(?<months>\d)mo)?(?:(?<weeks>\d{1,2})w)?(?:(?<days>\d{1,2})d)?(?:(?<hours>\d{1,4})h)?(?:(?<minutes>\d{1,5})m)?(?:(?<seconds>\d{1,6})s)?$",
RegexOptions.Compiled | RegexOptions.Multiline);
public string Input { get; set; }
public TimeSpan Time { get; set; }
private StoopidTime() { }
public static StoopidTime FromInput(string input)
{
var m = _regex.Match(input);
if (m.Length == 0)
throw new ArgumentException("Invalid string input format.");
var namesAndValues = new Dictionary<string, int>();
foreach (var groupName in _regex.GetGroupNames())
{
if (groupName == "0")
continue;
if (!int.TryParse(m.Groups[groupName].Value, out var value))
{
namesAndValues[groupName] = 0;
continue;
}
if (value < 1)
throw new ArgumentException($"Invalid {groupName} value.");
namesAndValues[groupName] = value;
}
var ts = new TimeSpan((30 * namesAndValues["months"]) + (7 * namesAndValues["weeks"]) + namesAndValues["days"],
namesAndValues["hours"],
namesAndValues["minutes"],
namesAndValues["seconds"]);
if (ts > TimeSpan.FromDays(90))
throw new ArgumentException("Time is too long.");
return new()
{
Input = input,
Time = ts
};
}
}

View File

@@ -1,50 +0,0 @@
#nullable disable
namespace NadekoBot.Common.TypeReaders;
public sealed class ModuleTypeReader : NadekoTypeReader<ModuleInfo>
{
private readonly CommandService _cmds;
public ModuleTypeReader(CommandService cmds)
=> _cmds = cmds;
public override ValueTask<TypeReaderResult<ModuleInfo>> ReadAsync(ICommandContext context, string input)
{
input = input.ToUpperInvariant();
var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule())
.FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)
?.Key;
if (module is null)
return new(TypeReaderResult.FromError<ModuleInfo>(CommandError.ParseFailed, "No such module found."));
return new(TypeReaderResult.FromSuccess(module));
}
}
public sealed class ModuleOrCrTypeReader : NadekoTypeReader<ModuleOrCrInfo>
{
private readonly CommandService _cmds;
public ModuleOrCrTypeReader(CommandService cmds)
=> _cmds = cmds;
public override ValueTask<TypeReaderResult<ModuleOrCrInfo>> ReadAsync(ICommandContext context, string input)
{
input = input.ToUpperInvariant();
var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule())
.FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)
?.Key;
if (module is null && input != "ACTUALEXPRESSIONS")
return new(TypeReaderResult.FromError<ModuleOrCrInfo>(CommandError.ParseFailed, "No such module found."));
return new(TypeReaderResult.FromSuccess(new ModuleOrCrInfo
{
Name = input
}));
}
}
public sealed class ModuleOrCrInfo
{
public string Name { get; set; }
}

View File

@@ -1,15 +0,0 @@
#nullable disable
namespace NadekoBot.Common.TypeReaders;
[MeansImplicitUse(ImplicitUseTargetFlags.Default | ImplicitUseTargetFlags.WithInheritors)]
public abstract class NadekoTypeReader<T> : TypeReader
{
public abstract ValueTask<TypeReaderResult<T>> ReadAsync(ICommandContext ctx, string input);
public override async Task<Discord.Commands.TypeReaderResult> ReadAsync(
ICommandContext ctx,
string input,
IServiceProvider services)
=> await ReadAsync(ctx, input);
}

View File

@@ -1,39 +0,0 @@
#nullable disable
using NadekoBot.Common.TypeReaders.Models;
namespace NadekoBot.Common.TypeReaders;
/// <summary>
/// Used instead of bool for more flexible keywords for true/false only in the permission module
/// </summary>
public sealed class PermissionActionTypeReader : NadekoTypeReader<PermissionAction>
{
public override ValueTask<TypeReaderResult<PermissionAction>> ReadAsync(ICommandContext context, string input)
{
input = input.ToUpperInvariant();
switch (input)
{
case "1":
case "T":
case "TRUE":
case "ENABLE":
case "ENABLED":
case "ALLOW":
case "PERMIT":
case "UNBAN":
return new(TypeReaderResult.FromSuccess(PermissionAction.Enable));
case "0":
case "F":
case "FALSE":
case "DENY":
case "DISABLE":
case "DISABLED":
case "DISALLOW":
case "BAN":
return new(TypeReaderResult.FromSuccess(PermissionAction.Disable));
default:
return new(TypeReaderResult.FromError<PermissionAction>(CommandError.ParseFailed,
"Did not receive a valid boolean value"));
}
}
}

View File

@@ -1,20 +0,0 @@
using Color = SixLabors.ImageSharp.Color;
#nullable disable
namespace NadekoBot.Common.TypeReaders;
public sealed class Rgba32TypeReader : NadekoTypeReader<Color>
{
public override ValueTask<TypeReaderResult<Color>> ReadAsync(ICommandContext context, string input)
{
input = input.Replace("#", "", StringComparison.InvariantCulture);
try
{
return ValueTask.FromResult(TypeReaderResult.FromSuccess(Color.ParseHex(input)));
}
catch
{
return ValueTask.FromResult(TypeReaderResult.FromError<Color>(CommandError.ParseFailed, "Parameter is not a valid color hex."));
}
}
}

View File

@@ -1,94 +0,0 @@
using System.Text.RegularExpressions;
using NadekoBot.Db;
using NadekoBot.Modules.Gambling.Services;
using NCalc;
using OneOf;
namespace NadekoBot.Common.TypeReaders;
public class BaseShmartInputAmountReader
{
private static readonly Regex _percentRegex = new(@"^((?<num>100|\d{1,2})%)$", RegexOptions.Compiled);
protected readonly DbService _db;
protected readonly GamblingConfigService _gambling;
public BaseShmartInputAmountReader(DbService db, GamblingConfigService gambling)
{
_db = db;
_gambling = gambling;
}
public async ValueTask<OneOf<long, OneOf.Types.Error<string>>> ReadAsync(ICommandContext context, string input)
{
var i = input.Trim().ToUpperInvariant();
i = i.Replace("K", "000");
//can't add m because it will conflict with max atm
if (await TryHandlePercentage(context, i) is long num)
{
return num;
}
try
{
var expr = new Expression(i, EvaluateOptions.IgnoreCase);
expr.EvaluateParameter += (str, ev) => EvaluateParam(str, ev, context).GetAwaiter().GetResult();
return (long)decimal.Parse(expr.Evaluate().ToString()!);
}
catch (Exception)
{
return new OneOf.Types.Error<string>($"Invalid input: {input}");
}
}
private async Task EvaluateParam(string name, ParameterArgs args, ICommandContext ctx)
{
switch (name.ToUpperInvariant())
{
case "PI":
args.Result = Math.PI;
break;
case "E":
args.Result = Math.E;
break;
case "ALL":
case "ALLIN":
args.Result = await Cur(ctx);
break;
case "HALF":
args.Result = await Cur(ctx) / 2;
break;
case "MAX":
args.Result = await Max(ctx);
break;
}
}
protected virtual async Task<long> Cur(ICommandContext ctx)
{
await using var uow = _db.GetDbContext();
return await uow.DiscordUser.GetUserCurrencyAsync(ctx.User.Id);
}
protected virtual async Task<long> Max(ICommandContext ctx)
{
var settings = _gambling.Data;
var max = settings.MaxBet;
return max == 0 ? await Cur(ctx) : max;
}
private async Task<long?> TryHandlePercentage(ICommandContext ctx, string input)
{
var m = _percentRegex.Match(input);
if (m.Captures.Count == 0)
return null;
if (!long.TryParse(m.Groups["num"].ToString(), out var percent))
return null;
return (long)(await Cur(ctx) * (percent / 100.0f));
}
}

View File

@@ -1,21 +0,0 @@
using NadekoBot.Modules.Gambling.Bank;
using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.Common.TypeReaders;
public sealed class ShmartBankInputAmountReader : BaseShmartInputAmountReader
{
private readonly IBankService _bank;
public ShmartBankInputAmountReader(IBankService bank, DbService db, GamblingConfigService gambling)
: base(db, gambling)
{
_bank = bank;
}
protected override Task<long> Cur(ICommandContext ctx)
=> _bank.GetBalanceAsync(ctx.User.Id);
protected override Task<long> Max(ICommandContext ctx)
=> Cur(ctx);
}

View File

@@ -1,57 +0,0 @@
#nullable disable
using NadekoBot.Modules.Gambling.Bank;
using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.Common.TypeReaders;
public sealed class BalanceTypeReader : TypeReader
{
private readonly BaseShmartInputAmountReader _tr;
public BalanceTypeReader(DbService db, GamblingConfigService gambling)
{
_tr = new BaseShmartInputAmountReader(db, gambling);
}
public override async Task<Discord.Commands.TypeReaderResult> ReadAsync(
ICommandContext context,
string input,
IServiceProvider services)
{
var result = await _tr.ReadAsync(context, input);
if (result.TryPickT0(out var val, out var err))
{
return Discord.Commands.TypeReaderResult.FromSuccess(val);
}
return Discord.Commands.TypeReaderResult.FromError(CommandError.Unsuccessful, err.Value);
}
}
public sealed class BankBalanceTypeReader : TypeReader
{
private readonly ShmartBankInputAmountReader _tr;
public BankBalanceTypeReader(IBankService bank, DbService db, GamblingConfigService gambling)
{
_tr = new ShmartBankInputAmountReader(bank, db, gambling);
}
public override async Task<Discord.Commands.TypeReaderResult> ReadAsync(
ICommandContext context,
string input,
IServiceProvider services)
{
var result = await _tr.ReadAsync(context, input);
if (result.TryPickT0(out var val, out var err))
{
return Discord.Commands.TypeReaderResult.FromSuccess(val);
}
return Discord.Commands.TypeReaderResult.FromError(CommandError.Unsuccessful, err.Value);
}
}

View File

@@ -1,22 +0,0 @@
#nullable disable
using NadekoBot.Common.TypeReaders.Models;
namespace NadekoBot.Common.TypeReaders;
public sealed class StoopidTimeTypeReader : NadekoTypeReader<StoopidTime>
{
public override ValueTask<TypeReaderResult<StoopidTime>> ReadAsync(ICommandContext context, string input)
{
if (string.IsNullOrWhiteSpace(input))
return new(TypeReaderResult.FromError<StoopidTime>(CommandError.Unsuccessful, "Input is empty."));
try
{
var time = StoopidTime.FromInput(input);
return new(TypeReaderResult.FromSuccess(time));
}
catch (Exception ex)
{
return new(TypeReaderResult.FromError<StoopidTime>(CommandError.Exception, ex.Message));
}
}
}

View File

@@ -1,30 +0,0 @@
namespace NadekoBot.Common.TypeReaders;
public readonly struct TypeReaderResult<T>
{
public bool IsSuccess
=> _result.IsSuccess;
public IReadOnlyCollection<TypeReaderValue> Values
=> _result.Values;
private readonly Discord.Commands.TypeReaderResult _result;
private TypeReaderResult(in Discord.Commands.TypeReaderResult result)
=> _result = result;
public static implicit operator TypeReaderResult<T>(in Discord.Commands.TypeReaderResult result)
=> new(result);
public static implicit operator Discord.Commands.TypeReaderResult(in TypeReaderResult<T> wrapper)
=> wrapper._result;
}
public static class TypeReaderResult
{
public static TypeReaderResult<T> FromError<T>(CommandError error, string reason)
=> Discord.Commands.TypeReaderResult.FromError(error, reason);
public static TypeReaderResult<T> FromSuccess<T>(in T value)
=> Discord.Commands.TypeReaderResult.FromSuccess(value);
}

View File

@@ -1,5 +1,9 @@
#nullable enable
using Nadeko.Snake;
using NadekoBot.Common;
using NadekoBot.Services;
[DIIgnore]
public sealed class BehaviorAdapter : ICustomBehavior
{

View File

@@ -1,4 +1,6 @@
internal class ContextAdapterFactory
using Nadeko.Snake;
internal class ContextAdapterFactory
{
public static AnyContext CreateNew(ICommandContext context, IMedusaStrings strings, IServiceProvider services)
=> context.Guild is null

View File

@@ -1,4 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using Nadeko.Snake;
using NadekoBot;
using NadekoBot.Common;
using NadekoBot.Services;
public sealed class DmContextAdapter : DmContext
{

View File

@@ -6,7 +6,7 @@ using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Administration.Services;
public class DiscordPermOverrideService : INService, IExecPreCommand
public class DiscordPermOverrideService : INService, IExecPreCommand, IDiscordPermOverrideService
{
public int Priority { get; } = int.MaxValue;
private readonly DbService _db;

View File

@@ -1,155 +0,0 @@
#nullable disable
using Nadeko.Common;
using NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
using NadekoBot.Modules.Games.Common;
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing;
public sealed class AnimalRace : IDisposable
{
public enum Phase
{
WaitingForPlayers,
Running,
Ended
}
public event Func<AnimalRace, Task> OnStarted = delegate { return Task.CompletedTask; };
public event Func<AnimalRace, Task> OnStartingFailed = delegate { return Task.CompletedTask; };
public event Func<AnimalRace, Task> OnStateUpdate = delegate { return Task.CompletedTask; };
public event Func<AnimalRace, Task> OnEnded = delegate { return Task.CompletedTask; };
public Phase CurrentPhase { get; private set; } = Phase.WaitingForPlayers;
public IReadOnlyCollection<AnimalRacingUser> Users
=> _users.ToList();
public List<AnimalRacingUser> FinishedUsers { get; } = new();
public int MaxUsers { get; }
private readonly SemaphoreSlim _locker = new(1, 1);
private readonly HashSet<AnimalRacingUser> _users = new();
private readonly ICurrencyService _currency;
private readonly RaceOptions _options;
private readonly Queue<RaceAnimal> _animalsQueue;
public AnimalRace(RaceOptions options, ICurrencyService currency, IEnumerable<RaceAnimal> availableAnimals)
{
_currency = currency;
_options = options;
_animalsQueue = new(availableAnimals);
MaxUsers = _animalsQueue.Count;
if (_animalsQueue.Count == 0)
CurrentPhase = Phase.Ended;
}
public void Initialize() //lame name
=> _ = Task.Run(async () =>
{
await Task.Delay(_options.StartTime * 1000);
await _locker.WaitAsync();
try
{
if (CurrentPhase != Phase.WaitingForPlayers)
return;
await Start();
}
finally { _locker.Release(); }
});
public async Task<AnimalRacingUser> JoinRace(ulong userId, string userName, long bet = 0)
{
if (bet < 0)
throw new ArgumentOutOfRangeException(nameof(bet));
var user = new AnimalRacingUser(userName, userId, bet);
await _locker.WaitAsync();
try
{
if (_users.Count == MaxUsers)
throw new AnimalRaceFullException();
if (CurrentPhase != Phase.WaitingForPlayers)
throw new AlreadyStartedException();
if (!await _currency.RemoveAsync(userId, bet, new("animalrace", "bet")))
throw new NotEnoughFundsException();
if (_users.Contains(user))
throw new AlreadyJoinedException();
var animal = _animalsQueue.Dequeue();
user.Animal = animal;
_users.Add(user);
if (_animalsQueue.Count == 0) //start if no more spots left
await Start();
return user;
}
finally { _locker.Release(); }
}
private async Task Start()
{
CurrentPhase = Phase.Running;
if (_users.Count <= 1)
{
foreach (var user in _users)
{
if (user.Bet > 0)
await _currency.AddAsync(user.UserId, user.Bet, new("animalrace", "refund"));
}
_ = OnStartingFailed?.Invoke(this);
CurrentPhase = Phase.Ended;
return;
}
_ = OnStarted?.Invoke(this);
_ = Task.Run(async () =>
{
var rng = new NadekoRandom();
while (!_users.All(x => x.Progress >= 60))
{
foreach (var user in _users)
{
user.Progress += rng.Next(1, 11);
if (user.Progress >= 60)
user.Progress = 60;
}
var finished = _users.Where(x => x.Progress >= 60 && !FinishedUsers.Contains(x)).Shuffle();
FinishedUsers.AddRange(finished);
_ = OnStateUpdate?.Invoke(this);
await Task.Delay(2500);
}
if (FinishedUsers[0].Bet > 0)
{
await _currency.AddAsync(FinishedUsers[0].UserId,
FinishedUsers[0].Bet * (_users.Count - 1),
new("animalrace", "win"));
}
_ = OnEnded?.Invoke(this);
});
}
public void Dispose()
{
CurrentPhase = Phase.Ended;
OnStarted = null;
OnEnded = null;
OnStartingFailed = null;
OnStateUpdate = null;
_locker.Dispose();
_users.Clear();
}
}

View File

@@ -1,9 +0,0 @@
#nullable disable
using NadekoBot.Modules.Gambling.Common.AnimalRacing;
namespace NadekoBot.Modules.Gambling.Services;
public class AnimalRaceService : INService
{
public ConcurrentDictionary<ulong, AnimalRace> AnimalRaces { get; } = new();
}

View File

@@ -1,184 +0,0 @@
#nullable disable
using Nadeko.Common;
using NadekoBot.Common.TypeReaders;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Common.AnimalRacing;
using NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Modules.Games.Services;
namespace NadekoBot.Modules.Gambling;
// wth is this, needs full rewrite
public partial class Gambling
{
[Group]
public partial class AnimalRacingCommands : GamblingSubmodule<AnimalRaceService>
{
private readonly ICurrencyService _cs;
private readonly DiscordSocketClient _client;
private readonly GamesConfigService _gamesConf;
private IUserMessage raceMessage;
public AnimalRacingCommands(
ICurrencyService cs,
DiscordSocketClient client,
GamblingConfigService gamblingConf,
GamesConfigService gamesConf)
: base(gamblingConf)
{
_cs = cs;
_client = client;
_gamesConf = gamesConf;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NadekoOptions<RaceOptions>]
public Task Race(params string[] args)
{
var (options, _) = OptionsParser.ParseFrom(new RaceOptions(), args);
var ar = new AnimalRace(options, _cs, _gamesConf.Data.RaceAnimals.Shuffle());
if (!_service.AnimalRaces.TryAdd(ctx.Guild.Id, ar))
return SendErrorAsync(GetText(strs.animal_race), GetText(strs.animal_race_already_started));
ar.Initialize();
var count = 0;
Task ClientMessageReceived(SocketMessage arg)
{
_ = Task.Run(() =>
{
try
{
if (arg.Channel.Id == ctx.Channel.Id)
{
if (ar.CurrentPhase == AnimalRace.Phase.Running && ++count % 9 == 0)
raceMessage = null;
}
}
catch { }
});
return Task.CompletedTask;
}
Task ArOnEnded(AnimalRace race)
{
_client.MessageReceived -= ClientMessageReceived;
_service.AnimalRaces.TryRemove(ctx.Guild.Id, out _);
var winner = race.FinishedUsers[0];
if (race.FinishedUsers[0].Bet > 0)
{
return SendConfirmAsync(GetText(strs.animal_race),
GetText(strs.animal_race_won_money(Format.Bold(winner.Username),
winner.Animal.Icon,
(race.FinishedUsers[0].Bet * (race.Users.Count - 1)) + CurrencySign)));
}
ar.Dispose();
return SendConfirmAsync(GetText(strs.animal_race),
GetText(strs.animal_race_won(Format.Bold(winner.Username), winner.Animal.Icon)));
}
ar.OnStartingFailed += Ar_OnStartingFailed;
ar.OnStateUpdate += Ar_OnStateUpdate;
ar.OnEnded += ArOnEnded;
ar.OnStarted += Ar_OnStarted;
_client.MessageReceived += ClientMessageReceived;
return SendConfirmAsync(GetText(strs.animal_race),
GetText(strs.animal_race_starting(options.StartTime)),
footer: GetText(strs.animal_race_join_instr(prefix)));
}
private Task Ar_OnStarted(AnimalRace race)
{
if (race.Users.Count == race.MaxUsers)
return SendConfirmAsync(GetText(strs.animal_race), GetText(strs.animal_race_full));
return SendConfirmAsync(GetText(strs.animal_race),
GetText(strs.animal_race_starting_with_x(race.Users.Count)));
}
private async Task Ar_OnStateUpdate(AnimalRace race)
{
var text = $@"|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚|
{string.Join("\n", race.Users.Select(p =>
{
var index = race.FinishedUsers.IndexOf(p);
var extra = index == -1 ? "" : $"#{index + 1} {(index == 0 ? "🏆" : "")}";
return $"{(int)(p.Progress / 60f * 100),-2}%|{new string('‣', p.Progress) + p.Animal.Icon + extra}";
}))}
|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚|";
var msg = raceMessage;
if (msg is null)
raceMessage = await SendConfirmAsync(text);
else
{
await msg.ModifyAsync(x => x.Embed = _eb.Create()
.WithTitle(GetText(strs.animal_race))
.WithDescription(text)
.WithOkColor()
.Build());
}
}
private Task Ar_OnStartingFailed(AnimalRace race)
{
_service.AnimalRaces.TryRemove(ctx.Guild.Id, out _);
race.Dispose();
return ReplyErrorLocalizedAsync(strs.animal_race_failed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task JoinRace([OverrideTypeReader(typeof(BalanceTypeReader))] long amount = default)
{
if (!await CheckBetOptional(amount))
return;
if (!_service.AnimalRaces.TryGetValue(ctx.Guild.Id, out var ar))
{
await ReplyErrorLocalizedAsync(strs.race_not_exist);
return;
}
try
{
var user = await ar.JoinRace(ctx.User.Id, ctx.User.ToString(), amount);
if (amount > 0)
{
await SendConfirmAsync(GetText(strs.animal_race_join_bet(ctx.User.Mention,
user.Animal.Icon,
amount + CurrencySign)));
}
else
await SendConfirmAsync(GetText(strs.animal_race_join(ctx.User.Mention, user.Animal.Icon)));
}
catch (ArgumentOutOfRangeException)
{
//ignore if user inputed an invalid amount
}
catch (AlreadyJoinedException)
{
// just ignore this
}
catch (AlreadyStartedException)
{
//ignore
}
catch (AnimalRaceFullException)
{
await SendConfirmAsync(GetText(strs.animal_race), GetText(strs.animal_race_full));
}
catch (NotEnoughFundsException)
{
await SendErrorAsync(GetText(strs.not_enough(CurrencySign)));
}
}
}
}

View File

@@ -1,26 +0,0 @@
#nullable disable
using NadekoBot.Modules.Games.Common;
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing;
public class AnimalRacingUser
{
public long Bet { get; }
public string Username { get; }
public ulong UserId { get; }
public RaceAnimal Animal { get; set; }
public int Progress { get; set; }
public AnimalRacingUser(string username, ulong userId, long bet)
{
Bet = bet;
Username = username;
UserId = userId;
}
public override bool Equals(object obj)
=> obj is AnimalRacingUser x ? x.UserId == UserId : false;
public override int GetHashCode()
=> UserId.GetHashCode();
}

View File

@@ -1,19 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
public class AlreadyJoinedException : Exception
{
public AlreadyJoinedException()
{
}
public AlreadyJoinedException(string message)
: base(message)
{
}
public AlreadyJoinedException(string message, Exception innerException)
: base(message, innerException)
{
}
}

View File

@@ -1,19 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
public class AlreadyStartedException : Exception
{
public AlreadyStartedException()
{
}
public AlreadyStartedException(string message)
: base(message)
{
}
public AlreadyStartedException(string message, Exception innerException)
: base(message, innerException)
{
}
}

View File

@@ -1,19 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
public class AnimalRaceFullException : Exception
{
public AnimalRaceFullException()
{
}
public AnimalRaceFullException(string message)
: base(message)
{
}
public AnimalRaceFullException(string message, Exception innerException)
: base(message, innerException)
{
}
}

View File

@@ -1,19 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
public class NotEnoughFundsException : Exception
{
public NotEnoughFundsException()
{
}
public NotEnoughFundsException(string message)
: base(message)
{
}
public NotEnoughFundsException(string message, Exception innerException)
: base(message, innerException)
{
}
}

View File

@@ -1,16 +0,0 @@
#nullable disable
using CommandLine;
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing;
public class RaceOptions : INadekoCommandOptions
{
[Option('s', "start-time", Default = 20, Required = false)]
public int StartTime { get; set; } = 20;
public void NormalizeOptions()
{
if (StartTime is < 10 or > 120)
StartTime = 20;
}
}

View File

@@ -1,122 +0,0 @@
using NadekoBot.Common.TypeReaders;
using NadekoBot.Modules.Gambling.Bank;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling;
public partial class Gambling
{
[Name("Bank")]
[Group("bank")]
public partial class BankCommands : GamblingModule<IBankService>
{
private readonly IBankService _bank;
private readonly DiscordSocketClient _client;
public BankCommands(GamblingConfigService gcs,
IBankService bank,
DiscordSocketClient client) : base(gcs)
{
_bank = bank;
_client = client;
}
[Cmd]
public async Task BankDeposit([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
{
if (amount <= 0)
return;
if (await _bank.DepositAsync(ctx.User.Id, amount))
{
await ReplyConfirmLocalizedAsync(strs.bank_deposited(N(amount)));
}
else
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
}
}
[Cmd]
public async Task BankWithdraw([OverrideTypeReader(typeof(BankBalanceTypeReader))] long amount)
{
if (amount <= 0)
return;
if (await _bank.WithdrawAsync(ctx.User.Id, amount))
{
await ReplyConfirmLocalizedAsync(strs.bank_withdrew(N(amount)));
}
else
{
await ReplyErrorLocalizedAsync(strs.bank_withdraw_insuff(CurrencySign));
}
}
[Cmd]
public async Task BankBalance()
{
var bal = await _bank.GetBalanceAsync(ctx.User.Id);
var eb = _eb.Create(ctx)
.WithOkColor()
.WithDescription(GetText(strs.bank_balance(N(bal))));
try
{
await ctx.User.EmbedAsync(eb);
await ctx.OkAsync();
}
catch
{
await ReplyErrorLocalizedAsync(strs.cant_dm);
}
}
private async Task BankTakeInternalAsync(long amount, ulong userId)
{
if (await _bank.TakeAsync(userId, amount))
{
await ReplyErrorLocalizedAsync(strs.take_fail(N(amount),
_client.GetUser(userId)?.ToString()
?? userId.ToString(),
CurrencySign));
return;
}
await ctx.OkAsync();
}
private async Task BankAwardInternalAsync(long amount, ulong userId)
{
if (await _bank.AwardAsync(userId, amount))
{
await ReplyErrorLocalizedAsync(strs.take_fail(N(amount),
_client.GetUser(userId)?.ToString()
?? userId.ToString(),
CurrencySign));
return;
}
await ctx.OkAsync();
}
[Cmd]
[OwnerOnly]
[Priority(1)]
public async Task BankTake(long amount, [Leftover] IUser user)
=> await BankTakeInternalAsync(amount, user.Id);
[Cmd]
[OwnerOnly]
[Priority(0)]
public async Task BankTake(long amount, ulong userId)
=> await BankTakeInternalAsync(amount, userId);
[Cmd]
[OwnerOnly]
public async Task BankAward(long amount, [Leftover] IUser user)
=> await BankAwardInternalAsync(amount, user.Id);
}
}

View File

@@ -1,120 +0,0 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Gambling.Bank;
public sealed class BankService : IBankService, INService
{
private readonly ICurrencyService _cur;
private readonly DbService _db;
public BankService(ICurrencyService cur, DbService db)
{
_cur = cur;
_db = db;
}
public async Task<bool> AwardAsync(ulong userId, long amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
await using var ctx = _db.GetDbContext();
await ctx.BankUsers
.ToLinqToDBTable()
.InsertOrUpdateAsync(() => new()
{
UserId = userId,
Balance = amount
},
(old) => new()
{
Balance = old.Balance + amount
},
() => new()
{
UserId = userId
});
return true;
}
public async Task<bool> TakeAsync(ulong userId, long amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
await using var ctx = _db.GetDbContext();
var rows = await ctx.BankUsers
.ToLinqToDBTable()
.Where(x => x.UserId == userId && x.Balance >= amount)
.UpdateAsync((old) => new()
{
Balance = old.Balance - amount
});
return rows > 0;
}
public async Task<bool> DepositAsync(ulong userId, long amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
if (!await _cur.RemoveAsync(userId, amount, new("bank", "deposit")))
return false;
await using var ctx = _db.GetDbContext();
await ctx.BankUsers
.ToLinqToDBTable()
.InsertOrUpdateAsync(() => new()
{
UserId = userId,
Balance = amount
},
(old) => new()
{
Balance = old.Balance + amount
},
() => new()
{
UserId = userId
});
return true;
}
public async Task<bool> WithdrawAsync(ulong userId, long amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
await using var ctx = _db.GetDbContext();
var rows = await ctx.BankUsers
.ToLinqToDBTable()
.Where(x => x.UserId == userId && x.Balance >= amount)
.UpdateAsync((old) => new()
{
Balance = old.Balance - amount
});
if (rows > 0)
{
await _cur.AddAsync(userId, amount, new("bank", "withdraw"));
return true;
}
return false;
}
public async Task<long> GetBalanceAsync(ulong userId)
{
await using var ctx = _db.GetDbContext();
return (await ctx.BankUsers
.ToLinqToDBTable()
.FirstOrDefaultAsync(x => x.UserId == userId))
?.Balance
?? 0;
}
}

View File

@@ -1,10 +0,0 @@
namespace NadekoBot.Modules.Gambling.Bank;
public interface IBankService
{
Task<bool> DepositAsync(ulong userId, long amount);
Task<bool> WithdrawAsync(ulong userId, long amount);
Task<long> GetBalanceAsync(ulong userId);
Task<bool> AwardAsync(ulong userId, long amount);
Task<bool> TakeAsync(ulong userId, long amount);
}

View File

@@ -1,184 +0,0 @@
#nullable disable
using Nadeko.Common;
using NadekoBot.Common.TypeReaders;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Common.Blackjack;
using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling;
public partial class Gambling
{
public partial class BlackJackCommands : GamblingSubmodule<BlackJackService>
{
public enum BjAction
{
Hit = int.MinValue,
Stand,
Double
}
private readonly ICurrencyService _cs;
private readonly DbService _db;
private IUserMessage msg;
public BlackJackCommands(ICurrencyService cs, DbService db, GamblingConfigService gamblingConf)
: base(gamblingConf)
{
_cs = cs;
_db = db;
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task BlackJack([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
{
if (!await CheckBetMandatory(amount))
return;
var newBj = new Blackjack(_cs);
Blackjack bj;
if (newBj == (bj = _service.Games.GetOrAdd(ctx.Channel.Id, newBj)))
{
if (!await bj.Join(ctx.User, amount))
{
_service.Games.TryRemove(ctx.Channel.Id, out _);
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
}
bj.StateUpdated += Bj_StateUpdated;
bj.GameEnded += Bj_GameEnded;
bj.Start();
await ReplyConfirmLocalizedAsync(strs.bj_created);
}
else
{
if (await bj.Join(ctx.User, amount))
await ReplyConfirmLocalizedAsync(strs.bj_joined);
else
{
Log.Information("{User} can't join a blackjack game as it's in {BlackjackState} state already",
ctx.User,
bj.State);
}
}
await ctx.Message.DeleteAsync();
}
private Task Bj_GameEnded(Blackjack arg)
{
_service.Games.TryRemove(ctx.Channel.Id, out _);
return Task.CompletedTask;
}
private async Task Bj_StateUpdated(Blackjack bj)
{
try
{
if (msg is not null)
_ = msg.DeleteAsync();
var c = bj.Dealer.Cards.Select(x => x.GetEmojiString())
.ToList();
var dealerIcon = "❔ ";
if (bj.State == Blackjack.GameState.Ended)
{
if (bj.Dealer.GetHandValue() == 21)
dealerIcon = "💰 ";
else if (bj.Dealer.GetHandValue() > 21)
dealerIcon = "💥 ";
else
dealerIcon = "🏁 ";
}
var cStr = string.Concat(c.Select(x => x[..^1] + " "));
cStr += "\n" + string.Concat(c.Select(x => x.Last() + " "));
var embed = _eb.Create()
.WithOkColor()
.WithTitle("BlackJack")
.AddField($"{dealerIcon} Dealer's Hand | Value: {bj.Dealer.GetHandValue()}", cStr);
if (bj.CurrentUser is not null)
embed.WithFooter($"Player to make a choice: {bj.CurrentUser.DiscordUser}");
foreach (var p in bj.Players)
{
c = p.Cards.Select(x => x.GetEmojiString()).ToList();
cStr = "-\t" + string.Concat(c.Select(x => x[..^1] + " "));
cStr += "\n-\t" + string.Concat(c.Select(x => x.Last() + " "));
var full = $"{p.DiscordUser.ToString().TrimTo(20)} | Bet: {N(p.Bet)} | Value: {p.GetHandValue()}";
if (bj.State == Blackjack.GameState.Ended)
{
if (p.State == User.UserState.Lost)
full = "❌ " + full;
else
full = "✅ " + full;
}
else if (p == bj.CurrentUser)
full = "▶ " + full;
else if (p.State == User.UserState.Stand)
full = "⏹ " + full;
else if (p.State == User.UserState.Bust)
full = "💥 " + full;
else if (p.State == User.UserState.Blackjack)
full = "💰 " + full;
embed.AddField(full, cStr);
}
msg = await ctx.Channel.EmbedAsync(embed);
}
catch
{
}
}
private string UserToString(User x)
{
var playerName = x.State == User.UserState.Bust
? Format.Strikethrough(x.DiscordUser.ToString().TrimTo(30))
: x.DiscordUser.ToString();
// var hand = $"{string.Concat(x.Cards.Select(y => "〖" + y.GetEmojiString() + "〗"))}";
return $"{playerName} | Bet: {x.Bet}\n";
}
[Cmd]
[RequireContext(ContextType.Guild)]
public Task Hit()
=> InternalBlackJack(BjAction.Hit);
[Cmd]
[RequireContext(ContextType.Guild)]
public Task Stand()
=> InternalBlackJack(BjAction.Stand);
[Cmd]
[RequireContext(ContextType.Guild)]
public Task Double()
=> InternalBlackJack(BjAction.Double);
private async Task InternalBlackJack(BjAction a)
{
if (!_service.Games.TryGetValue(ctx.Channel.Id, out var bj))
return;
if (a == BjAction.Hit)
await bj.Hit(ctx.User);
else if (a == BjAction.Stand)
await bj.Stand(ctx.User);
else if (a == BjAction.Double)
{
if (!await bj.Double(ctx.User))
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
}
await ctx.Message.DeleteAsync();
}
}
}

View File

@@ -1,9 +0,0 @@
#nullable disable
using NadekoBot.Modules.Gambling.Common.Blackjack;
namespace NadekoBot.Modules.Gambling.Services;
public class BlackJackService : INService
{
public ConcurrentDictionary<ulong, Blackjack> Games { get; } = new();
}

View File

@@ -1,329 +0,0 @@
#nullable disable
using Nadeko.Econ;
namespace NadekoBot.Modules.Gambling.Common.Blackjack;
public class Blackjack
{
public enum GameState
{
Starting,
Playing,
Ended
}
public event Func<Blackjack, Task> StateUpdated;
public event Func<Blackjack, Task> GameEnded;
private Deck Deck { get; } = new QuadDeck();
public Dealer Dealer { get; set; }
public List<User> Players { get; set; } = new();
public GameState State { get; set; } = GameState.Starting;
public User CurrentUser { get; private set; }
private TaskCompletionSource<bool> currentUserMove;
private readonly ICurrencyService _cs;
private readonly SemaphoreSlim _locker = new(1, 1);
public Blackjack(ICurrencyService cs)
{
_cs = cs;
Dealer = new();
}
public void Start()
=> _ = GameLoop();
public async Task GameLoop()
{
try
{
//wait for players to join
await Task.Delay(20000);
await _locker.WaitAsync();
try
{
State = GameState.Playing;
}
finally
{
_locker.Release();
}
await PrintState();
//if no users joined the game, end it
if (!Players.Any())
{
State = GameState.Ended;
_ = GameEnded?.Invoke(this);
return;
}
//give 1 card to the dealer and 2 to each player
Dealer.Cards.Add(Deck.Draw());
foreach (var usr in Players)
{
usr.Cards.Add(Deck.Draw());
usr.Cards.Add(Deck.Draw());
if (usr.GetHandValue() == 21)
usr.State = User.UserState.Blackjack;
}
//go through all users and ask them what they want to do
foreach (var usr in Players.Where(x => !x.Done))
{
while (!usr.Done)
{
Log.Information("Waiting for {DiscordUser}'s move", usr.DiscordUser);
await PromptUserMove(usr);
}
}
await PrintState();
State = GameState.Ended;
await Task.Delay(2500);
Log.Information("Dealer moves");
await DealerMoves();
await PrintState();
_ = GameEnded?.Invoke(this);
}
catch (Exception ex)
{
Log.Error(ex, "REPORT THE MESSAGE BELOW IN #NadekoLog SERVER PLEASE");
State = GameState.Ended;
_ = GameEnded?.Invoke(this);
}
}
private async Task PromptUserMove(User usr)
{
using var cts = new CancellationTokenSource();
var pause = Task.Delay(20000, cts.Token); //10 seconds to decide
CurrentUser = usr;
currentUserMove = new();
await PrintState();
// either wait for the user to make an action and
// if he doesn't - stand
var finished = await Task.WhenAny(pause, currentUserMove.Task);
if (finished == pause)
await Stand(usr);
else
cts.Cancel();
CurrentUser = null;
currentUserMove = null;
}
public async Task<bool> Join(IUser user, long bet)
{
await _locker.WaitAsync();
try
{
if (State != GameState.Starting)
return false;
if (Players.Count >= 5)
return false;
if (!await _cs.RemoveAsync(user, bet, new("blackjack", "gamble")))
return false;
Players.Add(new(user, bet));
_ = PrintState();
return true;
}
finally
{
_locker.Release();
}
}
public async Task<bool> Stand(IUser u)
{
var cu = CurrentUser;
if (cu is not null && cu.DiscordUser == u)
return await Stand(cu);
return false;
}
public async Task<bool> Stand(User u)
{
await _locker.WaitAsync();
try
{
if (State != GameState.Playing)
return false;
if (CurrentUser != u)
return false;
u.State = User.UserState.Stand;
currentUserMove.TrySetResult(true);
return true;
}
finally
{
_locker.Release();
}
}
private async Task DealerMoves()
{
var hw = Dealer.GetHandValue();
while (hw < 17
|| (hw == 17
&& Dealer.Cards.Count(x => x.Number == 1) > (Dealer.GetRawHandValue() - 17) / 10)) // hit on soft 17
{
/* Dealer has
A 6
That's 17, soft
hw == 17 => true
number of aces = 1
1 > 17-17 /10 => true
AA 5
That's 17, again soft, since one ace is worth 11, even though another one is 1
hw == 17 => true
number of aces = 2
2 > 27 - 17 / 10 => true
AA Q 5
That's 17, but not soft, since both aces are worth 1
hw == 17 => true
number of aces = 2
2 > 37 - 17 / 10 => false
* */
Dealer.Cards.Add(Deck.Draw());
hw = Dealer.GetHandValue();
}
if (hw > 21)
{
foreach (var usr in Players)
{
if (usr.State is User.UserState.Stand or User.UserState.Blackjack)
usr.State = User.UserState.Won;
else
usr.State = User.UserState.Lost;
}
}
else
{
foreach (var usr in Players)
{
if (usr.State == User.UserState.Blackjack)
usr.State = User.UserState.Won;
else if (usr.State == User.UserState.Stand)
usr.State = hw < usr.GetHandValue() ? User.UserState.Won : User.UserState.Lost;
else
usr.State = User.UserState.Lost;
}
}
foreach (var usr in Players)
{
if (usr.State is User.UserState.Won or User.UserState.Blackjack)
await _cs.AddAsync(usr.DiscordUser.Id, usr.Bet * 2, new("blackjack", "win"));
}
}
public async Task<bool> Double(IUser u)
{
var cu = CurrentUser;
if (cu is not null && cu.DiscordUser == u)
return await Double(cu);
return false;
}
public async Task<bool> Double(User u)
{
await _locker.WaitAsync();
try
{
if (State != GameState.Playing)
return false;
if (CurrentUser != u)
return false;
if (!await _cs.RemoveAsync(u.DiscordUser.Id, u.Bet, new("blackjack", "double")))
return false;
u.Bet *= 2;
u.Cards.Add(Deck.Draw());
if (u.GetHandValue() == 21)
//blackjack
u.State = User.UserState.Blackjack;
else if (u.GetHandValue() > 21)
// user busted
u.State = User.UserState.Bust;
else
//with double you just get one card, and then you're done
u.State = User.UserState.Stand;
currentUserMove.TrySetResult(true);
return true;
}
finally
{
_locker.Release();
}
}
public async Task<bool> Hit(IUser u)
{
var cu = CurrentUser;
if (cu is not null && cu.DiscordUser == u)
return await Hit(cu);
return false;
}
public async Task<bool> Hit(User u)
{
await _locker.WaitAsync();
try
{
if (State != GameState.Playing)
return false;
if (CurrentUser != u)
return false;
u.Cards.Add(Deck.Draw());
if (u.GetHandValue() == 21)
//blackjack
u.State = User.UserState.Blackjack;
else if (u.GetHandValue() > 21)
// user busted
u.State = User.UserState.Bust;
currentUserMove.TrySetResult(true);
return true;
}
finally
{
_locker.Release();
}
}
public Task PrintState()
{
if (StateUpdated is null)
return Task.CompletedTask;
return StateUpdated.Invoke(this);
}
}

View File

@@ -1,58 +0,0 @@
#nullable disable
using Nadeko.Econ;
namespace NadekoBot.Modules.Gambling.Common.Blackjack;
public abstract class Player
{
public List<Deck.Card> Cards { get; } = new();
public int GetHandValue()
{
var val = GetRawHandValue();
// while the hand value is greater than 21, for each ace you have in the deck
// reduce the value by 10 until it drops below 22
// (emulating the fact that ace is either a 1 or a 11)
var i = Cards.Count(x => x.Number == 1);
while (val > 21 && i-- > 0)
val -= 10;
return val;
}
public int GetRawHandValue()
=> Cards.Sum(x => x.Number == 1 ? 11 : x.Number >= 10 ? 10 : x.Number);
}
public class Dealer : Player
{
}
public class User : Player
{
public enum UserState
{
Waiting,
Stand,
Bust,
Blackjack,
Won,
Lost
}
public UserState State { get; set; } = UserState.Waiting;
public long Bet { get; set; }
public IUser DiscordUser { get; }
public bool Done
=> State != UserState.Waiting;
public User(IUser user, long bet)
{
if (bet <= 0)
throw new ArgumentOutOfRangeException(nameof(bet));
Bet = bet;
DiscordUser = user;
}
}

View File

@@ -1,409 +0,0 @@
#nullable disable
using CommandLine;
using System.Collections.Immutable;
namespace NadekoBot.Modules.Gambling.Common.Connect4;
public sealed class Connect4Game : IDisposable
{
public enum Field //temporary most likely
{
Empty,
P1,
P2
}
public enum Phase
{
Joining, // waiting for second player to join
P1Move,
P2Move,
Ended
}
public enum Result
{
Draw,
CurrentPlayerWon,
OtherPlayerWon
}
public const int NUMBER_OF_COLUMNS = 7;
public const int NUMBER_OF_ROWS = 6;
//public event Func<Connect4Game, Task> OnGameStarted;
public event Func<Connect4Game, Task> OnGameStateUpdated;
public event Func<Connect4Game, Task> OnGameFailedToStart;
public event Func<Connect4Game, Result, Task> OnGameEnded;
public Phase CurrentPhase { get; private set; } = Phase.Joining;
public ImmutableArray<Field> GameState
=> _gameState.ToImmutableArray();
public ImmutableArray<(ulong UserId, string Username)?> Players
=> _players.ToImmutableArray();
public (ulong UserId, string Username) CurrentPlayer
=> CurrentPhase == Phase.P1Move ? _players[0].Value : _players[1].Value;
public (ulong UserId, string Username) OtherPlayer
=> CurrentPhase == Phase.P2Move ? _players[0].Value : _players[1].Value;
//state is bottom to top, left to right
private readonly Field[] _gameState = new Field[NUMBER_OF_ROWS * NUMBER_OF_COLUMNS];
private readonly (ulong UserId, string Username)?[] _players = new (ulong, string)?[2];
private readonly SemaphoreSlim _locker = new(1, 1);
private readonly Options _options;
private readonly ICurrencyService _cs;
private readonly NadekoRandom _rng;
private Timer playerTimeoutTimer;
/* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
*/
public Connect4Game(
ulong userId,
string userName,
Options options,
ICurrencyService cs)
{
_players[0] = (userId, userName);
_options = options;
_cs = cs;
_rng = new();
for (var i = 0; i < NUMBER_OF_COLUMNS * NUMBER_OF_ROWS; i++)
_gameState[i] = Field.Empty;
}
public void Initialize()
{
if (CurrentPhase != Phase.Joining)
return;
_ = Task.Run(async () =>
{
await Task.Delay(15000);
await _locker.WaitAsync();
try
{
if (_players[1] is null)
{
_ = OnGameFailedToStart?.Invoke(this);
CurrentPhase = Phase.Ended;
await _cs.AddAsync(_players[0].Value.UserId, _options.Bet, new("connect4", "refund"));
}
}
finally { _locker.Release(); }
});
}
public async Task<bool> Join(ulong userId, string userName, int bet)
{
await _locker.WaitAsync();
try
{
if (CurrentPhase != Phase.Joining) //can't join if its not a joining phase
return false;
if (_players[0].Value.UserId == userId) // same user can't join own game
return false;
if (bet != _options.Bet) // can't join if bet amount is not the same
return false;
if (!await _cs.RemoveAsync(userId, bet, new("connect4", "bet"))) // user doesn't have enough money to gamble
return false;
if (_rng.Next(0, 2) == 0) //rolling from 0-1, if number is 0, join as first player
{
_players[1] = _players[0];
_players[0] = (userId, userName);
}
else //else join as a second player
_players[1] = (userId, userName);
CurrentPhase = Phase.P1Move; //start the game
playerTimeoutTimer = new(async _ =>
{
await _locker.WaitAsync();
try
{
EndGame(Result.OtherPlayerWon, OtherPlayer.UserId);
}
finally { _locker.Release(); }
},
null,
TimeSpan.FromSeconds(_options.TurnTimer),
TimeSpan.FromSeconds(_options.TurnTimer));
_ = OnGameStateUpdated?.Invoke(this);
return true;
}
finally { _locker.Release(); }
}
public async Task<bool> Input(ulong userId, int inputCol)
{
await _locker.WaitAsync();
try
{
inputCol -= 1;
if (CurrentPhase is Phase.Ended or Phase.Joining)
return false;
if (!((_players[0].Value.UserId == userId && CurrentPhase == Phase.P1Move)
|| (_players[1].Value.UserId == userId && CurrentPhase == Phase.P2Move)))
return false;
if (inputCol is < 0 or > NUMBER_OF_COLUMNS) //invalid input
return false;
if (IsColumnFull(inputCol)) //can't play there event?
return false;
var start = NUMBER_OF_ROWS * inputCol;
for (var i = start; i < start + NUMBER_OF_ROWS; i++)
{
if (_gameState[i] == Field.Empty)
{
_gameState[i] = GetPlayerPiece(userId);
break;
}
}
//check winnning condition
// ok, i'll go from [0-2] in rows (and through all columns) and check upward if 4 are connected
for (var i = 0; i < NUMBER_OF_ROWS - 3; i++)
{
if (CurrentPhase == Phase.Ended)
break;
for (var j = 0; j < NUMBER_OF_COLUMNS; j++)
{
if (CurrentPhase == Phase.Ended)
break;
var first = _gameState[i + (j * NUMBER_OF_ROWS)];
if (first != Field.Empty)
{
for (var k = 1; k < 4; k++)
{
var next = _gameState[i + k + (j * NUMBER_OF_ROWS)];
if (next == first)
{
if (k == 3)
EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId);
else
continue;
}
else
break;
}
}
}
}
// i'll go [0-1] in columns (and through all rows) and check to the right if 4 are connected
for (var i = 0; i < NUMBER_OF_COLUMNS - 3; i++)
{
if (CurrentPhase == Phase.Ended)
break;
for (var j = 0; j < NUMBER_OF_ROWS; j++)
{
if (CurrentPhase == Phase.Ended)
break;
var first = _gameState[j + (i * NUMBER_OF_ROWS)];
if (first != Field.Empty)
{
for (var k = 1; k < 4; k++)
{
var next = _gameState[j + ((i + k) * NUMBER_OF_ROWS)];
if (next == first)
{
if (k == 3)
EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId);
else
continue;
}
else
break;
}
}
}
}
//need to check diagonal now
for (var col = 0; col < NUMBER_OF_COLUMNS; col++)
{
if (CurrentPhase == Phase.Ended)
break;
for (var row = 0; row < NUMBER_OF_ROWS; row++)
{
if (CurrentPhase == Phase.Ended)
break;
var first = _gameState[row + (col * NUMBER_OF_ROWS)];
if (first != Field.Empty)
{
var same = 1;
//top left
for (var i = 1; i < 4; i++)
{
//while going top left, rows are increasing, columns are decreasing
var curRow = row + i;
var curCol = col - i;
//check if current values are in range
if (curRow is >= NUMBER_OF_ROWS or < 0)
break;
if (curCol is < 0 or >= NUMBER_OF_COLUMNS)
break;
var cur = _gameState[curRow + (curCol * NUMBER_OF_ROWS)];
if (cur == first)
same++;
else
break;
}
if (same == 4)
{
EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId);
break;
}
same = 1;
//top right
for (var i = 1; i < 4; i++)
{
//while going top right, rows are increasing, columns are increasing
var curRow = row + i;
var curCol = col + i;
//check if current values are in range
if (curRow is >= NUMBER_OF_ROWS or < 0)
break;
if (curCol is < 0 or >= NUMBER_OF_COLUMNS)
break;
var cur = _gameState[curRow + (curCol * NUMBER_OF_ROWS)];
if (cur == first)
same++;
else
break;
}
if (same == 4)
{
EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId);
break;
}
}
}
}
//check draw? if it's even possible
if (_gameState.All(x => x != Field.Empty))
EndGame(Result.Draw, null);
if (CurrentPhase != Phase.Ended)
{
if (CurrentPhase == Phase.P1Move)
CurrentPhase = Phase.P2Move;
else
CurrentPhase = Phase.P1Move;
ResetTimer();
}
_ = OnGameStateUpdated?.Invoke(this);
return true;
}
finally { _locker.Release(); }
}
private void ResetTimer()
=> playerTimeoutTimer.Change(TimeSpan.FromSeconds(_options.TurnTimer),
TimeSpan.FromSeconds(_options.TurnTimer));
private void EndGame(Result result, ulong? winId)
{
if (CurrentPhase == Phase.Ended)
return;
_ = OnGameEnded?.Invoke(this, result);
CurrentPhase = Phase.Ended;
if (result == Result.Draw)
{
_cs.AddAsync(CurrentPlayer.UserId, _options.Bet, new("connect4", "draw"));
_cs.AddAsync(OtherPlayer.UserId, _options.Bet, new("connect4", "draw"));
return;
}
if (winId is not null)
_cs.AddAsync(winId.Value, (long)(_options.Bet * 1.98), new("connect4", "win"));
}
private Field GetPlayerPiece(ulong userId)
=> _players[0].Value.UserId == userId ? Field.P1 : Field.P2;
//column is full if there are no empty fields
private bool IsColumnFull(int column)
{
var start = NUMBER_OF_ROWS * column;
for (var i = start; i < start + NUMBER_OF_ROWS; i++)
{
if (_gameState[i] == Field.Empty)
return false;
}
return true;
}
public void Dispose()
{
OnGameFailedToStart = null;
OnGameStateUpdated = null;
OnGameEnded = null;
playerTimeoutTimer?.Change(Timeout.Infinite, Timeout.Infinite);
}
public class Options : INadekoCommandOptions
{
[Option('t',
"turn-timer",
Required = false,
Default = 15,
HelpText = "Turn time in seconds. It has to be between 5 and 60. Default 15.")]
public int TurnTimer { get; set; } = 15;
[Option('b', "bet", Required = false, Default = 0, HelpText = "Amount you bet. Default 0.")]
public int Bet { get; set; }
public void NormalizeOptions()
{
if (TurnTimer is < 5 or > 60)
TurnTimer = 15;
if (Bet < 0)
Bet = 0;
}
}
}

View File

@@ -1,204 +0,0 @@
#nullable disable
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Common.Connect4;
using NadekoBot.Modules.Gambling.Services;
using System.Text;
namespace NadekoBot.Modules.Gambling;
public partial class Gambling
{
[Group]
public partial class Connect4Commands : GamblingSubmodule<GamblingService>
{
private static readonly string[] _numbers =
{
":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:"
};
private int RepostCounter
{
get => repostCounter;
set
{
if (value is < 0 or > 7)
repostCounter = 0;
else
repostCounter = value;
}
}
private readonly DiscordSocketClient _client;
private readonly ICurrencyService _cs;
private IUserMessage msg;
private int repostCounter;
public Connect4Commands(DiscordSocketClient client, ICurrencyService cs, GamblingConfigService gamb)
: base(gamb)
{
_client = client;
_cs = cs;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NadekoOptions<Connect4Game.Options>]
public async Task Connect4(params string[] args)
{
var (options, _) = OptionsParser.ParseFrom(new Connect4Game.Options(), args);
if (!await CheckBetOptional(options.Bet))
return;
var newGame = new Connect4Game(ctx.User.Id, ctx.User.ToString(), options, _cs);
Connect4Game game;
if ((game = _service.Connect4Games.GetOrAdd(ctx.Channel.Id, newGame)) != newGame)
{
if (game.CurrentPhase != Connect4Game.Phase.Joining)
return;
newGame.Dispose();
//means game already exists, try to join
await game.Join(ctx.User.Id, ctx.User.ToString(), options.Bet);
return;
}
if (options.Bet > 0)
{
if (!await _cs.RemoveAsync(ctx.User.Id, options.Bet, new("connect4", "bet")))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
_service.Connect4Games.TryRemove(ctx.Channel.Id, out _);
game.Dispose();
return;
}
}
game.OnGameStateUpdated += Game_OnGameStateUpdated;
game.OnGameFailedToStart += GameOnGameFailedToStart;
game.OnGameEnded += GameOnGameEnded;
_client.MessageReceived += ClientMessageReceived;
game.Initialize();
if (options.Bet == 0)
await ReplyConfirmLocalizedAsync(strs.connect4_created);
else
await ReplyErrorLocalizedAsync(strs.connect4_created_bet(N(options.Bet)));
Task ClientMessageReceived(SocketMessage arg)
{
if (ctx.Channel.Id != arg.Channel.Id)
return Task.CompletedTask;
_ = Task.Run(async () =>
{
var success = false;
if (int.TryParse(arg.Content, out var col))
success = await game.Input(arg.Author.Id, col);
if (success)
{
try { await arg.DeleteAsync(); }
catch { }
}
else
{
if (game.CurrentPhase is Connect4Game.Phase.Joining or Connect4Game.Phase.Ended)
return;
RepostCounter++;
if (RepostCounter == 0)
{
try { msg = await ctx.Channel.SendMessageAsync("", embed: (Embed)msg.Embeds.First()); }
catch { }
}
}
});
return Task.CompletedTask;
}
Task GameOnGameFailedToStart(Connect4Game arg)
{
if (_service.Connect4Games.TryRemove(ctx.Channel.Id, out var toDispose))
{
_client.MessageReceived -= ClientMessageReceived;
toDispose.Dispose();
}
return ErrorLocalizedAsync(strs.connect4_failed_to_start);
}
Task GameOnGameEnded(Connect4Game arg, Connect4Game.Result result)
{
if (_service.Connect4Games.TryRemove(ctx.Channel.Id, out var toDispose))
{
_client.MessageReceived -= ClientMessageReceived;
toDispose.Dispose();
}
string title;
if (result == Connect4Game.Result.CurrentPlayerWon)
{
title = GetText(strs.connect4_won(Format.Bold(arg.CurrentPlayer.Username),
Format.Bold(arg.OtherPlayer.Username)));
}
else if (result == Connect4Game.Result.OtherPlayerWon)
{
title = GetText(strs.connect4_won(Format.Bold(arg.OtherPlayer.Username),
Format.Bold(arg.CurrentPlayer.Username)));
}
else
title = GetText(strs.connect4_draw);
return msg.ModifyAsync(x => x.Embed = _eb.Create()
.WithTitle(title)
.WithDescription(GetGameStateText(game))
.WithOkColor()
.Build());
}
}
private async Task Game_OnGameStateUpdated(Connect4Game game)
{
var embed = _eb.Create()
.WithTitle($"{game.CurrentPlayer.Username} vs {game.OtherPlayer.Username}")
.WithDescription(GetGameStateText(game))
.WithOkColor();
if (msg is null)
msg = await ctx.Channel.EmbedAsync(embed);
else
await msg.ModifyAsync(x => x.Embed = embed.Build());
}
private string GetGameStateText(Connect4Game game)
{
var sb = new StringBuilder();
if (game.CurrentPhase is Connect4Game.Phase.P1Move or Connect4Game.Phase.P2Move)
sb.AppendLine(GetText(strs.connect4_player_to_move(Format.Bold(game.CurrentPlayer.Username))));
for (var i = Connect4Game.NUMBER_OF_ROWS; i > 0; i--)
{
for (var j = 0; j < Connect4Game.NUMBER_OF_COLUMNS; j++)
{
var cur = game.GameState[i + (j * Connect4Game.NUMBER_OF_ROWS) - 1];
if (cur == Connect4Game.Field.Empty)
sb.Append("⚫"); //black circle
else if (cur == Connect4Game.Field.P1)
sb.Append("🔴"); //red circle
else
sb.Append("🔵"); //blue circle
}
sb.AppendLine();
}
for (var i = 0; i < Connect4Game.NUMBER_OF_COLUMNS; i++)
sb.Append(_numbers[i]);
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,18 @@
using Nadeko.Bot.Common;
using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling;
// todo do we need both currencyprovider and currencyservice
public sealed class CurrencyProvider : ICurrencyProvider
{
private readonly GamblingConfigService _cs;
public CurrencyProvider(GamblingConfigService cs)
{
_cs = cs;
}
public string GetCurrencySign()
=> _cs.Data.Currency.Sign;
}

View File

@@ -1,224 +0,0 @@
#nullable disable
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System.Text.RegularExpressions;
using Image = SixLabors.ImageSharp.Image;
namespace NadekoBot.Modules.Gambling;
public partial class Gambling
{
[Group]
public partial class DiceRollCommands : NadekoModule
{
private static readonly Regex _dndRegex = new(@"^(?<n1>\d+)d(?<n2>\d+)(?:\+(?<add>\d+))?(?:\-(?<sub>\d+))?$",
RegexOptions.Compiled);
private static readonly Regex _fudgeRegex = new(@"^(?<n1>\d+)d(?:F|f)$", RegexOptions.Compiled);
private static readonly char[] _fateRolls = { '-', ' ', '+' };
private readonly IImageCache _images;
public DiceRollCommands(ImageCache images)
=> _images = images;
[Cmd]
public async Task Roll()
{
var rng = new NadekoRandom();
var gen = rng.Next(1, 101);
var num1 = gen / 10;
var num2 = gen % 10;
using var img1 = await GetDiceAsync(num1);
using var img2 = await GetDiceAsync(num2);
using var img = new[] { img1, img2 }.Merge(out var format);
await using var ms = await img.ToStreamAsync(format);
var fileName = $"dice.{format.FileExtensions.First()}";
var eb = _eb.Create(ctx)
.WithOkColor()
.WithAuthor(ctx.User)
.AddField(GetText(strs.roll2), gen)
.WithImageUrl($"attachment://{fileName}");
await ctx.Channel.SendFileAsync(ms,
fileName,
embed: eb.Build());
}
[Cmd]
[Priority(1)]
public async Task Roll(int num)
=> await InternalRoll(num, true);
[Cmd]
[Priority(1)]
public async Task Rolluo(int num = 1)
=> await InternalRoll(num, false);
[Cmd]
[Priority(0)]
public async Task Roll(string arg)
=> await InternallDndRoll(arg, true);
[Cmd]
[Priority(0)]
public async Task Rolluo(string arg)
=> await InternallDndRoll(arg, false);
private async Task InternalRoll(int num, bool ordered)
{
if (num is < 1 or > 30)
{
await ReplyErrorLocalizedAsync(strs.dice_invalid_number(1, 30));
return;
}
var rng = new NadekoRandom();
var dice = new List<Image<Rgba32>>(num);
var values = new List<int>(num);
for (var i = 0; i < num; i++)
{
var randomNumber = rng.Next(1, 7);
var toInsert = dice.Count;
if (ordered)
{
if (randomNumber == 6 || dice.Count == 0)
toInsert = 0;
else if (randomNumber != 1)
{
for (var j = 0; j < dice.Count; j++)
{
if (values[j] < randomNumber)
{
toInsert = j;
break;
}
}
}
}
else
toInsert = dice.Count;
dice.Insert(toInsert, await GetDiceAsync(randomNumber));
values.Insert(toInsert, randomNumber);
}
using var bitmap = dice.Merge(out var format);
await using var ms = bitmap.ToStream(format);
foreach (var d in dice)
d.Dispose();
var imageName = $"dice.{format.FileExtensions.First()}";
var eb = _eb.Create(ctx)
.WithOkColor()
.WithAuthor(ctx.User)
.AddField(GetText(strs.rolls), values.Select(x => Format.Code(x.ToString())).Join(' '), true)
.AddField(GetText(strs.total), values.Sum(), true)
.WithDescription(GetText(strs.dice_rolled_num(Format.Bold(values.Count.ToString()))))
.WithImageUrl($"attachment://{imageName}");
await ctx.Channel.SendFileAsync(ms,
imageName,
embed: eb.Build());
}
private async Task InternallDndRoll(string arg, bool ordered)
{
Match match;
if ((match = _fudgeRegex.Match(arg)).Length != 0
&& int.TryParse(match.Groups["n1"].ToString(), out var n1)
&& n1 is > 0 and < 500)
{
var rng = new NadekoRandom();
var rolls = new List<char>();
for (var i = 0; i < n1; i++)
rolls.Add(_fateRolls[rng.Next(0, _fateRolls.Length)]);
var embed = _eb.Create()
.WithOkColor()
.WithAuthor(ctx.User)
.WithDescription(GetText(strs.dice_rolled_num(Format.Bold(n1.ToString()))))
.AddField(Format.Bold("Result"),
string.Join(" ", rolls.Select(c => Format.Code($"[{c}]"))));
await ctx.Channel.EmbedAsync(embed);
}
else if ((match = _dndRegex.Match(arg)).Length != 0)
{
var rng = new NadekoRandom();
if (int.TryParse(match.Groups["n1"].ToString(), out n1)
&& int.TryParse(match.Groups["n2"].ToString(), out var n2)
&& n1 <= 50
&& n2 <= 100000
&& n1 > 0
&& n2 > 0)
{
if (!int.TryParse(match.Groups["add"].Value, out var add))
add = 0;
if (!int.TryParse(match.Groups["sub"].Value, out var sub))
sub = 0;
var arr = new int[n1];
for (var i = 0; i < n1; i++)
arr[i] = rng.Next(1, n2 + 1);
var sum = arr.Sum();
var embed = _eb.Create()
.WithOkColor()
.WithAuthor(ctx.User)
.WithDescription(GetText(strs.dice_rolled_num(n1 + $"`1 - {n2}`")))
.AddField(Format.Bold(GetText(strs.rolls)),
string.Join(" ",
(ordered ? arr.OrderBy(x => x).AsEnumerable() : arr).Select(x
=> Format.Code(x.ToString()))))
.AddField(Format.Bold("Sum"),
sum + " + " + add + " - " + sub + " = " + (sum + add - sub));
await ctx.Channel.EmbedAsync(embed);
}
}
}
[Cmd]
public async Task NRoll([Leftover] string range)
{
int rolled;
if (range.Contains("-"))
{
var arr = range.Split('-').Take(2).Select(int.Parse).ToArray();
if (arr[0] > arr[1])
{
await ReplyErrorLocalizedAsync(strs.second_larger_than_first);
return;
}
rolled = new NadekoRandom().Next(arr[0], arr[1] + 1);
}
else
rolled = new NadekoRandom().Next(0, int.Parse(range) + 1);
await ReplyConfirmLocalizedAsync(strs.dice_rolled(Format.Bold(rolled.ToString())));
}
private async Task<Image<Rgba32>> GetDiceAsync(int num)
{
if (num is < 0 or > 10)
throw new ArgumentOutOfRangeException(nameof(num));
if (num == 10)
{
using var imgOne = Image.Load<Rgba32>(await _images.GetDiceAsync(1));
using var imgZero = Image.Load<Rgba32>(await _images.GetDiceAsync(0));
return new[] { imgOne, imgZero }.Merge();
}
return Image.Load<Rgba32>(await _images.GetDiceAsync(num));
}
}
}

View File

@@ -1,234 +0,0 @@
#nullable disable
using Nadeko.Econ;
using NadekoBot.Common.TypeReaders;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Image = SixLabors.ImageSharp.Image;
namespace NadekoBot.Modules.Gambling;
public partial class Gambling
{
[Group]
public partial class DrawCommands : GamblingSubmodule<IGamblingService>
{
private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new();
private readonly IImageCache _images;
public DrawCommands(IImageCache images, GamblingConfigService gcs) : base(gcs)
=> _images = images;
private async Task InternalDraw(int count, ulong? guildId = null)
{
if (count is < 1 or > 10)
throw new ArgumentOutOfRangeException(nameof(count));
var cards = guildId is null ? new() : _allDecks.GetOrAdd(ctx.Guild, _ => new());
var images = new List<Image<Rgba32>>();
var cardObjects = new List<Deck.Card>();
for (var i = 0; i < count; i++)
{
if (cards.CardPool.Count == 0 && i != 0)
{
try
{
await ReplyErrorLocalizedAsync(strs.no_more_cards);
}
catch
{
// ignored
}
break;
}
var currentCard = cards.Draw();
cardObjects.Add(currentCard);
var image = await GetCardImageAsync(currentCard);
images.Add(image);
}
var imgName = "cards.jpg";
using var img = images.Merge();
foreach (var i in images)
i.Dispose();
var eb = _eb.Create(ctx)
.WithOkColor();
var toSend = string.Empty;
if (cardObjects.Count == 5)
eb.AddField(GetText(strs.hand_value), Deck.GetHandValue(cardObjects), true);
if (guildId is not null)
toSend += GetText(strs.cards_left(Format.Bold(cards.CardPool.Count.ToString())));
eb.WithDescription(toSend)
.WithAuthor(ctx.User)
.WithImageUrl($"attachment://{imgName}");
if (count > 1)
eb.AddField(GetText(strs.cards), count.ToString(), true);
await using var imageStream = await img.ToStreamAsync();
await ctx.Channel.SendFileAsync(imageStream,
imgName,
embed: eb.Build());
}
private async Task<Image<Rgba32>> GetCardImageAsync(RegularCard currentCard)
{
var cardName = currentCard.GetName().ToLowerInvariant().Replace(' ', '_');
var cardBytes = await File.ReadAllBytesAsync($"data/images/cards/{cardName}.jpg");
return Image.Load<Rgba32>(cardBytes);
}
private async Task<Image<Rgba32>> GetCardImageAsync(Deck.Card currentCard)
{
var cardName = currentCard.ToString().ToLowerInvariant().Replace(' ', '_');
var cardBytes = await File.ReadAllBytesAsync($"data/images/cards/{cardName}.jpg");
return Image.Load<Rgba32>(cardBytes);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Draw(int num = 1)
{
if (num < 1)
return;
if (num > 10)
num = 10;
await InternalDraw(num, ctx.Guild.Id);
}
[Cmd]
public async Task DrawNew(int num = 1)
{
if (num < 1)
return;
if (num > 10)
num = 10;
await InternalDraw(num);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task DeckShuffle()
{
//var channel = (ITextChannel)ctx.Channel;
_allDecks.AddOrUpdate(ctx.Guild,
_ => new(),
(_, c) =>
{
c.Restart();
return c;
});
await ReplyConfirmLocalizedAsync(strs.deck_reshuffled);
}
[Cmd]
[RequireContext(ContextType.Guild)]
public Task BetDraw([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, InputValueGuess val, InputColorGuess? col = null)
=> BetDrawInternal(amount, val, col);
[Cmd]
[RequireContext(ContextType.Guild)]
public Task BetDraw([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, InputColorGuess col, InputValueGuess? val = null)
=> BetDrawInternal(amount, val, col);
public async Task BetDrawInternal(long amount, InputValueGuess? val, InputColorGuess? col)
{
if (amount <= 0)
return;
var res = await _service.BetDrawAsync(ctx.User.Id,
amount,
(byte?)val,
(byte?)col);
if (!res.TryPickT0(out var result, out _))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
}
var eb = _eb.Create(ctx)
.WithOkColor()
.WithAuthor(ctx.User)
.WithDescription(result.Card.GetEmoji())
.AddField(GetText(strs.guess), GetGuessInfo(val, col), true)
.AddField(GetText(strs.card), GetCardInfo(result.Card), true)
.AddField(GetText(strs.won), N((long)result.Won), false)
.WithImageUrl("attachment://card.png");
using var img = await GetCardImageAsync(result.Card);
await using var imgStream = await img.ToStreamAsync();
await ctx.Channel.SendFileAsync(imgStream, "card.png", embed: eb.Build());
}
private string GetGuessInfo(InputValueGuess? valG, InputColorGuess? colG)
{
var val = valG switch
{
InputValueGuess.H => "Hi ⬆️",
InputValueGuess.L => "Lo ⬇️",
_ => "❓"
};
var col = colG switch
{
InputColorGuess.Red => "R 🔴",
InputColorGuess.Black => "B ⚫",
_ => "❓"
};
return $"{val} / {col}";
}
private string GetCardInfo(RegularCard card)
{
var val = (int)card.Value switch
{
< 7 => "Lo ⬇️",
> 7 => "Hi ⬆️",
_ => "7 💀"
};
var col = card.Value == RegularValue.Seven
? "7 💀"
: card.Suit switch
{
RegularSuit.Diamonds or RegularSuit.Hearts => "R 🔴",
_ => "B ⚫"
};
return $"{val} / {col}";
}
public enum InputValueGuess
{
High = 0,
H = 0,
Hi = 0,
Low = 1,
L = 1,
Lo = 1,
}
public enum InputColorGuess
{
R = 0,
Red = 0,
B = 1,
Bl = 1,
Black = 1,
}
}
}

View File

@@ -1,12 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Gambling.Services;
public sealed class EconomyResult
{
public decimal Cash { get; init; }
public decimal Planted { get; init; }
public decimal Waifus { get; init; }
public decimal OnePercent { get; init; }
public decimal Bank { get; init; }
public long Bot { get; init; }
}

View File

@@ -1,60 +0,0 @@
#nullable disable
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Common.Events;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Gambling;
public partial class Gambling
{
[Group]
public partial class CurrencyEventsCommands : GamblingSubmodule<CurrencyEventsService>
{
public CurrencyEventsCommands(GamblingConfigService gamblingConf)
: base(gamblingConf)
{
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NadekoOptions<EventOptions>]
[OwnerOnly]
public async Task EventStart(CurrencyEvent.Type ev, params string[] options)
{
var (opts, _) = OptionsParser.ParseFrom(new EventOptions(), options);
if (!await _service.TryCreateEventAsync(ctx.Guild.Id, ctx.Channel.Id, ev, opts, GetEmbed))
await ReplyErrorLocalizedAsync(strs.start_event_fail);
}
private IEmbedBuilder GetEmbed(CurrencyEvent.Type type, EventOptions opts, long currentPot)
=> type switch
{
CurrencyEvent.Type.Reaction => _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.event_title(type.ToString())))
.WithDescription(GetReactionDescription(opts.Amount, currentPot))
.WithFooter(GetText(strs.event_duration_footer(opts.Hours))),
CurrencyEvent.Type.GameStatus => _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.event_title(type.ToString())))
.WithDescription(GetGameStatusDescription(opts.Amount, currentPot))
.WithFooter(GetText(strs.event_duration_footer(opts.Hours))),
_ => throw new ArgumentOutOfRangeException(nameof(type))
};
private string GetReactionDescription(long amount, long potSize)
{
var potSizeStr = Format.Bold(potSize == 0 ? "∞" + CurrencySign : N(potSize));
return GetText(strs.new_reaction_event(CurrencySign, Format.Bold(N(amount)), potSizeStr));
}
private string GetGameStatusDescription(long amount, long potSize)
{
var potSizeStr = Format.Bold(potSize == 0 ? "∞" + CurrencySign : potSize + CurrencySign);
return GetText(strs.new_gamestatus_event(CurrencySign, Format.Bold(N(amount)), potSizeStr));
}
}
}

View File

@@ -1,67 +0,0 @@
#nullable disable
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Common.Events;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Gambling.Services;
public class CurrencyEventsService : INService
{
private readonly DiscordSocketClient _client;
private readonly ICurrencyService _cs;
private readonly GamblingConfigService _configService;
private readonly ConcurrentDictionary<ulong, ICurrencyEvent> _events = new();
public CurrencyEventsService(DiscordSocketClient client, ICurrencyService cs, GamblingConfigService configService)
{
_client = client;
_cs = cs;
_configService = configService;
}
public async Task<bool> TryCreateEventAsync(
ulong guildId,
ulong channelId,
CurrencyEvent.Type type,
EventOptions opts,
Func<CurrencyEvent.Type, EventOptions, long, IEmbedBuilder> embed)
{
var g = _client.GetGuild(guildId);
if (g?.GetChannel(channelId) is not ITextChannel ch)
return false;
ICurrencyEvent ce;
if (type == CurrencyEvent.Type.Reaction)
ce = new ReactionEvent(_client, _cs, g, ch, opts, _configService.Data, embed);
else if (type == CurrencyEvent.Type.GameStatus)
ce = new GameStatusEvent(_client, _cs, g, ch, opts, embed);
else
return false;
var added = _events.TryAdd(guildId, ce);
if (added)
{
try
{
ce.OnEnded += OnEventEnded;
await ce.StartEvent();
}
catch (Exception ex)
{
Log.Warning(ex, "Error starting event");
_events.TryRemove(guildId, out ce);
return false;
}
}
return added;
}
private Task OnEventEnded(ulong gid)
{
_events.TryRemove(gid, out _);
return Task.CompletedTask;
}
}

View File

@@ -1,39 +0,0 @@
#nullable disable
using CommandLine;
namespace NadekoBot.Modules.Gambling.Common.Events;
public class EventOptions : INadekoCommandOptions
{
[Option('a', "amount", Required = false, Default = 100, HelpText = "Amount of currency each user receives.")]
public long Amount { get; set; } = 100;
[Option('p',
"pot-size",
Required = false,
Default = 0,
HelpText = "The maximum amount of currency that can be rewarded. 0 means no limit.")]
public long PotSize { get; set; }
//[Option('t', "type", Required = false, Default = "reaction", HelpText = "Type of the event. reaction, gamestatus or joinserver.")]
//public string TypeString { get; set; } = "reaction";
[Option('d',
"duration",
Required = false,
Default = 24,
HelpText = "Number of hours the event should run for. Default 24.")]
public int Hours { get; set; } = 24;
public void NormalizeOptions()
{
if (Amount < 0)
Amount = 100;
if (PotSize < 0)
PotSize = 0;
if (Hours <= 0)
Hours = 24;
if (PotSize != 0 && PotSize < Amount)
PotSize = 0;
}
}

View File

@@ -1,196 +0,0 @@
#nullable disable
using NadekoBot.Services.Database.Models;
using System.Collections.Concurrent;
using Nadeko.Common;
namespace NadekoBot.Modules.Gambling.Common.Events;
public class GameStatusEvent : ICurrencyEvent
{
public event Func<ulong, Task> OnEnded;
private long PotSize { get; set; }
public bool Stopped { get; private set; }
public bool PotEmptied { get; private set; }
private readonly DiscordSocketClient _client;
private readonly IGuild _guild;
private IUserMessage msg;
private readonly ICurrencyService _cs;
private readonly long _amount;
private readonly Func<CurrencyEvent.Type, EventOptions, long, IEmbedBuilder> _embedFunc;
private readonly bool _isPotLimited;
private readonly ITextChannel _channel;
private readonly ConcurrentHashSet<ulong> _awardedUsers = new();
private readonly ConcurrentQueue<ulong> _toAward = new();
private readonly Timer _t;
private readonly Timer _timeout;
private readonly EventOptions _opts;
private readonly string _code;
private readonly char[] _sneakyGameStatusChars = Enumerable.Range(48, 10)
.Concat(Enumerable.Range(65, 26))
.Concat(Enumerable.Range(97, 26))
.Select(x => (char)x)
.ToArray();
private readonly object _stopLock = new();
private readonly object _potLock = new();
public GameStatusEvent(
DiscordSocketClient client,
ICurrencyService cs,
SocketGuild g,
ITextChannel ch,
EventOptions opt,
Func<CurrencyEvent.Type, EventOptions, long, IEmbedBuilder> embedFunc)
{
_client = client;
_guild = g;
_cs = cs;
_amount = opt.Amount;
PotSize = opt.PotSize;
_embedFunc = embedFunc;
_isPotLimited = PotSize > 0;
_channel = ch;
_opts = opt;
// generate code
_code = new(_sneakyGameStatusChars.Shuffle().Take(5).ToArray());
_t = new(OnTimerTick, null, Timeout.InfiniteTimeSpan, TimeSpan.FromSeconds(2));
if (_opts.Hours > 0)
_timeout = new(EventTimeout, null, TimeSpan.FromHours(_opts.Hours), Timeout.InfiniteTimeSpan);
}
private void EventTimeout(object state)
=> _ = StopEvent();
private async void OnTimerTick(object state)
{
var potEmpty = PotEmptied;
var toAward = new List<ulong>();
while (_toAward.TryDequeue(out var x))
toAward.Add(x);
if (!toAward.Any())
return;
try
{
await _cs.AddBulkAsync(toAward,
_amount,
new("event", "gamestatus")
);
if (_isPotLimited)
{
await msg.ModifyAsync(m =>
{
m.Embed = GetEmbed(PotSize).Build();
});
}
Log.Information("Game status event awarded {Count} users {Amount} currency.{Remaining}",
toAward.Count,
_amount,
_isPotLimited ? $" {PotSize} left." : "");
if (potEmpty)
_ = StopEvent();
}
catch (Exception ex)
{
Log.Warning(ex, "Error in OnTimerTick in gamestatusevent");
}
}
public async Task StartEvent()
{
msg = await _channel.EmbedAsync(GetEmbed(_opts.PotSize));
await _client.SetGameAsync(_code);
_client.MessageDeleted += OnMessageDeleted;
_client.MessageReceived += HandleMessage;
_t.Change(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2));
}
private IEmbedBuilder GetEmbed(long pot)
=> _embedFunc(CurrencyEvent.Type.GameStatus, _opts, pot);
private async Task OnMessageDeleted(Cacheable<IMessage, ulong> message, Cacheable<IMessageChannel, ulong> cacheable)
{
if (message.Id == msg.Id)
await StopEvent();
}
public Task StopEvent()
{
lock (_stopLock)
{
if (Stopped)
return Task.CompletedTask;
Stopped = true;
_client.MessageDeleted -= OnMessageDeleted;
_client.MessageReceived -= HandleMessage;
_t.Change(Timeout.Infinite, Timeout.Infinite);
_timeout?.Change(Timeout.Infinite, Timeout.Infinite);
_ = _client.SetGameAsync(null);
try
{
_ = msg.DeleteAsync();
}
catch { }
_ = OnEnded?.Invoke(_guild.Id);
}
return Task.CompletedTask;
}
private Task HandleMessage(SocketMessage message)
{
_ = Task.Run(async () =>
{
if (message.Author is not IGuildUser gu // no unknown users, as they could be bots, or alts
|| gu.IsBot // no bots
|| message.Content != _code // code has to be the same
|| (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5) // no recently created accounts
return;
// there has to be money left in the pot
// and the user wasn't rewarded
if (_awardedUsers.Add(message.Author.Id) && TryTakeFromPot())
{
_toAward.Enqueue(message.Author.Id);
if (_isPotLimited && PotSize < _amount)
PotEmptied = true;
}
try
{
await message.DeleteAsync(new()
{
RetryMode = RetryMode.AlwaysFail
});
}
catch { }
});
return Task.CompletedTask;
}
private bool TryTakeFromPot()
{
if (_isPotLimited)
{
lock (_potLock)
{
if (PotSize < _amount)
return false;
PotSize -= _amount;
return true;
}
}
return true;
}
}

View File

@@ -1,9 +0,0 @@
#nullable disable
namespace NadekoBot.Modules.Gambling.Common;
public interface ICurrencyEvent
{
event Func<ulong, Task> OnEnded;
Task StopEvent();
Task StartEvent();
}

View File

@@ -1,194 +0,0 @@
#nullable disable
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Gambling.Common.Events;
public class ReactionEvent : ICurrencyEvent
{
public event Func<ulong, Task> OnEnded;
private long PotSize { get; set; }
public bool Stopped { get; private set; }
public bool PotEmptied { get; private set; }
private readonly DiscordSocketClient _client;
private readonly IGuild _guild;
private IUserMessage msg;
private IEmote emote;
private readonly ICurrencyService _cs;
private readonly long _amount;
private readonly Func<CurrencyEvent.Type, EventOptions, long, IEmbedBuilder> _embedFunc;
private readonly bool _isPotLimited;
private readonly ITextChannel _channel;
private readonly ConcurrentHashSet<ulong> _awardedUsers = new();
private readonly System.Collections.Concurrent.ConcurrentQueue<ulong> _toAward = new();
private readonly Timer _t;
private readonly Timer _timeout;
private readonly bool _noRecentlyJoinedServer;
private readonly EventOptions _opts;
private readonly GamblingConfig _config;
private readonly object _stopLock = new();
private readonly object _potLock = new();
public ReactionEvent(
DiscordSocketClient client,
ICurrencyService cs,
SocketGuild g,
ITextChannel ch,
EventOptions opt,
GamblingConfig config,
Func<CurrencyEvent.Type, EventOptions, long, IEmbedBuilder> embedFunc)
{
_client = client;
_guild = g;
_cs = cs;
_amount = opt.Amount;
PotSize = opt.PotSize;
_embedFunc = embedFunc;
_isPotLimited = PotSize > 0;
_channel = ch;
_noRecentlyJoinedServer = false;
_opts = opt;
_config = config;
_t = new(OnTimerTick, null, Timeout.InfiniteTimeSpan, TimeSpan.FromSeconds(2));
if (_opts.Hours > 0)
_timeout = new(EventTimeout, null, TimeSpan.FromHours(_opts.Hours), Timeout.InfiniteTimeSpan);
}
private void EventTimeout(object state)
=> _ = StopEvent();
private async void OnTimerTick(object state)
{
var potEmpty = PotEmptied;
var toAward = new List<ulong>();
while (_toAward.TryDequeue(out var x))
toAward.Add(x);
if (!toAward.Any())
return;
try
{
await _cs.AddBulkAsync(toAward, _amount, new("event", "reaction"));
if (_isPotLimited)
{
await msg.ModifyAsync(m =>
{
m.Embed = GetEmbed(PotSize).Build();
});
}
Log.Information("Reaction Event awarded {Count} users {Amount} currency.{Remaining}",
toAward.Count,
_amount,
_isPotLimited ? $" {PotSize} left." : "");
if (potEmpty)
_ = StopEvent();
}
catch (Exception ex)
{
Log.Warning(ex, "Error adding bulk currency to users");
}
}
public async Task StartEvent()
{
if (Emote.TryParse(_config.Currency.Sign, out var parsedEmote))
emote = parsedEmote;
else
emote = new Emoji(_config.Currency.Sign);
msg = await _channel.EmbedAsync(GetEmbed(_opts.PotSize));
await msg.AddReactionAsync(emote);
_client.MessageDeleted += OnMessageDeleted;
_client.ReactionAdded += HandleReaction;
_t.Change(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2));
}
private IEmbedBuilder GetEmbed(long pot)
=> _embedFunc(CurrencyEvent.Type.Reaction, _opts, pot);
private async Task OnMessageDeleted(Cacheable<IMessage, ulong> message, Cacheable<IMessageChannel, ulong> cacheable)
{
if (message.Id == msg.Id)
await StopEvent();
}
public Task StopEvent()
{
lock (_stopLock)
{
if (Stopped)
return Task.CompletedTask;
Stopped = true;
_client.MessageDeleted -= OnMessageDeleted;
_client.ReactionAdded -= HandleReaction;
_t.Change(Timeout.Infinite, Timeout.Infinite);
_timeout?.Change(Timeout.Infinite, Timeout.Infinite);
try
{
_ = msg.DeleteAsync();
}
catch { }
_ = OnEnded?.Invoke(_guild.Id);
}
return Task.CompletedTask;
}
private Task HandleReaction(
Cacheable<IUserMessage, ulong> message,
Cacheable<IMessageChannel, ulong> cacheable,
SocketReaction r)
{
_ = Task.Run(() =>
{
if (emote.Name != r.Emote.Name)
return;
if ((r.User.IsSpecified
? r.User.Value
: null) is not IGuildUser gu // no unknown users, as they could be bots, or alts
|| message.Id != msg.Id // same message
|| gu.IsBot // no bots
|| (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5 // no recently created accounts
|| (_noRecentlyJoinedServer
&& // if specified, no users who joined the server in the last 24h
(gu.JoinedAt is null
|| (DateTime.UtcNow - gu.JoinedAt.Value).TotalDays
< 1))) // and no users for who we don't know when they joined
return;
// there has to be money left in the pot
// and the user wasn't rewarded
if (_awardedUsers.Add(r.UserId) && TryTakeFromPot())
{
_toAward.Enqueue(r.UserId);
if (_isPotLimited && PotSize < _amount)
PotEmptied = true;
}
});
return Task.CompletedTask;
}
private bool TryTakeFromPot()
{
if (_isPotLimited)
{
lock (_potLock)
{
if (PotSize < _amount)
return false;
PotSize -= _amount;
return true;
}
}
return true;
}
}

View File

@@ -1,141 +0,0 @@
#nullable disable
using Nadeko.Common;
using NadekoBot.Common.TypeReaders;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Image = SixLabors.ImageSharp.Image;
namespace NadekoBot.Modules.Gambling;
public partial class Gambling
{
[Group]
public partial class FlipCoinCommands : GamblingSubmodule<IGamblingService>
{
public enum BetFlipGuess : byte
{
H = 0,
Head = 0,
Heads = 0,
T = 1,
Tail = 1,
Tails = 1
}
private static readonly NadekoRandom _rng = new();
private readonly IImageCache _images;
private readonly ICurrencyService _cs;
private readonly ImagesConfig _ic;
public FlipCoinCommands(
IImageCache images,
ImagesConfig ic,
ICurrencyService cs,
GamblingConfigService gss)
: base(gss)
{
_ic = ic;
_images = images;
_cs = cs;
}
[Cmd]
public async Task Flip(int count = 1)
{
if (count is > 10 or < 1)
{
await ReplyErrorLocalizedAsync(strs.flip_invalid(10));
return;
}
var headCount = 0;
var tailCount = 0;
var imgs = new Image<Rgba32>[count];
var headsArr = await _images.GetHeadsImageAsync();
var tailsArr = await _images.GetTailsImageAsync();
var result = await _service.FlipAsync(count);
for (var i = 0; i < result.Length; i++)
{
if (result[i].Side == 0)
{
imgs[i] = Image.Load<Rgba32>(headsArr);
headCount++;
}
else
{
imgs[i] = Image.Load<Rgba32>(tailsArr);
tailCount++;
}
}
using var img = imgs.Merge(out var format);
await using var stream = await img.ToStreamAsync(format);
foreach (var i in imgs)
i.Dispose();
var imgName = $"coins.{format.FileExtensions.First()}";
var msg = count != 1
? Format.Bold(GetText(strs.flip_results(count, headCount, tailCount)))
: GetText(strs.flipped(headCount > 0
? Format.Bold(GetText(strs.heads))
: Format.Bold(GetText(strs.tails))));
var eb = _eb.Create(ctx)
.WithOkColor()
.WithAuthor(ctx.User)
.WithDescription(msg)
.WithImageUrl($"attachment://{imgName}");
await ctx.Channel.SendFileAsync(stream,
imgName,
embed: eb.Build());
}
[Cmd]
public async Task Betflip([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, BetFlipGuess guess)
{
if (!await CheckBetMandatory(amount) || amount == 1)
return;
var res = await _service.BetFlipAsync(ctx.User.Id, amount, (byte)guess);
if (!res.TryPickT0(out var result, out _))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
}
Uri imageToSend;
var coins = _ic.Data.Coins;
if (result.Side == 0)
{
imageToSend = coins.Heads[_rng.Next(0, coins.Heads.Length)];
}
else
{
imageToSend = coins.Tails[_rng.Next(0, coins.Tails.Length)];
}
string str;
var won = (long)result.Won;
if (won > 0)
{
str = Format.Bold(GetText(strs.flip_guess(N(won))));
}
else
{
str = Format.Bold(GetText(strs.better_luck));
}
await ctx.Channel.EmbedAsync(_eb.Create()
.WithAuthor(ctx.User)
.WithDescription(str)
.WithOkColor()
.WithImageUrl(imageToSend.ToString()));
}
}
}

View File

@@ -1,7 +0,0 @@
namespace Nadeko.Econ.Gambling;
public readonly struct FlipResult
{
public long Won { get; init; }
public int Side { get; init; }
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,387 +0,0 @@
#nullable disable
using Cloneable;
using NadekoBot.Common.Yml;
using SixLabors.ImageSharp.PixelFormats;
using YamlDotNet.Serialization;
using Color = SixLabors.ImageSharp.Color;
namespace NadekoBot.Modules.Gambling.Common;
[Cloneable]
public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
{
[Comment("""DO NOT CHANGE""")]
public int Version { get; set; } = 2;
[Comment("""Currency settings""")]
public CurrencyConfig Currency { get; set; }
[Comment("""Minimum amount users can bet (>=0)""")]
public int MinBet { get; set; } = 0;
[Comment("""
Maximum amount users can bet
Set 0 for unlimited
""")]
public int MaxBet { get; set; } = 0;
[Comment("""Settings for betflip command""")]
public BetFlipConfig BetFlip { get; set; }
[Comment("""Settings for betroll command""")]
public BetRollConfig BetRoll { get; set; }
[Comment("""Automatic currency generation settings.""")]
public GenerationConfig Generation { get; set; }
[Comment("""
Settings for timely command
(letting people claim X amount of currency every Y hours)
""")]
public TimelyConfig Timely { get; set; }
[Comment("""How much will each user's owned currency decay over time.""")]
public DecayConfig Decay { get; set; }
[Comment("""Settings for LuckyLadder command""")]
public LuckyLadderSettings LuckyLadder { get; set; }
[Comment("""Settings related to waifus""")]
public WaifuConfig Waifu { get; set; }
[Comment("""
Amount of currency selfhosters will get PER pledged dollar CENT.
1 = 100 currency per $. Used almost exclusively on public nadeko.
""")]
public decimal PatreonCurrencyPerCent { get; set; } = 1;
[Comment("""
Currency reward per vote.
This will work only if you've set up VotesApi and correct credentials for topgg and/or discords voting
""")]
public long VoteReward { get; set; } = 100;
[Comment("""Slot config""")]
public SlotsConfig Slots { get; set; }
public GamblingConfig()
{
BetRoll = new();
Waifu = new();
Currency = new();
BetFlip = new();
Generation = new();
Timely = new();
Decay = new();
Slots = new();
LuckyLadder = new();
}
}
public class CurrencyConfig
{
[Comment("""What is the emoji/character which represents the currency""")]
public string Sign { get; set; } = "🌸";
[Comment("""What is the name of the currency""")]
public string Name { get; set; } = "Nadeko Flower";
[Comment("""
For how long (in days) will the transactions be kept in the database (curtrs)
Set 0 to disable cleanup (keep transactions forever)
""")]
public int TransactionsLifetime { get; set; } = 0;
}
[Cloneable]
public partial class TimelyConfig
{
[Comment("""
How much currency will the users get every time they run .timely command
setting to 0 or less will disable this feature
""")]
public int Amount { get; set; } = 0;
[Comment("""
How often (in hours) can users claim currency with .timely command
setting to 0 or less will disable this feature
""")]
public int Cooldown { get; set; } = 24;
}
[Cloneable]
public partial class BetFlipConfig
{
[Comment("""Bet multiplier if user guesses correctly""")]
public decimal Multiplier { get; set; } = 1.95M;
}
[Cloneable]
public partial class BetRollConfig
{
[Comment("""
When betroll is played, user will roll a number 0-100.
This setting will describe which multiplier is used for when the roll is higher than the given number.
Doesn't have to be ordered.
""")]
public BetRollPair[] Pairs { get; set; } = Array.Empty<BetRollPair>();
public BetRollConfig()
=> Pairs = new BetRollPair[]
{
new()
{
WhenAbove = 99,
MultiplyBy = 10
},
new()
{
WhenAbove = 90,
MultiplyBy = 4
},
new()
{
WhenAbove = 66,
MultiplyBy = 2
}
};
}
[Cloneable]
public partial class GenerationConfig
{
[Comment("""
when currency is generated, should it also have a random password
associated with it which users have to type after the .pick command
in order to get it
""")]
public bool HasPassword { get; set; } = true;
[Comment("""
Every message sent has a certain % chance to generate the currency
specify the percentage here (1 being 100%, 0 being 0% - for example
default is 0.02, which is 2%
""")]
public decimal Chance { get; set; } = 0.02M;
[Comment("""How many seconds have to pass for the next message to have a chance to spawn currency""")]
public int GenCooldown { get; set; } = 10;
[Comment("""Minimum amount of currency that can spawn""")]
public int MinAmount { get; set; } = 1;
[Comment("""
Maximum amount of currency that can spawn.
Set to the same value as MinAmount to always spawn the same amount
""")]
public int MaxAmount { get; set; } = 1;
}
[Cloneable]
public partial class DecayConfig
{
[Comment("""
Percentage of user's current currency which will be deducted every 24h.
0 - 1 (1 is 100%, 0.5 50%, 0 disabled)
""")]
public decimal Percent { get; set; } = 0;
[Comment("""Maximum amount of user's currency that can decay at each interval. 0 for unlimited.""")]
public int MaxDecay { get; set; } = 0;
[Comment("""Only users who have more than this amount will have their currency decay.""")]
public int MinThreshold { get; set; } = 99;
[Comment("""How often, in hours, does the decay run. Default is 24 hours""")]
public int HourInterval { get; set; } = 24;
}
[Cloneable]
public partial class LuckyLadderSettings
{
[Comment("""Self-Explanatory. Has to have 8 values, otherwise the command won't work.""")]
public decimal[] Multipliers { get; set; }
public LuckyLadderSettings()
=> Multipliers = new[] { 2.4M, 1.7M, 1.5M, 1.2M, 0.5M, 0.3M, 0.2M, 0.1M };
}
[Cloneable]
public sealed partial class WaifuConfig
{
[Comment("""Minimum price a waifu can have""")]
public long MinPrice { get; set; } = 50;
public MultipliersData Multipliers { get; set; } = new();
[Comment("""
Settings for periodic waifu price decay.
Waifu price decays only if the waifu has no claimer.
""")]
public WaifuDecayConfig Decay { get; set; } = new();
[Comment("""
List of items available for gifting.
If negative is true, gift will instead reduce waifu value.
""")]
public List<WaifuItemModel> Items { get; set; } = new();
public WaifuConfig()
=> Items = new()
{
new("🥔", 5, "Potato"),
new("🍪", 10, "Cookie"),
new("🥖", 20, "Bread"),
new("🍭", 30, "Lollipop"),
new("🌹", 50, "Rose"),
new("🍺", 70, "Beer"),
new("🌮", 85, "Taco"),
new("💌", 100, "LoveLetter"),
new("🥛", 125, "Milk"),
new("🍕", 150, "Pizza"),
new("🍫", 200, "Chocolate"),
new("🍦", 250, "Icecream"),
new("🍣", 300, "Sushi"),
new("🍚", 400, "Rice"),
new("🍉", 500, "Watermelon"),
new("🍱", 600, "Bento"),
new("🎟", 800, "MovieTicket"),
new("🍰", 1000, "Cake"),
new("📔", 1500, "Book"),
new("🐱", 2000, "Cat"),
new("🐶", 2001, "Dog"),
new("🐼", 2500, "Panda"),
new("💄", 3000, "Lipstick"),
new("👛", 3500, "Purse"),
new("📱", 4000, "iPhone"),
new("👗", 4500, "Dress"),
new("💻", 5000, "Laptop"),
new("🎻", 7500, "Violin"),
new("🎹", 8000, "Piano"),
new("🚗", 9000, "Car"),
new("💍", 10000, "Ring"),
new("🛳", 12000, "Ship"),
new("🏠", 15000, "House"),
new("🚁", 20000, "Helicopter"),
new("🚀", 30000, "Spaceship"),
new("🌕", 50000, "Moon")
};
public class WaifuDecayConfig
{
[Comment("""
Percentage (0 - 100) of the waifu value to reduce.
Set 0 to disable
For example if a waifu has a price of 500$, setting this value to 10 would reduce the waifu value by 10% (50$)
""")]
public int Percent { get; set; } = 0;
[Comment("""How often to decay waifu values, in hours""")]
public int HourInterval { get; set; } = 24;
[Comment("""
Minimum waifu price required for the decay to be applied.
For example if this value is set to 300, any waifu with the price 300 or less will not experience decay.
""")]
public long MinPrice { get; set; } = 300;
}
}
[Cloneable]
public sealed partial class MultipliersData
{
[Comment("""
Multiplier for waifureset. Default 150.
Formula (at the time of writing this):
price = (waifu_price * 1.25f) + ((number_of_divorces + changes_of_heart + 2) * WaifuReset) rounded up
""")]
public int WaifuReset { get; set; } = 150;
[Comment("""
The minimum amount of currency that you have to pay
in order to buy a waifu who doesn't have a crush on you.
Default is 1.1
Example: If a waifu is worth 100, you will have to pay at least 100 * NormalClaim currency to claim her.
(100 * 1.1 = 110)
""")]
public decimal NormalClaim { get; set; } = 1.1m;
[Comment("""
The minimum amount of currency that you have to pay
in order to buy a waifu that has a crush on you.
Default is 0.88
Example: If a waifu is worth 100, you will have to pay at least 100 * CrushClaim currency to claim her.
(100 * 0.88 = 88)
""")]
public decimal CrushClaim { get; set; } = 0.88M;
[Comment("""
When divorcing a waifu, her new value will be her current value multiplied by this number.
Default 0.75 (meaning will lose 25% of her value)
""")]
public decimal DivorceNewValue { get; set; } = 0.75M;
[Comment("""
All gift prices will be multiplied by this number.
Default 1 (meaning no effect)
""")]
public decimal AllGiftPrices { get; set; } = 1.0M;
[Comment("""
What percentage of the value of the gift will a waifu gain when she's gifted.
Default 0.95 (meaning 95%)
Example: If a waifu is worth 1000, and she receives a gift worth 100, her new value will be 1095)
""")]
public decimal GiftEffect { get; set; } = 0.95M;
[Comment("""
What percentage of the value of the gift will a waifu lose when she's gifted a gift marked as 'negative'.
Default 0.5 (meaning 50%)
Example: If a waifu is worth 1000, and she receives a negative gift worth 100, her new value will be 950)
""")]
public decimal NegativeGiftEffect { get; set; } = 0.50M;
}
public sealed class SlotsConfig
{
[Comment("""Hex value of the color which the numbers on the slot image will have.""")]
public Rgba32 CurrencyFontColor { get; set; } = Color.Red;
}
[Cloneable]
public sealed partial class WaifuItemModel
{
public string ItemEmoji { get; set; }
public long Price { get; set; }
public string Name { get; set; }
[YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
public bool Negative { get; set; }
public WaifuItemModel()
{
}
public WaifuItemModel(
string itemEmoji,
long price,
string name,
bool negative = false)
{
ItemEmoji = itemEmoji;
Price = price;
Name = name;
Negative = negative;
}
public override string ToString()
=> Name;
}
[Cloneable]
public sealed partial class BetRollPair
{
public int WhenAbove { get; set; }
public float MultiplyBy { get; set; }
}

Some files were not shown because too many files have changed in this diff Show More