mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05:00 
			
		
		
		
	Gambling moved to a separate project. Project builds
This commit is contained in:
		@@ -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;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
@@ -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))
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
{
 | 
			
		||||
    
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Common.Attributes;
 | 
			
		||||
 | 
			
		||||
[AttributeUsage(AttributeTargets.Method)]
 | 
			
		||||
public sealed class NadekoOptionsAttribute<TOption> : Attribute
 | 
			
		||||
    where TOption: INadekoCommandOptions
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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"));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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; }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public interface INadekoCommandOptions
 | 
			
		||||
{
 | 
			
		||||
    void NormalizeOptions();
 | 
			
		||||
}
 | 
			
		||||
@@ -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; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
}
 | 
			
		||||
@@ -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());
 | 
			
		||||
}
 | 
			
		||||
@@ -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()
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
@@ -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");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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; }
 | 
			
		||||
}
 | 
			
		||||
@@ -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; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Common.Pokemon;
 | 
			
		||||
 | 
			
		||||
public class PokemonNameId
 | 
			
		||||
{
 | 
			
		||||
    public int Id { get; set; }
 | 
			
		||||
    public string Name { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -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}";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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; }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -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));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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"));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)));
 | 
			
		||||
}
 | 
			
		||||
@@ -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();
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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; }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
}
 | 
			
		||||
@@ -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"));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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."));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,9 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
 | 
			
		||||
using Nadeko.Snake;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
[DIIgnore]
 | 
			
		||||
public sealed class BehaviorAdapter : ICustomBehavior
 | 
			
		||||
{
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -1,4 +1,8 @@
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
using Nadeko.Snake;
 | 
			
		||||
using NadekoBot;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class DmContextAdapter : DmContext
 | 
			
		||||
{
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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();
 | 
			
		||||
}
 | 
			
		||||
@@ -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)));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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();
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
}
 | 
			
		||||
@@ -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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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();
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								src/NadekoBot/Modules/Gambling/CurrencyProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/NadekoBot/Modules/Gambling/CurrencyProvider.cs
									
									
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
@@ -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));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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; }
 | 
			
		||||
}
 | 
			
		||||
@@ -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));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
 | 
			
		||||
public interface ICurrencyEvent
 | 
			
		||||
{
 | 
			
		||||
    event Func<ulong, Task> OnEnded;
 | 
			
		||||
    Task StopEvent();
 | 
			
		||||
    Task StartEvent();
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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()));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
											
										
									
								
							@@ -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
		Reference in New Issue
	
	Block a user