Removed module projects because it can't work like that atm. Commented out package commands.

This commit is contained in:
Master Kwoth 2017-10-15 09:39:46 +02:00
parent 90e71a3a30
commit 696a0eb2a7
180 changed files with 21625 additions and 1058 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,129 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace NadekoBot.Migrations
{
public partial class clearandloadedpackage : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ClashCallers");
migrationBuilder.DropTable(
name: "ModulePrefixes");
migrationBuilder.DropTable(
name: "ClashOfClans");
migrationBuilder.CreateTable(
name: "LoadedPackages",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
BotConfigId = table.Column<int>(type: "INTEGER", nullable: true),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_LoadedPackages", x => x.Id);
table.ForeignKey(
name: "FK_LoadedPackages_BotConfig_BotConfigId",
column: x => x.BotConfigId,
principalTable: "BotConfig",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_LoadedPackages_BotConfigId",
table: "LoadedPackages",
column: "BotConfigId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "LoadedPackages");
migrationBuilder.CreateTable(
name: "ClashOfClans",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ChannelId = table.Column<ulong>(nullable: false),
DateAdded = table.Column<DateTime>(nullable: true),
EnemyClan = table.Column<string>(nullable: true),
GuildId = table.Column<ulong>(nullable: false),
Size = table.Column<int>(nullable: false),
StartedAt = table.Column<DateTime>(nullable: false),
WarState = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClashOfClans", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ModulePrefixes",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
BotConfigId = table.Column<int>(nullable: true),
DateAdded = table.Column<DateTime>(nullable: true),
ModuleName = table.Column<string>(nullable: true),
Prefix = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ModulePrefixes", x => x.Id);
table.ForeignKey(
name: "FK_ModulePrefixes_BotConfig_BotConfigId",
column: x => x.BotConfigId,
principalTable: "BotConfig",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "ClashCallers",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
BaseDestroyed = table.Column<bool>(nullable: false),
CallUser = table.Column<string>(nullable: true),
ClashWarId = table.Column<int>(nullable: false),
DateAdded = table.Column<DateTime>(nullable: true),
SequenceNumber = table.Column<int>(nullable: true),
Stars = table.Column<int>(nullable: false),
TimeAdded = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClashCallers", x => x.Id);
table.ForeignKey(
name: "FK_ClashCallers_ClashOfClans_ClashWarId",
column: x => x.ClashWarId,
principalTable: "ClashOfClans",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ClashCallers_ClashWarId",
table: "ClashCallers",
column: "ClashWarId");
migrationBuilder.CreateIndex(
name: "IX_ModulePrefixes_BotConfigId",
table: "ModulePrefixes",
column: "BotConfigId");
}
}
}

View File

@ -200,56 +200,6 @@ namespace NadekoBot.Migrations
b.ToTable("BotConfig"); b.ToTable("BotConfig");
}); });
modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.ClashCaller", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("BaseDestroyed");
b.Property<string>("CallUser");
b.Property<int>("ClashWarId");
b.Property<DateTime?>("DateAdded");
b.Property<int?>("SequenceNumber");
b.Property<int>("Stars");
b.Property<DateTime>("TimeAdded");
b.HasKey("Id");
b.HasIndex("ClashWarId");
b.ToTable("ClashCallers");
});
modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.ClashWar", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<ulong>("ChannelId");
b.Property<DateTime?>("DateAdded");
b.Property<string>("EnemyClan");
b.Property<ulong>("GuildId");
b.Property<int>("Size");
b.Property<DateTime>("StartedAt");
b.Property<int>("WarState");
b.HasKey("Id");
b.ToTable("ClashOfClans");
});
modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.ClubApplicants", b => modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.ClubApplicants", b =>
{ {
b.Property<int>("ClubId"); b.Property<int>("ClubId");
@ -800,6 +750,24 @@ namespace NadekoBot.Migrations
b.ToTable("IgnoredVoicePresenceCHannels"); b.ToTable("IgnoredVoicePresenceCHannels");
}); });
modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.LoadedPackage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int?>("BotConfigId");
b.Property<DateTime?>("DateAdded");
b.Property<string>("Name");
b.HasKey("Id");
b.HasIndex("BotConfigId");
b.ToTable("LoadedPackages");
});
modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.LogSetting", b => modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.LogSetting", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -874,26 +842,6 @@ namespace NadekoBot.Migrations
b.ToTable("LogSettings"); b.ToTable("LogSettings");
}); });
modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.ModulePrefix", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int?>("BotConfigId");
b.Property<DateTime?>("DateAdded");
b.Property<string>("ModuleName");
b.Property<string>("Prefix");
b.HasKey("Id");
b.HasIndex("BotConfigId");
b.ToTable("ModulePrefixes");
});
modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.MusicPlaylist", b => modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.MusicPlaylist", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -1633,14 +1581,6 @@ namespace NadekoBot.Migrations
.HasForeignKey("BotConfigId1"); .HasForeignKey("BotConfigId1");
}); });
modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.ClashCaller", b =>
{
b.HasOne("NadekoBot.Core.Services.Database.Models.ClashWar", "ClashWar")
.WithMany("Bases")
.HasForeignKey("ClashWarId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.ClubApplicants", b => modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.ClubApplicants", b =>
{ {
b.HasOne("NadekoBot.Core.Services.Database.Models.ClubInfo", "Club") b.HasOne("NadekoBot.Core.Services.Database.Models.ClubInfo", "Club")
@ -1789,10 +1729,10 @@ namespace NadekoBot.Migrations
.HasForeignKey("LogSettingId"); .HasForeignKey("LogSettingId");
}); });
modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.ModulePrefix", b => modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.LoadedPackage", b =>
{ {
b.HasOne("NadekoBot.Core.Services.Database.Models.BotConfig") b.HasOne("NadekoBot.Core.Services.Database.Models.BotConfig")
.WithMany("ModulePrefixes") .WithMany("LoadedPackages")
.HasForeignKey("BotConfigId"); .HasForeignKey("BotConfigId");
}); });

View File

@ -1,195 +0,0 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace NadekoBot.Modules.Administration.Common.Migration
{
public class CommandPrefixes0_9
{
public string Administration { get; set; }
public string Searches { get; set; }
public string NSFW { get; set; }
public string Conversations { get; set; }
public string ClashOfClans { get; set; }
public string Help { get; set; }
public string Music { get; set; }
public string Trello { get; set; }
public string Games { get; set; }
public string Gambling { get; set; }
public string Permissions { get; set; }
public string Programming { get; set; }
public string Pokemon { get; set; }
public string Utility { get; set; }
}
public class Config0_9
{
public bool DontJoinServers { get; set; }
public bool ForwardMessages { get; set; }
public bool ForwardToAllOwners { get; set; }
public bool IsRotatingStatus { get; set; }
public int BufferSize { get; set; }
public List<string> RaceAnimals { get; set; }
public string RemindMessageFormat { get; set; }
public Dictionary<string, List<string>> CustomReactions { get; set; }
public List<string> RotatingStatuses { get; set; }
public CommandPrefixes0_9 CommandPrefixes { get; set; }
public List<ulong> ServerBlacklist { get; set; }
public List<ulong> ChannelBlacklist { get; set; }
public List<ulong> UserBlacklist { get; set; }
public List<string> _8BallResponses { get; set; }
public string CurrencySign { get; set; }
public string CurrencyName { get; set; }
public string DMHelpString { get; set; }
public string HelpString { get; set; }
}
/// <summary>
/// Holds a permission list
/// </summary>
public class Permissions
{
/// <summary>
/// Name of the parent object whose permissions these are
/// </summary>
public string Name { get; set; }
/// <summary>
/// Module name with allowed/disallowed
/// </summary>
public ConcurrentDictionary<string, bool> Modules { get; set; }
/// <summary>
/// Command name with allowed/disallowed
/// </summary>
public ConcurrentDictionary<string, bool> Commands { get; set; }
/// <summary>
/// Should the bot filter invites to other discord servers (and ref links in the future)
/// </summary>
public bool FilterInvites { get; set; }
/// <summary>
/// Should the bot filter words which are specified in the Words hashset
/// </summary>
public bool FilterWords { get; set; }
public Permissions(string name)
{
Name = name;
Modules = new ConcurrentDictionary<string, bool>();
Commands = new ConcurrentDictionary<string, bool>();
FilterInvites = false;
FilterWords = false;
}
public void CopyFrom(Permissions other)
{
Modules.Clear();
foreach (var mp in other.Modules)
Modules.AddOrUpdate(mp.Key, mp.Value, (s, b) => mp.Value);
Commands.Clear();
foreach (var cp in other.Commands)
Commands.AddOrUpdate(cp.Key, cp.Value, (s, b) => cp.Value);
FilterInvites = other.FilterInvites;
FilterWords = other.FilterWords;
}
public override string ToString()
{
var toReturn = "";
var bannedModules = Modules.Where(kvp => kvp.Value == false);
var bannedModulesArray = bannedModules as KeyValuePair<string, bool>[] ?? bannedModules.ToArray();
if (bannedModulesArray.Any())
{
toReturn += "`Banned Modules:`\n";
toReturn = bannedModulesArray.Aggregate(toReturn, (current, m) => current + $"\t`[x] {m.Key}`\n");
}
var bannedCommands = Commands.Where(kvp => kvp.Value == false);
var bannedCommandsArr = bannedCommands as KeyValuePair<string, bool>[] ?? bannedCommands.ToArray();
if (bannedCommandsArr.Any())
{
toReturn += "`Banned Commands:`\n";
toReturn = bannedCommandsArr.Aggregate(toReturn, (current, c) => current + $"\t`[x] {c.Key}`\n");
}
return toReturn;
}
}
public class ServerPermissions0_9
{
/// <summary>
/// The guy who can edit the permissions
/// </summary>
public string PermissionsControllerRole { get; set; }
/// <summary>
/// Does it print the error when a restriction occurs
/// </summary>
public bool Verbose { get; set; }
/// <summary>
/// The id of the thing (user/server/channel)
/// </summary>
public ulong Id { get; set; } //a string because of the role name.
/// <summary>
/// Permission object bound to the id of something/role name
/// </summary>
public Permissions Permissions { get; set; }
/// <summary>
/// Banned words, usually profanities, like word "java"
/// </summary>
public HashSet<string> Words { get; set; }
public Dictionary<ulong, Permissions> UserPermissions { get; set; }
public Dictionary<ulong, Permissions> ChannelPermissions { get; set; }
public Dictionary<ulong, Permissions> RolePermissions { get; set; }
/// <summary>
/// Dictionary of command names with their respective cooldowns
/// </summary>
public ConcurrentDictionary<string, int> CommandCooldowns { get; set; }
public ServerPermissions0_9(ulong id, string name)
{
Id = id;
PermissionsControllerRole = "Nadeko";
Verbose = true;
Permissions = new Permissions(name);
Permissions.Modules.TryAdd("NSFW", false);
UserPermissions = new Dictionary<ulong, Permissions>();
ChannelPermissions = new Dictionary<ulong, Permissions>();
RolePermissions = new Dictionary<ulong, Permissions>();
CommandCooldowns = new ConcurrentDictionary<string, int>();
Words = new HashSet<string>();
}
}
public class ServerSpecificConfig
{
public bool VoicePlusTextEnabled { get; set; }
public bool SendPrivateMessageOnMention { get; set; }
public ulong? LogChannel { get; set; } = null;
public ulong? LogPresenceChannel { get; set; } = null;
public HashSet<ulong> LogserverIgnoreChannels { get; set; }
public ConcurrentDictionary<ulong, ulong> VoiceChannelLog { get; set; }
public HashSet<ulong> ListOfSelfAssignableRoles { get; set; }
public ulong AutoAssignedRole { get; set; }
public ConcurrentDictionary<ulong, int> GenerateCurrencyChannels { get; set; }
public bool AutoDeleteMessagesOnCommand { get; set; }
public bool ExclusiveSelfAssignedRoles { get; set; }
public float DefaultMusicVolume { get; set; }
public HashSet<StreamNotificationConfig0_9> ObservingStreams { get; set; }
}
public class StreamNotificationConfig0_9
{
public string Username { get; set; }
public StreamType Type { get; set; }
public ulong ServerId { get; set; }
public ulong ChannelId { get; set; }
public bool LastStatus { get; set; }
public enum StreamType
{
Twitch,
Beam,
Hitbox,
YoutubeGaming
}
}
}

View File

@ -1,9 +0,0 @@
using System;
namespace NadekoBot.Modules.Administration.Common.Migration
{
public class MigrationException : Exception
{
}
}

View File

@ -1,381 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Discord.Commands;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using Newtonsoft.Json;
using System.Collections.Concurrent;
using NadekoBot.Extensions;
using NadekoBot.Core.Services.Database;
using Microsoft.Data.Sqlite;
using NadekoBot.Common.Attributes;
using NadekoBot.Common.Collections;
using NadekoBot.Modules.Administration.Common.Migration;
namespace NadekoBot.Modules.Administration
{
public partial class Administration
{
[Group]
public class MigrationCommands : NadekoSubmodule
{
private const int CURRENT_VERSION = 1;
private readonly DbService _db;
public MigrationCommands(DbService db)
{
_db = db;
}
[NadekoCommand, Usage, Description, Aliases]
[OwnerOnly]
public async Task MigrateData()
{
var version = 0;
using (var uow = _db.UnitOfWork)
{
version = uow.BotConfig.GetOrCreate().MigrationVersion;
}
try
{
for (var i = version; i < CURRENT_VERSION; i++)
{
switch (i)
{
case 0:
Migrate0_9To1_0();
break;
}
}
await ReplyConfirmLocalized("migration_done").ConfigureAwait(false);
}
catch (Exception ex)
{
_log.Error(ex);
await ReplyErrorLocalized("migration_error").ConfigureAwait(false);
}
}
private void Migrate0_9To1_0()
{
using (var uow = _db.UnitOfWork)
{
var botConfig = uow.BotConfig.GetOrCreate();
MigrateConfig0_9(uow, botConfig);
MigratePermissions0_9(uow);
MigrateServerSpecificConfigs0_9(uow);
MigrateDb0_9(uow);
//NOW save it
_log.Warn("Writing to disc");
uow.Complete();
botConfig.MigrationVersion = 1;
}
}
private void MigrateDb0_9(IUnitOfWork uow)
{
var db = new SqliteConnection("Data Source=data/nadekobot.sqlite");
if (!File.Exists("data/nadekobot.sqlite"))
{
_log.Warn("No data from the old database will be migrated.");
return;
}
db.Open();
var com = db.CreateCommand();
var i = 0;
try
{
com.CommandText = "SELECT * FROM Announcement";
var reader = com.ExecuteReader();
while (reader.Read())
{
var gid = (ulong)(long)reader["ServerId"];
var greet = (long)reader["Greet"] == 1;
var greetDM = (long)reader["GreetPM"] == 1;
var greetChannel = (ulong)(long)reader["GreetChannelId"];
var greetMsg = (string)reader["GreetText"];
var bye = (long)reader["Bye"] == 1;
var byeChannel = (ulong)(long)reader["ByeChannelId"];
var byeMsg = (string)reader["ByeText"];
var gc = uow.GuildConfigs.For(gid, set => set);
if (greetDM)
gc.SendDmGreetMessage = greet;
else
gc.SendChannelGreetMessage = greet;
gc.GreetMessageChannelId = greetChannel;
gc.ChannelGreetMessageText = greetMsg;
gc.SendChannelByeMessage = bye;
gc.ByeMessageChannelId = byeChannel;
gc.ChannelByeMessageText = byeMsg;
_log.Info(++i);
}
}
catch {
_log.Warn("Greet/bye messages won't be migrated");
}
var com2 = db.CreateCommand();
com2.CommandText = "SELECT * FROM CurrencyState GROUP BY UserId";
i = 0;
try
{
var reader2 = com2.ExecuteReader();
while (reader2.Read())
{
_log.Info(++i);
var curr = new Currency()
{
Amount = (long)reader2["Value"],
UserId = (ulong)(long)reader2["UserId"]
};
uow.Currency.Add(curr);
}
}
catch
{
_log.Warn("Currency won't be migrated");
}
db.Close();
try { File.Move("data/nadekobot.sqlite", "data/DELETE_ME_nadekobot.sqlite"); } catch { }
}
private void MigrateServerSpecificConfigs0_9(IUnitOfWork uow)
{
const string specificConfigsPath = "data/ServerSpecificConfigs.json";
if (!File.Exists(specificConfigsPath))
{
_log.Warn($"No data from {specificConfigsPath} will be migrated.");
return;
}
var configs = new ConcurrentDictionary<ulong, ServerSpecificConfig>();
try
{
configs = JsonConvert
.DeserializeObject<ConcurrentDictionary<ulong, ServerSpecificConfig>>(
File.ReadAllText(specificConfigsPath), new JsonSerializerSettings()
{
Error = (s, e) =>
{
if (e.ErrorContext.Member.ToString() == "GenerateCurrencyChannels")
{
e.ErrorContext.Handled = true;
}
}
});
}
catch (Exception ex)
{
_log.Warn(ex, "ServerSpecificConfig deserialization failed");
return;
}
var i = 0;
var selfAssRoles = new ConcurrentHashSet<SelfAssignedRole>();
configs
.Select(p => new { data = p.Value, gconfig = uow.GuildConfigs.For(p.Key) })
.AsParallel()
.ForAll(config =>
{
try
{
var guildConfig = config.gconfig;
var data = config.data;
guildConfig.AutoAssignRoleId = data.AutoAssignedRole;
guildConfig.DeleteMessageOnCommand = data.AutoDeleteMessagesOnCommand;
guildConfig.DefaultMusicVolume = data.DefaultMusicVolume;
guildConfig.ExclusiveSelfAssignedRoles = data.ExclusiveSelfAssignedRoles;
guildConfig.GenerateCurrencyChannelIds = new HashSet<GCChannelId>(data.GenerateCurrencyChannels.Select(gc => new GCChannelId() { ChannelId = gc.Key }));
selfAssRoles.AddRange(data.ListOfSelfAssignableRoles.Select(r => new SelfAssignedRole() { GuildId = guildConfig.GuildId, RoleId = r }).ToArray());
guildConfig.LogSetting.IgnoredChannels = new HashSet<IgnoredLogChannel>(data.LogserverIgnoreChannels.Select(id => new IgnoredLogChannel() { ChannelId = id }));
guildConfig.LogSetting.LogUserPresenceId = data.LogPresenceChannel;
guildConfig.FollowedStreams = new HashSet<FollowedStream>(data.ObservingStreams.Select(x =>
{
FollowedStream.FollowedStreamType type = FollowedStream.FollowedStreamType.Twitch;
switch (x.Type)
{
case StreamNotificationConfig0_9.StreamType.Twitch:
type = FollowedStream.FollowedStreamType.Twitch;
break;
case StreamNotificationConfig0_9.StreamType.Beam:
type = FollowedStream.FollowedStreamType.Mixer;
break;
case StreamNotificationConfig0_9.StreamType.Hitbox:
type = FollowedStream.FollowedStreamType.Smashcast;
break;
default:
break;
}
return new FollowedStream()
{
ChannelId = x.ChannelId,
GuildId = guildConfig.GuildId,
Username = x.Username.ToLowerInvariant(),
Type = type
};
}));
guildConfig.VoicePlusTextEnabled = data.VoicePlusTextEnabled;
_log.Info("Migrating SpecificConfig for {0} done ({1})", guildConfig.GuildId, ++i);
}
catch (Exception ex)
{
_log.Error(ex);
}
});
uow.SelfAssignedRoles.AddRange(selfAssRoles.ToArray());
try { File.Move("data/ServerSpecificConfigs.json", "data/DELETE_ME_ServerSpecificCOnfigs.json"); } catch { }
}
private void MigratePermissions0_9(IUnitOfWork uow)
{
var permissionsDict = new ConcurrentDictionary<ulong, ServerPermissions0_9>();
if (!Directory.Exists("data/permissions/"))
{
_log.Warn("No data from permissions will be migrated.");
return;
}
foreach (var file in Directory.EnumerateFiles("data/permissions/"))
{
try
{
var strippedFileName = Path.GetFileNameWithoutExtension(file);
if (string.IsNullOrWhiteSpace(strippedFileName)) continue;
var id = ulong.Parse(strippedFileName);
var data = JsonConvert.DeserializeObject<ServerPermissions0_9>(File.ReadAllText(file));
permissionsDict.TryAdd(id, data);
}
catch
{
// ignored
}
}
var i = 0;
permissionsDict
.Select(p => new { data = p.Value, gconfig = uow.GuildConfigs.For(p.Key) })
.AsParallel()
.ForAll(perms =>
{
try
{
var data = perms.data;
var gconfig = perms.gconfig;
gconfig.PermissionRole = data.PermissionsControllerRole;
gconfig.VerbosePermissions = data.Verbose;
gconfig.FilteredWords = new HashSet<FilteredWord>(data.Words.Select(w => w.ToLowerInvariant())
.Distinct()
.Select(w => new FilteredWord() { Word = w }));
gconfig.FilterWords = data.Permissions.FilterWords;
gconfig.FilterInvites = data.Permissions.FilterInvites;
gconfig.FilterInvitesChannelIds = new HashSet<FilterChannelId>();
gconfig.FilterInvitesChannelIds.AddRange(data.ChannelPermissions.Where(kvp => kvp.Value.FilterInvites)
.Select(cp => new FilterChannelId()
{
ChannelId = cp.Key
}));
gconfig.FilterWordsChannelIds = new HashSet<FilterChannelId>();
gconfig.FilterWordsChannelIds.AddRange(data.ChannelPermissions.Where(kvp => kvp.Value.FilterWords)
.Select(cp => new FilterChannelId()
{
ChannelId = cp.Key
}));
gconfig.CommandCooldowns = new HashSet<CommandCooldown>(data.CommandCooldowns
.Where(cc => !string.IsNullOrWhiteSpace(cc.Key) && cc.Value > 0)
.Select(cc => new CommandCooldown()
{
CommandName = cc.Key,
Seconds = cc.Value
}));
_log.Info("Migrating data from permissions folder for {0} done ({1})", gconfig.GuildId, ++i);
}
catch (Exception ex)
{
_log.Error(ex);
}
});
try { Directory.Move("data/permissions", "data/DELETE_ME_permissions"); } catch { }
}
private void MigrateConfig0_9(IUnitOfWork uow, BotConfig botConfig)
{
Config0_9 oldConfig;
const string configPath = "data/config.json";
try
{
oldConfig = JsonConvert.DeserializeObject<Config0_9>(File.ReadAllText(configPath));
}
catch (FileNotFoundException)
{
_log.Warn("config.json not found");
return;
}
catch (Exception)
{
_log.Error("Unknown error while deserializing file config.json, pls check its integrity, aborting migration");
throw new MigrationException();
}
//Basic
botConfig.ForwardMessages = oldConfig.ForwardMessages;
botConfig.ForwardToAllOwners = oldConfig.ForwardToAllOwners;
botConfig.BufferSize = (ulong)oldConfig.BufferSize;
botConfig.RemindMessageFormat = oldConfig.RemindMessageFormat;
botConfig.CurrencySign = oldConfig.CurrencySign;
botConfig.CurrencyName = oldConfig.CurrencyName;
botConfig.DMHelpString = oldConfig.DMHelpString;
botConfig.HelpString = oldConfig.HelpString;
//messages
botConfig.RotatingStatuses = oldConfig.IsRotatingStatus;
var messages = new List<PlayingStatus>();
oldConfig.RotatingStatuses.ForEach(i => messages.Add(new PlayingStatus { Status = i }));
botConfig.RotatingStatusMessages = messages;
//Blacklist
var blacklist = new HashSet<BlacklistItem>(oldConfig.ServerBlacklist.Select(server => new BlacklistItem() { ItemId = server, Type = BlacklistType.Server }));
blacklist.AddRange(oldConfig.ChannelBlacklist.Select(channel => new BlacklistItem() { ItemId = channel, Type = BlacklistType.Channel }));
blacklist.AddRange(oldConfig.UserBlacklist.Select(user => new BlacklistItem() { ItemId = user, Type = BlacklistType.User }));
botConfig.Blacklist = blacklist;
//Eightball
botConfig.EightBallResponses = new HashSet<EightBallResponse>(oldConfig._8BallResponses.Select(response => new EightBallResponse() { Text = response }));
//customreactions
uow.CustomReactions.AddRange(oldConfig.CustomReactions.SelectMany(cr =>
{
return cr.Value.Select(res => new CustomReaction()
{
GuildId = null,
IsRegex = false,
OwnerOnly = false,
Response = res,
Trigger = cr.Key.ToLowerInvariant(),
});
}).ToArray());
try { File.Move(configPath, "./data/DELETE_ME_config.json"); } catch { }
}
}
}
}

View File

@ -1,91 +1,91 @@
using Discord.Commands; //using Discord.Commands;
using NadekoBot.Common.Attributes; //using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Administration.Services; //using NadekoBot.Modules.Administration.Services;
using NadekoBot.Extensions; //using NadekoBot.Extensions;
using System; //using System;
using System.IO; //using System.IO;
using System.Reflection; //using System.Reflection;
using System.Text.RegularExpressions; //using System.Text.RegularExpressions;
using System.Threading.Tasks; //using System.Threading.Tasks;
using System.Linq; //using System.Linq;
namespace NadekoBot.Modules.Administration //namespace NadekoBot.Modules.Administration
{ //{
public partial class Administration // public partial class Administration
{ // {
[Group] // [Group]
public class PackagesCommands : NadekoSubmodule<PackagesService> // public class PackagesCommands : NadekoSubmodule<PackagesService>
{ // {
private readonly NadekoBot _bot; // private readonly NadekoBot _bot;
public PackagesCommands(NadekoBot bot) // public PackagesCommands(NadekoBot bot)
{ // {
_bot = bot; // _bot = bot;
} // }
[NadekoCommand, Usage, Description, Aliases] // [NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)] // [RequireContext(ContextType.Guild)]
public async Task PackageList() // public async Task PackageList()
{ // {
_service.ReloadAvailablePackages(); // _service.ReloadAvailablePackages();
await Context.Channel.SendConfirmAsync( // await Context.Channel.SendConfirmAsync(
string.Join( // string.Join(
"\n", // "\n",
_service.Packages // _service.Packages
.Select(x => _bot.LoadedPackages.Contains(x) // .Select(x => _bot.LoadedPackages.Contains(x)
? "✅ " + x // ? "【✘】" + x
: x))); // : "【 】" + x)));
} // }
[NadekoCommand, Usage, Description, Aliases] // [NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)] // [RequireContext(ContextType.Guild)]
[OwnerOnly] // [OwnerOnly]
public async Task PackageUnload(string name) // public async Task PackageUnload(string name)
{ // {
if (name.Contains(":") || name.Contains(".") || name.Contains("\\") || name.Contains("/") || name.Contains("~")) // if (name.Contains(":") || name.Contains(".") || name.Contains("\\") || name.Contains("/") || name.Contains("~"))
return; // return;
name = name.ToTitleCase(); // name = name.ToTitleCase();
var package = Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, // var package = Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory,
"modules", // "modules",
$"NadekoBot.Modules.{name}", // $"NadekoBot.Modules.{name}",
$"NadekoBot.Modules.{name}.dll")); // $"NadekoBot.Modules.{name}.dll"));
await _bot.UnloadPackage(name).ConfigureAwait(false); // await _bot.UnloadPackage(name).ConfigureAwait(false);
await ReplyAsync(":ok:"); // await ReplyAsync(":ok:");
} // }
[NadekoCommand, Usage, Description, Aliases] // [NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)] // [RequireContext(ContextType.Guild)]
[OwnerOnly] // [OwnerOnly]
public async Task PackageLoad(string name) // public async Task PackageLoad(string name)
{ // {
if (name.Contains(".") || name.Contains("\\") || name.Contains("/") || name.Contains("~")) // if (name.Contains(".") || name.Contains("\\") || name.Contains("/") || name.Contains("~"))
return; // return;
name = name.ToTitleCase(); // name = name.ToTitleCase();
if (await _bot.LoadPackage(name)) // if (await _bot.LoadPackage(name))
await ReplyAsync(":ok:"); // await ReplyAsync(":ok:");
else // else
await ReplyAsync(":x:"); // await ReplyAsync(":x:");
} // }
[NadekoCommand, Usage, Description, Aliases] // [NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)] // [RequireContext(ContextType.Guild)]
[OwnerOnly] // [OwnerOnly]
public async Task PackageReload(string name) // public async Task PackageReload(string name)
{ // {
if (name.Contains(".") || name.Contains("\\") || name.Contains("/") || name.Contains("~")) // if (name.Contains(".") || name.Contains("\\") || name.Contains("/") || name.Contains("~"))
return; // return;
name = name.ToTitleCase(); // name = name.ToTitleCase();
if (await _bot.UnloadPackage(name)) // if (await _bot.UnloadPackage(name))
{ // {
await _bot.LoadPackage(name); // await _bot.LoadPackage(name);
await ReplyAsync(":ok:"); // await ReplyAsync(":ok:");
} // }
else // else
await ReplyAsync(":x:"); // await ReplyAsync(":x:");
} // }
} // }
} // }
} //}

View File

@ -0,0 +1,172 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using System;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
using NadekoBot.Modules.Gambling.Common.AnimalRacing;
using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class AnimalRacingCommands : NadekoSubmodule<AnimalRaceService>
{
private readonly IBotConfigProvider _bc;
private readonly CurrencyService _cs;
private readonly DiscordSocketClient _client;
public AnimalRacingCommands(IBotConfigProvider bc, CurrencyService cs, DiscordSocketClient client)
{
_bc = bc;
_cs = cs;
_client = client;
}
private IUserMessage raceMessage = null;
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public Task Race()
{
var ar = new AnimalRace(_cs, _bc.BotConfig.RaceAnimals.Shuffle().ToArray());
if (!_service.AnimalRaces.TryAdd(Context.Guild.Id, ar))
return Context.Channel.SendErrorAsync(GetText("animal_race"), GetText("animal_race_already_started"));
ar.Initialize();
var count = 0;
Task _client_MessageReceived(SocketMessage arg)
{
var _ = Task.Run(() => {
try
{
if (arg.Channel.Id == Context.Channel.Id)
{
if (ar.CurrentPhase == AnimalRace.Phase.Running && ++count % 9 == 0)
{
raceMessage = null;
}
}
}
catch { }
});
return Task.CompletedTask;
}
Task Ar_OnEnded(AnimalRace race)
{
_client.MessageReceived -= _client_MessageReceived;
_service.AnimalRaces.TryRemove(Context.Guild.Id, out _);
var winner = race.FinishedUsers[0];
if (race.FinishedUsers[0].Bet > 0)
{
return Context.Channel.SendConfirmAsync(GetText("animal_race"),
GetText("animal_race_won_money", Format.Bold(winner.Username),
winner.Animal.Icon, (race.FinishedUsers[0].Bet * (race.Users.Length - 1)) + _bc.BotConfig.CurrencySign));
}
else
{
return Context.Channel.SendConfirmAsync(GetText("animal_race"),
GetText("animal_race_won", Format.Bold(winner.Username), winner.Animal.Icon));
}
}
ar.OnStartingFailed += Ar_OnStartingFailed;
ar.OnStateUpdate += Ar_OnStateUpdate;
ar.OnEnded += Ar_OnEnded;
ar.OnStarted += Ar_OnStarted;
_client.MessageReceived += _client_MessageReceived;
return Context.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_starting"),
footer: GetText("animal_race_join_instr", Prefix));
}
private Task Ar_OnStarted(AnimalRace race)
{
if(race.Users.Length == race.MaxUsers)
return Context.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_full"));
else
return Context.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_starting_with_x", race.Users.Length));
}
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 == null)
raceMessage = await Context.Channel.SendConfirmAsync(text)
.ConfigureAwait(false);
else
await msg.ModifyAsync(x => x.Embed = new EmbedBuilder()
.WithTitle(GetText("animal_race"))
.WithDescription(text)
.WithOkColor()
.Build())
.ConfigureAwait(false);
}
private Task Ar_OnStartingFailed(AnimalRace race)
{
_service.AnimalRaces.TryRemove(Context.Guild.Id, out _);
return ReplyErrorLocalized("animal_race_failed");
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task JoinRace(int amount = 0)
{
if (!_service.AnimalRaces.TryGetValue(Context.Guild.Id, out var ar))
{
await ReplyErrorLocalized("race_not_exist").ConfigureAwait(false);
return;
}
try
{
var user = await ar.JoinRace(Context.User.Id, Context.User.ToString(), amount)
.ConfigureAwait(false);
if (amount > 0)
await Context.Channel.SendConfirmAsync(GetText("animal_race_join_bet", Context.User.Mention, user.Animal.Icon, amount + _bc.BotConfig.CurrencySign)).ConfigureAwait(false);
else
await Context.Channel.SendConfirmAsync(GetText("animal_race_join", Context.User.Mention, user.Animal.Icon)).ConfigureAwait(false);
}
catch (ArgumentOutOfRangeException)
{
//ignore if user inputed an invalid amount
}
catch (AlreadyJoinedException)
{
// just ignore this
}
catch (AlreadyStartedException)
{
//ignore
}
catch (AnimalRaceFullException)
{
await Context.Channel.SendConfirmAsync(GetText("animal_race"), GetText("animal_race_full"))
.ConfigureAwait(false);
}
catch (NotEnoughFundsException)
{
await Context.Channel.SendErrorAsync(GetText("not_enough", _bc.BotConfig.CurrencySign)).ConfigureAwait(false);
}
}
}
}
}

View File

@ -0,0 +1,161 @@
using NadekoBot.Common;
using NadekoBot.Extensions;
using NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing
{
public class AnimalRace : IDisposable
{
public enum Phase
{
WaitingForPlayers,
Running,
Ended,
}
private const int _startingDelayMiliseconds = 20_000;
public Phase CurrentPhase = Phase.WaitingForPlayers;
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 ImmutableArray<AnimalRacingUser> Users => _users.ToImmutableArray();
public List<AnimalRacingUser> FinishedUsers { get; } = new List<AnimalRacingUser>();
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
private readonly HashSet<AnimalRacingUser> _users = new HashSet<AnimalRacingUser>();
private readonly CurrencyService _currency;
private readonly Queue<RaceAnimal> _animalsQueue;
public int MaxUsers { get; }
public AnimalRace(CurrencyService currency, RaceAnimal[] availableAnimals)
{
this._currency = currency;
this._animalsQueue = new Queue<RaceAnimal>(availableAnimals);
this.MaxUsers = availableAnimals.Length;
if (this._animalsQueue.Count == 0)
CurrentPhase = Phase.Ended;
}
public void Initialize() //lame name
{
var _t = Task.Run(async () =>
{
await Task.Delay(_startingDelayMiliseconds).ConfigureAwait(false);
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (CurrentPhase != Phase.WaitingForPlayers)
return;
await Start().ConfigureAwait(false);
}
finally { _locker.Release(); }
});
}
public async Task<AnimalRacingUser> JoinRace(ulong userId, string userName, int bet = 0)
{
if (bet < 0)
throw new ArgumentOutOfRangeException(nameof(bet));
var user = new AnimalRacingUser(userName, userId, bet);
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (_users.Count == MaxUsers)
throw new AnimalRaceFullException();
if (CurrentPhase != Phase.WaitingForPlayers)
throw new AlreadyStartedException();
if (!await _currency.RemoveAsync(userId, "BetRace", bet).ConfigureAwait(false))
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().ConfigureAwait(false);
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, "Race refund", user.Bet).ConfigureAwait(false);
}
var _sf = OnStartingFailed?.Invoke(this);
CurrentPhase = Phase.Ended;
return;
}
var _ = OnStarted?.Invoke(this);
var _t = 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);
var _ignore = OnStateUpdate?.Invoke(this);
await Task.Delay(2500).ConfigureAwait(false);
}
if (FinishedUsers[0].Bet > 0)
await _currency.AddAsync(FinishedUsers[0].UserId, "Won a Race", FinishedUsers[0].Bet * (_users.Count - 1))
.ConfigureAwait(false);
var _ended = OnEnded?.Invoke(this);
});
}
public void Dispose()
{
CurrentPhase = Phase.Ended;
OnStarted = null;
OnEnded = null;
OnStartingFailed = null;
OnStateUpdate = null;
_locker.Dispose();
_users.Clear();
}
}
}

View File

@ -0,0 +1,32 @@
using NadekoBot.Core.Services.Database.Models;
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing
{
public class AnimalRacingUser
{
public int 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, int bet)
{
this.Bet = bet;
this.Username = username;
this.UserId = userId;
}
public override bool Equals(object obj)
{
return obj is AnimalRacingUser x
? x.UserId == this.UserId
: false;
}
public override int GetHashCode()
{
return this.UserId.GetHashCode();
}
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions
{
public class AlreadyJoinedException : Exception
{
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions
{
public class AlreadyStartedException : Exception
{
}
}

View File

@ -0,0 +1,8 @@
using System;
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions
{
public class AnimalRaceFullException : Exception
{
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions
{
public class NotEnoughFundsException : Exception
{
}
}

View File

@ -0,0 +1,234 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NadekoBot.Common;
using NadekoBot.Extensions;
namespace NadekoBot.Modules.Gambling.Common
{
public class Cards
{
private static readonly Dictionary<int, string> cardNames = new Dictionary<int, string>() {
{ 1, "Ace" },
{ 2, "Two" },
{ 3, "Three" },
{ 4, "Four" },
{ 5, "Five" },
{ 6, "Six" },
{ 7, "Seven" },
{ 8, "Eight" },
{ 9, "Nine" },
{ 10, "Ten" },
{ 11, "Jack" },
{ 12, "Queen" },
{ 13, "King" }
};
private static Dictionary<string, Func<List<Card>, bool>> handValues;
public enum CardSuit
{
Spades = 1,
Hearts = 2,
Diamonds = 3,
Clubs = 4
}
public class Card : IComparable
{
public CardSuit Suit { get; }
public int Number { get; }
public string Name
{
get
{
var str = "";
if (Number <= 10 && Number > 1)
{
str += "_" + Number;
}
else {
str += GetName().ToLower();
}
return str + "_of_" + Suit.ToString().ToLower();
}
}
public Card(CardSuit s, int cardNum)
{
this.Suit = s;
this.Number = cardNum;
}
public string GetName() => cardNames[Number];
public override string ToString() => cardNames[Number] + " Of " + Suit;
public int CompareTo(object obj)
{
if (!(obj is Card)) return 0;
var c = (Card)obj;
return this.Number - c.Number;
}
}
private List<Card> cardPool;
public List<Card> CardPool
{
get { return cardPool; }
set { cardPool = value; }
}
/// <summary>
/// Creates a new instance of the BlackJackGame, this allows you to create multiple games running at one time.
/// </summary>
public Cards()
{
cardPool = new List<Card>(52);
RefillPool();
InitHandValues();
}
/// <summary>
/// Restart the game of blackjack. It will only refill the pool for now. Probably wont be used, unless you want to have only 1 bjg running at one time,
/// then you will restart the same game every time.
/// </summary>
public void Restart() => RefillPool();
/// <summary>
/// Removes all cards from the pool and refills the pool with all of the possible cards. NOTE: I think this is too expensive.
/// We should probably make it so it copies another premade list with all the cards, or something.
/// </summary>
private void RefillPool()
{
cardPool.Clear();
//foreach suit
for (var j = 1; j < 14; j++)
{
// and number
for (var i = 1; i < 5; i++)
{
//generate a card of that suit and number and add it to the pool
// the pool will go from ace of spades,hears,diamonds,clubs all the way to the king of spades. hearts, ...
cardPool.Add(new Card((CardSuit)i, j));
}
}
}
private Random r = new NadekoRandom();
/// <summary>
/// Take a card from the pool, you either take it from the top if the deck is shuffled, or from a random place if the deck is in the default order.
/// </summary>
/// <returns>A card from the pool</returns>
public Card DrawACard()
{
if (CardPool.Count == 0)
Restart();
//you can either do this if your deck is not shuffled
var num = r.Next(0, cardPool.Count);
var c = cardPool[num];
cardPool.RemoveAt(num);
return c;
// if you want to shuffle when you fill, then take the first one
/*
Card c = cardPool[0];
cardPool.RemoveAt(0);
return c;
*/
}
/// <summary>
/// Shuffles the deck. Use this if you want to take cards from the top of the deck, instead of randomly. See DrawACard method.
/// </summary>
private void Shuffle()
{
if (cardPool.Count <= 1) return;
var orderedPool = cardPool.Shuffle();
cardPool = cardPool as List<Card> ?? orderedPool.ToList();
}
public override string ToString() => string.Concat(cardPool.Select(c => c.ToString())) + Environment.NewLine;
private static void InitHandValues()
{
Func<List<Card>, bool> hasPair =
cards => cards.GroupBy(card => card.Number)
.Count(group => group.Count() == 2) == 1;
Func<List<Card>, bool> isPair =
cards => cards.GroupBy(card => card.Number)
.Count(group => group.Count() == 3) == 0
&& hasPair(cards);
Func<List<Card>, bool> isTwoPair =
cards => cards.GroupBy(card => card.Number)
.Count(group => group.Count() == 2) == 2;
Func<List<Card>, bool> isStraight =
cards =>
{
if (cards.GroupBy(card => card.Number).Count() != cards.Count())
return false;
var toReturn = (cards.Max(card => (int)card.Number)
- cards.Min(card => (int)card.Number) == 4);
if (toReturn || cards.All(c => c.Number != 1)) return toReturn;
var newCards = cards.Select(c => c.Number == 1 ? new Card(c.Suit, 14) : c);
return (newCards.Max(card => (int)card.Number)
- newCards.Min(card => (int)card.Number) == 4);
};
Func<List<Card>, bool> hasThreeOfKind =
cards => cards.GroupBy(card => card.Number)
.Any(group => group.Count() == 3);
Func<List<Card>, bool> isThreeOfKind =
cards => hasThreeOfKind(cards) && !hasPair(cards);
Func<List<Card>, bool> isFlush =
cards => cards.GroupBy(card => card.Suit).Count() == 1;
Func<List<Card>, bool> isFourOfKind =
cards => cards.GroupBy(card => card.Number)
.Any(group => group.Count() == 4);
Func<List<Card>, bool> isFullHouse =
cards => hasPair(cards) && hasThreeOfKind(cards);
Func<List<Card>, bool> hasStraightFlush =
cards => isFlush(cards) && isStraight(cards);
Func<List<Card>, bool> isRoyalFlush =
cards => cards.Min(card => card.Number) == 1 &&
cards.Max(card => card.Number) == 13
&& hasStraightFlush(cards);
Func<List<Card>, bool> isStraightFlush =
cards => hasStraightFlush(cards) && !isRoyalFlush(cards);
handValues = new Dictionary<string, Func<List<Card>, bool>>
{
{ "Royal Flush", isRoyalFlush },
{ "Straight Flush", isStraightFlush },
{ "Four Of A Kind", isFourOfKind },
{ "Full House", isFullHouse },
{ "Flush", isFlush },
{ "Straight", isStraight },
{ "Three Of A Kind", isThreeOfKind },
{ "Two Pairs", isTwoPair },
{ "A Pair", isPair }
};
}
public static string GetHandValue(List<Card> cards)
{
if (handValues == null)
InitHandValues();
foreach (var kvp in handValues.Where(x => x.Value(cards)))
{
return kvp.Key;
}
return "High card " + (cards.FirstOrDefault(c => c.Number == 1)?.GetName() ?? cards.Max().GetName());
}
}
}

View File

@ -0,0 +1,12 @@
using Discord;
using Discord.Commands;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Gambling.Common
{
public abstract class CurrencyEvent
{
public abstract Task Stop();
public abstract Task Start(IUserMessage msg, ICommandContext channel);
}
}

View File

@ -0,0 +1,144 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Common.Collections;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NLog;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Gambling.Common
{
public class ReactionEvent : CurrencyEvent
{
private readonly ConcurrentHashSet<ulong> _reactionAwardedUsers = new ConcurrentHashSet<ulong>();
private readonly BotConfig _bc;
private readonly Logger _log;
private readonly DiscordSocketClient _client;
private readonly CurrencyService _cs;
private readonly SocketSelfUser _botUser;
private IUserMessage StartingMessage { get; set; }
private CancellationTokenSource Source { get; }
private CancellationToken CancelToken { get; }
private readonly ConcurrentQueue<ulong> _toGiveTo = new ConcurrentQueue<ulong>();
private readonly int _amount;
public ReactionEvent(BotConfig bc, DiscordSocketClient client, CurrencyService cs, int amount)
{
_bc = bc;
_log = LogManager.GetCurrentClassLogger();
_client = client;
_cs = cs;
_botUser = client.CurrentUser;
_amount = amount;
Source = new CancellationTokenSource();
CancelToken = Source.Token;
var _ = Task.Run(async () =>
{
var users = new List<ulong>();
while (!CancelToken.IsCancellationRequested)
{
await Task.Delay(1000).ConfigureAwait(false);
while (_toGiveTo.TryDequeue(out var usrId))
{
users.Add(usrId);
}
if (users.Count > 0)
{
await _cs.AddToManyAsync("Reaction Event", _amount, users.ToArray()).ConfigureAwait(false);
}
users.Clear();
}
}, CancelToken);
}
public override async Task Stop()
{
if (StartingMessage != null)
await StartingMessage.DeleteAsync().ConfigureAwait(false);
if (!Source.IsCancellationRequested)
Source.Cancel();
_client.MessageDeleted -= MessageDeletedEventHandler;
}
private Task MessageDeletedEventHandler(Cacheable<IMessage, ulong> msg, ISocketMessageChannel channel)
{
if (StartingMessage?.Id == msg.Id)
{
_log.Warn("Stopping flower reaction event because message is deleted.");
var __ = Task.Run(Stop);
}
return Task.CompletedTask;
}
public override async Task Start(IUserMessage umsg, ICommandContext context)
{
StartingMessage = umsg;
_client.MessageDeleted += MessageDeletedEventHandler;
IEmote iemote;
if (Emote.TryParse(_bc.CurrencySign, out var emote))
{
iemote = emote;
}
else
iemote = new Emoji(_bc.CurrencySign);
try { await StartingMessage.AddReactionAsync(iemote).ConfigureAwait(false); }
catch
{
try { await StartingMessage.AddReactionAsync(iemote).ConfigureAwait(false); }
catch
{
try { await StartingMessage.DeleteAsync().ConfigureAwait(false); }
catch { return; }
}
}
using (StartingMessage.OnReaction(_client, (r) =>
{
try
{
if (r.UserId == _botUser.Id)
return;
if (r.Emote.Name == iemote.Name && r.User.IsSpecified && ((DateTime.UtcNow - r.User.Value.CreatedAt).TotalDays > 5) && _reactionAwardedUsers.Add(r.User.Value.Id))
{
_toGiveTo.Enqueue(r.UserId);
}
}
catch
{
// ignored
}
}))
{
try
{
await Task.Delay(TimeSpan.FromHours(24), CancelToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
if (CancelToken.IsCancellationRequested)
return;
_log.Warn("Stopping flower reaction event because it expired.");
await Stop();
}
}
}
}

View File

@ -0,0 +1,99 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Common;
using NadekoBot.Common.Collections;
using NadekoBot.Core.Services;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Gambling.Common.CurrencyEvents
{
public class SneakyEvent : CurrencyEvent
{
public event Action OnEnded;
public string Code { get; private set; } = string.Empty;
private readonly ConcurrentHashSet<ulong> _sneakyGameAwardedUsers = new ConcurrentHashSet<ulong>();
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 CurrencyService _cs;
private readonly DiscordSocketClient _client;
private readonly IBotConfigProvider _bc;
private readonly int _length;
public SneakyEvent(CurrencyService cs, DiscordSocketClient client,
IBotConfigProvider bc, int len)
{
_cs = cs;
_client = client;
_bc = bc;
_length = len;
}
public override async Task Start(IUserMessage msg, ICommandContext channel)
{
GenerateCode();
//start the event
_client.MessageReceived += SneakyGameMessageReceivedEventHandler;
await _client.SetGameAsync($"type {Code} for " + _bc.BotConfig.CurrencyPluralName)
.ConfigureAwait(false);
await Task.Delay(_length * 1000).ConfigureAwait(false);
await Stop().ConfigureAwait(false);
}
private void GenerateCode()
{
var rng = new NadekoRandom();
for (var i = 0; i < 5; i++)
{
Code += _sneakyGameStatusChars[rng.Next(0, _sneakyGameStatusChars.Length)];
}
}
public override async Task Stop()
{
try
{
_client.MessageReceived -= SneakyGameMessageReceivedEventHandler;
Code = string.Empty;
_sneakyGameAwardedUsers.Clear();
await _client.SetGameAsync(null).ConfigureAwait(false);
}
catch { }
finally
{
OnEnded?.Invoke();
}
}
private Task SneakyGameMessageReceivedEventHandler(SocketMessage arg)
{
if (arg.Content == Code &&
_sneakyGameAwardedUsers.Add(arg.Author.Id))
{
var _ = Task.Run(async () =>
{
await _cs.AddAsync(arg.Author, "Sneaky Game Event", 100, false)
.ConfigureAwait(false);
try { await arg.DeleteAsync(new RequestOptions() { RetryMode = RetryMode.AlwaysFail }).ConfigureAwait(false); }
catch
{
// ignored
}
});
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,45 @@
using NadekoBot.Common;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Gambling.Common.WheelOfFortune
{
public class WheelOfFortune
{
private static readonly NadekoRandom _rng = new NadekoRandom();
private static readonly ImmutableArray<string> _emojis = new string[] {
"⬆",
"↖",
"⬅",
"↙",
"⬇",
"↘",
"➡",
"↗" }.ToImmutableArray();
public static readonly ImmutableArray<float> Multipliers = new float[] {
1.7f,
1.5f,
0.2f,
0.1f,
0.3f,
0.5f,
1.2f,
2.4f,
}.ToImmutableArray();
public int Result { get; }
public string Emoji => _emojis[Result];
public float Multiplier => Multipliers[Result];
public WheelOfFortune()
{
this.Result = _rng.Next(0, 8);
}
}
}

View File

@ -0,0 +1,91 @@
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using System.Threading.Tasks;
using Discord.WebSocket;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Modules.Gambling.Common.CurrencyEvents;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class CurrencyEventsCommands : NadekoSubmodule<CurrencyEventsService>
{
public enum CurrencyEvent
{
Reaction,
SneakyGameStatus
}
private readonly DiscordSocketClient _client;
private readonly IBotConfigProvider _bc;
private readonly CurrencyService _cs;
public CurrencyEventsCommands(DiscordSocketClient client, IBotConfigProvider bc, CurrencyService cs)
{
_client = client;
_bc = bc;
_cs = cs;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task StartEvent(CurrencyEvent e, int arg = -1)
{
switch (e)
{
case CurrencyEvent.Reaction:
await ReactionEvent(Context, arg).ConfigureAwait(false);
break;
case CurrencyEvent.SneakyGameStatus:
await SneakyGameStatusEvent(Context, arg).ConfigureAwait(false);
break;
}
}
private async Task SneakyGameStatusEvent(ICommandContext context, int num)
{
if (num < 10 || num > 600)
num = 60;
var ev = new SneakyEvent(_cs, _client, _bc, num);
if (!await _service.StartSneakyEvent(ev, context.Message, context))
return;
try
{
var title = GetText("sneakygamestatus_title");
var desc = GetText("sneakygamestatus_desc",
Format.Bold(100.ToString()) + _bc.BotConfig.CurrencySign,
Format.Bold(num.ToString()));
await context.Channel.SendConfirmAsync(title, desc)
.ConfigureAwait(false);
}
catch
{
// ignored
}
}
public async Task ReactionEvent(ICommandContext context, int amount)
{
if (amount <= 0)
amount = 100;
var title = GetText("reaction_title");
var desc = GetText("reaction_desc", _bc.BotConfig.CurrencySign, Format.Bold(amount.ToString()) + _bc.BotConfig.CurrencySign);
var footer = GetText("reaction_footer", 24);
var re = new ReactionEvent(_bc.BotConfig, _client, _cs, amount);
var msg = await context.Channel.SendConfirmAsync(title,
desc, footer: footer)
.ConfigureAwait(false);
await re.Start(msg, context);
}
}
}
}

View File

@ -0,0 +1,241 @@
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using Image = ImageSharp.Image;
using ImageSharp;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class DriceRollCommands : NadekoSubmodule
{
private readonly Regex dndRegex = new Regex(@"^(?<n1>\d+)d(?<n2>\d+)(?:\+(?<add>\d+))?(?:\-(?<sub>\d+))?$", RegexOptions.Compiled);
private readonly Regex fudgeRegex = new Regex(@"^(?<n1>\d+)d(?:F|f)$", RegexOptions.Compiled);
private readonly char[] _fateRolls = { '-', ' ', '+' };
private readonly IImagesService _images;
public DriceRollCommands(IImagesService images)
{
_images = images;
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Roll()
{
var rng = new NadekoRandom();
var gen = rng.Next(1, 101);
var num1 = gen / 10;
var num2 = gen % 10;
var imageStream = await Task.Run(() =>
{
var ms = new MemoryStream();
new[] { GetDice(num1), GetDice(num2) }.Merge().SaveAsPng(ms);
ms.Position = 0;
return ms;
}).ConfigureAwait(false);
await Context.Channel.SendFileAsync(imageStream,
"dice.png",
Context.User.Mention + " " + GetText("dice_rolled", Format.Code(gen.ToString()))).ConfigureAwait(false);
}
public enum RollOrderType
{
Ordered,
Unordered
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(1)]
public async Task Roll(int num)
{
await InternalRoll(num, true).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(1)]
public async Task Rolluo(int num = 1)
{
await InternalRoll(num, false).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(0)]
public async Task Roll(string arg)
{
await InternallDndRoll(arg, true).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(0)]
public async Task Rolluo(string arg)
{
await InternallDndRoll(arg, false).ConfigureAwait(false);
}
private async Task InternalRoll(int num, bool ordered)
{
if (num < 1 || num > 30)
{
await ReplyErrorLocalized("dice_invalid_number", 1, 30).ConfigureAwait(false);
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, GetDice(randomNumber));
values.Insert(toInsert, randomNumber);
}
var bitmap = dice.Merge();
var ms = new MemoryStream();
bitmap.SaveAsPng(ms);
ms.Position = 0;
await Context.Channel.SendFileAsync(ms, "dice.png",
Context.User.Mention + " " +
GetText("dice_rolled_num", Format.Bold(values.Count.ToString())) +
" " + GetText("total_average",
Format.Bold(values.Sum().ToString()),
Format.Bold((values.Sum() / (1.0f * values.Count)).ToString("N2")))).ConfigureAwait(false);
}
private async Task InternallDndRoll(string arg, bool ordered)
{
Match match;
int n1;
int n2;
if ((match = fudgeRegex.Match(arg)).Length != 0 &&
int.TryParse(match.Groups["n1"].ToString(), out n1) &&
n1 > 0 && n1 < 500)
{
var rng = new NadekoRandom();
var rolls = new List<char>();
for (int i = 0; i < n1; i++)
{
rolls.Add(_fateRolls[rng.Next(0, _fateRolls.Length)]);
}
var embed = new EmbedBuilder().WithOkColor().WithDescription(Context.User.Mention + " " + GetText("dice_rolled_num", Format.Bold(n1.ToString())))
.AddField(efb => efb.WithName(Format.Bold("Result"))
.WithValue(string.Join(" ", rolls.Select(c => Format.Code($"[{c}]")))));
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
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 n2) &&
n1 <= 50 && n2 <= 100000 && n1 > 0 && n2 > 0)
{
var add = 0;
var sub = 0;
int.TryParse(match.Groups["add"].Value, out add);
int.TryParse(match.Groups["sub"].Value, out sub);
var arr = new int[n1];
for (int i = 0; i < n1; i++)
{
arr[i] = rng.Next(1, n2 + 1);
}
var sum = arr.Sum();
var embed = new EmbedBuilder().WithOkColor().WithDescription(Context.User.Mention + " " +GetText("dice_rolled_num", n1) + $"`1 - {n2}`")
.AddField(efb => efb.WithName(Format.Bold("Rolls"))
.WithValue(string.Join(" ", (ordered ? arr.OrderBy(x => x).AsEnumerable() : arr).Select(x => Format.Code(x.ToString())))))
.AddField(efb => efb.WithName(Format.Bold("Sum"))
.WithValue(sum + " + " + add + " - " + sub + " = " + (sum + add - sub)));
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
}
}
[NadekoCommand, Usage, Description, Aliases]
public async Task NRoll([Remainder] string range)
{
int rolled;
if (range.Contains("-"))
{
var arr = range.Split('-')
.Take(2)
.Select(int.Parse)
.ToArray();
if (arr[0] > arr[1])
{
await ReplyErrorLocalized("second_larger_than_first").ConfigureAwait(false);
return;
}
rolled = new NadekoRandom().Next(arr[0], arr[1] + 1);
}
else
{
rolled = new NadekoRandom().Next(0, int.Parse(range) + 1);
}
await ReplyConfirmLocalized("dice_rolled", Format.Bold(rolled.ToString())).ConfigureAwait(false);
}
private Image<Rgba32> GetDice(int num)
{
if (num < 0 || num > 10)
throw new ArgumentOutOfRangeException(nameof(num));
if (num == 10)
{
var images = _images.Dice;
using (var imgOneStream = images[1].ToStream())
using (var imgZeroStream = images[0].ToStream())
{
var imgOne = Image.Load(imgOneStream);
var imgZero = Image.Load(imgZeroStream);
return new[] { imgOne, imgZero }.Merge();
}
}
using (var die = _images.Dice[num].ToStream())
{
return Image.Load(die);
}
}
}
}
}

View File

@ -0,0 +1,106 @@
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Gambling.Common;
using Image = ImageSharp.Image;
using ImageSharp;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class DrawCommands : NadekoSubmodule
{
private static readonly ConcurrentDictionary<IGuild, Cards> _allDecks = new ConcurrentDictionary<IGuild, Cards>();
private const string _cardsPath = "data/images/cards";
private async Task<(Stream ImageStream, string ToSend)> InternalDraw(int num, ulong? guildId = null)
{
if (num < 1 || num > 10)
throw new ArgumentOutOfRangeException(nameof(num));
Cards cards = guildId == null ? new Cards() : _allDecks.GetOrAdd(Context.Guild, (s) => new Cards());
var images = new List<Image<Rgba32>>();
var cardObjects = new List<Cards.Card>();
for (var i = 0; i < num; i++)
{
if (cards.CardPool.Count == 0 && i != 0)
{
try
{
await ReplyErrorLocalized("no_more_cards").ConfigureAwait(false);
}
catch
{
// ignored
}
break;
}
var currentCard = cards.DrawACard();
cardObjects.Add(currentCard);
using (var stream = File.OpenRead(Path.Combine(_cardsPath, currentCard.ToString().ToLowerInvariant() + ".jpg").Replace(' ', '_')))
images.Add(Image.Load(stream));
}
MemoryStream bitmapStream = new MemoryStream();
images.Merge().SaveAsPng(bitmapStream);
bitmapStream.Position = 0;
var toSend = $"{Context.User.Mention}";
if (cardObjects.Count == 5)
toSend += $" drew `{Cards.GetHandValue(cardObjects)}`";
return (bitmapStream, toSend);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Draw(int num = 1)
{
if (num < 1)
num = 1;
if (num > 10)
num = 10;
var data = await InternalDraw(num, Context.Guild.Id).ConfigureAwait(false);
await Context.Channel.SendFileAsync(data.ImageStream, num + " cards.jpg", data.ToSend).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task DrawNew(int num = 1)
{
if (num < 1)
num = 1;
if (num > 10)
num = 10;
var data = await InternalDraw(num).ConfigureAwait(false);
await Context.Channel.SendFileAsync(data.ImageStream, num + " cards.jpg", data.ToSend).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task DeckShuffle()
{
//var channel = (ITextChannel)Context.Channel;
_allDecks.AddOrUpdate(Context.Guild,
(g) => new Cards(),
(g, c) =>
{
c.Restart();
return c;
});
await ReplyConfirmLocalized("deck_reshuffled").ConfigureAwait(false);
}
}
}
}

View File

@ -0,0 +1,133 @@
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using Image = ImageSharp.Image;
using ImageSharp;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class FlipCoinCommands : NadekoSubmodule
{
private readonly IImagesService _images;
private readonly IBotConfigProvider _bc;
private readonly CurrencyService _cs;
private readonly NadekoRandom rng = new NadekoRandom();
public FlipCoinCommands(IImagesService images, CurrencyService cs, IBotConfigProvider bc)
{
_images = images;
_bc = bc;
_cs = cs;
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Flip(int count = 1)
{
if (count == 1)
{
if (rng.Next(0, 2) == 1)
{
using (var heads = _images.Heads.ToStream())
{
await Context.Channel.SendFileAsync(heads, "heads.jpg", Context.User.Mention + " " + GetText("flipped", Format.Bold(GetText("heads")))).ConfigureAwait(false);
}
}
else
{
using (var tails = _images.Tails.ToStream())
{
await Context.Channel.SendFileAsync(tails, "tails.jpg", Context.User.Mention + " " + GetText("flipped", Format.Bold(GetText("tails")))).ConfigureAwait(false);
}
}
return;
}
if (count > 10 || count < 1)
{
await ReplyErrorLocalized("flip_invalid", 10).ConfigureAwait(false);
return;
}
var imgs = new Image<Rgba32>[count];
for (var i = 0; i < count; i++)
{
using (var heads = _images.Heads.ToStream())
using (var tails = _images.Tails.ToStream())
{
if (rng.Next(0, 10) < 5)
{
imgs[i] = Image.Load(heads);
}
else
{
imgs[i] = Image.Load(tails);
}
}
}
await Context.Channel.SendFileAsync(imgs.Merge().ToStream(), $"{count} coins.png").ConfigureAwait(false);
}
public enum BetFlipGuess
{
H = 1,
Head = 1,
Heads = 1,
T = 2,
Tail = 2,
Tails = 2
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Betflip(int amount, BetFlipGuess guess)
{
if (amount < _bc.BotConfig.MinimumBetAmount)
{
await ReplyErrorLocalized("min_bet_limit", _bc.BotConfig.MinimumBetAmount + _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
var removed = await _cs.RemoveAsync(Context.User, "Betflip Gamble", amount, false).ConfigureAwait(false);
if (!removed)
{
await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencyPluralName).ConfigureAwait(false);
return;
}
BetFlipGuess result;
IEnumerable<byte> imageToSend;
if (rng.Next(0, 2) == 1)
{
imageToSend = _images.Heads;
result = BetFlipGuess.Heads;
}
else
{
imageToSend = _images.Tails;
result = BetFlipGuess.Tails;
}
string str;
if (guess == result)
{
var toWin = (int)Math.Round(amount * _bc.BotConfig.BetflipMultiplier);
str = Context.User.Mention + " " + GetText("flip_guess", toWin + _bc.BotConfig.CurrencySign);
await _cs.AddAsync(Context.User, "Betflip Gamble", toWin, false).ConfigureAwait(false);
}
else
{
str = Context.User.Mention + " " + GetText("better_luck");
}
using (var toSend = imageToSend.ToStream())
{
await Context.Channel.SendFileAsync(toSend, "result.png", str).ConfigureAwait(false);
}
}
}
}
}

View File

@ -0,0 +1,366 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Common.Collections;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class FlowerShopCommands : NadekoSubmodule
{
private readonly IBotConfigProvider _bc;
private readonly DbService _db;
private readonly CurrencyService _cs;
private readonly DiscordSocketClient _client;
public enum Role
{
Role
}
public enum List
{
List
}
public FlowerShopCommands(IBotConfigProvider bc, DbService db, CurrencyService cs, DiscordSocketClient client)
{
_db = db;
_bc = bc;
_cs = cs;
_client = client;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Shop(int page = 1)
{
if (--page < 0)
return;
List<ShopEntry> entries;
using (var uow = _db.UnitOfWork)
{
entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.For(Context.Guild.Id,
set => set.Include(x => x.ShopEntries)
.ThenInclude(x => x.Items)).ShopEntries);
}
await Context.Channel.SendPaginatedConfirmAsync(_client, page, (curPage) =>
{
var theseEntries = entries.Skip(curPage * 9).Take(9).ToArray();
if (!theseEntries.Any())
return new EmbedBuilder().WithErrorColor()
.WithDescription(GetText("shop_none"));
var embed = new EmbedBuilder().WithOkColor()
.WithTitle(GetText("shop", _bc.BotConfig.CurrencySign));
for (int i = 0; i < theseEntries.Length; i++)
{
var entry = theseEntries[i];
embed.AddField(efb => efb.WithName($"#{curPage * 9 + i + 1} - {entry.Price}{_bc.BotConfig.CurrencySign}").WithValue(EntryToString(entry)).WithIsInline(true));
}
return embed;
}, entries.Count / 9, true);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Buy(int index, [Remainder]string message = null)
{
index -= 1;
if (index < 0)
return;
ShopEntry entry;
using (var uow = _db.UnitOfWork)
{
var config = uow.GuildConfigs.For(Context.Guild.Id, set => set
.Include(x => x.ShopEntries)
.ThenInclude(x => x.Items));
var entries = new IndexedCollection<ShopEntry>(config.ShopEntries);
entry = entries.ElementAtOrDefault(index);
uow.Complete();
}
if (entry == null)
{
await ReplyErrorLocalized("shop_item_not_found").ConfigureAwait(false);
return;
}
if (entry.Type == ShopEntryType.Role)
{
var guser = (IGuildUser)Context.User;
var role = Context.Guild.GetRole(entry.RoleId);
if (role == null)
{
await ReplyErrorLocalized("shop_role_not_found").ConfigureAwait(false);
return;
}
if (await _cs.RemoveAsync(Context.User.Id, $"Shop purchase - {entry.Type}", entry.Price))
{
try
{
await guser.AddRoleAsync(role).ConfigureAwait(false);
}
catch (Exception ex)
{
_log.Warn(ex);
await _cs.AddAsync(Context.User.Id, $"Shop error refund", entry.Price);
await ReplyErrorLocalized("shop_role_purchase_error").ConfigureAwait(false);
return;
}
await _cs.AddAsync(entry.AuthorId, $"Shop sell item - {entry.Type}", GetProfitAmount(entry.Price));
await ReplyConfirmLocalized("shop_role_purchase", Format.Bold(role.Name)).ConfigureAwait(false);
return;
}
else
{
await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
}
else if (entry.Type == ShopEntryType.List)
{
if (entry.Items.Count == 0)
{
await ReplyErrorLocalized("out_of_stock").ConfigureAwait(false);
return;
}
var item = entry.Items.ToArray()[new NadekoRandom().Next(0, entry.Items.Count)];
if (await _cs.RemoveAsync(Context.User.Id, $"Shop purchase - {entry.Type}", entry.Price))
{
int removed;
using (var uow = _db.UnitOfWork)
{
var x = uow._context.Set<ShopEntryItem>().Remove(item);
removed = uow.Complete();
}
try
{
await (await Context.User.GetOrCreateDMChannelAsync())
.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithTitle(GetText("shop_purchase", Context.Guild.Name))
.AddField(efb => efb.WithName(GetText("item")).WithValue(item.Text).WithIsInline(false))
.AddField(efb => efb.WithName(GetText("price")).WithValue(entry.Price.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("name")).WithValue(entry.Name).WithIsInline(true)))
.ConfigureAwait(false);
await _cs.AddAsync(entry.AuthorId,
$"Shop sell item - {entry.Name}",
GetProfitAmount(entry.Price)).ConfigureAwait(false);
}
catch
{
using (var uow = _db.UnitOfWork)
{
uow._context.Set<ShopEntryItem>().Add(item);
uow.Complete();
await _cs.AddAsync(Context.User.Id,
$"Shop error refund - {entry.Name}",
entry.Price,
uow).ConfigureAwait(false);
}
await ReplyErrorLocalized("shop_buy_error").ConfigureAwait(false);
return;
}
await ReplyConfirmLocalized("shop_item_purchase").ConfigureAwait(false);
}
else
{
await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
}
}
private long GetProfitAmount(int price) =>
(int)(Math.Ceiling(0.90 * price));
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[RequireUserPermission(GuildPermission.Administrator)]
[RequireBotPermission(GuildPermission.ManageRoles)]
public async Task ShopAdd(Role _, int price, [Remainder] IRole role)
{
var entry = new ShopEntry()
{
Name = "-",
Price = price,
Type = ShopEntryType.Role,
AuthorId = Context.User.Id,
RoleId = role.Id,
RoleName = role.Name
};
using (var uow = _db.UnitOfWork)
{
var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.For(Context.Guild.Id,
set => set.Include(x => x.ShopEntries)
.ThenInclude(x => x.Items)).ShopEntries)
{
entry
};
uow.GuildConfigs.For(Context.Guild.Id, set => set).ShopEntries = entries;
uow.Complete();
}
await Context.Channel.EmbedAsync(EntryToEmbed(entry)
.WithTitle(GetText("shop_item_add")));
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[RequireUserPermission(GuildPermission.Administrator)]
public async Task ShopAdd(List _, int price, [Remainder]string name)
{
var entry = new ShopEntry()
{
Name = name.TrimTo(100),
Price = price,
Type = ShopEntryType.List,
AuthorId = Context.User.Id,
Items = new HashSet<ShopEntryItem>(),
};
using (var uow = _db.UnitOfWork)
{
var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.For(Context.Guild.Id,
set => set.Include(x => x.ShopEntries)
.ThenInclude(x => x.Items)).ShopEntries)
{
entry
};
uow.GuildConfigs.For(Context.Guild.Id, set => set).ShopEntries = entries;
uow.Complete();
}
await Context.Channel.EmbedAsync(EntryToEmbed(entry)
.WithTitle(GetText("shop_item_add")));
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[RequireUserPermission(GuildPermission.Administrator)]
public async Task ShopListAdd(int index, [Remainder] string itemText)
{
index -= 1;
if (index < 0)
return;
var item = new ShopEntryItem()
{
Text = itemText
};
ShopEntry entry;
bool rightType = false;
bool added = false;
using (var uow = _db.UnitOfWork)
{
var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigs.For(Context.Guild.Id,
set => set.Include(x => x.ShopEntries)
.ThenInclude(x => x.Items)).ShopEntries);
entry = entries.ElementAtOrDefault(index);
if (entry != null && (rightType = (entry.Type == ShopEntryType.List)))
{
if (added = entry.Items.Add(item))
{
uow.Complete();
}
}
}
if (entry == null)
await ReplyErrorLocalized("shop_item_not_found").ConfigureAwait(false);
else if (!rightType)
await ReplyErrorLocalized("shop_item_wrong_type").ConfigureAwait(false);
else if (added == false)
await ReplyErrorLocalized("shop_list_item_not_unique").ConfigureAwait(false);
else
await ReplyConfirmLocalized("shop_list_item_added").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[RequireUserPermission(GuildPermission.Administrator)]
public async Task ShopRemove(int index)
{
index -= 1;
if (index < 0)
return;
ShopEntry removed;
using (var uow = _db.UnitOfWork)
{
var config = uow.GuildConfigs.For(Context.Guild.Id, set => set
.Include(x => x.ShopEntries)
.ThenInclude(x => x.Items));
var entries = new IndexedCollection<ShopEntry>(config.ShopEntries);
removed = entries.ElementAtOrDefault(index);
if (removed != null)
{
entries.Remove(removed);
config.ShopEntries = entries;
uow.Complete();
}
}
if (removed == null)
await ReplyErrorLocalized("shop_item_not_found").ConfigureAwait(false);
else
await Context.Channel.EmbedAsync(EntryToEmbed(removed)
.WithTitle(GetText("shop_item_rm")));
}
public EmbedBuilder EntryToEmbed(ShopEntry entry)
{
var embed = new EmbedBuilder().WithOkColor();
if (entry.Type == ShopEntryType.Role)
return embed.AddField(efb => efb.WithName(GetText("name")).WithValue(GetText("shop_role", Format.Bold(Context.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE"))).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("price")).WithValue(entry.Price.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("type")).WithValue(entry.Type.ToString()).WithIsInline(true));
else if (entry.Type == ShopEntryType.List)
return embed.AddField(efb => efb.WithName(GetText("name")).WithValue(entry.Name).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("price")).WithValue(entry.Price.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("type")).WithValue(GetText("random_unique_item")).WithIsInline(true));
//else if (entry.Type == ShopEntryType.Infinite_List)
// return embed.AddField(efb => efb.WithName(GetText("name")).WithValue(GetText("shop_role", Format.Bold(entry.RoleName))).WithIsInline(true))
// .AddField(efb => efb.WithName(GetText("price")).WithValue(entry.Price.ToString()).WithIsInline(true))
// .AddField(efb => efb.WithName(GetText("type")).WithValue(entry.Type.ToString()).WithIsInline(true));
else return null;
}
public string EntryToString(ShopEntry entry)
{
if (entry.Type == ShopEntryType.Role)
{
return GetText("shop_role", Format.Bold(Context.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE"));
}
else if (entry.Type == ShopEntryType.List)
{
return GetText("unique_items_left", entry.Items.Count) + "\n" + entry.Name;
}
//else if (entry.Type == ShopEntryType.Infinite_List)
//{
//}
return "";
}
}
}
}

View File

@ -0,0 +1,295 @@
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using System.Collections.Generic;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling : NadekoTopLevelModule
{
private readonly IBotConfigProvider _bc;
private readonly DbService _db;
private readonly CurrencyService _currency;
private string CurrencyName => _bc.BotConfig.CurrencyName;
private string CurrencyPluralName => _bc.BotConfig.CurrencyPluralName;
private string CurrencySign => _bc.BotConfig.CurrencySign;
public Gambling(IBotConfigProvider bc, DbService db, CurrencyService currency)
{
_bc = bc;
_db = db;
_currency = currency;
}
public long GetCurrency(ulong id)
{
using (var uow = _db.UnitOfWork)
{
return uow.Currency.GetUserCurrency(id);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Raffle([Remainder] IRole role = null)
{
role = role ?? Context.Guild.EveryoneRole;
var members = (await role.GetMembersAsync()).Where(u => u.Status != UserStatus.Offline);
var membersArray = members as IUser[] ?? members.ToArray();
if (membersArray.Length == 0)
{
}
var usr = membersArray[new NadekoRandom().Next(0, membersArray.Length)];
await Context.Channel.SendConfirmAsync("🎟 "+ GetText("raffled_user"), $"**{usr.Username}#{usr.Discriminator}**", footer: $"ID: {usr.Id}").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(1)]
public async Task Cash([Remainder] IUser user = null)
{
if(user == null)
await ConfirmLocalized("has", Format.Bold(Context.User.ToString()), $"{GetCurrency(Context.User.Id)} {CurrencySign}").ConfigureAwait(false);
else
await ReplyConfirmLocalized("has", Format.Bold(user.ToString()), $"{GetCurrency(user.Id)} {CurrencySign}").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[Priority(0)]
public async Task Cash(ulong userId)
{
await ReplyConfirmLocalized("has", Format.Code(userId.ToString()), $"{GetCurrency(userId)} {CurrencySign}").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Give(long amount, [Remainder] IGuildUser receiver)
{
if (amount <= 0 || Context.User.Id == receiver.Id)
return;
var success = await _currency.RemoveAsync((IGuildUser)Context.User, $"Gift to {receiver.Username} ({receiver.Id}).", amount, false).ConfigureAwait(false);
if (!success)
{
await ReplyErrorLocalized("not_enough", CurrencyPluralName).ConfigureAwait(false);
return;
}
await _currency.AddAsync(receiver, $"Gift from {Context.User.Username} ({Context.User.Id}).", amount, true).ConfigureAwait(false);
await ReplyConfirmLocalized("gifted", amount + CurrencySign, Format.Bold(receiver.ToString()))
.ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
[Priority(0)]
public Task Award(int amount, [Remainder] IGuildUser usr) =>
Award(amount, usr.Id);
[NadekoCommand, Usage, Description, Aliases]
[OwnerOnly]
[Priority(1)]
public async Task Award(int amount, ulong usrId)
{
if (amount <= 0)
return;
await _currency.AddAsync(usrId, $"Awarded by bot owner. ({Context.User.Username}/{Context.User.Id})", amount).ConfigureAwait(false);
await ReplyConfirmLocalized("awarded", amount + CurrencySign, $"<@{usrId}>").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
[Priority(2)]
public async Task Award(int amount, [Remainder] IRole role)
{
var users = (await Context.Guild.GetUsersAsync())
.Where(u => u.GetRoles().Contains(role))
.ToList();
await Task.WhenAll(users.Select(u => _currency.AddAsync(u.Id,
$"Awarded by bot owner to **{role.Name}** role. ({Context.User.Username}/{Context.User.Id})",
amount)))
.ConfigureAwait(false);
await ReplyConfirmLocalized("mass_award",
amount + CurrencySign,
Format.Bold(users.Count.ToString()),
Format.Bold(role.Name)).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task Take(long amount, [Remainder] IGuildUser user)
{
if (amount <= 0)
return;
if (await _currency.RemoveAsync(user, $"Taken by bot owner.({Context.User.Username}/{Context.User.Id})", amount, true).ConfigureAwait(false))
await ReplyConfirmLocalized("take", amount+CurrencySign, Format.Bold(user.ToString())).ConfigureAwait(false);
else
await ReplyErrorLocalized("take_fail", amount + CurrencySign, Format.Bold(user.ToString()), CurrencyPluralName).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[OwnerOnly]
public async Task Take(long amount, [Remainder] ulong usrId)
{
if (amount <= 0)
return;
if (await _currency.RemoveAsync(usrId, $"Taken by bot owner.({Context.User.Username}/{Context.User.Id})", amount).ConfigureAwait(false))
await ReplyConfirmLocalized("take", amount + CurrencySign, $"<@{usrId}>").ConfigureAwait(false);
else
await ReplyErrorLocalized("take_fail", amount + CurrencySign, Format.Code(usrId.ToString()), CurrencyPluralName).ConfigureAwait(false);
}
//[NadekoCommand, Usage, Description, Aliases]
//[OwnerOnly]
//public Task BrTest(int tests = 1000)
//{
// var t = Task.Run(async () =>
// {
// if (tests <= 0)
// return;
// //multi vs how many times it occured
// var dict = new Dictionary<int, int>();
// var generator = new NadekoRandom();
// for (int i = 0; i < tests; i++)
// {
// var rng = generator.Next(0, 101);
// var mult = 0;
// if (rng < 67)
// {
// mult = 0;
// }
// else if (rng < 91)
// {
// mult = 2;
// }
// else if (rng < 100)
// {
// mult = 4;
// }
// else
// mult = 10;
// if (dict.ContainsKey(mult))
// dict[mult] += 1;
// else
// dict.Add(mult, 1);
// }
// var sb = new StringBuilder();
// const int bet = 1;
// int payout = 0;
// foreach (var key in dict.Keys.OrderByDescending(x => x))
// {
// sb.AppendLine($"x{key} occured {dict[key]} times. {dict[key] * 1.0f / tests * 100}%");
// payout += key * dict[key];
// }
// try
// {
// await Context.Channel.SendConfirmAsync("BetRoll Test Results", sb.ToString(),
// footer: $"Total Bet: {tests * bet} | Payout: {payout * bet} | {payout * 1.0f / tests * 100}%");
// }
// catch { }
// });
// return Task.CompletedTask;
//}
[NadekoCommand, Usage, Description, Aliases]
public async Task BetRoll(long amount)
{
if (amount < 1)
return;
if (!await _currency.RemoveAsync(Context.User, "Betroll Gamble", amount, false).ConfigureAwait(false))
{
await ReplyErrorLocalized("not_enough", CurrencyPluralName).ConfigureAwait(false);
return;
}
var rnd = new NadekoRandom().Next(0, 101);
var str = Context.User.Mention + Format.Code(GetText("roll", rnd));
if (rnd < 67)
{
str += GetText("better_luck");
}
else
{
if (rnd < 91)
{
str += GetText("br_win", (amount * _bc.BotConfig.Betroll67Multiplier) + CurrencySign, 66);
await _currency.AddAsync(Context.User, "Betroll Gamble",
(int) (amount * _bc.BotConfig.Betroll67Multiplier), false).ConfigureAwait(false);
}
else if (rnd < 100)
{
str += GetText("br_win", (amount * _bc.BotConfig.Betroll91Multiplier) + CurrencySign, 90);
await _currency.AddAsync(Context.User, "Betroll Gamble",
(int) (amount * _bc.BotConfig.Betroll91Multiplier), false).ConfigureAwait(false);
}
else
{
str += GetText("br_win", (amount * _bc.BotConfig.Betroll100Multiplier) + CurrencySign, 100) + " 👑";
await _currency.AddAsync(Context.User, "Betroll Gamble",
(int) (amount * _bc.BotConfig.Betroll100Multiplier), false).ConfigureAwait(false);
}
}
await Context.Channel.SendConfirmAsync(str).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Leaderboard(int page = 1)
{
if (page < 1)
return;
List<Currency> richest;
using (var uow = _db.UnitOfWork)
{
richest = uow.Currency.GetTopRichest(9, 9 * (page - 1)).ToList();
}
var embed = new EmbedBuilder()
.WithOkColor()
.WithTitle(CurrencySign +
" " + GetText("leaderboard"))
.WithFooter(efb => efb.WithText(GetText("page", page)));
if (!richest.Any())
{
embed.WithDescription(GetText("no_users_found"));
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
return;
}
for (var i = 0; i < richest.Count; i++)
{
var x = richest[i];
var usr = await Context.Guild.GetUserAsync(x.UserId).ConfigureAwait(false);
var usrStr = usr == null
? x.UserId.ToString()
: usr.Username?.TrimTo(20, true);
var j = i;
embed.AddField(efb => efb.WithName("#" + (9 * (page - 1) + j + 1) + " " + usrStr)
.WithValue(x.Amount.ToString() + " " + CurrencySign)
.WithIsInline(true));
}
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,21 @@
using System.Threading.Tasks;
using NadekoBot.Core.Services;
using System.Collections.Concurrent;
using NadekoBot.Modules.Gambling.Common.AnimalRacing;
namespace NadekoBot.Modules.Gambling.Services
{
public class AnimalRaceService : INService, IUnloadableService
{
public ConcurrentDictionary<ulong, AnimalRace> AnimalRaces { get; } = new ConcurrentDictionary<ulong, AnimalRace>();
public Task Unload()
{
foreach (var kvp in AnimalRaces)
{
try { kvp.Value.Dispose(); } catch { }
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,66 @@
using Discord;
using Discord.Commands;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Common.CurrencyEvents;
using NadekoBot.Core.Services;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Gambling.Services
{
public class CurrencyEventsService : INService, IUnloadableService
{
public ConcurrentDictionary<ulong, List<ReactionEvent>> ReactionEvents { get; }
public SneakyEvent SneakyEvent { get; private set; } = null;
private SemaphoreSlim _sneakyLock = new SemaphoreSlim(1, 1);
public CurrencyEventsService()
{
ReactionEvents = new ConcurrentDictionary<ulong, List<ReactionEvent>>();
}
public async Task<bool> StartSneakyEvent(SneakyEvent ev, IUserMessage msg, ICommandContext ctx)
{
await _sneakyLock.WaitAsync().ConfigureAwait(false);
try
{
if (SneakyEvent != null)
return false;
SneakyEvent = ev;
ev.OnEnded += () => SneakyEvent = null;
var _ = SneakyEvent.Start(msg, ctx).ConfigureAwait(false);
}
finally
{
_sneakyLock.Release();
}
return true;
}
public async Task Unload()
{
foreach (var kvp in ReactionEvents)
{
foreach (var ev in kvp.Value)
{
try { await ev.Stop().ConfigureAwait(false); } catch { }
}
}
ReactionEvents.Clear();
await _sneakyLock.WaitAsync().ConfigureAwait(false);
try
{
await SneakyEvent.Stop().ConfigureAwait(false);
}
finally
{
_sneakyLock.Release();
}
}
}
}

View File

@ -0,0 +1,12 @@
using NadekoBot.Core.Services;
using System;
using System.Collections.Concurrent;
namespace NadekoBot.Modules.Gambling.Services
{
public class WaifuService : INService
{
public ConcurrentDictionary<ulong, DateTime> DivorceCooldowns { get; } = new ConcurrentDictionary<ulong, DateTime>();
public ConcurrentDictionary<ulong, DateTime> AffinityCooldowns { get; } = new ConcurrentDictionary<ulong, DateTime>();
}
}

View File

@ -0,0 +1,239 @@
using Discord;
using Discord.Commands;
using ImageSharp;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using SixLabors.Primitives;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
[Group]
public class SlotCommands : NadekoSubmodule
{
private static int _totalBet;
private static int _totalPaidOut;
private static readonly HashSet<ulong> _runningUsers = new HashSet<ulong>();
private readonly IBotConfigProvider _bc;
private const int _alphaCutOut = byte.MaxValue / 3;
//here is a payout chart
//https://lh6.googleusercontent.com/-i1hjAJy_kN4/UswKxmhrbPI/AAAAAAAAB1U/82wq_4ZZc-Y/DE6B0895-6FC1-48BE-AC4F-14D1B91AB75B.jpg
//thanks to judge for helping me with this
private readonly IImagesService _images;
private readonly CurrencyService _cs;
public SlotCommands(IImagesService images, IBotConfigProvider bc, CurrencyService cs)
{
_images = images;
_bc = bc;
_cs = cs;
}
public class SlotMachine
{
public const int MaxValue = 5;
static readonly List<Func<int[], int>> _winningCombos = new List<Func<int[], int>>()
{
//three flowers
(arr) => arr.All(a=>a==MaxValue) ? 30 : 0,
//three of the same
(arr) => !arr.Any(a => a != arr[0]) ? 10 : 0,
//two flowers
(arr) => arr.Count(a => a == MaxValue) == 2 ? 4 : 0,
//one flower
(arr) => arr.Any(a => a == MaxValue) ? 1 : 0,
};
public static SlotResult Pull()
{
var numbers = new int[3];
for (var i = 0; i < numbers.Length; i++)
{
numbers[i] = new NadekoRandom().Next(0, MaxValue + 1);
}
var multi = 0;
foreach (var t in _winningCombos)
{
multi = t(numbers);
if (multi != 0)
break;
}
return new SlotResult(numbers, multi);
}
public struct SlotResult
{
public int[] Numbers { get; }
public int Multiplier { get; }
public SlotResult(int[] nums, int multi)
{
Numbers = nums;
Multiplier = multi;
}
}
}
[NadekoCommand, Usage, Description, Aliases]
[OwnerOnly]
public async Task SlotStats()
{
//i remembered to not be a moron
var paid = _totalPaidOut;
var bet = _totalBet;
if (bet <= 0)
bet = 1;
var embed = new EmbedBuilder()
.WithOkColor()
.WithTitle("Slot Stats")
.AddField(efb => efb.WithName("Total Bet").WithValue(bet.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName("Paid Out").WithValue(paid.ToString()).WithIsInline(true))
.WithFooter(efb => efb.WithText($"Payout Rate: {paid * 1.0 / bet * 100:f4}%"));
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[OwnerOnly]
public async Task SlotTest(int tests = 1000)
{
if (tests <= 0)
return;
//multi vs how many times it occured
var dict = new Dictionary<int, int>();
for (int i = 0; i < tests; i++)
{
var res = SlotMachine.Pull();
if (dict.ContainsKey(res.Multiplier))
dict[res.Multiplier] += 1;
else
dict.Add(res.Multiplier, 1);
}
var sb = new StringBuilder();
const int bet = 1;
int payout = 0;
foreach (var key in dict.Keys.OrderByDescending(x => x))
{
sb.AppendLine($"x{key} occured {dict[key]} times. {dict[key] * 1.0f / tests * 100}%");
payout += key * dict[key];
}
await Context.Channel.SendConfirmAsync("Slot Test Results", sb.ToString(),
footer: $"Total Bet: {tests * bet} | Payout: {payout * bet} | {payout * 1.0f / tests * 100}%");
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Slot(int amount = 0)
{
if (!_runningUsers.Add(Context.User.Id))
return;
try
{
if (amount < 1)
{
await ReplyErrorLocalized("min_bet_limit", 1 + _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
const int maxAmount = 9999;
if (amount > maxAmount)
{
GetText("slot_maxbet", maxAmount + _bc.BotConfig.CurrencySign);
await ReplyErrorLocalized("max_bet_limit", maxAmount + _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
if (!await _cs.RemoveAsync(Context.User, "Slot Machine", amount, false))
{
await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
Interlocked.Add(ref _totalBet, amount);
using (var bgFileStream = _images.SlotBackground.ToStream())
{
var bgImage = ImageSharp.Image.Load(bgFileStream);
var result = SlotMachine.Pull();
int[] numbers = result.Numbers;
for (int i = 0; i < 3; i++)
{
using (var file = _images.SlotEmojis[numbers[i]].ToStream())
using (var randomImage = ImageSharp.Image.Load(file))
{
bgImage.DrawImage(randomImage, 100, default, new Point(95 + 142 * i, 330));
}
}
var won = amount * result.Multiplier;
var printWon = won;
var n = 0;
do
{
var digit = printWon % 10;
using (var fs = _images.SlotNumbers[digit].ToStream())
using (var img = ImageSharp.Image.Load(fs))
{
bgImage.DrawImage(img, 100, default, new Point(230 - n * 16, 462));
}
n++;
} while ((printWon /= 10) != 0);
var printAmount = amount;
n = 0;
do
{
var digit = printAmount % 10;
using (var fs = _images.SlotNumbers[digit].ToStream())
using (var img = ImageSharp.Image.Load(fs))
{
bgImage.DrawImage(img, 100, default, new Point(395 - n * 16, 462));
}
n++;
} while ((printAmount /= 10) != 0);
var msg = GetText("better_luck");
if (result.Multiplier != 0)
{
await _cs.AddAsync(Context.User, $"Slot Machine x{result.Multiplier}", amount * result.Multiplier, false);
Interlocked.Add(ref _totalPaidOut, amount * result.Multiplier);
if (result.Multiplier == 1)
msg = GetText("slot_single", _bc.BotConfig.CurrencySign, 1);
else if (result.Multiplier == 4)
msg = GetText("slot_two", _bc.BotConfig.CurrencySign, 4);
else if (result.Multiplier == 10)
msg = GetText("slot_three", 10);
else if (result.Multiplier == 30)
msg = GetText("slot_jackpot", 30);
}
await Context.Channel.SendFileAsync(bgImage.ToStream(), "result.png", Context.User.Mention + " " + msg + $"\n`{GetText("slot_bet")}:`{amount} `{GetText("slot_won")}:` {amount * result.Multiplier}{_bc.BotConfig.CurrencySign}").ConfigureAwait(false);
}
}
finally
{
var _ = Task.Run(async () =>
{
await Task.Delay(1500);
_runningUsers.Remove(Context.User.Id);
});
}
}
}
}
}

View File

@ -0,0 +1,617 @@
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
public enum ClaimTitles
{
Lonely,
Devoted,
Rookie,
Schemer,
Dilettante,
Intermediate,
Seducer,
Expert,
Veteran,
Incubis,
Harem_King,
Harem_God,
}
public enum AffinityTitles
{
Pure,
Faithful,
Defiled,
Cheater,
Tainted,
Corrupted,
Lewd,
Sloot,
Depraved,
Harlot
}
[Group]
public class WaifuClaimCommands : NadekoSubmodule<WaifuService>
{
enum WaifuClaimResult
{
Success,
NotEnoughFunds,
InsufficientAmount
}
public WaifuClaimCommands(IBotConfigProvider bc, CurrencyService cs, DbService db)
{
_bc = bc;
_cs = cs;
_db = db;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task WaifuClaim(int amount, [Remainder]IUser target)
{
if (amount < 50)
{
await ReplyErrorLocalized("waifu_isnt_cheap", 50 + _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
if (target.Id == Context.User.Id)
{
await ReplyErrorLocalized("waifu_not_yourself").ConfigureAwait(false);
return;
}
WaifuClaimResult result;
WaifuInfo w;
bool isAffinity;
using (var uow = _db.UnitOfWork)
{
w = uow.Waifus.ByWaifuUserId(target.Id);
isAffinity = (w?.Affinity?.UserId == Context.User.Id);
if (w == null)
{
var claimer = uow.DiscordUsers.GetOrCreate(Context.User);
var waifu = uow.DiscordUsers.GetOrCreate(target);
if (!await _cs.RemoveAsync(Context.User.Id, "Claimed Waifu", amount, uow).ConfigureAwait(false))
{
result = WaifuClaimResult.NotEnoughFunds;
}
else
{
uow.Waifus.Add(w = new WaifuInfo()
{
Waifu = waifu,
Claimer = claimer,
Affinity = null,
Price = amount
});
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = waifu,
Old = null,
New = claimer,
UpdateType = WaifuUpdateType.Claimed
});
result = WaifuClaimResult.Success;
}
}
else if (isAffinity && amount > w.Price * 0.88f)
{
if (!await _cs.RemoveAsync(Context.User.Id, "Claimed Waifu", amount, uow).ConfigureAwait(false))
{
result = WaifuClaimResult.NotEnoughFunds;
}
else
{
var oldClaimer = w.Claimer;
w.Claimer = uow.DiscordUsers.GetOrCreate(Context.User);
w.Price = amount + (amount / 4);
result = WaifuClaimResult.Success;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = w.Waifu,
Old = oldClaimer,
New = w.Claimer,
UpdateType = WaifuUpdateType.Claimed
});
}
}
else if (amount >= w.Price * 1.1f) // if no affinity
{
if (!await _cs.RemoveAsync(Context.User.Id, "Claimed Waifu", amount, uow).ConfigureAwait(false))
{
result = WaifuClaimResult.NotEnoughFunds;
}
else
{
var oldClaimer = w.Claimer;
w.Claimer = uow.DiscordUsers.GetOrCreate(Context.User);
w.Price = amount;
result = WaifuClaimResult.Success;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = w.Waifu,
Old = oldClaimer,
New = w.Claimer,
UpdateType = WaifuUpdateType.Claimed
});
}
}
else
result = WaifuClaimResult.InsufficientAmount;
await uow.CompleteAsync().ConfigureAwait(false);
}
if (result == WaifuClaimResult.InsufficientAmount)
{
await ReplyErrorLocalized("waifu_not_enough", Math.Ceiling(w.Price * (isAffinity ? 0.88f : 1.1f))).ConfigureAwait(false);
return;
}
if (result == WaifuClaimResult.NotEnoughFunds)
{
await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
var msg = GetText("waifu_claimed",
Format.Bold(target.ToString()),
amount + _bc.BotConfig.CurrencySign);
if (w.Affinity?.UserId == Context.User.Id)
msg += "\n" + GetText("waifu_fulfilled", target, w.Price + _bc.BotConfig.CurrencySign);
else
msg = " " + msg;
await Context.Channel.SendConfirmAsync(Context.User.Mention + msg).ConfigureAwait(false);
}
public enum DivorceResult
{
Success,
SucessWithPenalty,
NotYourWife,
Cooldown
}
private static readonly TimeSpan _divorceLimit = TimeSpan.FromHours(6);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public Task Divorce([Remainder]IGuildUser target) => Divorce(target.Id);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public async Task Divorce([Remainder]ulong targetId)
{
if (targetId == Context.User.Id)
return;
DivorceResult result;
var difference = TimeSpan.Zero;
var amount = 0;
WaifuInfo w = null;
using (var uow = _db.UnitOfWork)
{
w = uow.Waifus.ByWaifuUserId(targetId);
var now = DateTime.UtcNow;
if (w?.Claimer == null || w.Claimer.UserId != Context.User.Id)
result = DivorceResult.NotYourWife;
else if (_service.DivorceCooldowns.AddOrUpdate(Context.User.Id,
now,
(key, old) => ((difference = now.Subtract(old)) > _divorceLimit) ? now : old) != now)
{
result = DivorceResult.Cooldown;
}
else
{
amount = w.Price / 2;
if (w.Affinity?.UserId == Context.User.Id)
{
await _cs.AddAsync(w.Waifu.UserId, "Waifu Compensation", amount, uow).ConfigureAwait(false);
w.Price = (int)Math.Floor(w.Price * 0.75f);
result = DivorceResult.SucessWithPenalty;
}
else
{
await _cs.AddAsync(Context.User.Id, "Waifu Refund", amount, uow).ConfigureAwait(false);
result = DivorceResult.Success;
}
var oldClaimer = w.Claimer;
w.Claimer = null;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = w.Waifu,
Old = oldClaimer,
New = null,
UpdateType = WaifuUpdateType.Claimed
});
}
await uow.CompleteAsync().ConfigureAwait(false);
}
if (result == DivorceResult.SucessWithPenalty)
{
await ReplyConfirmLocalized("waifu_divorced_like", Format.Bold(w.Waifu.ToString()), amount + _bc.BotConfig.CurrencySign).ConfigureAwait(false);
}
else if (result == DivorceResult.Success)
{
await ReplyConfirmLocalized("waifu_divorced_notlike", amount + _bc.BotConfig.CurrencySign).ConfigureAwait(false);
}
else if (result == DivorceResult.NotYourWife)
{
await ReplyErrorLocalized("waifu_not_yours").ConfigureAwait(false);
}
else
{
var remaining = _divorceLimit.Subtract(difference);
await ReplyErrorLocalized("waifu_recent_divorce",
Format.Bold(((int)remaining.TotalHours).ToString()),
Format.Bold(remaining.Minutes.ToString())).ConfigureAwait(false);
}
}
private static readonly TimeSpan _affinityLimit = TimeSpan.FromMinutes(30);
private readonly IBotConfigProvider _bc;
private readonly CurrencyService _cs;
private readonly DbService _db;
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task WaifuClaimerAffinity([Remainder]IGuildUser u = null)
{
if (u?.Id == Context.User.Id)
{
await ReplyErrorLocalized("waifu_egomaniac").ConfigureAwait(false);
return;
}
DiscordUser oldAff = null;
var sucess = false;
var cooldown = false;
var difference = TimeSpan.Zero;
using (var uow = _db.UnitOfWork)
{
var w = uow.Waifus.ByWaifuUserId(Context.User.Id);
var newAff = u == null ? null : uow.DiscordUsers.GetOrCreate(u);
var now = DateTime.UtcNow;
if (w?.Affinity?.UserId == u?.Id)
{
}
else if (_service.AffinityCooldowns.AddOrUpdate(Context.User.Id,
now,
(key, old) => ((difference = now.Subtract(old)) > _affinityLimit) ? now : old) != now)
{
cooldown = true;
}
else if (w == null)
{
var thisUser = uow.DiscordUsers.GetOrCreate(Context.User);
uow.Waifus.Add(new WaifuInfo()
{
Affinity = newAff,
Waifu = thisUser,
Price = 1,
Claimer = null
});
sucess = true;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = thisUser,
Old = null,
New = newAff,
UpdateType = WaifuUpdateType.AffinityChanged
});
}
else
{
if (w.Affinity != null)
oldAff = w.Affinity;
w.Affinity = newAff;
sucess = true;
uow._context.WaifuUpdates.Add(new WaifuUpdate()
{
User = w.Waifu,
Old = oldAff,
New = newAff,
UpdateType = WaifuUpdateType.AffinityChanged
});
}
await uow.CompleteAsync().ConfigureAwait(false);
}
if (!sucess)
{
if (cooldown)
{
var remaining = _affinityLimit.Subtract(difference);
await ReplyErrorLocalized("waifu_affinity_cooldown",
Format.Bold(((int)remaining.TotalHours).ToString()),
Format.Bold(remaining.Minutes.ToString())).ConfigureAwait(false);
}
else
{
await ReplyErrorLocalized("waifu_affinity_already").ConfigureAwait(false);
}
return;
}
if (u == null)
{
await ReplyConfirmLocalized("waifu_affinity_reset").ConfigureAwait(false);
}
else if (oldAff == null)
{
await ReplyConfirmLocalized("waifu_affinity_set", Format.Bold(u.ToString())).ConfigureAwait(false);
}
else
{
await ReplyConfirmLocalized("waifu_affinity_changed", Format.Bold(oldAff.ToString()), Format.Bold(u.ToString())).ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task WaifuLeaderboard(int page = 1)
{
page--;
if (page < 0)
return;
IList<WaifuInfo> waifus;
using (var uow = _db.UnitOfWork)
{
waifus = uow.Waifus.GetTop(9, page * 9);
}
if (waifus.Count == 0)
{
await ReplyConfirmLocalized("waifus_none").ConfigureAwait(false);
return;
}
var embed = new EmbedBuilder()
.WithTitle(GetText("waifus_top_waifus"))
.WithOkColor();
for (var i = 0; i < waifus.Count; i++)
{
var w = waifus[i];
var j = i;
embed.AddField(efb => efb.WithName("#" + ((page * 9) + j + 1) + " - " + w.Price + _bc.BotConfig.CurrencySign).WithValue(w.ToString()).WithIsInline(false));
}
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task WaifuInfo([Remainder]IGuildUser target = null)
{
if (target == null)
target = (IGuildUser)Context.User;
WaifuInfo w;
IList<WaifuInfo> claims;
int divorces;
using (var uow = _db.UnitOfWork)
{
w = uow.Waifus.ByWaifuUserId(target.Id);
claims = uow.Waifus.ByClaimerUserId(target.Id);
divorces = uow._context.WaifuUpdates.Count(x => x.Old != null &&
x.Old.UserId == target.Id &&
x.UpdateType == WaifuUpdateType.Claimed &&
x.New == null);
if (w == null)
{
uow.Waifus.Add(w = new WaifuInfo()
{
Affinity = null,
Claimer = null,
Price = 1,
Waifu = uow.DiscordUsers.GetOrCreate(target),
});
}
w.Waifu.Username = target.Username;
w.Waifu.Discriminator = target.Discriminator;
await uow.CompleteAsync().ConfigureAwait(false);
}
var claimInfo = GetClaimTitle(target.Id);
var affInfo = GetAffinityTitle(target.Id);
var rng = new NadekoRandom();
var nobody = GetText("nobody");
var embed = new EmbedBuilder()
.WithOkColor()
.WithTitle("Waifu " + w.Waifu + " - \"the " + claimInfo.Title + "\"")
.AddField(efb => efb.WithName(GetText("price")).WithValue(w.Price.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("claimed_by")).WithValue(w.Claimer?.ToString() ?? nobody).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("likes")).WithValue(w.Affinity?.ToString() ?? nobody).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("changes_of_heart")).WithValue($"{affInfo.Count} - \"the {affInfo.Title}\"").WithIsInline(true))
.AddField(efb => efb.WithName(GetText("divorces")).WithValue(divorces.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("gifts")).WithValue(!w.Items.Any() ? "-" : string.Join("\n", w.Items.OrderBy(x => x.Price).GroupBy(x => x.ItemEmoji).Select(x => $"{x.Key} x{x.Count()}"))).WithIsInline(false))
.AddField(efb => efb.WithName($"Waifus ({claims.Count})").WithValue(claims.Count == 0 ? nobody : string.Join("\n", claims.OrderBy(x => rng.Next()).Take(30).Select(x => x.Waifu))).WithIsInline(false));
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public async Task WaifuGift()
{
var embed = new EmbedBuilder()
.WithTitle(GetText("waifu_gift_shop"))
.WithOkColor();
Enum.GetValues(typeof(WaifuItem.ItemName))
.Cast<WaifuItem.ItemName>()
.Select(x => WaifuItem.GetItem(x))
.ForEach(x => embed.AddField(f => f.WithName(x.ItemEmoji + " " + x.Item).WithValue(x.Price).WithIsInline(true)));
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public async Task WaifuGift(WaifuItem.ItemName item, [Remainder] IUser waifu)
{
if (waifu.Id == Context.User.Id)
return;
var itemObj = WaifuItem.GetItem(item);
using (var uow = _db.UnitOfWork)
{
var w = uow.Waifus.ByWaifuUserId(waifu.Id);
//try to buy the item first
if (!await _cs.RemoveAsync(Context.User.Id, "Bought waifu item", itemObj.Price, uow))
{
await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
if (w == null)
{
uow.Waifus.Add(w = new WaifuInfo()
{
Affinity = null,
Claimer = null,
Price = 1,
Waifu = uow.DiscordUsers.GetOrCreate(waifu),
});
w.Waifu.Username = waifu.Username;
w.Waifu.Discriminator = waifu.Discriminator;
}
w.Items.Add(itemObj);
if (w.Claimer?.UserId == Context.User.Id)
{
w.Price += itemObj.Price;
}
else
w.Price += itemObj.Price / 2;
await uow.CompleteAsync().ConfigureAwait(false);
}
await ReplyConfirmLocalized("waifu_gift", Format.Bold(item.ToString() + " " +itemObj.ItemEmoji), Format.Bold(waifu.ToString())).ConfigureAwait(false);
}
public struct WaifuProfileTitle
{
public int Count { get; }
public string Title { get; }
public WaifuProfileTitle(int count, string title)
{
Count = count;
Title = title;
}
}
private WaifuProfileTitle GetClaimTitle(ulong userId)
{
int count;
using (var uow = _db.UnitOfWork)
{
count = uow.Waifus.ByClaimerUserId(userId).Count;
}
ClaimTitles title;
if (count == 0)
title = ClaimTitles.Lonely;
else if (count == 1)
title = ClaimTitles.Devoted;
else if (count < 4)
title = ClaimTitles.Rookie;
else if (count < 6)
title = ClaimTitles.Schemer;
else if (count < 8)
title = ClaimTitles.Dilettante;
else if (count < 10)
title = ClaimTitles.Intermediate;
else if (count < 12)
title = ClaimTitles.Seducer;
else if (count < 15)
title = ClaimTitles.Expert;
else if (count < 17)
title = ClaimTitles.Veteran;
else if (count < 25)
title = ClaimTitles.Incubis;
else if (count < 50)
title = ClaimTitles.Harem_King;
else
title = ClaimTitles.Harem_God;
return new WaifuProfileTitle(count, title.ToString().Replace('_', ' '));
}
private WaifuProfileTitle GetAffinityTitle(ulong userId)
{
int count;
using (var uow = _db.UnitOfWork)
{
count = uow._context.WaifuUpdates
.Where(w => w.User.UserId == userId && w.UpdateType == WaifuUpdateType.AffinityChanged && w.New != null)
.GroupBy(x => x.New)
.Count();
}
AffinityTitles title;
if (count < 1)
title = AffinityTitles.Pure;
else if (count < 2)
title = AffinityTitles.Faithful;
else if (count < 4)
title = AffinityTitles.Defiled;
else if (count < 7)
title = AffinityTitles.Cheater;
else if (count < 9)
title = AffinityTitles.Tainted;
else if (count < 11)
title = AffinityTitles.Corrupted;
else if (count < 13)
title = AffinityTitles.Lewd;
else if (count < 15)
title = AffinityTitles.Sloot;
else if (count < 17)
title = AffinityTitles.Depraved;
else
title = AffinityTitles.Harlot;
return new WaifuProfileTitle(count, title.ToString().Replace('_', ' '));
}
}
}
}

View File

@ -0,0 +1,81 @@
using Discord;
using Discord.Commands;
using NadekoBot.Common.Attributes;
using NadekoBot.Extensions;
using NadekoBot.Modules.Gambling.Common.WheelOfFortune;
using NadekoBot.Core.Services;
using System.Threading.Tasks;
using Wof = NadekoBot.Modules.Gambling.Common.WheelOfFortune.WheelOfFortune;
namespace NadekoBot.Modules.Gambling
{
public partial class Gambling
{
public class WheelOfFortuneCommands : NadekoSubmodule
{
private readonly CurrencyService _cs;
private readonly IBotConfigProvider _bc;
public WheelOfFortuneCommands(CurrencyService cs, IBotConfigProvider bc)
{
_cs = cs;
_bc = bc;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task WheelOfFortune(int bet)
{
const int minBet = 10;
if (bet < minBet)
{
await ReplyErrorLocalized("min_bet_limit", minBet + _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
if (!await _cs.RemoveAsync(Context.User.Id, "Wheel Of Fortune - bet", bet).ConfigureAwait(false))
{
await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
var wof = new WheelOfFortune();
var amount = (int)(bet * wof.Multiplier);
if (amount > 0)
await _cs.AddAsync(Context.User.Id, "Wheel Of Fortune - won", amount).ConfigureAwait(false);
await Context.Channel.SendConfirmAsync(
Format.Bold($@"{Context.User.ToString()} won: {amount + _bc.BotConfig.CurrencySign}
{Wof.Multipliers[1]} {Wof.Multipliers[0]} {Wof.Multipliers[7]}
{Wof.Multipliers[2]} {wof.Emoji} {Wof.Multipliers[6]}
{Wof.Multipliers[3]} {Wof.Multipliers[4]} {Wof.Multipliers[5]}")).ConfigureAwait(false);
}
//[NadekoCommand, Usage, Description, Aliases]
//[RequireContext(ContextType.Guild)]
//public async Task WofTest(int length = 1000)
//{
// var mults = new Dictionary<float, int>();
// for (int i = 0; i < length; i++)
// {
// var x = new Wof();
// if (mults.ContainsKey(x.Multiplier))
// ++mults[x.Multiplier];
// else
// mults.Add(x.Multiplier, 1);
// }
// var payout = mults.Sum(x => x.Key * x.Value);
// await Context.Channel.SendMessageAsync($"Total bet: {length}\n" +
// $"Paid out: {payout}\n" +
// $"Total Payout: {payout / length:F3}x")
// .ConfigureAwait(false);
//}
}
}
}

View File

@ -0,0 +1,148 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Games.Common.Acrophobia;
using NadekoBot.Modules.Games.Services;
namespace NadekoBot.Modules.Games
{
public partial class Games
{
[Group]
public class AcropobiaCommands : NadekoSubmodule<GamesService>
{
private readonly DiscordSocketClient _client;
public AcropobiaCommands(DiscordSocketClient client)
{
_client = client;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Acro(int submissionTime = 30)
{
if (submissionTime < 10 || submissionTime > 120)
return;
var channel = (ITextChannel)Context.Channel;
var game = new Acrophobia(submissionTime);
if (_service.AcrophobiaGames.TryAdd(channel.Id, game))
{
try
{
game.OnStarted += Game_OnStarted;
game.OnEnded += Game_OnEnded;
game.OnVotingStarted += Game_OnVotingStarted;
game.OnUserVoted += Game_OnUserVoted;
_client.MessageReceived += _client_MessageReceived;
await game.Run().ConfigureAwait(false);
}
finally
{
_client.MessageReceived -= _client_MessageReceived;
_service.AcrophobiaGames.TryRemove(channel.Id, out game);
game.Dispose();
}
}
else
{
await ReplyErrorLocalized("acro_running").ConfigureAwait(false);
}
Task _client_MessageReceived(SocketMessage msg)
{
if (msg.Channel.Id != Context.Channel.Id)
return Task.CompletedTask;
var _ = Task.Run(async () =>
{
try
{
var success = await game.UserInput(msg.Author.Id, msg.Author.ToString(), msg.Content)
.ConfigureAwait(false);
if (success)
await msg.DeleteAsync().ConfigureAwait(false);
}
catch { }
});
return Task.CompletedTask;
}
}
private Task Game_OnStarted(Acrophobia game)
{
var embed = new EmbedBuilder().WithOkColor()
.WithTitle(GetText("acrophobia"))
.WithDescription(GetText("acro_started", Format.Bold(string.Join(".", game.StartingLetters))))
.WithFooter(efb => efb.WithText(GetText("acro_started_footer", game.SubmissionPhaseLength)));
return Context.Channel.EmbedAsync(embed);
}
private Task Game_OnUserVoted(string user)
{
return Context.Channel.SendConfirmAsync(
GetText("acrophobia"),
GetText("acro_vote_cast", Format.Bold(user)));
}
private async Task Game_OnVotingStarted(Acrophobia game, ImmutableArray<KeyValuePair<AcrophobiaUser, int>> submissions)
{
if (submissions.Length == 0)
{
await Context.Channel.SendErrorAsync(GetText("acrophobia"), GetText("acro_ended_no_sub"));
return;
}
if (submissions.Length == 1)
{
await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithDescription(
GetText("acro_winner_only",
Format.Bold(submissions.First().Key.UserName)))
.WithFooter(efb => efb.WithText(submissions.First().Key.Input)))
.ConfigureAwait(false);
return;
}
var i = 0;
var embed = new EmbedBuilder()
.WithOkColor()
.WithTitle(GetText("acrophobia") + " - " + GetText("submissions_closed"))
.WithDescription(GetText("acro_nym_was", Format.Bold(string.Join(".", game.StartingLetters)) + "\n" +
$@"--
{submissions.Aggregate("", (agg, cur) => agg + $"`{++i}.` **{cur.Key.Input}**\n")}
--"))
.WithFooter(efb => efb.WithText(GetText("acro_vote")));
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
private async Task Game_OnEnded(Acrophobia game, ImmutableArray<KeyValuePair<AcrophobiaUser, int>> votes)
{
if (!votes.Any() || votes.All(x => x.Value == 0))
{
await Context.Channel.SendErrorAsync(GetText("acrophobia"), GetText("acro_no_votes_cast")).ConfigureAwait(false);
return;
}
var table = votes.OrderByDescending(v => v.Value);
var winner = table.First();
var embed = new EmbedBuilder().WithOkColor()
.WithTitle(GetText("acrophobia"))
.WithDescription(GetText("acro_winner", Format.Bold(winner.Key.UserName),
Format.Bold(winner.Value.ToString())))
.WithFooter(efb => efb.WithText(winner.Key.Input));
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
}
}
}

View File

@ -0,0 +1,56 @@
using Discord;
using Discord.Commands;
using NadekoBot.Core.Services;
using System;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Games.Services;
using NadekoBot.Modules.Games.Common.ChatterBot;
namespace NadekoBot.Modules.Games
{
public partial class Games
{
[Group]
public class ChatterBotCommands : NadekoSubmodule<ChatterBotService>
{
private readonly DbService _db;
public ChatterBotCommands(DbService db)
{
_db = db;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[RequireUserPermission(GuildPermission.ManageMessages)]
public async Task Cleverbot()
{
var channel = (ITextChannel)Context.Channel;
if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out _))
{
using (var uow = _db.UnitOfWork)
{
uow.GuildConfigs.SetCleverbotEnabled(Context.Guild.Id, false);
await uow.CompleteAsync().ConfigureAwait(false);
}
await ReplyConfirmLocalized("cleverbot_disabled").ConfigureAwait(false);
return;
}
_service.ChatterBotGuilds.TryAdd(channel.Guild.Id, new Lazy<IChatterBotSession>(() => _service.CreateSession(), true));
using (var uow = _db.UnitOfWork)
{
uow.GuildConfigs.SetCleverbotEnabled(Context.Guild.Id, true);
await uow.CompleteAsync().ConfigureAwait(false);
}
await ReplyConfirmLocalized("cleverbot_enabled").ConfigureAwait(false);
}
}
}
}

View File

@ -0,0 +1,177 @@
using NadekoBot.Common;
using NadekoBot.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common.Acrophobia
{
/// <summary>
/// Platform-agnostic acrophobia game
/// </summary>
public class Acrophobia : IDisposable
{
private const int VotingPhaseLength = 30;
public enum Phase
{
Submission,
Voting,
Ended
}
public enum UserInputResult
{
Submitted,
SubmissionFailed,
Voted,
VotingFailed,
Failed
}
public int SubmissionPhaseLength { get; }
public Phase CurrentPhase { get; private set; } = Phase.Submission;
public ImmutableArray<char> StartingLetters { get; private set; }
private readonly Dictionary<AcrophobiaUser, int> submissions = new Dictionary<AcrophobiaUser, int>();
private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);
private readonly NadekoRandom _rng;
public event Func<Acrophobia, Task> OnStarted = delegate { return Task.CompletedTask; };
public event Func<Acrophobia, ImmutableArray<KeyValuePair<AcrophobiaUser, int>>, Task> OnVotingStarted = delegate { return Task.CompletedTask; };
public event Func<string, Task> OnUserVoted = delegate { return Task.CompletedTask; };
public event Func<Acrophobia, ImmutableArray<KeyValuePair<AcrophobiaUser, int>>, Task> OnEnded = delegate { return Task.CompletedTask; };
private readonly HashSet<ulong> _usersWhoVoted = new HashSet<ulong>();
public Acrophobia(int submissionPhaseLength = 30)
{
_rng = new NadekoRandom();
SubmissionPhaseLength = submissionPhaseLength;
InitializeStartingLetters();
}
public async Task Run()
{
await OnStarted(this).ConfigureAwait(false);
await Task.Delay(SubmissionPhaseLength * 1000);
await locker.WaitAsync().ConfigureAwait(false);
try
{
if (submissions.Count == 0)
{
CurrentPhase = Phase.Ended;
await OnVotingStarted(this, ImmutableArray.Create<KeyValuePair<AcrophobiaUser, int>>()).ConfigureAwait(false);
return;
}
if (submissions.Count == 1)
{
CurrentPhase = Phase.Ended;
await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false);
return;
}
CurrentPhase = Phase.Voting;
await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false);
}
finally { locker.Release(); }
await Task.Delay(VotingPhaseLength * 1000);
await locker.WaitAsync().ConfigureAwait(false);
try
{
CurrentPhase = Phase.Ended;
await OnEnded(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false) ;
}
finally { locker.Release(); }
}
private void InitializeStartingLetters()
{
var wordCount = _rng.Next(3, 6);
var lettersArr = new char[wordCount];
for (int i = 0; i < wordCount; i++)
{
var randChar = (char)_rng.Next(65, 91);
lettersArr[i] = randChar == 'X' ? (char)_rng.Next(65, 88) : randChar;
}
StartingLetters = lettersArr.ToImmutableArray();
}
public async Task<bool> UserInput(ulong userId, string userName, string input)
{
var user = new AcrophobiaUser(userId, userName, input.ToLowerInvariant().ToTitleCase());
await locker.WaitAsync();
try
{
switch (CurrentPhase)
{
case Phase.Submission:
if (submissions.ContainsKey(user) || !IsValidAnswer(input))
break;
submissions.Add(user, 0);
return true;
case Phase.Voting:
AcrophobiaUser toVoteFor;
if (!int.TryParse(input, out var index)
|| --index < 0
|| index >= submissions.Count
|| (toVoteFor = submissions.ToArray()[index].Key).UserId == user.UserId
|| !_usersWhoVoted.Add(userId))
break;
++submissions[toVoteFor];
var _ = Task.Run(() => OnUserVoted(userName));
return true;
default:
break;
}
return false;
}
finally
{
locker.Release();
}
}
private bool IsValidAnswer(string input)
{
input = input.ToUpperInvariant();
var inputWords = input.Split(' ');
if (inputWords.Length != StartingLetters.Length) // number of words must be the same as the number of the starting letters
return false;
for (int i = 0; i < StartingLetters.Length; i++)
{
var letter = StartingLetters[i];
if (!inputWords[i].StartsWith(letter.ToString())) // all first letters must match
return false;
}
return true;
}
public void Dispose()
{
this.CurrentPhase = Phase.Ended;
OnStarted = null;
OnEnded = null;
OnUserVoted = null;
OnVotingStarted = null;
_usersWhoVoted.Clear();
submissions.Clear();
locker.Dispose();
}
}
}

View File

@ -0,0 +1,28 @@
namespace NadekoBot.Modules.Games.Common.Acrophobia
{
public class AcrophobiaUser
{
public string UserName { get; }
public ulong UserId { get; }
public string Input { get; }
public AcrophobiaUser(ulong userId, string userName, string input)
{
this.UserName = userName;
this.UserId = userId;
this.Input = input;
}
public override int GetHashCode()
{
return UserId.GetHashCode();
}
public override bool Equals(object obj)
{
return obj is AcrophobiaUser x
? x.UserId == this.UserId
: false;
}
}
}

View File

@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Games.Common.ChatterBot
{
public class ChatterBotResponse
{
public string Convo_id { get; set; }
public string BotSay { get; set; }
}
}

View File

@ -0,0 +1,37 @@
using System.Net.Http;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Extensions;
using Newtonsoft.Json;
namespace NadekoBot.Modules.Games.Common.ChatterBot
{
public class ChatterBotSession : IChatterBotSession
{
private static NadekoRandom Rng { get; } = new NadekoRandom();
private readonly string _chatterBotId;
private int _botId = 6;
public ChatterBotSession()
{
_chatterBotId = Rng.Next(0, 1000000).ToString().ToBase64();
}
private string apiEndpoint => "http://api.program-o.com/v2/chatbot/" +
$"?bot_id={_botId}&" +
"say={0}&" +
$"convo_id=nadekobot_{_chatterBotId}&" +
"format=json";
public async Task<string> Think(string message)
{
using (var http = new HttpClient())
{
var res = await http.GetStringAsync(string.Format(apiEndpoint, message)).ConfigureAwait(false);
var cbr = JsonConvert.DeserializeObject<ChatterBotResponse>(res);
return cbr.BotSay.Replace("<br/>", "\n");
}
}
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common.ChatterBot
{
public class CleverbotResponse
{
public string Cs { get; set; }
public string Output { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common.ChatterBot
{
public interface IChatterBotSession
{
Task<string> Think(string input);
}
}

View File

@ -0,0 +1,33 @@
using Newtonsoft.Json;
using System.Net.Http;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common.ChatterBot
{
public class OfficialCleverbotSession : IChatterBotSession
{
private readonly string _apiKey;
private string _cs = null;
private string queryString => $"https://www.cleverbot.com/getreply?key={_apiKey}" +
"&wrapper=nadekobot" +
"&input={0}" +
"&cs={1}";
public OfficialCleverbotSession(string apiKey)
{
this._apiKey = apiKey;
}
public async Task<string> Think(string input)
{
using (var http = new HttpClient())
{
var dataString = await http.GetStringAsync(string.Format(queryString, input, _cs ?? "")).ConfigureAwait(false);
var data = JsonConvert.DeserializeObject<CleverbotResponse>(dataString);
_cs = data?.Cs;
return data?.Output;
}
}
}
}

View File

@ -0,0 +1,364 @@
using NadekoBot.Common;
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common.Connect4
{
public class Connect4Game : IDisposable
{
public enum Phase
{
Joining, // waiting for second player to join
P1Move,
P2Move,
Ended,
}
public enum Field //temporary most likely
{
Empty,
P1,
P2,
}
public enum Result
{
Draw,
CurrentPlayerWon,
OtherPlayerWon,
}
public const int NumberOfColumns = 6;
public const int NumberOfRows = 7;
public Phase CurrentPhase { get; private set; } = Phase.Joining;
//state is bottom to top, left to right
private readonly Field[] _gameState = new Field[NumberOfRows * NumberOfColumns];
private readonly (ulong UserId, string Username)?[] _players = new(ulong, string)?[2];
public ImmutableArray<Field> GameState => _gameState.ToImmutableArray();
public ImmutableArray<(ulong UserId, string Username)?> Players => _players.ToImmutableArray();
public string CurrentPlayer => CurrentPhase == Phase.P1Move
? _players[0].Value.Username
: _players[1].Value.Username;
public string OtherPlayer => CurrentPhase == Phase.P2Move
? _players[0].Value.Username
: _players[1].Value.Username;
//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;
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
private readonly NadekoRandom _rng;
private Timer _playerTimeoutTimer;
/* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
* [ ][ ][ ][ ][ ][ ]
*/
public Connect4Game(ulong userId, string userName)
{
_players[0] = (userId, userName);
_rng = new NadekoRandom();
for (int i = 0; i < NumberOfColumns * NumberOfRows; i++)
{
_gameState[i] = Field.Empty;
}
}
public void Initialize()
{
if (CurrentPhase != Phase.Joining)
return;
var _ = Task.Run(async () =>
{
await Task.Delay(15000).ConfigureAwait(false);
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (_players[1] == null)
{
var __ = OnGameFailedToStart?.Invoke(this);
CurrentPhase = Phase.Ended;
return;
}
}
finally { _locker.Release(); }
});
}
public async Task<bool> Join(ulong userId, string userName)
{
await _locker.WaitAsync().ConfigureAwait(false);
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 (_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 Timer(async state =>
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
EndGame(Result.OtherPlayerWon);
}
finally { _locker.Release(); }
}, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
var __ = OnGameStateUpdated?.Invoke(this);
return true;
}
finally { _locker.Release(); }
}
public async Task<bool> Input(ulong userId, string userName, int inputCol)
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
inputCol -= 1;
if (CurrentPhase == Phase.Ended || CurrentPhase == 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 < 0 || inputCol > NumberOfColumns) //invalid input
return false;
if (IsColumnFull(inputCol)) //can't play there event?
return false;
var start = NumberOfRows * inputCol;
for (int i = start; i < start + NumberOfRows; 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 (int i = 0; i < NumberOfRows - 3; i++)
{
if (CurrentPhase == Phase.Ended)
break;
for (int j = 0; j < NumberOfColumns; j++)
{
if (CurrentPhase == Phase.Ended)
break;
var first = _gameState[i + j * NumberOfRows];
if (first != Field.Empty)
{
//Console.WriteLine(i + j * NumberOfRows);
for (int k = 1; k < 4; k++)
{
var next = _gameState[i + k + j * NumberOfRows];
if (next == first)
{
//Console.WriteLine(i + k + j * NumberOfRows);
if (k == 3)
EndGame(Result.CurrentPlayerWon);
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 (int i = 0; i < NumberOfColumns - 3; i++)
{
if (CurrentPhase == Phase.Ended)
break;
for (int j = 0; j < NumberOfRows; j++)
{
if (CurrentPhase == Phase.Ended)
break;
var first = _gameState[j + i * NumberOfRows];
if (first != Field.Empty)
{
for (int k = 1; k < 4; k++)
{
var next = _gameState[j + (i + k) * NumberOfRows];
if (next == first)
if (k == 3)
EndGame(Result.CurrentPlayerWon);
else
continue;
else break;
}
}
}
}
//need to check diagonal now
for (int col = 0; col < NumberOfColumns; col++)
{
if (CurrentPhase == Phase.Ended)
break;
for (int row = 0; row < NumberOfRows; row++)
{
if (CurrentPhase == Phase.Ended)
break;
var first = _gameState[row + col * NumberOfRows];
if (first != Field.Empty)
{
var same = 1;
//top left
for (int 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 >= NumberOfRows || curRow < 0)
break;
if (curCol < 0 || curCol >= NumberOfColumns)
break;
var cur = _gameState[curRow + curCol * NumberOfRows];
if (cur == first)
same++;
else break;
}
if (same == 4)
{
Console.WriteLine($"Won top left diagonal starting from {row + col * NumberOfRows}");
EndGame(Result.CurrentPlayerWon);
break;
}
same = 1;
//top right
for (int 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 >= NumberOfRows || curRow < 0)
break;
if (curCol < 0 || curCol >= NumberOfColumns)
break;
var cur = _gameState[curRow + curCol * NumberOfRows];
if (cur == first)
same++;
else break;
}
if (same == 4)
{
Console.WriteLine($"Won top right diagonal starting from {row + col * NumberOfRows}");
EndGame(Result.CurrentPlayerWon);
break;
}
}
}
}
//check draw? if it's even possible
if (_gameState.All(x => x != Field.Empty))
{
EndGame(Result.Draw);
}
if (CurrentPhase != Phase.Ended)
{
if (CurrentPhase == Phase.P1Move)
CurrentPhase = Phase.P2Move;
else
CurrentPhase = Phase.P1Move;
ResetTimer();
}
var _ = OnGameStateUpdated?.Invoke(this);
return true;
}
finally { _locker.Release(); }
}
private void ResetTimer()
{
_playerTimeoutTimer.Change(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
}
private void EndGame(Result result)
{
if (CurrentPhase == Phase.Ended)
return;
var _ = OnGameEnded?.Invoke(this, result);
CurrentPhase = Phase.Ended;
}
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 = NumberOfRows * column;
for (int i = start; i < start + NumberOfRows; 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);
}
}
}

View File

@ -0,0 +1,73 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using ImageSharp;
using NadekoBot.Common;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using NLog;
using SixLabors.Primitives;
namespace NadekoBot.Modules.Games.Common
{
public class GirlRating
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
public double Crazy { get; }
public double Hot { get; }
public int Roll { get; }
public string Advice { get; }
public AsyncLazy<string> Url { get; }
public GirlRating(IImagesService _images, double crazy, double hot, int roll, string advice)
{
Crazy = crazy;
Hot = hot;
Roll = roll;
Advice = advice; // convenient to have it here, even though atm there are only few different ones.
Url = new AsyncLazy<string>(async () =>
{
try
{
using (var ms = new MemoryStream(_images.WifeMatrix.ToArray(), false))
using (var img = Image.Load(ms))
{
const int minx = 35;
const int miny = 385;
const int length = 345;
var pointx = (int)(minx + length * (Hot / 10));
var pointy = (int)(miny - length * ((Crazy - 4) / 6));
using (var pointMs = new MemoryStream(_images.RategirlDot.ToArray(), false))
using (var pointImg = Image.Load(pointMs))
{
img.DrawImage(pointImg, 100, default(Size), new Point(pointx - 10, pointy - 10));
}
string url;
using (var http = new HttpClient())
using (var imgStream = new MemoryStream())
{
img.SaveAsPng(imgStream);
var byteContent = new ByteArrayContent(imgStream.ToArray());
http.AddFakeHeaders();
var reponse = await http.PutAsync("https://transfer.sh/img.png", byteContent);
url = await reponse.Content.ReadAsStringAsync();
}
return url;
}
}
catch (Exception ex)
{
_log.Warn(ex);
return null;
}
});
}
}
}

View File

@ -0,0 +1,11 @@
using System;
namespace NadekoBot.Modules.Games.Common.Hangman.Exceptions
{
public class TermNotFoundException : Exception
{
public TermNotFoundException() : base("Term of that type couldn't be found")
{
}
}
}

View File

@ -0,0 +1,172 @@
using NadekoBot.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common.Hangman
{
public class Hangman : IDisposable
{
public string TermType { get; }
public TermPool TermPool { get; }
public HangmanObject Term { get; }
public string ScrambledWord => "`" + String.Concat(Term.Word.Select(c =>
{
if (c == ' ')
return " \u2000";
if (!(char.IsLetter(c) || char.IsDigit(c)))
return $" {c}";
c = char.ToLowerInvariant(c);
return _previousGuesses.Contains(c) ? $" {c}" : " ◯";
})) + "`";
private Phase _currentPhase = Phase.Active;
public Phase CurrentPhase
{
get => _currentPhase;
set
{
if (value == Phase.Ended)
_endingCompletionSource.TrySetResult(true);
_currentPhase = value;
}
}
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
private readonly HashSet<ulong> _recentUsers = new HashSet<ulong>();
public uint Errors { get; private set; } = 0;
public uint MaxErrors { get; } = 6;
public event Func<Hangman, string, Task> OnGameEnded = delegate { return Task.CompletedTask; };
public event Func<Hangman, string, char, Task> OnLetterAlreadyUsed = delegate { return Task.CompletedTask; };
public event Func<Hangman, string, char, Task> OnGuessFailed = delegate { return Task.CompletedTask; };
public event Func<Hangman, string, char, Task> OnGuessSucceeded = delegate { return Task.CompletedTask; };
private readonly HashSet<char> _previousGuesses = new HashSet<char>();
public ImmutableArray<char> PreviousGuesses => _previousGuesses.ToImmutableArray();
private readonly TaskCompletionSource<bool> _endingCompletionSource = new TaskCompletionSource<bool>();
public Task EndedTask => _endingCompletionSource.Task;
public Hangman(string type, TermPool tp = null)
{
this.TermType = type.Trim().ToLowerInvariant().ToTitleCase();
this.TermPool = tp ?? new TermPool();
this.Term = this.TermPool.GetTerm(type);
}
private void AddError()
{
Errors++;
if (Errors > MaxErrors)
{
var _ = OnGameEnded(this, null);
CurrentPhase = Phase.Ended;
}
}
public string GetHangman() => $@". ┌─────┐
................
................
.{(Errors > 0 ? ".............😲" : "")}
.{(Errors > 1 ? "............./" : "")} {(Errors > 2 ? "|" : "")} {(Errors > 3 ? "\\" : "")}
.{(Errors > 4 ? "............../" : "")} {(Errors > 5 ? "\\" : "")}
/-\";
public async Task Input(ulong userId, string userName, string input)
{
if (CurrentPhase == Phase.Ended)
return;
if (string.IsNullOrWhiteSpace(input))
return;
input = input.Trim().ToLowerInvariant();
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (CurrentPhase == Phase.Ended)
return;
if (input.Length > 1) // tried to guess the whole word
{
if (input != Term.Word) // failed
return;
var _ = OnGameEnded?.Invoke(this, userName);
CurrentPhase = Phase.Ended;
return;
}
var ch = input[0];
if (!(char.IsLetterOrDigit(ch)))
return;
if (!_recentUsers.Add(userId)) // don't let a single user spam guesses
return;
if (!_previousGuesses.Add(ch)) // that latter was already guessed
{
var _ = OnLetterAlreadyUsed?.Invoke(this, userName, ch);
AddError();
}
else if (!Term.Word.Contains(ch)) // guessed letter doesn't exist
{
var _ = OnGuessFailed?.Invoke(this, userName, ch);
AddError();
}
else if (Term.Word.All(x => _previousGuesses.IsSupersetOf(Term.Word.ToLowerInvariant()
.Where(c => char.IsLetterOrDigit(c)))))
{
var _ = OnGameEnded.Invoke(this, userName); //if all letters are guessed
CurrentPhase = Phase.Ended;
}
else //guessed but not last letter
{
var _ = OnGuessSucceeded?.Invoke(this, userName, ch);
_recentUsers.Remove(userId); // he can guess again right away
return;
}
var clearSpam = Task.Run(async () =>
{
await Task.Delay(3000).ConfigureAwait(false); // remove the user from the spamlist after 5 seconds
_recentUsers.Remove(userId);
});
}
finally { _locker.Release(); }
}
public async Task Stop()
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
CurrentPhase = Phase.Ended;
}
finally { _locker.Release(); }
}
public void Dispose()
{
OnGameEnded = null;
OnGuessFailed = null;
OnGuessSucceeded = null;
OnLetterAlreadyUsed = null;
_previousGuesses.Clear();
_recentUsers.Clear();
_locker.Dispose();
}
}
}

View File

@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Games.Common.Hangman
{
public class HangmanObject
{
public string Word { get; set; }
public string ImageUrl { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common.Hangman
{
public enum Phase
{
Active,
Ended,
}
}

View File

@ -0,0 +1,54 @@
using NadekoBot.Common;
using NadekoBot.Modules.Games.Common.Hangman.Exceptions;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
namespace NadekoBot.Modules.Games.Common.Hangman
{
public class TermPool
{
const string termsPath = "data/hangman3.json";
private readonly Logger _log;
public IReadOnlyDictionary<string, HangmanObject[]> Data { get; } = new Dictionary<string, HangmanObject[]>();
public TermPool()
{
_log = LogManager.GetCurrentClassLogger();
try
{
Data = JsonConvert.DeserializeObject<Dictionary<string, HangmanObject[]>>(File.ReadAllText(termsPath));
Data = Data.ToDictionary(
x => x.Key.ToLowerInvariant(),
x => x.Value);
}
catch (Exception ex)
{
_log.Warn(ex);
}
}
public HangmanObject GetTerm(string type)
{
type = type?.Trim().ToLowerInvariant();
var rng = new NadekoRandom();
if (type == "random")
{
var keys = Data.Keys.ToArray();
type = Data.Keys.ToArray()[rng.Next(0, Data.Keys.Count())];
}
if (!Data.TryGetValue(type, out var termTypes) || termTypes.Length == 0)
throw new TermNotFoundException();
var obj = termTypes[rng.Next(0, termTypes.Length)];
obj.Word = obj.Word.Trim().ToLowerInvariant();
return obj;
}
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common.Hangman
{
[Flags]
public enum TermType
{
Countries = 0,
Movies = 1,
Animals = 2,
Things = 4,
Random = 8,
}
}

View File

@ -0,0 +1,181 @@
using NadekoBot.Common;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common.Nunchi
{
public class Nunchi : IDisposable
{
public enum Phase
{
Joining,
Playing,
WaitingForNextRound,
Ended,
}
public int CurrentNumber { get; private set; } = new NadekoRandom().Next(0, 100);
public Phase CurrentPhase { get; private set; } = Phase.Joining;
public event Func<Nunchi, Task> OnGameStarted;
public event Func<Nunchi, int, Task> OnRoundStarted;
public event Func<Nunchi, Task> OnUserGuessed;
public event Func<Nunchi, (ulong Id, string Name)?, Task> OnRoundEnded; // tuple of the user who failed
public event Func<Nunchi, string, Task> OnGameEnded; // name of the user who won
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
private HashSet<(ulong Id, string Name)> _participants = new HashSet<(ulong Id, string Name)>();
private HashSet<(ulong Id, string Name)> _passed = new HashSet<(ulong Id, string Name)>();
public ImmutableArray<(ulong Id, string Name)> Participants => _participants.ToImmutableArray();
public int ParticipantCount => _participants.Count;
private const int _killTimeout = 20 * 1000;
private const int _nextRoundTimeout = 5 * 1000;
private Timer _killTimer;
public Nunchi(ulong creatorId, string creatorName)
{
_participants.Add((creatorId, creatorName));
}
public async Task<bool> Join(ulong userId, string userName)
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (CurrentPhase != Phase.Joining)
return false;
return _participants.Add((userId, userName));
}
finally { _locker.Release(); }
}
public async Task<bool> Initialize()
{
CurrentPhase = Phase.Joining;
await Task.Delay(30000).ConfigureAwait(false);
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (_participants.Count < 3)
{
CurrentPhase = Phase.Ended;
return false;
}
_killTimer = new Timer(async state =>
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (CurrentPhase != Phase.Playing)
return;
//if some players took too long to type a number, boot them all out and start a new round
_participants = new HashSet<(ulong, string)>(_passed);
EndRound();
}
finally { _locker.Release(); }
}, null, _killTimeout, _killTimeout);
CurrentPhase = Phase.Playing;
var _ = OnGameStarted?.Invoke(this);
var __ = OnRoundStarted?.Invoke(this, CurrentNumber);
return true;
}
finally { _locker.Release(); }
}
public async Task Input(ulong userId, string userName, int input)
{
await _locker.WaitAsync().ConfigureAwait(false);
try
{
if (CurrentPhase != Phase.Playing)
return;
var userTuple = (Id: userId, Name: userName);
// if the user is not a member of the race,
// or he already successfully typed the number
// ignore the input
if (!_participants.Contains(userTuple) || !_passed.Add(userTuple))
return;
//if the number is correct
if (CurrentNumber == input - 1)
{
//increment current number
++CurrentNumber;
if (_passed.Count == _participants.Count - 1)
{
// if only n players are left, and n - 1 type the correct number, round is over
// if only 2 players are left, game is over
if (_participants.Count == 2)
{
_killTimer.Change(Timeout.Infinite, Timeout.Infinite);
CurrentPhase = Phase.Ended;
var _ = OnGameEnded?.Invoke(this, userTuple.Name);
}
else // else just start the new round without the user who was the last
{
var failure = _participants.Except(_passed).First();
EndRound(failure);
}
}
var __ = OnUserGuessed?.Invoke(this);
}
else
{
//if the user failed
EndRound(userTuple);
}
}
finally { _locker.Release(); }
}
private void EndRound((ulong, string)? failure = null)
{
_killTimer.Change(_killTimeout, _killTimeout);
CurrentNumber = new NadekoRandom().Next(0, 100); // reset the counter
_passed.Clear(); // reset all users who passed (new round starts)
if(failure != null)
_participants.Remove(failure.Value); // remove the dude who failed from the list of players
var __ = OnRoundEnded?.Invoke(this, failure);
if (_participants.Count <= 1) // means we have a winner or everyone was booted out
{
_killTimer.Change(Timeout.Infinite, Timeout.Infinite);
CurrentPhase = Phase.Ended;
var _ = OnGameEnded?.Invoke(this, _participants.Count > 0 ? _participants.First().Name : null);
return;
}
CurrentPhase = Phase.WaitingForNextRound;
var throwawayDelay = Task.Run(async () =>
{
await Task.Delay(_nextRoundTimeout).ConfigureAwait(false);
CurrentPhase = Phase.Playing;
var ___ = OnRoundStarted?.Invoke(this, CurrentNumber);
});
}
public void Dispose()
{
OnGameEnded = null;
OnGameStarted = null;
OnRoundEnded = null;
OnRoundStarted = null;
OnUserGuessed = null;
}
}
}

View File

@ -0,0 +1,129 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using NadekoBot.Extensions;
using NadekoBot.Core.Services.Impl;
namespace NadekoBot.Modules.Games.Common
{
public class Poll
{
private readonly IUserMessage _originalMessage;
private readonly IGuild _guild;
private readonly string[] answers;
private readonly ConcurrentDictionary<ulong, int> _participants = new ConcurrentDictionary<ulong, int>();
private readonly string _question;
private readonly DiscordSocketClient _client;
private readonly NadekoStrings _strings;
private bool running = false;
public event Action<ulong> OnEnded = delegate { };
public Poll(DiscordSocketClient client, NadekoStrings strings, IUserMessage umsg, string question, IEnumerable<string> enumerable)
{
_client = client;
_strings = strings;
_originalMessage = umsg;
_guild = ((ITextChannel)umsg.Channel).Guild;
_question = question;
answers = enumerable as string[] ?? enumerable.ToArray();
}
public EmbedBuilder GetStats(string title)
{
var results = _participants.GroupBy(kvp => kvp.Value)
.ToDictionary(x => x.Key, x => x.Sum(kvp => 1))
.OrderByDescending(kvp => kvp.Value)
.ToArray();
var eb = new EmbedBuilder().WithTitle(title);
var sb = new StringBuilder()
.AppendLine(Format.Bold(_question))
.AppendLine();
var totalVotesCast = 0;
if (results.Length == 0)
{
sb.AppendLine(GetText("no_votes_cast"));
}
else
{
for (int i = 0; i < results.Length; i++)
{
var result = results[i];
sb.AppendLine(GetText("poll_result",
result.Key,
Format.Bold(answers[result.Key - 1]),
Format.Bold(result.Value.ToString())));
totalVotesCast += result.Value;
}
}
eb.WithDescription(sb.ToString())
.WithFooter(efb => efb.WithText(GetText("x_votes_cast", totalVotesCast)));
return eb;
}
public async Task StartPoll()
{
var msgToSend = GetText("poll_created", Format.Bold(_originalMessage.Author.Username)) + "\n\n" + Format.Bold(_question) + "\n";
var num = 1;
msgToSend = answers.Aggregate(msgToSend, (current, answ) => current + $"`{num++}.` **{answ}**\n");
msgToSend += "\n" + Format.Bold(GetText("poll_vote_public"));
await _originalMessage.Channel.SendConfirmAsync(msgToSend).ConfigureAwait(false);
running = true;
}
public async Task StopPoll()
{
running = false;
OnEnded(_guild.Id);
await _originalMessage.Channel.EmbedAsync(GetStats("POLL CLOSED")).ConfigureAwait(false);
}
public async Task<bool> TryVote(IUserMessage msg)
{
// has to be a user message
if (msg == null || msg.Author.IsBot || !running)
return false;
// has to be an integer
if (!int.TryParse(msg.Content, out int vote))
return false;
if (vote < 1 || vote > answers.Length)
return false;
IMessageChannel ch;
//if public, channel must be the same the poll started in
if (_originalMessage.Channel.Id != msg.Channel.Id)
return false;
ch = msg.Channel;
//user can vote only once
if (_participants.TryAdd(msg.Author.Id, vote))
{
var toDelete = await ch.SendConfirmAsync(GetText("poll_voted", Format.Bold(msg.Author.ToString()))).ConfigureAwait(false);
toDelete.DeleteAfter(5);
try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
return true;
}
return false;
}
private string GetText(string key, params object[] replacements)
=> _strings.GetText(key,
_guild.Id,
"Games".ToLowerInvariant(),
replacements);
}
}

View File

@ -0,0 +1,277 @@
using Discord;
using Discord.WebSocket;
using NadekoBot.Extensions;
using NadekoBot.Core.Services.Impl;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games.Common
{
public class TicTacToe
{
enum Phase
{
Starting,
Started,
Ended
}
private readonly ITextChannel _channel;
private readonly IGuildUser[] _users;
private readonly int?[,] _state;
private Phase _phase;
private int _curUserIndex;
private readonly SemaphoreSlim _moveLock;
private IGuildUser _winner;
private readonly string[] _numbers = { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:", ":nine:" };
public Action<TicTacToe> OnEnded;
private IUserMessage _previousMessage;
private Timer _timeoutTimer;
private readonly NadekoStrings _strings;
private readonly DiscordSocketClient _client;
public TicTacToe(NadekoStrings strings, DiscordSocketClient client, ITextChannel channel, IGuildUser firstUser)
{
_channel = channel;
_strings = strings;
_client = client;
_users = new[] { firstUser, null };
_state = new int?[,] {
{ null, null, null },
{ null, null, null },
{ null, null, null },
};
_phase = Phase.Starting;
_moveLock = new SemaphoreSlim(1, 1);
}
private string GetText(string key, params object[] replacements) =>
_strings.GetText(key,
_channel.GuildId,
typeof(Games).Name.ToLowerInvariant(),
replacements);
public string GetState()
{
var sb = new StringBuilder();
for (var i = 0; i < _state.GetLength(0); i++)
{
for (var j = 0; j < _state.GetLength(1); j++)
{
sb.Append(_state[i, j] == null ? _numbers[i * 3 + j] : GetIcon(_state[i, j]));
if (j < _state.GetLength(1) - 1)
sb.Append("┃");
}
if (i < _state.GetLength(0) - 1)
sb.AppendLine("\n──────────");
}
return sb.ToString();
}
public EmbedBuilder GetEmbed(string title = null)
{
var embed = new EmbedBuilder()
.WithOkColor()
.WithDescription(Environment.NewLine + GetState())
.WithAuthor(eab => eab.WithName(GetText("vs", _users[0], _users[1])));
if (!string.IsNullOrWhiteSpace(title))
embed.WithTitle(title);
if (_winner == null)
{
if (_phase == Phase.Ended)
embed.WithFooter(efb => efb.WithText(GetText("ttt_no_moves")));
else
embed.WithFooter(efb => efb.WithText(GetText("ttt_users_move", _users[_curUserIndex])));
}
else
embed.WithFooter(efb => efb.WithText(GetText("ttt_has_won", _winner)));
return embed;
}
private static string GetIcon(int? val)
{
switch (val)
{
case 0:
return "❌";
case 1:
return "⭕";
case 2:
return "❎";
case 3:
return "🅾";
default:
return "⬛";
}
}
public async Task Start(IGuildUser user)
{
if (_phase == Phase.Started || _phase == Phase.Ended)
{
await _channel.SendErrorAsync(user.Mention + GetText("ttt_already_running")).ConfigureAwait(false);
return;
}
else if (_users[0] == user)
{
await _channel.SendErrorAsync(user.Mention + GetText("ttt_against_yourself")).ConfigureAwait(false);
return;
}
_users[1] = user;
_phase = Phase.Started;
_timeoutTimer = new Timer(async (_) =>
{
await _moveLock.WaitAsync();
try
{
if (_phase == Phase.Ended)
return;
_phase = Phase.Ended;
if (_users[1] != null)
{
_winner = _users[_curUserIndex ^= 1];
var del = _previousMessage?.DeleteAsync();
try
{
await _channel.EmbedAsync(GetEmbed(GetText("ttt_time_expired"))).ConfigureAwait(false);
if (del != null)
await del.ConfigureAwait(false);
}
catch { }
}
OnEnded?.Invoke(this);
}
catch { }
finally
{
_moveLock.Release();
}
}, null, 15000, Timeout.Infinite);
_client.MessageReceived += Client_MessageReceived;
_previousMessage = await _channel.EmbedAsync(GetEmbed(GetText("game_started"))).ConfigureAwait(false);
}
private bool IsDraw()
{
for (var i = 0; i < 3; i++)
{
for (var j = 0; j < 3; j++)
{
if (_state[i, j] == null)
return false;
}
}
return true;
}
private Task Client_MessageReceived(SocketMessage msg)
{
var _ = Task.Run(async () =>
{
await _moveLock.WaitAsync().ConfigureAwait(false);
try
{
var curUser = _users[_curUserIndex];
if (_phase == Phase.Ended || msg.Author?.Id != curUser.Id)
return;
if (int.TryParse(msg.Content, out var index) &&
--index >= 0 &&
index <= 9 &&
_state[index / 3, index % 3] == null)
{
_state[index / 3, index % 3] = _curUserIndex;
// i'm lazy
if (_state[index / 3, 0] == _state[index / 3, 1] && _state[index / 3, 1] == _state[index / 3, 2])
{
_state[index / 3, 0] = _curUserIndex + 2;
_state[index / 3, 1] = _curUserIndex + 2;
_state[index / 3, 2] = _curUserIndex + 2;
_phase = Phase.Ended;
}
else if (_state[0, index % 3] == _state[1, index % 3] && _state[1, index % 3] == _state[2, index % 3])
{
_state[0, index % 3] = _curUserIndex + 2;
_state[1, index % 3] = _curUserIndex + 2;
_state[2, index % 3] = _curUserIndex + 2;
_phase = Phase.Ended;
}
else if (_curUserIndex == _state[0, 0] && _state[0, 0] == _state[1, 1] && _state[1, 1] == _state[2, 2])
{
_state[0, 0] = _curUserIndex + 2;
_state[1, 1] = _curUserIndex + 2;
_state[2, 2] = _curUserIndex + 2;
_phase = Phase.Ended;
}
else if (_curUserIndex == _state[0, 2] && _state[0, 2] == _state[1, 1] && _state[1, 1] == _state[2, 0])
{
_state[0, 2] = _curUserIndex + 2;
_state[1, 1] = _curUserIndex + 2;
_state[2, 0] = _curUserIndex + 2;
_phase = Phase.Ended;
}
var reason = "";
if (_phase == Phase.Ended) // if user won, stop receiving moves
{
reason = GetText("ttt_matched_three");
_winner = _users[_curUserIndex];
_client.MessageReceived -= Client_MessageReceived;
OnEnded?.Invoke(this);
}
else if (IsDraw())
{
reason = GetText("ttt_a_draw");
_phase = Phase.Ended;
_client.MessageReceived -= Client_MessageReceived;
OnEnded?.Invoke(this);
}
var sendstate = Task.Run(async () =>
{
var del1 = msg.DeleteAsync();
var del2 = _previousMessage?.DeleteAsync();
try { _previousMessage = await _channel.EmbedAsync(GetEmbed(reason)); } catch { }
try { await del1; } catch { }
try { if (del2 != null) await del2; } catch { }
});
_curUserIndex ^= 1;
_timeoutTimer.Change(15000, Timeout.Infinite);
}
}
finally
{
_moveLock.Release();
}
});
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,266 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Discord;
using Discord.Net;
using Discord.WebSocket;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Impl;
using NLog;
namespace NadekoBot.Modules.Games.Common.Trivia
{
public class TriviaGame
{
private readonly SemaphoreSlim _guessLock = new SemaphoreSlim(1, 1);
private readonly Logger _log;
private readonly NadekoStrings _strings;
private readonly DiscordSocketClient _client;
private readonly IBotConfigProvider _bc;
private readonly CurrencyService _cs;
public IGuild Guild { get; }
public ITextChannel Channel { get; }
private readonly int _questionDurationMiliseconds = 30000;
private readonly int _hintTimeoutMiliseconds = 6000;
public bool ShowHints { get; }
public bool IsPokemon { get; }
private CancellationTokenSource _triviaCancelSource;
public TriviaQuestion CurrentQuestion { get; private set; }
public HashSet<TriviaQuestion> OldQuestions { get; } = new HashSet<TriviaQuestion>();
public ConcurrentDictionary<IGuildUser, int> Users { get; } = new ConcurrentDictionary<IGuildUser, int>();
public bool GameActive { get; private set; }
public bool ShouldStopGame { get; private set; }
public int WinRequirement { get; }
public TriviaGame(NadekoStrings strings, DiscordSocketClient client, IBotConfigProvider bc,
CurrencyService cs, IGuild guild, ITextChannel channel,
bool showHints, int winReq, bool isPokemon)
{
_log = LogManager.GetCurrentClassLogger();
_strings = strings;
_client = client;
_bc = bc;
_cs = cs;
ShowHints = showHints;
Guild = guild;
Channel = channel;
WinRequirement = winReq;
IsPokemon = isPokemon;
}
private string GetText(string key, params object[] replacements) =>
_strings.GetText(key,
Channel.GuildId,
typeof(Games).Name.ToLowerInvariant(),
replacements);
public async Task StartGame()
{
while (!ShouldStopGame)
{
// reset the cancellation source
_triviaCancelSource = new CancellationTokenSource();
// load question
CurrentQuestion = TriviaQuestionPool.Instance.GetRandomQuestion(OldQuestions, IsPokemon);
if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer) || string.IsNullOrWhiteSpace(CurrentQuestion.Question))
{
await Channel.SendErrorAsync(GetText("trivia_game"), GetText("failed_loading_question")).ConfigureAwait(false);
return;
}
OldQuestions.Add(CurrentQuestion); //add it to exclusion list so it doesn't show up again
EmbedBuilder questionEmbed;
IUserMessage questionMessage;
try
{
questionEmbed = new EmbedBuilder().WithOkColor()
.WithTitle(GetText("trivia_game"))
.AddField(eab => eab.WithName(GetText("category")).WithValue(CurrentQuestion.Category))
.AddField(eab => eab.WithName(GetText("question")).WithValue(CurrentQuestion.Question));
if (Uri.IsWellFormedUriString(CurrentQuestion.ImageUrl, UriKind.Absolute))
questionEmbed.WithImageUrl(CurrentQuestion.ImageUrl);
questionMessage = await Channel.EmbedAsync(questionEmbed).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound ||
ex.HttpCode == System.Net.HttpStatusCode.Forbidden ||
ex.HttpCode == System.Net.HttpStatusCode.BadRequest)
{
return;
}
catch (Exception ex)
{
_log.Warn(ex);
await Task.Delay(2000).ConfigureAwait(false);
continue;
}
//receive messages
try
{
_client.MessageReceived += PotentialGuess;
//allow people to guess
GameActive = true;
try
{
//hint
await Task.Delay(_hintTimeoutMiliseconds, _triviaCancelSource.Token).ConfigureAwait(false);
if (ShowHints)
try
{
await questionMessage.ModifyAsync(m => m.Embed = questionEmbed.WithFooter(efb => efb.WithText(CurrentQuestion.GetHint())).Build())
.ConfigureAwait(false);
}
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound || ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
{
break;
}
catch (Exception ex) { _log.Warn(ex); }
//timeout
await Task.Delay(_questionDurationMiliseconds - _hintTimeoutMiliseconds, _triviaCancelSource.Token).ConfigureAwait(false);
}
catch (TaskCanceledException) { } //means someone guessed the answer
}
finally
{
GameActive = false;
_client.MessageReceived -= PotentialGuess;
}
if (!_triviaCancelSource.IsCancellationRequested)
{
try
{
var embed = new EmbedBuilder().WithErrorColor()
.WithTitle(GetText("trivia_game"))
.WithDescription(GetText("trivia_times_up", Format.Bold(CurrentQuestion.Answer)));
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
await Channel.EmbedAsync(embed).ConfigureAwait(false);
}
catch (Exception ex)
{
_log.Warn(ex);
}
}
await Task.Delay(5000).ConfigureAwait(false);
}
}
public async Task EnsureStopped()
{
ShouldStopGame = true;
await Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithAuthor(eab => eab.WithName("Trivia Game Ended"))
.WithTitle("Final Results")
.WithDescription(GetLeaderboard())).ConfigureAwait(false);
}
public async Task StopGame()
{
var old = ShouldStopGame;
ShouldStopGame = true;
if (!old)
try { await Channel.SendConfirmAsync(GetText("trivia_game"), GetText("trivia_stopping")).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); }
}
private Task PotentialGuess(SocketMessage imsg)
{
var _ = Task.Run(async () =>
{
try
{
if (imsg.Author.IsBot)
return;
var umsg = imsg as SocketUserMessage;
var textChannel = umsg?.Channel as ITextChannel;
if (textChannel == null || textChannel.Guild != Guild)
return;
var guildUser = (IGuildUser)umsg.Author;
var guess = false;
await _guessLock.WaitAsync().ConfigureAwait(false);
try
{
if (GameActive && CurrentQuestion.IsAnswerCorrect(umsg.Content) && !_triviaCancelSource.IsCancellationRequested)
{
Users.AddOrUpdate(guildUser, 1, (gu, old) => ++old);
guess = true;
}
}
finally { _guessLock.Release(); }
if (!guess) return;
_triviaCancelSource.Cancel();
if (Users[guildUser] == WinRequirement)
{
ShouldStopGame = true;
try
{
var embedS = new EmbedBuilder().WithOkColor()
.WithTitle(GetText("trivia_game"))
.WithDescription(GetText("trivia_win",
guildUser.Mention,
Format.Bold(CurrentQuestion.Answer)));
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
embedS.WithImageUrl(CurrentQuestion.AnswerImageUrl);
await Channel.EmbedAsync(embedS).ConfigureAwait(false);
}
catch
{
// ignored
}
var reward = _bc.BotConfig.TriviaCurrencyReward;
if (reward > 0)
await _cs.AddAsync(guildUser, "Won trivia", reward, true).ConfigureAwait(false);
return;
}
var embed = new EmbedBuilder().WithOkColor()
.WithTitle(GetText("trivia_game"))
.WithDescription(GetText("trivia_guess", guildUser.Mention, Format.Bold(CurrentQuestion.Answer)));
if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute))
embed.WithImageUrl(CurrentQuestion.AnswerImageUrl);
await Channel.EmbedAsync(embed).ConfigureAwait(false);
}
catch (Exception ex) { _log.Warn(ex); }
});
return Task.CompletedTask;
}
public string GetLeaderboard()
{
if (Users.Count == 0)
return GetText("no_results");
var sb = new StringBuilder();
foreach (var kvp in Users.OrderByDescending(kvp => kvp.Value))
{
sb.AppendLine(GetText("trivia_points", Format.Bold(kvp.Key.ToString()), kvp.Value).SnPl(kvp.Value));
}
return sb.ToString();
}
}
}

View File

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using NadekoBot.Extensions;
// THANKS @ShoMinamimoto for suggestions and coding help
namespace NadekoBot.Modules.Games.Common.Trivia
{
public class TriviaQuestion
{
//represents the min size to judge levDistance with
private static readonly HashSet<Tuple<int, int>> strictness = new HashSet<Tuple<int, int>> {
new Tuple<int, int>(9, 0),
new Tuple<int, int>(14, 1),
new Tuple<int, int>(19, 2),
new Tuple<int, int>(22, 3),
};
public static int maxStringLength = 22;
public string Category { get; set; }
public string Question { get; set; }
public string ImageUrl { get; set; }
public string AnswerImageUrl { get; set; }
public string Answer { get; set; }
private string _cleanAnswer;
public string CleanAnswer => _cleanAnswer ?? (_cleanAnswer = Clean(Answer));
public TriviaQuestion(string q, string a, string c, string img = null, string answerImage = null)
{
this.Question = q;
this.Answer = a;
this.Category = c;
this.ImageUrl = img;
this.AnswerImageUrl = answerImage ?? img;
}
public string GetHint() => Scramble(Answer);
public bool IsAnswerCorrect(string guess)
{
if (Answer.Equals(guess))
{
return true;
}
var cleanGuess = Clean(guess);
if (CleanAnswer.Equals(cleanGuess))
{
return true;
}
int levDistanceClean = CleanAnswer.LevenshteinDistance(cleanGuess);
int levDistanceNormal = Answer.LevenshteinDistance(guess);
return JudgeGuess(CleanAnswer.Length, cleanGuess.Length, levDistanceClean)
|| JudgeGuess(Answer.Length, guess.Length, levDistanceNormal);
}
private bool JudgeGuess(int guessLength, int answerLength, int levDistance)
{
foreach (Tuple<int, int> level in strictness)
{
if (guessLength <= level.Item1 || answerLength <= level.Item1)
{
if (levDistance <= level.Item2)
return true;
else
return false;
}
}
return false;
}
private string Clean(string str)
{
str = " " + str.ToLower() + " ";
str = Regex.Replace(str, "\\s+", " ");
str = Regex.Replace(str, "[^\\w\\d\\s]", "");
//Here's where custom modification can be done
str = Regex.Replace(str, "\\s(a|an|the|of|in|for|to|as|at|be)\\s", " ");
//End custom mod and cleanup whitespace
str = Regex.Replace(str, "^\\s+", "");
str = Regex.Replace(str, "\\s+$", "");
//Trim the really long answers
str = str.Length <= maxStringLength ? str : str.Substring(0, maxStringLength);
return str;
}
private static string Scramble(string word)
{
var letters = word.ToCharArray();
var count = 0;
for (var i = 0; i < letters.Length; i++)
{
if (letters[i] == ' ')
continue;
count++;
if (count <= letters.Length / 5)
continue;
if (count % 3 == 0)
continue;
if (letters[i] != ' ')
letters[i] = '_';
}
return string.Join(" ", new string(letters).Replace(" ", " \u2000").AsEnumerable());
}
}
}

View File

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using NadekoBot.Common;
using NadekoBot.Extensions;
using Newtonsoft.Json;
namespace NadekoBot.Modules.Games.Common.Trivia
{
public class TriviaQuestionPool
{
public class PokemonNameId
{
public int Id { get; set; }
public string Name { get; set; }
}
private static TriviaQuestionPool _instance;
public static TriviaQuestionPool Instance { get; } = _instance ?? (_instance = new TriviaQuestionPool());
private const string questionsFile = "data/trivia_questions.json";
private const string pokemonMapPath = "data/pokemon/name-id_map4.json";
private readonly int maxPokemonId;
private Random rng { get; } = new NadekoRandom();
private TriviaQuestion[] pool { get; }
private ImmutableDictionary<int, string> map { get; }
static TriviaQuestionPool() { }
private TriviaQuestionPool()
{
pool = JsonConvert.DeserializeObject<TriviaQuestion[]>(File.ReadAllText(questionsFile));
map = JsonConvert.DeserializeObject<PokemonNameId[]>(File.ReadAllText(pokemonMapPath))
.ToDictionary(x => x.Id, x => x.Name)
.ToImmutableDictionary();
maxPokemonId = 721; //xd
}
public TriviaQuestion GetRandomQuestion(HashSet<TriviaQuestion> exclude, bool isPokemon)
{
if (pool.Length == 0)
return null;
if (isPokemon)
{
var num = rng.Next(1, maxPokemonId + 1);
return new TriviaQuestion("Who's That Pokémon?",
map[num].ToTitleCase(),
"Pokemon",
$@"http://nadekobot.me/images/pokemon/shadows/{num}.png",
$@"http://nadekobot.me/images/pokemon/real/{num}.png");
}
TriviaQuestion randomQuestion;
while (exclude.Contains(randomQuestion = pool[rng.Next(0, pool.Length)])) ;
return randomQuestion;
}
}
}

View File

@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Games.Common
{
public class TypingArticle
{
public string Title { get; set; }
public string Text { get; set; }
}
}

View File

@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using NadekoBot.Common;
using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Services;
using NLog;
namespace NadekoBot.Modules.Games.Common
{
public class TypingGame
{
public const float WORD_VALUE = 4.5f;
public ITextChannel Channel { get; }
public string CurrentSentence { get; private set; }
public bool IsActive { get; private set; }
private readonly Stopwatch sw;
private readonly List<ulong> finishedUserIds;
private readonly DiscordSocketClient _client;
private readonly GamesService _games;
private readonly string _prefix;
private Logger _log { get; }
public TypingGame(GamesService games, DiscordSocketClient client, ITextChannel channel, string prefix) //kek@prefix
{
_log = LogManager.GetCurrentClassLogger();
_games = games;
_client = client;
_prefix = prefix;
this.Channel = channel;
IsActive = false;
sw = new Stopwatch();
finishedUserIds = new List<ulong>();
}
public async Task<bool> Stop()
{
if (!IsActive) return false;
_client.MessageReceived -= AnswerReceived;
finishedUserIds.Clear();
IsActive = false;
sw.Stop();
sw.Reset();
try { await Channel.SendConfirmAsync("Typing contest stopped.").ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); }
return true;
}
public async Task Start()
{
if (IsActive) return; // can't start running game
IsActive = true;
CurrentSentence = GetRandomSentence();
var i = (int)(CurrentSentence.Length / WORD_VALUE * 1.7f);
try
{
await Channel.SendConfirmAsync($@":clock2: Next contest will last for {i} seconds. Type the bolded text as fast as you can.").ConfigureAwait(false);
var msg = await Channel.SendMessageAsync("Starting new typing contest in **3**...").ConfigureAwait(false);
await Task.Delay(1000).ConfigureAwait(false);
try
{
await msg.ModifyAsync(m => m.Content = "Starting new typing contest in **2**...").ConfigureAwait(false);
await Task.Delay(1000).ConfigureAwait(false);
await msg.ModifyAsync(m => m.Content = "Starting new typing contest in **1**...").ConfigureAwait(false);
await Task.Delay(1000).ConfigureAwait(false);
}
catch (Exception ex) { _log.Warn(ex); }
await msg.ModifyAsync(m => m.Content = Format.Bold(Format.Sanitize(CurrentSentence.Replace(" ", " \x200B")).SanitizeMentions())).ConfigureAwait(false);
sw.Start();
HandleAnswers();
while (i > 0)
{
await Task.Delay(1000).ConfigureAwait(false);
i--;
if (!IsActive)
return;
}
}
catch { }
finally
{
await Stop().ConfigureAwait(false);
}
}
public string GetRandomSentence()
{
if (_games.TypingArticles.Any())
return _games.TypingArticles[new NadekoRandom().Next(0, _games.TypingArticles.Count)].Text;
else
return $"No typing articles found. Use {_prefix}typeadd command to add a new article for typing.";
}
private void HandleAnswers()
{
_client.MessageReceived += AnswerReceived;
}
private Task AnswerReceived(SocketMessage imsg)
{
var _ = Task.Run(async () =>
{
try
{
if (imsg.Author.IsBot)
return;
var msg = imsg as SocketUserMessage;
if (msg == null)
return;
if (this.Channel == null || this.Channel.Id != msg.Channel.Id) return;
var guess = msg.Content;
var distance = CurrentSentence.LevenshteinDistance(guess);
var decision = Judge(distance, guess.Length);
if (decision && !finishedUserIds.Contains(msg.Author.Id))
{
var elapsed = sw.Elapsed;
var wpm = CurrentSentence.Length / WORD_VALUE / elapsed.TotalSeconds * 60;
finishedUserIds.Add(msg.Author.Id);
await this.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithTitle($"{msg.Author} finished the race!")
.AddField(efb => efb.WithName("Place").WithValue($"#{finishedUserIds.Count}").WithIsInline(true))
.AddField(efb => efb.WithName("WPM").WithValue($"{wpm:F1} *[{elapsed.TotalSeconds:F2}sec]*").WithIsInline(true))
.AddField(efb => efb.WithName("Errors").WithValue(distance.ToString()).WithIsInline(true)))
.ConfigureAwait(false);
if (finishedUserIds.Count % 4 == 0)
{
await this.Channel.SendConfirmAsync($":exclamation: A lot of people finished, here is the text for those still typing:\n\n**{Format.Sanitize(CurrentSentence.Replace(" ", " \x200B")).SanitizeMentions()}**").ConfigureAwait(false);
}
}
}
catch (Exception ex) { _log.Warn(ex); }
});
return Task.CompletedTask;
}
private bool Judge(int errors, int textLength) => errors <= textLength / 25;
}
}

View File

@ -0,0 +1,183 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Common.Attributes;
using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Common.Connect4;
using NadekoBot.Modules.Games.Services;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games
{
public partial class Games
{
[Group]
public class Connect4Commands : NadekoSubmodule<GamesService>
{
private readonly DiscordSocketClient _client;
private readonly string[] numbers = new string[] { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:"};
public Connect4Commands(DiscordSocketClient client)
{
_client = client;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Connect4()
{
var newGame = new Connect4Game(Context.User.Id, Context.User.ToString());
Connect4Game game;
if ((game = _service.Connect4Games.GetOrAdd(Context.Channel.Id, newGame)) != newGame)
{
if (game.CurrentPhase != Connect4Game.Phase.Joining)
return;
newGame.Dispose();
//means game already exists, try to join
var joined = await game.Join(Context.User.Id, Context.User.ToString()).ConfigureAwait(false);
return;
}
game.OnGameStateUpdated += Game_OnGameStateUpdated;
game.OnGameFailedToStart += Game_OnGameFailedToStart;
game.OnGameEnded += Game_OnGameEnded;
_client.MessageReceived += _client_MessageReceived;
game.Initialize();
await ReplyConfirmLocalized("connect4_created").ConfigureAwait(false);
Task _client_MessageReceived(SocketMessage arg)
{
if (Context.Channel.Id != arg.Channel.Id)
return Task.CompletedTask;
var _ = Task.Run(async () =>
{
bool success = false;
if (int.TryParse(arg.Content, out var col))
{
success = await game.Input(arg.Author.Id, arg.Author.ToString(), col).ConfigureAwait(false);
}
if (success)
try { await arg.DeleteAsync().ConfigureAwait(false); } catch { }
else
{
if (game.CurrentPhase == Connect4Game.Phase.Joining
|| game.CurrentPhase == Connect4Game.Phase.Ended)
{
return;
}
RepostCounter++;
if (RepostCounter == 0)
try { msg = await Context.Channel.SendMessageAsync("", embed: (Embed)msg.Embeds.First()); } catch { }
}
});
return Task.CompletedTask;
}
Task Game_OnGameFailedToStart(Connect4Game arg)
{
if (_service.Connect4Games.TryRemove(Context.Channel.Id, out var toDispose))
{
_client.MessageReceived -= _client_MessageReceived;
toDispose.Dispose();
}
return ErrorLocalized("connect4_failed_to_start");
}
Task Game_OnGameEnded(Connect4Game arg, Connect4Game.Result result)
{
if (_service.Connect4Games.TryRemove(Context.Channel.Id, out var toDispose))
{
_client.MessageReceived -= _client_MessageReceived;
toDispose.Dispose();
}
string title;
if (result == Connect4Game.Result.CurrentPlayerWon)
{
title = GetText("connect4_won", Format.Bold(arg.CurrentPlayer), Format.Bold(arg.OtherPlayer));
}
else if (result == Connect4Game.Result.OtherPlayerWon)
{
title = GetText("connect4_won", Format.Bold(arg.OtherPlayer), Format.Bold(arg.CurrentPlayer));
}
else
title = GetText("connect4_draw");
return msg.ModifyAsync(x => x.Embed = new EmbedBuilder()
.WithTitle(title)
.WithDescription(GetGameStateText(game))
.WithOkColor()
.Build());
}
}
private IUserMessage msg;
private int _repostCounter = 0;
private int RepostCounter
{
get => _repostCounter;
set
{
if (value < 0 || value > 7)
_repostCounter = 0;
else _repostCounter = value;
}
}
private async Task Game_OnGameStateUpdated(Connect4Game game)
{
var embed = new EmbedBuilder()
.WithTitle($"{game.CurrentPlayer} vs {game.OtherPlayer}")
.WithDescription(GetGameStateText(game))
.WithOkColor();
if (msg == null)
msg = await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
else
await msg.ModifyAsync(x => x.Embed = embed.Build()).ConfigureAwait(false);
}
private string GetGameStateText(Connect4Game game)
{
var sb = new StringBuilder();
if (game.CurrentPhase == Connect4Game.Phase.P1Move ||
game.CurrentPhase == Connect4Game.Phase.P2Move)
sb.AppendLine(GetText("connect4_player_to_move", Format.Bold(game.CurrentPlayer)));
for (int i = Connect4Game.NumberOfRows; i > 0; i--)
{
for (int j = 0; j < Connect4Game.NumberOfColumns; j++)
{
//Console.WriteLine(i + (j * Connect4Game.NumberOfRows) - 1);
var cur = game.GameState[i + (j * Connect4Game.NumberOfRows) - 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 (int i = 0; i < Connect4Game.NumberOfColumns; i++)
{
sb.Append(/*new string(' ', 1 + ((i + 1) / 2)) + */numbers[i]);
}
return sb.ToString();
}
}
}
}

View File

@ -0,0 +1,209 @@
using Discord.Commands;
using Discord;
using NadekoBot.Core.Services;
using System.Threading.Tasks;
using System;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Common;
using NadekoBot.Modules.Games.Services;
namespace NadekoBot.Modules.Games
{
/* more games
- Blackjack
- Shiritori
- Simple RPG adventure
*/
public partial class Games : NadekoTopLevelModule<GamesService>
{
private readonly IImagesService _images;
public Games(IImagesService images)
{
_images = images;
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Choose([Remainder] string list = null)
{
if (string.IsNullOrWhiteSpace(list))
return;
var listArr = list.Split(';');
if (listArr.Length < 2)
return;
var rng = new NadekoRandom();
await Context.Channel.SendConfirmAsync("🤔", listArr[rng.Next(0, listArr.Length)]).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task EightBall([Remainder] string question = null)
{
if (string.IsNullOrWhiteSpace(question))
return;
await Context.Channel.EmbedAsync(new EmbedBuilder().WithColor(NadekoBot.OkColor)
.AddField(efb => efb.WithName("test❓ " + GetText("question") ).WithValue(question).WithIsInline(false))
.AddField(efb => efb.WithName("🎱 " + GetText("8ball")).WithValue(_service.EightBallResponses[new NadekoRandom().Next(0, _service.EightBallResponses.Length)]).WithIsInline(false)));
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Rps(string input)
{
Func<int,string> getRpsPick = (p) =>
{
switch (p)
{
case 0:
return "🚀";
case 1:
return "📎";
default:
return "✂️";
}
};
int pick;
switch (input)
{
case "r":
case "rock":
case "rocket":
pick = 0;
break;
case "p":
case "paper":
case "paperclip":
pick = 1;
break;
case "scissors":
case "s":
pick = 2;
break;
default:
return;
}
var nadekoPick = new NadekoRandom().Next(0, 3);
string msg;
if (pick == nadekoPick)
msg = GetText("rps_draw", getRpsPick(pick));
else if ((pick == 0 && nadekoPick == 1) ||
(pick == 1 && nadekoPick == 2) ||
(pick == 2 && nadekoPick == 0))
msg = GetText("rps_win", Context.Client.CurrentUser.Mention,
getRpsPick(nadekoPick), getRpsPick(pick));
else
msg = GetText("rps_win", Context.User.Mention, getRpsPick(pick),
getRpsPick(nadekoPick));
await Context.Channel.SendConfirmAsync(msg).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task RateGirl(IGuildUser usr)
{
var gr = _service.GirlRatings.GetOrAdd(usr.Id, GetGirl);
var img = await gr.Url;
await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithTitle("Girl Rating For " + usr)
.AddField(efb => efb.WithName("Hot").WithValue(gr.Hot.ToString("F2")).WithIsInline(true))
.AddField(efb => efb.WithName("Crazy").WithValue(gr.Crazy.ToString("F2")).WithIsInline(true))
.AddField(efb => efb.WithName("Advice").WithValue(gr.Advice).WithIsInline(false))
.WithImageUrl(img)).ConfigureAwait(false);
}
private double NextDouble(double x, double y)
{
var rng = new Random();
return rng.NextDouble() * (y - x) + x;
}
private GirlRating GetGirl(ulong uid)
{
var rng = new NadekoRandom();
var roll = rng.Next(1, 1001);
if ((uid == 185968432783687681 ||
uid == 265642040950390784) && roll >= 900)
roll = 1000;
double hot;
double crazy;
string advice;
if (roll < 500)
{
hot = NextDouble(0, 5);
crazy = NextDouble(4, 10);
advice =
"This is your NO-GO ZONE. We do not hang around, and date, and marry women who are at least, in our mind, a 5. " +
"So, this is your no-go zone. You don't go here. You just rule this out. Life is better this way, that's the way it is.";
}
else if (roll < 750)
{
hot = NextDouble(5, 8);
crazy = NextDouble(4, .6 * hot + 4);
advice = "Above a 5, and to about an 8, and below the crazy line - this is your FUN ZONE. You can " +
"hang around here, and meet these girls and spend time with them. Keep in mind, while you're " +
"in the fun zone, you want to move OUT of the fun zone to a more permanent location. " +
"These girls are most of the time not crazy.";
}
else if (roll < 900)
{
hot = NextDouble(5, 10);
crazy = NextDouble(.61 * hot + 4, 10);
advice = "Above the crazy line - it's the DANGER ZONE. This is redheads, strippers, anyone named Tiffany, " +
"hairdressers... This is where your car gets keyed, you get bunny in the pot, your tires get slashed, " +
"and you wind up in jail.";
}
else if (roll < 951)
{
hot = NextDouble(8, 10);
crazy = NextDouble(7, .6 * hot + 4);
advice = "Below the crazy line, above an 8 hot, but still about 7 crazy. This is your DATE ZONE. " +
"You can stay in the date zone indefinitely. These are the girls you introduce to your friends and your family. " +
"They're good looking, and they're reasonably not crazy most of the time. You can stay here indefinitely.";
}
else if (roll < 990)
{
hot = NextDouble(8, 10);
crazy = NextDouble(5, 7);
advice = "Above an 8 hot, and between about 7 and a 5 crazy - this is WIFE ZONE. If you meet this girl, you should consider long-term " +
"relationship. Rare.";
}
else if (roll < 999)
{
hot = NextDouble(8, 10);
crazy = NextDouble(2, 3.99d);
advice = "You've met a girl she's above 8 hot, and not crazy at all (below 4)... totally cool?" +
" You should be careful. That's a dude. You're talking to a tranny!";
}
else
{
hot = NextDouble(8, 10);
crazy = NextDouble(4, 5);
advice = "Below 5 crazy, and above 8 hot, this is the UNICORN ZONE, these things don't exist." +
"If you find a unicorn, please capture it safely, keep it alive, we'd like to study it, " +
"and maybe look at how to replicate that.";
}
return new GirlRating(_images, crazy, hot, roll, advice);
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Linux(string guhnoo, string loonix)
{
await Context.Channel.SendConfirmAsync(
$@"I'd just like to interject for moment. What you're refering to as {loonix}, is in fact, {guhnoo}/{loonix}, or as I've recently taken to calling it, {guhnoo} plus {loonix}. {loonix} is not an operating system unto itself, but rather another free component of a fully functioning {guhnoo} system made useful by the {guhnoo} corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX.
Many computer users run a modified version of the {guhnoo} system every day, without realizing it. Through a peculiar turn of events, the version of {guhnoo} which is widely used today is often called {loonix}, and many of its users are not aware that it is basically the {guhnoo} system, developed by the {guhnoo} Project.
There really is a {loonix}, and these people are using it, but it is just a part of the system they use. {loonix} is the kernel: the program in the system that allocates the machine's resources to the other programs that you run. The kernel is an essential part of an operating system, but useless by itself; it can only function in the context of a complete operating system. {loonix} is normally used in combination with the {guhnoo} operating system: the whole system is basically {guhnoo} with {loonix} added, or {guhnoo}/{loonix}. All the so-called {loonix} distributions are really distributions of {guhnoo}/{loonix}."
).ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,135 @@
using Discord.Commands;
using NadekoBot.Extensions;
using System;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Games.Common.Hangman;
using NadekoBot.Modules.Games.Services;
namespace NadekoBot.Modules.Games
{
public partial class Games
{
[Group]
public class HangmanCommands : NadekoSubmodule<GamesService>
{
private readonly DiscordSocketClient _client;
public HangmanCommands(DiscordSocketClient client)
{
_client = client;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Hangmanlist()
{
await Context.Channel.SendConfirmAsync(Format.Code(GetText("hangman_types", Prefix)) + "\n" + string.Join("\n", _service.TermPool.Data.Keys));
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Hangman([Remainder]string type = "random")
{
var hm = new Hangman(type, _service.TermPool);
if (!_service.HangmanGames.TryAdd(Context.Channel.Id, hm))
{
hm.Dispose();
await ReplyErrorLocalized("hangman_running").ConfigureAwait(false);
return;
}
hm.OnGameEnded += Hm_OnGameEnded;
hm.OnGuessFailed += Hm_OnGuessFailed;
hm.OnGuessSucceeded += Hm_OnGuessSucceeded;
hm.OnLetterAlreadyUsed += Hm_OnLetterAlreadyUsed;
_client.MessageReceived += _client_MessageReceived;
try
{
await Context.Channel.SendConfirmAsync(GetText("hangman_game_started") + $" ({hm.TermType})",
hm.ScrambledWord + "\n" + hm.GetHangman())
.ConfigureAwait(false);
}
catch { }
await hm.EndedTask.ConfigureAwait(false);
_client.MessageReceived -= _client_MessageReceived;
_service.HangmanGames.TryRemove(Context.Channel.Id, out _);
hm.Dispose();
Task _client_MessageReceived(SocketMessage msg)
{
var _ = Task.Run(() =>
{
if (Context.Channel.Id == msg.Channel.Id)
return hm.Input(msg.Author.Id, msg.Author.ToString(), msg.Content);
else
return Task.CompletedTask;
});
return Task.CompletedTask;
}
}
Task Hm_OnGameEnded(Hangman game, string winner)
{
if (winner == null)
{
var loseEmbed = new EmbedBuilder().WithTitle($"Hangman Game ({game.TermType}) - Ended")
.WithDescription(Format.Bold("You lose."))
.AddField(efb => efb.WithName("It was").WithValue(game.Term.Word.ToTitleCase()))
.WithFooter(efb => efb.WithText(string.Join(" ", game.PreviousGuesses)))
.WithErrorColor();
if (Uri.IsWellFormedUriString(game.Term.ImageUrl, UriKind.Absolute))
loseEmbed.WithImageUrl(game.Term.ImageUrl);
return Context.Channel.EmbedAsync(loseEmbed);
}
var winEmbed = new EmbedBuilder().WithTitle($"Hangman Game ({game.TermType}) - Ended")
.WithDescription(Format.Bold($"{winner} Won."))
.AddField(efb => efb.WithName("It was").WithValue(game.Term.Word.ToTitleCase()))
.WithFooter(efb => efb.WithText(string.Join(" ", game.PreviousGuesses)))
.WithOkColor();
if (Uri.IsWellFormedUriString(game.Term.ImageUrl, UriKind.Absolute))
winEmbed.WithImageUrl(game.Term.ImageUrl);
return Context.Channel.EmbedAsync(winEmbed);
}
private Task Hm_OnLetterAlreadyUsed(Hangman game, string user, char guess)
{
return Context.Channel.SendErrorAsync($"Hangman Game ({game.TermType})", $"{user} Letter `{guess}` has already been used. You can guess again in 3 seconds.\n" + game.ScrambledWord + "\n" + game.GetHangman(),
footer: string.Join(" ", game.PreviousGuesses));
}
private Task Hm_OnGuessSucceeded(Hangman game, string user, char guess)
{
return Context.Channel.SendConfirmAsync($"Hangman Game ({game.TermType})", $"{user} guessed a letter `{guess}`!\n" + game.ScrambledWord + "\n" + game.GetHangman(),
footer: string.Join(" ", game.PreviousGuesses));
}
private Task Hm_OnGuessFailed(Hangman game, string user, char guess)
{
return Context.Channel.SendErrorAsync($"Hangman Game ({game.TermType})", $"{user} Letter `{guess}` does not exist. You can guess again in 3 seconds.\n" + game.ScrambledWord + "\n" + game.GetHangman(),
footer: string.Join(" ", game.PreviousGuesses));
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task HangmanStop()
{
if (_service.HangmanGames.TryRemove(Context.Channel.Id, out var removed))
{
await removed.Stop().ConfigureAwait(false);
await ReplyConfirmLocalized("hangman_stopped").ConfigureAwait(false);
}
}
}
}
}

View File

@ -0,0 +1,305 @@
using Discord.Commands;
using NadekoBot.Extensions;
using System.Text;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
// taken from
// http://www.codeproject.com/Tips/207582/L-t-Tr-nsl-t-r-Leet-Translator (thanks)
// because i don't want to waste my time on this cancerous command
namespace NadekoBot.Modules.Games
{
public partial class Games
{
[NadekoCommand, Usage, Description, Aliases]
public async Task Leet(int level, [Remainder] string text = null)
{
text = text.Trim();
if (string.IsNullOrWhiteSpace(text))
return;
await Context.Channel.SendConfirmAsync("L33t", ToLeet(text, level).SanitizeMentions()).ConfigureAwait(false);
}
/// <summary>
/// Translate text to Leet - Extension methods for string class
/// </summary>
/// <param name="text">Orginal text</param>
/// <param name="degree">Degree of translation (1 - 3)</param>
/// <returns>Leet translated text</returns>
private static string ToLeet(string text, int degree = 1) =>
Translate(text, degree);
/// <summary>
/// Translate text to Leet
/// </summary>
/// <param name="text">Orginal text</param>
/// <param name="degree">Degree of translation (1 - 3)</param>
/// <returns>Leet translated text</returns>
private static string Translate(string text, int degree = 1)
{
if (degree > 6)
degree = 6;
if (degree <= 0)
return text;
// StringBuilder to store result.
StringBuilder sb = new StringBuilder(text.Length);
foreach (char c in text)
{
#region Degree 1
if (degree == 1)
{
switch (c)
{
case 'a': sb.Append("4"); break;
case 'e': sb.Append("3"); break;
case 'i': sb.Append("1"); break;
case 'o': sb.Append("0"); break;
case 'A': sb.Append("4"); break;
case 'E': sb.Append("3"); break;
case 'I': sb.Append("1"); break;
case 'O': sb.Append("0"); break;
default: sb.Append(c); break;
}
}
#endregion
#region Degree 2
else if (degree == 2)
{
switch (c)
{
case 'a': sb.Append("4"); break;
case 'e': sb.Append("3"); break;
case 'i': sb.Append("1"); break;
case 'o': sb.Append("0"); break;
case 'A': sb.Append("4"); break;
case 'E': sb.Append("3"); break;
case 'I': sb.Append("1"); break;
case 'O': sb.Append("0"); break;
case 's': sb.Append("$"); break;
case 'S': sb.Append("$"); break;
case 'l': sb.Append("£"); break;
case 'L': sb.Append("£"); break;
case 'c': sb.Append("("); break;
case 'C': sb.Append("("); break;
case 'y': sb.Append("¥"); break;
case 'Y': sb.Append("¥"); break;
case 'u': sb.Append("µ"); break;
case 'U': sb.Append("µ"); break;
case 'd': sb.Append("Ð"); break;
case 'D': sb.Append("Ð"); break;
default: sb.Append(c); break;
}
}
#endregion
#region Degree 3
else if (degree == 3)
{
switch (c)
{
case 'a': sb.Append("4"); break;
case 'e': sb.Append("3"); break;
case 'i': sb.Append("1"); break;
case 'o': sb.Append("0"); break;
case 'A': sb.Append("4"); break;
case 'E': sb.Append("3"); break;
case 'I': sb.Append("1"); break;
case 'O': sb.Append("0"); break;
case 'k': sb.Append("|{"); break;
case 'K': sb.Append("|{"); break;
case 's': sb.Append("$"); break;
case 'S': sb.Append("$"); break;
case 'g': sb.Append("9"); break;
case 'G': sb.Append("9"); break;
case 'l': sb.Append("£"); break;
case 'L': sb.Append("£"); break;
case 'c': sb.Append("("); break;
case 'C': sb.Append("("); break;
case 't': sb.Append("7"); break;
case 'T': sb.Append("7"); break;
case 'z': sb.Append("2"); break;
case 'Z': sb.Append("2"); break;
case 'y': sb.Append("¥"); break;
case 'Y': sb.Append("¥"); break;
case 'u': sb.Append("µ"); break;
case 'U': sb.Append("µ"); break;
case 'f': sb.Append("ƒ"); break;
case 'F': sb.Append("ƒ"); break;
case 'd': sb.Append("Ð"); break;
case 'D': sb.Append("Ð"); break;
default: sb.Append(c); break;
}
}
#endregion
#region Degree 4
else if (degree == 4)
{
switch (c)
{
case 'a': sb.Append("4"); break;
case 'e': sb.Append("3"); break;
case 'i': sb.Append("1"); break;
case 'o': sb.Append("0"); break;
case 'A': sb.Append("4"); break;
case 'E': sb.Append("3"); break;
case 'I': sb.Append("1"); break;
case 'O': sb.Append("0"); break;
case 'k': sb.Append("|{"); break;
case 'K': sb.Append("|{"); break;
case 's': sb.Append("$"); break;
case 'S': sb.Append("$"); break;
case 'g': sb.Append("9"); break;
case 'G': sb.Append("9"); break;
case 'l': sb.Append("£"); break;
case 'L': sb.Append("£"); break;
case 'c': sb.Append("("); break;
case 'C': sb.Append("("); break;
case 't': sb.Append("7"); break;
case 'T': sb.Append("7"); break;
case 'z': sb.Append("2"); break;
case 'Z': sb.Append("2"); break;
case 'y': sb.Append("¥"); break;
case 'Y': sb.Append("¥"); break;
case 'u': sb.Append("µ"); break;
case 'U': sb.Append("µ"); break;
case 'f': sb.Append("ƒ"); break;
case 'F': sb.Append("ƒ"); break;
case 'd': sb.Append("Ð"); break;
case 'D': sb.Append("Ð"); break;
case 'n': sb.Append(@"|\\|"); break;
case 'N': sb.Append(@"|\\|"); break;
case 'w': sb.Append(@"\\/\\/"); break;
case 'W': sb.Append(@"\\/\\/"); break;
case 'h': sb.Append(@"|-|"); break;
case 'H': sb.Append(@"|-|"); break;
case 'v': sb.Append(@"\\/"); break;
case 'V': sb.Append(@"\\/"); break;
case 'm': sb.Append(@"|\\/|"); break;
case 'M': sb.Append(@"|\/|"); break;
default: sb.Append(c); break;
}
}
#endregion
#region Degree 5
else if (degree == 5)
{
switch (c)
{
case 'a': sb.Append("4"); break;
case 'e': sb.Append("3"); break;
case 'i': sb.Append("1"); break;
case 'o': sb.Append("0"); break;
case 'A': sb.Append("4"); break;
case 'E': sb.Append("3"); break;
case 'I': sb.Append("1"); break;
case 'O': sb.Append("0"); break;
case 's': sb.Append("$"); break;
case 'S': sb.Append("$"); break;
case 'g': sb.Append("9"); break;
case 'G': sb.Append("9"); break;
case 'l': sb.Append("£"); break;
case 'L': sb.Append("£"); break;
case 'c': sb.Append("("); break;
case 'C': sb.Append("("); break;
case 't': sb.Append("7"); break;
case 'T': sb.Append("7"); break;
case 'z': sb.Append("2"); break;
case 'Z': sb.Append("2"); break;
case 'y': sb.Append("¥"); break;
case 'Y': sb.Append("¥"); break;
case 'u': sb.Append("µ"); break;
case 'U': sb.Append("µ"); break;
case 'f': sb.Append("ƒ"); break;
case 'F': sb.Append("ƒ"); break;
case 'd': sb.Append("Ð"); break;
case 'D': sb.Append("Ð"); break;
case 'n': sb.Append(@"|\\|"); break;
case 'N': sb.Append(@"|\\|"); break;
case 'w': sb.Append(@"\\/\\/"); break;
case 'W': sb.Append(@"\\/\\/"); break;
case 'h': sb.Append("|-|"); break;
case 'H': sb.Append("|-|"); break;
case 'v': sb.Append("\\/"); break;
case 'V': sb.Append(@"\\/"); break;
case 'k': sb.Append("|{"); break;
case 'K': sb.Append("|{"); break;
case 'r': sb.Append("®"); break;
case 'R': sb.Append("®"); break;
case 'm': sb.Append(@"|\\/|"); break;
case 'M': sb.Append(@"|\\/|"); break;
case 'b': sb.Append("ß"); break;
case 'B': sb.Append("ß"); break;
case 'q': sb.Append("Q"); break;
case 'Q': sb.Append("Q¸"); break;
case 'x': sb.Append(")("); break;
case 'X': sb.Append(")("); break;
default: sb.Append(c); break;
}
}
#endregion
#region Degree 6
else if (degree == 6)
{
switch (c)
{
case 'a': sb.Append("4"); break;
case 'e': sb.Append("3"); break;
case 'i': sb.Append("1"); break;
case 'o': sb.Append("0"); break;
case 'A': sb.Append("4"); break;
case 'E': sb.Append("3"); break;
case 'I': sb.Append("1"); break;
case 'O': sb.Append("0"); break;
case 's': sb.Append("$"); break;
case 'S': sb.Append("$"); break;
case 'g': sb.Append("9"); break;
case 'G': sb.Append("9"); break;
case 'l': sb.Append("£"); break;
case 'L': sb.Append("£"); break;
case 'c': sb.Append("("); break;
case 'C': sb.Append("("); break;
case 't': sb.Append("7"); break;
case 'T': sb.Append("7"); break;
case 'z': sb.Append("2"); break;
case 'Z': sb.Append("2"); break;
case 'y': sb.Append("¥"); break;
case 'Y': sb.Append("¥"); break;
case 'u': sb.Append("µ"); break;
case 'U': sb.Append("µ"); break;
case 'f': sb.Append("ƒ"); break;
case 'F': sb.Append("ƒ"); break;
case 'd': sb.Append("Ð"); break;
case 'D': sb.Append("Ð"); break;
case 'n': sb.Append(@"|\\|"); break;
case 'N': sb.Append(@"|\\|"); break;
case 'w': sb.Append(@"\\/\\/"); break;
case 'W': sb.Append(@"\\/\\/"); break;
case 'h': sb.Append("|-|"); break;
case 'H': sb.Append("|-|"); break;
case 'v': sb.Append(@"\\/"); break;
case 'V': sb.Append(@"\\/"); break;
case 'k': sb.Append("|{"); break;
case 'K': sb.Append("|{"); break;
case 'r': sb.Append("®"); break;
case 'R': sb.Append("®"); break;
case 'm': sb.Append(@"|\\/|"); break;
case 'M': sb.Append(@"|\\/|"); break;
case 'b': sb.Append("ß"); break;
case 'B': sb.Append("ß"); break;
case 'j': sb.Append("_|"); break;
case 'J': sb.Append("_|"); break;
case 'P': sb.Append("|°"); break;
case 'q': sb.Append("¶"); break;
case 'Q': sb.Append("¶¸"); break;
case 'x': sb.Append(")("); break;
case 'X': sb.Append(")("); break;
default: sb.Append(c); break;
}
}
#endregion
}
return sb.ToString().TrimTo(1995); // Return result.
}
}
}

View File

@ -0,0 +1,128 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Games.Common.Nunchi;
using NadekoBot.Modules.Games.Services;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Games
{
public partial class Games
{
[Group]
public class NunchiCommands : NadekoSubmodule<GamesService>
{
private readonly DiscordSocketClient _client;
public NunchiCommands(DiscordSocketClient client)
{
_client = client;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Nunchi()
{
var newNunchi = new Nunchi(Context.User.Id, Context.User.ToString());
Nunchi nunchi;
//if a game was already active
if ((nunchi = _service.NunchiGames.GetOrAdd(Context.Guild.Id, newNunchi)) != newNunchi)
{
// join it
if (!await nunchi.Join(Context.User.Id, Context.User.ToString()))
{
// if you failed joining, that means game is running or just ended
// await ReplyErrorLocalized("nunchi_already_started").ConfigureAwait(false);
return;
}
await ReplyConfirmLocalized("nunchi_joined", nunchi.ParticipantCount).ConfigureAwait(false);
return;
}
try { await ConfirmLocalized("nunchi_created").ConfigureAwait(false); } catch { }
nunchi.OnGameEnded += Nunchi_OnGameEnded;
//nunchi.OnGameStarted += Nunchi_OnGameStarted;
nunchi.OnRoundEnded += Nunchi_OnRoundEnded;
nunchi.OnUserGuessed += Nunchi_OnUserGuessed;
nunchi.OnRoundStarted += Nunchi_OnRoundStarted;
_client.MessageReceived += _client_MessageReceived;
var success = await nunchi.Initialize().ConfigureAwait(false);
if (!success)
{
if (_service.NunchiGames.TryRemove(Context.Guild.Id, out var game))
game.Dispose();
await ConfirmLocalized("nunchi_failed_to_start").ConfigureAwait(false);
}
Task _client_MessageReceived(SocketMessage arg)
{
var _ = Task.Run(async () =>
{
if (arg.Channel.Id != Context.Channel.Id)
return;
if (!int.TryParse(arg.Content, out var number))
return;
try
{
await nunchi.Input(arg.Author.Id, arg.Author.ToString(), number).ConfigureAwait(false);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
});
return Task.CompletedTask;
}
Task Nunchi_OnGameEnded(Nunchi arg1, string arg2)
{
if (_service.NunchiGames.TryRemove(Context.Guild.Id, out var game))
{
_client.MessageReceived -= _client_MessageReceived;
game.Dispose();
}
if (arg2 == null)
return ConfirmLocalized("nunchi_ended_no_winner", Format.Bold(arg2));
else
return ConfirmLocalized("nunchi_ended", Format.Bold(arg2));
}
}
private Task Nunchi_OnRoundStarted(Nunchi arg, int cur)
{
return ConfirmLocalized("nunchi_round_started",
Format.Bold(arg.ParticipantCount.ToString()),
Format.Bold(cur.ToString()));
}
private Task Nunchi_OnUserGuessed(Nunchi arg)
{
return ConfirmLocalized("nunchi_next_number", Format.Bold(arg.CurrentNumber.ToString()));
}
private Task Nunchi_OnRoundEnded(Nunchi arg1, (ulong Id, string Name)? arg2)
{
if(arg2.HasValue)
return ConfirmLocalized("nunchi_round_ended", Format.Bold(arg2.Value.Name));
else
return ConfirmLocalized("nunchi_round_ended_boot",
Format.Bold("\n" + string.Join("\n, ", arg1.Participants.Select(x => x.Name)))); // this won't work if there are too many users
}
private Task Nunchi_OnGameStarted(Nunchi arg)
{
return ConfirmLocalized("nunchi_started", Format.Bold(arg.ParticipantCount.ToString()));
}
}
}
}

View File

@ -0,0 +1,144 @@
using Discord;
using Discord.Commands;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Games.Services;
namespace NadekoBot.Modules.Games
{
public partial class Games
{
/// <summary>
/// Flower picking/planting idea is given to me by its
/// inceptor Violent Crumble from Game Developers League discord server
/// (he has !cookie and !nom) Thanks a lot Violent!
/// Check out GDL (its a growing gamedev community):
/// https://discord.gg/0TYNJfCU4De7YIk8
/// </summary>
[Group]
public class PlantPickCommands : NadekoSubmodule<GamesService>
{
private readonly CurrencyService _cs;
private readonly IBotConfigProvider _bc;
private readonly DbService _db;
public PlantPickCommands(IBotConfigProvider bc, CurrencyService cs,
DbService db)
{
_bc = bc;
_cs = cs;
_db = db;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Pick()
{
var channel = (ITextChannel)Context.Channel;
if (!(await channel.Guild.GetCurrentUserAsync()).GetPermissions(channel).ManageMessages)
return;
try { await Context.Message.DeleteAsync().ConfigureAwait(false); } catch { }
if (!_service.PlantedFlowers.TryRemove(channel.Id, out List<IUserMessage> msgs))
return;
await Task.WhenAll(msgs.Where(m => m != null).Select(toDelete => toDelete.DeleteAsync())).ConfigureAwait(false);
await _cs.AddAsync((IGuildUser)Context.User, $"Picked {_bc.BotConfig.CurrencyPluralName}", msgs.Count, false).ConfigureAwait(false);
var msg = await ReplyConfirmLocalized("picked", msgs.Count + _bc.BotConfig.CurrencySign)
.ConfigureAwait(false);
msg.DeleteAfter(10);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Plant(int amount = 1)
{
if (amount < 1)
return;
var removed = await _cs.RemoveAsync((IGuildUser)Context.User, $"Planted a {_bc.BotConfig.CurrencyName}", amount, false).ConfigureAwait(false);
if (!removed)
{
await ReplyErrorLocalized("not_enough", _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
var imgData = _service.GetRandomCurrencyImage();
var msgToSend = GetText("planted",
Format.Bold(Context.User.ToString()),
amount + _bc.BotConfig.CurrencySign,
Prefix);
if (amount > 1)
msgToSend += " " + GetText("pick_pl", Prefix);
else
msgToSend += " " + GetText("pick_sn", Prefix);
IUserMessage msg;
using (var toSend = imgData.Data.ToStream())
{
msg = await Context.Channel.SendFileAsync(toSend, imgData.Name, msgToSend).ConfigureAwait(false);
}
var msgs = new IUserMessage[amount];
msgs[0] = msg;
_service.PlantedFlowers.AddOrUpdate(Context.Channel.Id, msgs.ToList(), (id, old) =>
{
old.AddRange(msgs);
return old;
});
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[RequireUserPermission(GuildPermission.ManageMessages)]
#if GLOBAL_NADEKO
[OwnerOnly]
#endif
public async Task GenCurrency()
{
var channel = (ITextChannel)Context.Channel;
bool enabled;
using (var uow = _db.UnitOfWork)
{
var guildConfig = uow.GuildConfigs.For(channel.Guild.Id, set => set.Include(gc => gc.GenerateCurrencyChannelIds));
var toAdd = new GCChannelId() { ChannelId = channel.Id };
if (!guildConfig.GenerateCurrencyChannelIds.Contains(toAdd))
{
guildConfig.GenerateCurrencyChannelIds.Add(toAdd);
_service.GenerationChannels.Add(channel.Id);
enabled = true;
}
else
{
guildConfig.GenerateCurrencyChannelIds.Remove(toAdd);
_service.GenerationChannels.TryRemove(channel.Id);
enabled = false;
}
await uow.CompleteAsync();
}
if (enabled)
{
await ReplyConfirmLocalized("curgen_enabled").ConfigureAwait(false);
}
else
{
await ReplyConfirmLocalized("curgen_disabled").ConfigureAwait(false);
}
}
}
}
}

View File

@ -0,0 +1,60 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Extensions;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Games.Services;
namespace NadekoBot.Modules.Games
{
public partial class Games
{
[Group]
public class PollCommands : NadekoSubmodule<PollService>
{
private readonly DiscordSocketClient _client;
public PollCommands(DiscordSocketClient client)
{
_client = client;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireUserPermission(GuildPermission.ManageMessages)]
[RequireContext(ContextType.Guild)]
public Task Poll([Remainder] string arg = null)
=> InternalStartPoll(arg);
[NadekoCommand, Usage, Description, Aliases]
[RequireUserPermission(GuildPermission.ManageMessages)]
[RequireContext(ContextType.Guild)]
public async Task PollStats()
{
if (!_service.ActivePolls.TryGetValue(Context.Guild.Id, out var poll))
return;
await Context.Channel.EmbedAsync(poll.GetStats(GetText("current_poll_results")));
}
private async Task InternalStartPoll(string arg)
{
if(await _service.StartPoll((ITextChannel)Context.Channel, Context.Message, arg) == false)
await ReplyErrorLocalized("poll_already_running").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireUserPermission(GuildPermission.ManageMessages)]
[RequireContext(ContextType.Guild)]
public async Task Pollend()
{
var channel = (ITextChannel)Context.Channel;
_service.ActivePolls.TryRemove(channel.Guild.Id, out var poll);
await poll.StopPoll().ConfigureAwait(false);
}
}
}
}

View File

@ -0,0 +1,145 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Extensions;
using NadekoBot.Modules.Permissions.Common;
using NadekoBot.Modules.Permissions.Services;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Core.Services.Impl;
using NLog;
using NadekoBot.Modules.Games.Common.ChatterBot;
namespace NadekoBot.Modules.Games.Services
{
public class ChatterBotService : IEarlyBlockingExecutor, INService
{
private readonly DiscordSocketClient _client;
private readonly Logger _log;
private readonly PermissionService _perms;
private readonly CommandHandler _cmd;
private readonly NadekoStrings _strings;
private readonly IBotCredentials _creds;
public ConcurrentDictionary<ulong, Lazy<IChatterBotSession>> ChatterBotGuilds { get; }
public ChatterBotService(DiscordSocketClient client, PermissionService perms,
NadekoBot bot, CommandHandler cmd, NadekoStrings strings,
IBotCredentials creds)
{
_client = client;
_log = LogManager.GetCurrentClassLogger();
_perms = perms;
_cmd = cmd;
_strings = strings;
_creds = creds;
ChatterBotGuilds = new ConcurrentDictionary<ulong, Lazy<IChatterBotSession>>(
bot.AllGuildConfigs
.Where(gc => gc.CleverbotEnabled)
.ToDictionary(gc => gc.GuildId, gc => new Lazy<IChatterBotSession>(() => CreateSession(), true)));
}
public IChatterBotSession CreateSession()
{
if (string.IsNullOrWhiteSpace(_creds.CleverbotApiKey))
return new ChatterBotSession();
else
return new OfficialCleverbotSession(_creds.CleverbotApiKey);
}
public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot)
{
var channel = msg.Channel as ITextChannel;
cleverbot = null;
if (channel == null)
return null;
if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out Lazy<IChatterBotSession> lazyCleverbot))
return null;
cleverbot = lazyCleverbot.Value;
var nadekoId = _client.CurrentUser.Id;
var normalMention = $"<@{nadekoId}> ";
var nickMention = $"<@!{nadekoId}> ";
string message;
if (msg.Content.StartsWith(normalMention))
{
message = msg.Content.Substring(normalMention.Length).Trim();
}
else if (msg.Content.StartsWith(nickMention))
{
message = msg.Content.Substring(nickMention.Length).Trim();
}
else
{
return null;
}
return message;
}
public async Task<bool> TryAsk(IChatterBotSession cleverbot, ITextChannel channel, string message)
{
await channel.TriggerTypingAsync().ConfigureAwait(false);
var response = await cleverbot.Think(message).ConfigureAwait(false);
try
{
await channel.SendConfirmAsync(response.SanitizeMentions()).ConfigureAwait(false);
}
catch
{
await channel.SendConfirmAsync(response.SanitizeMentions()).ConfigureAwait(false); // try twice :\
}
return true;
}
public async Task<bool> TryExecuteEarly(DiscordSocketClient client, IGuild guild, IUserMessage usrMsg)
{
if (!(guild is SocketGuild sg))
return false;
try
{
var message = PrepareMessage(usrMsg, out IChatterBotSession cbs);
if (message == null || cbs == null)
return false;
var pc = _perms.GetCache(guild.Id);
if (!pc.Permissions.CheckPermissions(usrMsg,
"cleverbot",
"Games".ToLowerInvariant(),
out int index))
{
if (pc.Verbose)
{
var returnMsg = _strings.GetText("trigger", guild.Id, "Permissions".ToLowerInvariant(), index + 1, Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild)));
try { await usrMsg.Channel.SendErrorAsync(returnMsg).ConfigureAwait(false); } catch { }
_log.Info(returnMsg);
}
return true;
}
var cleverbotExecuted = await TryAsk(cbs, (ITextChannel)usrMsg.Channel, message).ConfigureAwait(false);
if (cleverbotExecuted)
{
_log.Info($@"CleverBot Executed
Server: {guild.Name} [{guild.Id}]
Channel: {usrMsg.Channel?.Name} [{usrMsg.Channel?.Id}]
UserId: {usrMsg.Author} [{usrMsg.Author.Id}]
Message: {usrMsg.Content}");
return true;
}
}
catch (Exception ex) { _log.Warn(ex, "Error in cleverbot"); }
return false;
}
}
}

View File

@ -0,0 +1,215 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using NadekoBot.Common;
using NadekoBot.Common.Collections;
using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Common;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Core.Services.Impl;
using Newtonsoft.Json;
using NLog;
using NadekoBot.Modules.Games.Common.Acrophobia;
using NadekoBot.Modules.Games.Common.Connect4;
using NadekoBot.Modules.Games.Common.Hangman;
using NadekoBot.Modules.Games.Common.Trivia;
using NadekoBot.Modules.Games.Common.Nunchi;
namespace NadekoBot.Modules.Games.Services
{
public class GamesService : INService, IUnloadableService
{
private readonly IBotConfigProvider _bc;
public readonly ConcurrentDictionary<ulong, GirlRating> GirlRatings = new ConcurrentDictionary<ulong, GirlRating>();
public readonly ImmutableArray<string> EightBallResponses;
private readonly Timer _t;
private readonly CommandHandler _cmd;
private readonly NadekoStrings _strings;
private readonly IImagesService _images;
private readonly Logger _log;
public readonly string TypingArticlesPath = "data/typing_articles2.json";
private readonly CommandHandler _cmdHandler;
public List<TypingArticle> TypingArticles { get; } = new List<TypingArticle>();
//channelId, game
public ConcurrentDictionary<ulong, Acrophobia> AcrophobiaGames { get; } = new ConcurrentDictionary<ulong, Acrophobia>();
public ConcurrentDictionary<ulong, Connect4Game> Connect4Games { get; } = new ConcurrentDictionary<ulong, Connect4Game>();
public ConcurrentDictionary<ulong, Hangman> HangmanGames { get; } = new ConcurrentDictionary<ulong, Hangman>();
public TermPool TermPool { get; } = new TermPool();
public ConcurrentDictionary<ulong, TriviaGame> RunningTrivias { get; } = new ConcurrentDictionary<ulong, TriviaGame>();
public Dictionary<ulong, TicTacToe> TicTacToeGames { get; } = new Dictionary<ulong, TicTacToe>();
public ConcurrentDictionary<ulong, TypingGame> RunningContests { get; } = new ConcurrentDictionary<ulong, TypingGame>();
public ConcurrentDictionary<ulong, Nunchi> NunchiGames { get; } = new ConcurrentDictionary<ulong, Common.Nunchi.Nunchi>();
public GamesService(CommandHandler cmd, IBotConfigProvider bc, NadekoBot bot,
NadekoStrings strings, IImagesService images, CommandHandler cmdHandler)
{
_bc = bc;
_cmd = cmd;
_strings = strings;
_images = images;
_cmdHandler = cmdHandler;
_log = LogManager.GetCurrentClassLogger();
//8ball
EightBallResponses = _bc.BotConfig.EightBallResponses.Select(ebr => ebr.Text).ToImmutableArray();
//girl ratings
_t = new Timer((_) =>
{
GirlRatings.Clear();
}, null, TimeSpan.FromDays(1), TimeSpan.FromDays(1));
//plantpick
_cmd.OnMessageNoTrigger += PotentialFlowerGeneration;
GenerationChannels = new ConcurrentHashSet<ulong>(bot
.AllGuildConfigs
.SelectMany(c => c.GenerateCurrencyChannelIds.Select(obj => obj.ChannelId)));
try
{
TypingArticles = JsonConvert.DeserializeObject<List<TypingArticle>>(File.ReadAllText(TypingArticlesPath));
}
catch (Exception ex)
{
_log.Warn("Error while loading typing articles {0}", ex.ToString());
TypingArticles = new List<TypingArticle>();
}
}
public async Task Unload()
{
_t.Change(Timeout.Infinite, Timeout.Infinite);
_cmd.OnMessageNoTrigger -= PotentialFlowerGeneration;
AcrophobiaGames.ForEach(x => x.Value.Dispose());
AcrophobiaGames.Clear();
Connect4Games.ForEach(x => x.Value.Dispose());
Connect4Games.Clear();
HangmanGames.ForEach(x => x.Value.Dispose());
HangmanGames.Clear();
await Task.WhenAll(RunningTrivias.Select(x => x.Value.StopGame()));
RunningTrivias.Clear();
TicTacToeGames.Clear();
await Task.WhenAll(RunningContests.Select(x => x.Value.Stop()))
.ConfigureAwait(false);
RunningContests.Clear();
NunchiGames.ForEach(x => x.Value.Dispose());
NunchiGames.Clear();
}
private void DisposeElems(IEnumerable<IDisposable> xs)
{
xs.ForEach(x => x.Dispose());
}
public void AddTypingArticle(IUser user, string text)
{
TypingArticles.Add(new TypingArticle
{
Title = $"Text added on {DateTime.UtcNow} by {user}",
Text = text.SanitizeMentions(),
});
File.WriteAllText(TypingArticlesPath, JsonConvert.SerializeObject(TypingArticles));
}
public ConcurrentHashSet<ulong> GenerationChannels { get; }
//channelid/message
public ConcurrentDictionary<ulong, List<IUserMessage>> PlantedFlowers { get; } = new ConcurrentDictionary<ulong, List<IUserMessage>>();
//channelId/last generation
public ConcurrentDictionary<ulong, DateTime> LastGenerations { get; } = new ConcurrentDictionary<ulong, DateTime>();
private ConcurrentDictionary<ulong, object> _locks { get; } = new ConcurrentDictionary<ulong, object>();
public (string Name, ImmutableArray<byte> Data) GetRandomCurrencyImage()
{
var rng = new NadekoRandom();
return _images.Currency[rng.Next(0, _images.Currency.Length)];
}
private string GetText(ITextChannel ch, string key, params object[] rep)
=> _strings.GetText(key, ch.GuildId, "Games".ToLowerInvariant(), rep);
private Task PotentialFlowerGeneration(IUserMessage imsg)
{
var msg = imsg as SocketUserMessage;
if (msg == null || msg.Author.IsBot)
return Task.CompletedTask;
var channel = imsg.Channel as ITextChannel;
if (channel == null)
return Task.CompletedTask;
if (!GenerationChannels.Contains(channel.Id))
return Task.CompletedTask;
var _ = Task.Run(async () =>
{
try
{
var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue);
var rng = new NadekoRandom();
if (DateTime.UtcNow - TimeSpan.FromSeconds(_bc.BotConfig.CurrencyGenerationCooldown) < lastGeneration) //recently generated in this channel, don't generate again
return;
var num = rng.Next(1, 101) + _bc.BotConfig.CurrencyGenerationChance * 100;
if (num > 100 && LastGenerations.TryUpdate(channel.Id, DateTime.UtcNow, lastGeneration))
{
var dropAmount = _bc.BotConfig.CurrencyDropAmount;
var dropAmountMax = _bc.BotConfig.CurrencyDropAmountMax;
if (dropAmountMax != null && dropAmountMax > dropAmount)
dropAmount = new NadekoRandom().Next(dropAmount, dropAmountMax.Value + 1);
if (dropAmount > 0)
{
var msgs = new IUserMessage[dropAmount];
var prefix = _cmdHandler.GetPrefix(channel.Guild.Id);
var toSend = dropAmount == 1
? GetText(channel, "curgen_sn", _bc.BotConfig.CurrencySign)
+ " " + GetText(channel, "pick_sn", prefix)
: GetText(channel, "curgen_pl", dropAmount, _bc.BotConfig.CurrencySign)
+ " " + GetText(channel, "pick_pl", prefix);
var file = GetRandomCurrencyImage();
using (var fileStream = file.Data.ToStream())
{
var sent = await channel.SendFileAsync(
fileStream,
file.Name,
toSend).ConfigureAwait(false);
msgs[0] = sent;
}
PlantedFlowers.AddOrUpdate(channel.Id, msgs.ToList(), (id, old) => { old.AddRange(msgs); return old; });
}
}
}
catch (Exception ex)
{
LogManager.GetCurrentClassLogger().Warn(ex);
}
});
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,71 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Modules.Games.Common;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Impl;
using NLog;
namespace NadekoBot.Modules.Games.Services
{
public class PollService : IEarlyBlockingExecutor, INService
{
public ConcurrentDictionary<ulong, Poll> ActivePolls = new ConcurrentDictionary<ulong, Poll>();
private readonly Logger _log;
private readonly DiscordSocketClient _client;
private readonly NadekoStrings _strings;
public PollService(DiscordSocketClient client, NadekoStrings strings)
{
_log = LogManager.GetCurrentClassLogger();
_client = client;
_strings = strings;
}
public async Task<bool?> StartPoll(ITextChannel channel, IUserMessage msg, string arg)
{
if (string.IsNullOrWhiteSpace(arg) || !arg.Contains(";"))
return null;
var data = arg.Split(';');
if (data.Length < 3)
return null;
var poll = new Poll(_client, _strings, msg, data[0], data.Skip(1));
if (ActivePolls.TryAdd(channel.Guild.Id, poll))
{
poll.OnEnded += (gid) =>
{
ActivePolls.TryRemove(gid, out _);
};
await poll.StartPoll().ConfigureAwait(false);
return true;
}
return false;
}
public async Task<bool> TryExecuteEarly(DiscordSocketClient client, IGuild guild, IUserMessage msg)
{
if (guild == null)
return false;
if (!ActivePolls.TryGetValue(guild.Id, out var poll))
return false;
try
{
return await poll.TryVote(msg).ConfigureAwait(false);
}
catch (Exception ex)
{
_log.Warn(ex);
}
return false;
}
}
}

View File

@ -0,0 +1,120 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Extensions;
using Newtonsoft.Json;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Games.Common;
using NadekoBot.Modules.Games.Services;
namespace NadekoBot.Modules.Games
{
public partial class Games
{
[Group]
public class SpeedTypingCommands : NadekoSubmodule<GamesService>
{
private readonly GamesService _games;
private readonly DiscordSocketClient _client;
public SpeedTypingCommands(DiscordSocketClient client, GamesService games)
{
_games = games;
_client = client;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task TypeStart()
{
var channel = (ITextChannel)Context.Channel;
var game = _service.RunningContests.GetOrAdd(channel.Guild.Id, id => new TypingGame(_games, _client, channel, Prefix));
if (game.IsActive)
{
await channel.SendErrorAsync(
$"Contest already running in " +
$"{game.Channel.Mention} channel.")
.ConfigureAwait(false);
}
else
{
await game.Start().ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task TypeStop()
{
var channel = (ITextChannel)Context.Channel;
if (_service.RunningContests.TryRemove(channel.Guild.Id, out TypingGame game))
{
await game.Stop().ConfigureAwait(false);
return;
}
await channel.SendErrorAsync("No contest to stop on this channel.").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task Typeadd([Remainder] string text)
{
var channel = (ITextChannel)Context.Channel;
if (string.IsNullOrWhiteSpace(text))
return;
_games.AddTypingArticle(Context.User, text);
await channel.SendConfirmAsync("Added new article for typing game.").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Typelist(int page = 1)
{
var channel = (ITextChannel)Context.Channel;
if (page < 1)
return;
var articles = _games.TypingArticles.Skip((page - 1) * 15).Take(15).ToArray();
if (!articles.Any())
{
await channel.SendErrorAsync($"{Context.User.Mention} `No articles found on that page.`").ConfigureAwait(false);
return;
}
var i = (page - 1) * 15;
await channel.SendConfirmAsync("List of articles for Type Race", string.Join("\n", articles.Select(a => $"`#{++i}` - {a.Text.TrimTo(50)}")))
.ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task Typedel(int index)
{
var channel = (ITextChannel)Context.Channel;
index -= 1;
if (index < 0 || index >= _games.TypingArticles.Count)
return;
var removed = _games.TypingArticles[index];
_games.TypingArticles.RemoveAt(index);
File.WriteAllText(_games.TypingArticlesPath, JsonConvert.SerializeObject(_games.TypingArticles));
await channel.SendConfirmAsync($"`Removed typing article:` #{index + 1} - {removed.Text.TrimTo(50)}")
.ConfigureAwait(false);
}
}
}
}

View File

@ -0,0 +1,62 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Extensions;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Services.Impl;
using NadekoBot.Modules.Games.Services;
using NadekoBot.Modules.Games.Common;
namespace NadekoBot.Modules.Games
{
public partial class Games
{
[Group]
public class TicTacToeCommands : NadekoSubmodule<GamesService>
{
private readonly SemaphoreSlim _sem = new SemaphoreSlim(1, 1);
private readonly DiscordSocketClient _client;
public TicTacToeCommands(DiscordSocketClient client)
{
_client = client;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task TicTacToe()
{
var channel = (ITextChannel)Context.Channel;
await _sem.WaitAsync(1000);
try
{
if (_service.TicTacToeGames.TryGetValue(channel.Id, out TicTacToe game))
{
var _ = Task.Run(async () =>
{
await game.Start((IGuildUser)Context.User);
});
return;
}
game = new TicTacToe(base._strings, this._client, channel, (IGuildUser)Context.User);
_service.TicTacToeGames.Add(channel.Id, game);
await ReplyConfirmLocalized("ttt_created").ConfigureAwait(false);
game.OnEnded += (g) =>
{
_service.TicTacToeGames.Remove(channel.Id);
};
}
finally
{
_sem.Release();
}
}
}
}
}

View File

@ -0,0 +1,98 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Extensions;
using NadekoBot.Core.Services;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Games.Common.Trivia;
using NadekoBot.Modules.Games.Services;
namespace NadekoBot.Modules.Games
{
public partial class Games
{
[Group]
public class TriviaCommands : NadekoSubmodule<GamesService>
{
private readonly CurrencyService _cs;
private readonly DiscordSocketClient _client;
private readonly IBotConfigProvider _bc;
public TriviaCommands(DiscordSocketClient client, IBotConfigProvider bc, CurrencyService cs)
{
_cs = cs;
_client = client;
_bc = bc;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public Task Trivia([Remainder] string additionalArgs = "")
=> InternalTrivia(10, additionalArgs);
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public Task Trivia(int winReq = 10, [Remainder] string additionalArgs = "")
=> InternalTrivia(winReq, additionalArgs);
public async Task InternalTrivia(int winReq, string additionalArgs = "")
{
var channel = (ITextChannel)Context.Channel;
additionalArgs = additionalArgs?.Trim()?.ToLowerInvariant();
var showHints = !additionalArgs.Contains("nohint");
var isPokemon = additionalArgs.Contains("pokemon");
var trivia = new TriviaGame(_strings, _client, _bc, _cs, channel.Guild, channel, showHints, winReq, isPokemon);
if (_service.RunningTrivias.TryAdd(channel.Guild.Id, trivia))
{
try
{
await trivia.StartGame().ConfigureAwait(false);
}
finally
{
_service.RunningTrivias.TryRemove(channel.Guild.Id, out trivia);
await trivia.EnsureStopped().ConfigureAwait(false);
}
return;
}
await Context.Channel.SendErrorAsync(GetText("trivia_already_running") + "\n" + trivia.CurrentQuestion)
.ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Tl()
{
var channel = (ITextChannel)Context.Channel;
if (_service.RunningTrivias.TryGetValue(channel.Guild.Id, out TriviaGame trivia))
{
await channel.SendConfirmAsync(GetText("leaderboard"), trivia.GetLeaderboard()).ConfigureAwait(false);
return;
}
await ReplyErrorLocalized("trivia_none").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Tq()
{
var channel = (ITextChannel)Context.Channel;
if (_service.RunningTrivias.TryGetValue(channel.Guild.Id, out TriviaGame trivia))
{
await trivia.StopGame().ConfigureAwait(false);
return;
}
await ReplyErrorLocalized("trivia_none").ConfigureAwait(false);
}
}
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace NadekoBot.Modules.Music.Common.Exceptions
{
public class NotInVoiceChannelException : Exception
{
public NotInVoiceChannelException() : base("You're not in the voice channel on this server.") { }
}
}

View File

@ -0,0 +1,12 @@
using System;
namespace NadekoBot.Modules.Music.Common.Exceptions
{
public class QueueFullException : Exception
{
public QueueFullException(string message) : base(message)
{
}
public QueueFullException() : base("Queue is full.") { }
}
}

View File

@ -0,0 +1,12 @@
using System;
namespace NadekoBot.Modules.Music.Common.Exceptions
{
public class SongNotFoundException : Exception
{
public SongNotFoundException(string message) : base(message)
{
}
public SongNotFoundException() : base("Song is not found.") { }
}
}

View File

@ -0,0 +1,670 @@
using Discord;
using Discord.Audio;
using System;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using System.Linq;
using NadekoBot.Extensions;
using System.Diagnostics;
using NadekoBot.Common.Collections;
using NadekoBot.Modules.Music.Services;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
namespace NadekoBot.Modules.Music.Common
{
public enum StreamState
{
Resolving,
Queued,
Playing,
Completed
}
public class MusicPlayer
{
private readonly Thread _player;
public IVoiceChannel VoiceChannel { get; private set; }
private readonly Logger _log;
private MusicQueue Queue { get; } = new MusicQueue();
public bool Exited { get; set; } = false;
public bool Stopped { get; private set; } = false;
public float Volume { get; private set; } = 1.0f;
public bool Paused => pauseTaskSource != null;
private TaskCompletionSource<bool> pauseTaskSource { get; set; } = null;
public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%";
public string PrettyCurrentTime
{
get
{
var time = CurrentTime.ToString(@"mm\:ss");
var hrs = (int)CurrentTime.TotalHours;
if (hrs > 0)
return hrs + ":" + time;
else
return time;
}
}
public string PrettyFullTime => PrettyCurrentTime + " / " + (Queue.Current.Song?.PrettyTotalTime ?? "?");
private CancellationTokenSource SongCancelSource { get; set; }
public ITextChannel OutputTextChannel { get; set; }
public (int Index, SongInfo Current) Current
{
get
{
if (Stopped)
return (0, null);
return Queue.Current;
}
}
public bool RepeatCurrentSong { get; private set; }
public bool Shuffle { get; private set; }
public bool Autoplay { get; private set; }
public bool RepeatPlaylist { get; private set; } = false;
public uint MaxQueueSize
{
get => Queue.MaxQueueSize;
set { lock (locker) Queue.MaxQueueSize = value; }
}
private bool _fairPlay;
public bool FairPlay
{
get => _fairPlay;
set
{
if (value)
{
var cur = Queue.Current;
if (cur.Song != null)
RecentlyPlayedUsers.Add(cur.Song.QueuerName);
}
else
{
RecentlyPlayedUsers.Clear();
}
_fairPlay = value;
}
}
public bool AutoDelete { get; set; }
public uint MaxPlaytimeSeconds { get; set; }
const int _frameBytes = 3840;
const float _miliseconds = 20.0f;
public TimeSpan CurrentTime => TimeSpan.FromSeconds(_bytesSent / (float)_frameBytes / (1000 / _miliseconds));
private int _bytesSent = 0;
private IAudioClient _audioClient;
private readonly object locker = new object();
private MusicService _musicService;
#region events
public event Action<MusicPlayer, (int Index, SongInfo Song)> OnStarted;
public event Action<MusicPlayer, SongInfo> OnCompleted;
public event Action<MusicPlayer, bool> OnPauseChanged;
#endregion
private bool manualSkip = false;
private bool manualIndex = false;
private bool newVoiceChannel = false;
private readonly IGoogleApiService _google;
private bool cancel = false;
private ConcurrentHashSet<string> RecentlyPlayedUsers { get; } = new ConcurrentHashSet<string>();
public TimeSpan TotalPlaytime
{
get
{
var songs = Queue.ToArray().Songs;
return songs.Any(s => s.TotalTime == TimeSpan.MaxValue)
? TimeSpan.MaxValue
: new TimeSpan(songs.Sum(s => s.TotalTime.Ticks));
}
}
public MusicPlayer(MusicService musicService, IGoogleApiService google, IVoiceChannel vch, ITextChannel output, float volume)
{
_log = LogManager.GetCurrentClassLogger();
this.Volume = volume;
this.VoiceChannel = vch;
this.SongCancelSource = new CancellationTokenSource();
this.OutputTextChannel = output;
this._musicService = musicService;
this._google = google;
_log.Info("Initialized");
_player = new Thread(new ThreadStart(PlayerLoop));
_player.Start();
_log.Info("Loop started");
}
private async void PlayerLoop()
{
while (!Exited)
{
_bytesSent = 0;
cancel = false;
CancellationToken cancelToken;
(int Index, SongInfo Song) data;
lock (locker)
{
data = Queue.Current;
cancelToken = SongCancelSource.Token;
manualSkip = false;
manualIndex = false;
}
if (data.Song != null)
{
_log.Info("Starting");
AudioOutStream pcm = null;
SongBuffer b = null;
try
{
b = new SongBuffer(await data.Song.Uri(), "", data.Song.ProviderType == MusicType.Local);
//_log.Info("Created buffer, buffering...");
//var bufferTask = b.StartBuffering(cancelToken);
//var timeout = Task.Delay(10000);
//if (Task.WhenAny(bufferTask, timeout) == timeout)
//{
// _log.Info("Buffering failed due to a timeout.");
// continue;
//}
//else if (!bufferTask.Result)
//{
// _log.Info("Buffering failed due to a cancel or error.");
// continue;
//}
//_log.Info("Buffered. Getting audio client...");
var ac = await GetAudioClient();
_log.Info("Got Audio client");
if (ac == null)
{
_log.Info("Can't join");
await Task.Delay(900, cancelToken);
// just wait some time, maybe bot doesn't even have perms to join that voice channel,
// i don't want to spam connection attempts
continue;
}
pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500);
_log.Info("Created pcm stream");
OnStarted?.Invoke(this, data);
byte[] buffer = new byte[3840];
int bytesRead = 0;
while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0
&& (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds))
{
AdjustVolume(buffer, Volume);
await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false);
unchecked { _bytesSent += bytesRead; }
await (pauseTaskSource?.Task ?? Task.CompletedTask);
}
}
catch (OperationCanceledException)
{
_log.Info("Song Canceled");
cancel = true;
}
catch (Exception ex)
{
_log.Warn(ex);
}
finally
{
if (pcm != null)
{
// flush is known to get stuck from time to time,
// just skip flushing if it takes more than 1 second
var flushCancel = new CancellationTokenSource();
var flushToken = flushCancel.Token;
var flushDelay = Task.Delay(1000, flushToken);
await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken));
flushCancel.Cancel();
pcm.Dispose();
}
if (b != null)
b.Dispose();
OnCompleted?.Invoke(this, data.Song);
if (_bytesSent == 0 && !cancel)
{
lock (locker)
Queue.RemoveSong(data.Song);
_log.Info("Song removed because it can't play");
}
}
try
{
//if repeating current song, just ignore other settings,
// and play this song again (don't change the index)
// ignore rcs if song is manually skipped
int queueCount;
bool stopped;
int currentIndex;
lock (locker)
{
queueCount = Queue.Count;
stopped = Stopped;
currentIndex = Queue.CurrentIndex;
}
if (AutoDelete && !RepeatCurrentSong && !RepeatPlaylist && data.Song != null)
{
Queue.RemoveSong(data.Song);
}
if (!manualIndex && (!RepeatCurrentSong || manualSkip))
{
if (Shuffle)
{
_log.Info("Random song");
Queue.Random(); //if shuffle is set, set current song index to a random number
}
else
{
//if last song, and autoplay is enabled, and if it's a youtube song
// do autplay magix
if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == MusicType.YouTube)
{
try
{
_log.Info("Loading related song");
await _musicService.TryQueueRelatedSongAsync(data.Song, OutputTextChannel, VoiceChannel);
if(!AutoDelete)
Queue.Next();
}
catch
{
_log.Info("Loading related song failed.");
}
}
else if (FairPlay)
{
lock (locker)
{
_log.Info("Next fair song");
var q = Queue.ToArray().Songs.Shuffle().ToArray();
bool found = false;
for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently
{
var item = q[i];
if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index
{
Queue.CurrentIndex = i;
found = true;
break;
}
}
if (!found) //if it's not
{
RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played)
Queue.Random(); //go to a random song (to prevent looping on the first few songs)
var cur = Current;
if (cur.Current != null) // add newely scheduled song's queuer to the recently played list
RecentlyPlayedUsers.Add(cur.Current.QueuerName);
}
}
}
else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip)
{
_log.Info("Stopping because repeatplaylist is disabled");
lock (locker)
{
Stop();
}
}
else
{
_log.Info("Next song");
lock (locker)
{
if (!Stopped)
if(!AutoDelete)
Queue.Next();
}
}
}
}
}
catch (Exception ex)
{
_log.Error(ex);
}
}
do
{
await Task.Delay(500);
}
while ((Queue.Count == 0 || Stopped) && !Exited);
}
}
private async Task<IAudioClient> GetAudioClient(bool reconnect = false)
{
if (_audioClient == null ||
_audioClient.ConnectionState != ConnectionState.Connected ||
reconnect ||
newVoiceChannel)
try
{
try
{
var t = _audioClient?.StopAsync();
if (t != null)
{
_log.Info("Stopping audio client");
await t;
_log.Info("Disposing audio client");
_audioClient.Dispose();
}
}
catch
{
}
newVoiceChannel = false;
_log.Info("Get current user");
var curUser = await VoiceChannel.Guild.GetCurrentUserAsync();
if (curUser.VoiceChannel != null)
{
_log.Info("Connecting");
var ac = await VoiceChannel.ConnectAsync();
_log.Info("Connected, stopping");
await ac.StopAsync();
_log.Info("Disconnected");
await Task.Delay(1000);
}
_log.Info("Connecting");
_audioClient = await VoiceChannel.ConnectAsync();
}
catch
{
return null;
}
return _audioClient;
}
public int Enqueue(SongInfo song)
{
lock (locker)
{
if (Exited)
return -1;
Queue.Add(song);
return Queue.Count - 1;
}
}
public int EnqueueNext(SongInfo song)
{
lock (locker)
{
if (Exited)
return -1;
return Queue.AddNext(song);
}
}
public void SetIndex(int index)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
lock (locker)
{
if (Exited)
return;
if (AutoDelete && index >= Queue.CurrentIndex && index > 0)
index--;
Queue.CurrentIndex = index;
manualIndex = true;
Stopped = false;
CancelCurrentSong();
}
}
public void Next(int skipCount = 1)
{
lock (locker)
{
if (Exited)
return;
manualSkip = true;
// if player is stopped, and user uses .n, it should play current song.
// It's a bit weird, but that's the least annoying solution
if (!Stopped)
if (!RepeatPlaylist && Queue.IsLast()) // if it's the last song in the queue, and repeat playlist is disabled
{ //stop the queue
Stop();
return;
}
else
Queue.Next(skipCount - 1);
else
Queue.CurrentIndex = 0;
Stopped = false;
CancelCurrentSong();
Unpause();
}
}
public void Stop(bool clearQueue = false)
{
lock (locker)
{
Stopped = true;
//Queue.ResetCurrent();
if (clearQueue)
Queue.Clear();
Unpause();
CancelCurrentSong();
}
}
private void Unpause()
{
lock (locker)
{
if (pauseTaskSource != null)
{
pauseTaskSource.TrySetResult(true);
pauseTaskSource = null;
}
}
}
public void TogglePause()
{
lock (locker)
{
if (pauseTaskSource == null)
pauseTaskSource = new TaskCompletionSource<bool>();
else
{
Unpause();
}
}
OnPauseChanged?.Invoke(this, pauseTaskSource != null);
}
public void SetVolume(int volume)
{
if (volume < 0 || volume > 100)
throw new ArgumentOutOfRangeException(nameof(volume));
lock (locker)
{
Volume = ((float)volume) / 100;
}
}
public SongInfo RemoveAt(int index)
{
lock (locker)
{
var cur = Queue.Current;
var toReturn = Queue.RemoveAt(index);
if (cur.Index == index)
Next();
return toReturn;
}
}
private void CancelCurrentSong()
{
lock (locker)
{
var cs = SongCancelSource;
SongCancelSource = new CancellationTokenSource();
cs.Cancel();
}
}
public void ClearQueue()
{
lock (locker)
{
Queue.Clear();
}
}
public (int CurrentIndex, SongInfo[] Songs) QueueArray()
{
lock (locker)
return Queue.ToArray();
}
//aidiakapi ftw
public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume)
{
if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples;
// 16-bit precision for the multiplication
var volumeFixed = (int)Math.Round(volume * 65536d);
var count = audioSamples.Length / 2;
fixed (byte* srcBytes = audioSamples)
{
var src = (short*)srcBytes;
for (var i = count; i != 0; i--, src++)
*src = (short)(((*src) * volumeFixed) >> 16);
}
return audioSamples;
}
public bool ToggleRepeatSong()
{
lock (locker)
{
return RepeatCurrentSong = !RepeatCurrentSong;
}
}
public async Task Destroy()
{
_log.Info("Destroying");
lock (locker)
{
Stop();
Exited = true;
Unpause();
OnCompleted = null;
OnPauseChanged = null;
OnStarted = null;
}
var ac = _audioClient;
if (ac != null)
await ac.StopAsync();
}
public bool ToggleShuffle()
{
lock (locker)
{
return Shuffle = !Shuffle;
}
}
public bool ToggleAutoplay()
{
lock (locker)
{
return Autoplay = !Autoplay;
}
}
public bool ToggleRepeatPlaylist()
{
lock (locker)
{
return RepeatPlaylist = !RepeatPlaylist;
}
}
public async Task SetVoiceChannel(IVoiceChannel vch)
{
lock (locker)
{
if (Exited)
return;
VoiceChannel = vch;
}
_audioClient = await vch.ConnectAsync();
}
public async Task UpdateSongDurationsAsync()
{
var sw = Stopwatch.StartNew();
var (_, songs) = Queue.ToArray();
var toUpdate = songs
.Where(x => x.ProviderType == MusicType.YouTube
&& x.TotalTime == TimeSpan.Zero);
var vIds = toUpdate.Select(x => x.VideoId);
sw.Stop();
_log.Info(sw.Elapsed.TotalSeconds);
if (!vIds.Any())
return;
var durations = await _google.GetVideoDurationsAsync(vIds);
foreach (var x in toUpdate)
{
if (durations.TryGetValue(x.VideoId, out var dur))
x.TotalTime = dur;
}
}
public SongInfo MoveSong(int n1, int n2)
=> Queue.MoveSong(n1, n2);
//// this should be written better
//public TimeSpan TotalPlaytime =>
// _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ?
// TimeSpan.MaxValue :
// new TimeSpan(_playlist.Sum(s => s.TotalTime.Ticks));
}
}

View File

@ -0,0 +1,215 @@
using NadekoBot.Extensions;
using NadekoBot.Modules.Music.Common.Exceptions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common;
namespace NadekoBot.Modules.Music.Common
{
public class MusicQueue : IDisposable
{
private LinkedList<SongInfo> Songs { get; set; } = new LinkedList<SongInfo>();
private int _currentIndex = 0;
public int CurrentIndex
{
get
{
return _currentIndex;
}
set
{
lock (locker)
{
if (Songs.Count == 0)
_currentIndex = 0;
else
_currentIndex = value %= Songs.Count;
}
}
}
public (int Index, SongInfo Song) Current
{
get
{
var cur = CurrentIndex;
return (cur, Songs.ElementAtOrDefault(cur));
}
}
private readonly object locker = new object();
private TaskCompletionSource<bool> nextSource { get; } = new TaskCompletionSource<bool>();
public int Count
{
get
{
lock (locker)
{
return Songs.Count;
}
}
}
private uint _maxQueueSize;
public uint MaxQueueSize
{
get => _maxQueueSize;
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value));
lock (locker)
{
_maxQueueSize = value;
}
}
}
public void Add(SongInfo song)
{
song.ThrowIfNull(nameof(song));
lock (locker)
{
if(MaxQueueSize != 0 && Songs.Count >= MaxQueueSize)
throw new QueueFullException();
Songs.AddLast(song);
}
}
public int AddNext(SongInfo song)
{
song.ThrowIfNull(nameof(song));
lock (locker)
{
if (MaxQueueSize != 0 && Songs.Count >= MaxQueueSize)
throw new QueueFullException();
var curSong = Current.Song;
if (curSong == null)
{
Songs.AddLast(song);
return Songs.Count;
}
var songlist = Songs.ToList();
songlist.Insert(CurrentIndex + 1, song);
Songs = new LinkedList<SongInfo>(songlist);
return CurrentIndex + 1;
}
}
public void Next(int skipCount = 1)
{
lock(locker)
CurrentIndex += skipCount;
}
public void Dispose()
{
Clear();
}
public SongInfo RemoveAt(int index)
{
lock (locker)
{
if (index < 0 || index >= Songs.Count)
throw new ArgumentOutOfRangeException(nameof(index));
var current = Songs.First.Value;
for (int i = 0; i < Songs.Count; i++)
{
if (i == index)
{
current = Songs.ElementAt(index);
Songs.Remove(current);
if (CurrentIndex != 0)
{
if (CurrentIndex >= index)
{
--CurrentIndex;
}
}
break;
}
}
return current;
}
}
public void Clear()
{
lock (locker)
{
Songs.Clear();
CurrentIndex = 0;
}
}
public (int CurrentIndex, SongInfo[] Songs) ToArray()
{
lock (locker)
{
return (CurrentIndex, Songs.ToArray());
}
}
public void ResetCurrent()
{
lock (locker)
{
CurrentIndex = 0;
}
}
public void Random()
{
lock (locker)
{
CurrentIndex = new NadekoRandom().Next(Songs.Count);
}
}
public SongInfo MoveSong(int n1, int n2)
{
lock (locker)
{
var currentSong = Current.Song;
var playlist = Songs.ToList();
if (n1 >= playlist.Count || n2 >= playlist.Count || n1 == n2)
return null;
var s = playlist[n1];
playlist.RemoveAt(n1);
playlist.Insert(n2, s);
Songs = new LinkedList<SongInfo>(playlist);
if (currentSong != null)
CurrentIndex = playlist.IndexOf(currentSong);
return s;
}
}
public void RemoveSong(SongInfo song)
{
lock (locker)
{
Songs.Remove(song);
}
}
public bool IsLast()
{
lock (locker)
return CurrentIndex == Songs.Count - 1;
}
}
}
//O O [O] O O O O
//
// 3

View File

@ -0,0 +1,95 @@
using NLog;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
namespace NadekoBot.Modules.Music.Common
{
public class SongBuffer : IDisposable
{
const int readSize = 81920;
private Process p;
private Stream _outStream;
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
private readonly Logger _log;
public string SongUri { get; private set; }
public SongBuffer(string songUri, string skipTo, bool isLocal)
{
_log = LogManager.GetCurrentClassLogger();
this.SongUri = songUri;
this._isLocal = isLocal;
try
{
this.p = StartFFmpegProcess(SongUri, 0);
this._outStream = this.p.StandardOutput.BaseStream;
}
catch (System.ComponentModel.Win32Exception)
{
_log.Error(@"You have not properly installed or configured FFMPEG.
Please install and configure FFMPEG to play music.
Check the guides for your platform on how to setup ffmpeg correctly:
Windows Guide: https://goo.gl/OjKk8F
Linux Guide: https://goo.gl/ShjCUo");
}
catch (OperationCanceledException) { }
catch (InvalidOperationException) { } // when ffmpeg is disposed
catch (Exception ex)
{
_log.Info(ex);
}
}
private Process StartFFmpegProcess(string songUri, float skipTo = 0)
{
var args = $"-err_detect ignore_err -i {songUri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel error";
if (!_isLocal)
args = "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 " + args;
return Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = false,
CreateNoWindow = true,
});
}
private readonly object locker = new object();
private readonly bool _isLocal;
public int Read(byte[] b, int offset, int toRead)
{
lock (locker)
return _outStream.Read(b, offset, toRead);
}
public void Dispose()
{
try
{
this.p.StandardOutput.Dispose();
}
catch (Exception ex)
{
_log.Error(ex);
}
try
{
if(!this.p.HasExited)
this.p.Kill();
}
catch
{
}
_outStream.Dispose();
this.p.Dispose();
}
}
}

View File

@ -0,0 +1,9 @@
using NLog;
namespace NadekoBot.Modules.Music.Common
{
public static class SongHandler
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
}
}

View File

@ -0,0 +1,79 @@
using Discord;
using NadekoBot.Extensions;
using NadekoBot.Core.Services.Database.Models;
using System;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Common
{
public class SongInfo
{
public string Provider { get; set; }
public MusicType ProviderType { get; set; }
public string Query { get; set; }
public string Title { get; set; }
public Func<Task<string>> Uri { get; set; }
public string Thumbnail { get; set; }
public string QueuerName { get; set; }
public TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
public string PrettyProvider => (Provider ?? "???");
//public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime;
public string PrettyName => $"**[{Title.TrimTo(65)}]({SongUrl})**";
public string PrettyInfo => $"{PrettyTotalTime} | {PrettyProvider} | {QueuerName}";
public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {Format.Sanitize(QueuerName.TrimTo(15))}`";
public string PrettyTotalTime
{
get
{
if (TotalTime == TimeSpan.Zero)
return "(?)";
if (TotalTime == TimeSpan.MaxValue)
return "∞";
var time = TotalTime.ToString(@"mm\:ss");
var hrs = (int)TotalTime.TotalHours;
if (hrs > 0)
return hrs + ":" + time;
return time;
}
}
public string SongUrl
{
get
{
switch (ProviderType)
{
case MusicType.YouTube:
return Query;
case MusicType.Soundcloud:
return Query;
case MusicType.Local:
return $"https://google.com/search?q={ WebUtility.UrlEncode(Title).Replace(' ', '+') }";
case MusicType.Radio:
return $"https://google.com/search?q={Title}";
default:
return "";
}
}
}
private string _videoId = null;
public string VideoId
{
get
{
if (ProviderType == MusicType.YouTube)
return _videoId = _videoId ?? videoIdRegex.Match(Query)?.ToString();
return _videoId ?? "";
}
set => _videoId = value;
}
private readonly Regex videoIdRegex = new Regex("<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+", RegexOptions.Compiled);
}
}

View File

@ -0,0 +1,11 @@
using NadekoBot.Modules.Music.Common.SongResolver.Strategies;
using NadekoBot.Core.Services.Database.Models;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Common.SongResolver
{
public interface ISongResolverFactory
{
Task<IResolveStrategy> GetResolveStrategy(string query, MusicType? musicType);
}
}

View File

@ -0,0 +1,41 @@
using System.Threading.Tasks;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Core.Services.Impl;
using NadekoBot.Modules.Music.Common.SongResolver.Strategies;
namespace NadekoBot.Modules.Music.Common.SongResolver
{
public class SongResolverFactory : ISongResolverFactory
{
private readonly SoundCloudApiService _sc;
public SongResolverFactory(SoundCloudApiService sc)
{
_sc = sc;
}
public async Task<IResolveStrategy> GetResolveStrategy(string query, MusicType? musicType)
{
await Task.Yield(); //for async warning
switch (musicType)
{
case MusicType.YouTube:
return new YoutubeResolveStrategy();
case MusicType.Radio:
return new RadioResolveStrategy();
case MusicType.Local:
return new LocalSongResolveStrategy();
case MusicType.Soundcloud:
return new SoundcloudResolveStrategy(_sc);
default:
if (_sc.IsSoundCloudLink(query))
return new SoundcloudResolveStrategy(_sc);
else if (RadioResolveStrategy.IsRadioLink(query))
return new RadioResolveStrategy();
// maybe add a check for local files in the future
else
return new YoutubeResolveStrategy();
}
}
}
}

View File

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
{
public interface IResolveStrategy
{
Task<SongInfo> ResolveSong(string query);
}
}

View File

@ -0,0 +1,22 @@
using NadekoBot.Core.Services.Database.Models;
using System.IO;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
{
public class LocalSongResolveStrategy : IResolveStrategy
{
public Task<SongInfo> ResolveSong(string query)
{
return Task.FromResult(new SongInfo
{
Uri = () => Task.FromResult("\"" + Path.GetFullPath(query) + "\""),
Title = Path.GetFileNameWithoutExtension(query),
Provider = "Local File",
ProviderType = MusicType.Local,
Query = query,
Thumbnail = "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png",
});
}
}
}

View File

@ -0,0 +1,138 @@
using NadekoBot.Core.Services.Database.Models;
using NLog;
using System;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
{
public class RadioResolveStrategy : IResolveStrategy
{
private readonly Regex plsRegex = new Regex("File1=(?<url>.*?)\\n", RegexOptions.Compiled);
private readonly Regex m3uRegex = new Regex("(?<url>^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline);
private readonly Regex asxRegex = new Regex("<ref href=\"(?<url>.*?)\"", RegexOptions.Compiled);
private readonly Regex xspfRegex = new Regex("<location>(?<url>.*?)</location>", RegexOptions.Compiled);
private readonly Logger _log;
public RadioResolveStrategy()
{
_log = LogManager.GetCurrentClassLogger();
}
public async Task<SongInfo> ResolveSong(string query)
{
if (IsRadioLink(query))
query = await HandleStreamContainers(query);
return new SongInfo
{
Uri = () => Task.FromResult(query),
Title = query,
Provider = "Radio Stream",
ProviderType = MusicType.Radio,
Query = query,
TotalTime = TimeSpan.MaxValue,
Thumbnail = "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png",
};
}
public static bool IsRadioLink(string query) =>
(query.StartsWith("http") ||
query.StartsWith("ww"))
&&
(query.Contains(".pls") ||
query.Contains(".m3u") ||
query.Contains(".asx") ||
query.Contains(".xspf"));
private async Task<string> HandleStreamContainers(string query)
{
string file = null;
try
{
using (var http = new HttpClient())
{
file = await http.GetStringAsync(query).ConfigureAwait(false);
}
}
catch
{
return query;
}
if (query.Contains(".pls"))
{
//File1=http://armitunes.com:8000/
//Regex.Match(query)
try
{
var m = plsRegex.Match(file);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
catch
{
_log.Warn($"Failed reading .pls:\n{file}");
return null;
}
}
if (query.Contains(".m3u"))
{
/*
# This is a comment
C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3
C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3
*/
try
{
var m = m3uRegex.Match(file);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
catch
{
_log.Warn($"Failed reading .m3u:\n{file}");
return null;
}
}
if (query.Contains(".asx"))
{
//<ref href="http://armitunes.com:8000"/>
try
{
var m = asxRegex.Match(file);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
catch
{
_log.Warn($"Failed reading .asx:\n{file}");
return null;
}
}
if (query.Contains(".xspf"))
{
/*
<?xml version="1.0" encoding="UTF-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
<trackList>
<track><location>file:///mp3s/song_1.mp3</location></track>
*/
try
{
var m = xspfRegex.Match(file);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
catch
{
_log.Warn($"Failed reading .xspf:\n{file}");
return null;
}
}
return query;
}
}
}

View File

@ -0,0 +1,27 @@
using NadekoBot.Modules.Music.Extensions;
using NadekoBot.Core.Services.Impl;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
{
public class SoundcloudResolveStrategy : IResolveStrategy
{
private readonly SoundCloudApiService _sc;
public SoundcloudResolveStrategy(SoundCloudApiService sc)
{
_sc = sc;
}
public async Task<SongInfo> ResolveSong(string query)
{
var svideo = !_sc.IsSoundCloudLink(query) ?
await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false) :
await _sc.ResolveVideoAsync(query).ConfigureAwait(false);
if (svideo == null)
return null;
return await svideo.GetSongInfo();
}
}
}

View File

@ -0,0 +1,69 @@
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Core.Services.Impl;
using NLog;
using System;
using System.Globalization;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Common.SongResolver.Strategies
{
public class YoutubeResolveStrategy : IResolveStrategy
{
private readonly Logger _log;
public YoutubeResolveStrategy()
{
_log = LogManager.GetCurrentClassLogger();
}
public async Task<SongInfo> ResolveSong(string query)
{
_log.Info("Getting link");
string[] data;
try
{
using (var ytdl = new YtdlOperation())
{
data = (await ytdl.GetDataAsync(query)).Split('\n');
}
if (data.Length < 6)
{
_log.Info("No song found. Data less than 6");
return null;
}
TimeSpan time;
if (!TimeSpan.TryParseExact(data[4], new[] { "ss", "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss" }, CultureInfo.InvariantCulture, out time))
time = TimeSpan.FromHours(24);
return new SongInfo()
{
Title = data[0],
VideoId = data[1],
Uri = async () =>
{
using (var ytdl = new YtdlOperation())
{
data = (await ytdl.GetDataAsync(query)).Split('\n');
}
if (data.Length < 6)
{
_log.Info("No song found. Data less than 6");
return null;
}
return data[2];
},
Thumbnail = data[3],
TotalTime = time,
Provider = "YouTube",
ProviderType = MusicType.YouTube,
Query = "https://youtube.com/watch?v=" + data[1],
};
}
catch (Exception ex)
{
_log.Warn(ex);
return null;
}
}
}
}

View File

@ -0,0 +1,23 @@
using NadekoBot.Modules.Music.Common;
using NadekoBot.Core.Services.Database.Models;
using NadekoBot.Core.Services.Impl;
using System;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Extensions
{
public static class Extensions
{
public static Task<SongInfo> GetSongInfo(this SoundCloudVideo svideo) =>
Task.FromResult(new SongInfo
{
Title = svideo.FullName,
Provider = "SoundCloud",
Uri = () => svideo.StreamLink(),
ProviderType = MusicType.Soundcloud,
Query = svideo.TrackLink,
Thumbnail = svideo.artwork_url,
TotalTime = TimeSpan.FromMilliseconds(svideo.Duration)
});
}
}

View File

@ -0,0 +1,901 @@
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Core.Services;
using Discord;
using System.Threading.Tasks;
using System;
using System.Linq;
using NadekoBot.Extensions;
using System.Collections.Generic;
using NadekoBot.Core.Services.Database.Models;
using System.IO;
using System.Net.Http;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Common.Collections;
using Newtonsoft.Json.Linq;
using NadekoBot.Core.Services.Impl;
using NadekoBot.Modules.Music.Services;
using NadekoBot.Modules.Music.Common.Exceptions;
using NadekoBot.Modules.Music.Common;
using NadekoBot.Modules.Music.Extensions;
namespace NadekoBot.Modules.Music
{
[NoPublicBot]
public class Music : NadekoTopLevelModule<MusicService>
{
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private readonly IGoogleApiService _google;
private readonly DbService _db;
public Music(DiscordSocketClient client,
IBotCredentials creds,
IGoogleApiService google,
DbService db)
{
_client = client;
_creds = creds;
_google = google;
_db = db;
}
//todo 50 changing server region is bugged again
//private Task Client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState oldState, SocketVoiceState newState)
//{
// var t = Task.Run(() =>
// {
// var usr = iusr as SocketGuildUser;
// if (usr == null ||
// oldState.VoiceChannel == newState.VoiceChannel)
// return;
// var player = _music.GetPlayerOrDefault(usr.Guild.Id);
// if (player == null)
// return;
// try
// {
// //if bot moved
// if ((player.VoiceChannel == oldState.VoiceChannel) &&
// usr.Id == _client.CurrentUser.Id)
// {
// //if (player.Paused && newState.VoiceChannel.Users.Count > 1) //unpause if there are people in the new channel
// // player.TogglePause();
// //else if (!player.Paused && newState.VoiceChannel.Users.Count <= 1) // pause if there are no users in the new channel
// // player.TogglePause();
// // player.SetVoiceChannel(newState.VoiceChannel);
// return;
// }
// ////if some other user moved
// //if ((player.VoiceChannel == newState.VoiceChannel && //if joined first, and player paused, unpause
// // player.Paused &&
// // newState.VoiceChannel.Users.Count >= 2) || // keep in mind bot is in the channel (+1)
// // (player.VoiceChannel == oldState.VoiceChannel && // if left last, and player unpaused, pause
// // !player.Paused &&
// // oldState.VoiceChannel.Users.Count == 1))
// //{
// // player.TogglePause();
// // return;
// //}
// }
// catch
// {
// // ignored
// }
// });
// return Task.CompletedTask;
//}
private async Task InternalQueue(MusicPlayer mp, SongInfo songInfo, bool silent, bool queueFirst = false)
{
if (songInfo == null)
{
if(!silent)
await ReplyErrorLocalized("song_not_found").ConfigureAwait(false);
return;
}
int index;
try
{
index = queueFirst
? mp.EnqueueNext(songInfo)
: mp.Enqueue(songInfo);
}
catch (QueueFullException)
{
await ReplyErrorLocalized("queue_full", mp.MaxQueueSize).ConfigureAwait(false);
throw;
}
if (index != -1)
{
if (!silent)
{
try
{
var embed = new EmbedBuilder().WithOkColor()
.WithAuthor(eab => eab.WithName(GetText("queued_song") + " #" + (index + 1)).WithMusicIcon())
.WithDescription($"{songInfo.PrettyName}\n{GetText("queue")} ")
.WithFooter(ef => ef.WithText(songInfo.PrettyProvider));
if (Uri.IsWellFormedUriString(songInfo.Thumbnail, UriKind.Absolute))
embed.WithThumbnailUrl(songInfo.Thumbnail);
var queuedMessage = await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false);
if (mp.Stopped)
{
(await ReplyErrorLocalized("queue_stopped", Format.Code(Prefix + "play")).ConfigureAwait(false)).DeleteAfter(10);
}
queuedMessage?.DeleteAfter(10);
}
catch
{
// ignored
}
}
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Play([Remainder] string query = null)
{
var mp = await _service.GetOrCreatePlayer(Context);
if (string.IsNullOrWhiteSpace(query))
{
await Next();
}
else if (int.TryParse(query, out var index))
if (index >= 1)
mp.SetIndex(index - 1);
else
return;
else
{
try
{
await Queue(query);
}
catch { }
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Queue([Remainder] string query)
{
var mp = await _service.GetOrCreatePlayer(Context);
var songInfo = await _service.ResolveSong(query, Context.User.ToString());
try { await InternalQueue(mp, songInfo, false); } catch (QueueFullException) { return; }
if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages)
{
Context.Message.DeleteAfter(10);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task QueueNext([Remainder] string query)
{
var mp = await _service.GetOrCreatePlayer(Context);
var songInfo = await _service.ResolveSong(query, Context.User.ToString());
try { await InternalQueue(mp, songInfo, false, true); } catch (QueueFullException) { return; }
if ((await Context.Guild.GetCurrentUserAsync()).GetPermissions((IGuildChannel)Context.Channel).ManageMessages)
{
Context.Message.DeleteAfter(10);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task QueueSearch([Remainder] string query)
{
var videos = (await _google.GetVideoInfosByKeywordAsync(query, 5))
.ToArray();
if (!videos.Any())
{
await ReplyErrorLocalized("song_not_found").ConfigureAwait(false);
return;
}
var msg = await Context.Channel.SendConfirmAsync(string.Join("\n", videos.Select((x, i) => $"`{i + 1}.`\n\t{Format.Bold(x.Name)}\n\t{x.Url}")));
try
{
var input = await GetUserInputAsync(Context.User.Id, Context.Channel.Id);
if (input == null
|| !int.TryParse(input, out var index)
|| (index -= 1) < 0
|| index >= videos.Length)
{
try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
return;
}
query = videos[index].Url;
await Queue(query).ConfigureAwait(false);
}
finally
{
try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ListQueue(int page = 0)
{
var mp = await _service.GetOrCreatePlayer(Context);
var (current, songs) = mp.QueueArray();
if (!songs.Any())
{
await ReplyErrorLocalized("no_player").ConfigureAwait(false);
return;
}
if (--page < -1)
return;
try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { }
const int itemsPerPage = 10;
if (page == -1)
page = current / itemsPerPage;
//if page is 0 (-1 after this decrement) that means default to the page current song is playing from
var total = mp.TotalPlaytime;
var totalStr = total == TimeSpan.MaxValue ? "∞" : GetText("time_format",
(int)total.TotalHours,
total.Minutes,
total.Seconds);
var maxPlaytime = mp.MaxPlaytimeSeconds;
var lastPage = songs.Length / itemsPerPage;
Func<int, EmbedBuilder> printAction = curPage =>
{
var startAt = itemsPerPage * curPage;
var number = 0 + startAt;
var desc = string.Join("\n", songs
.Skip(startAt)
.Take(itemsPerPage)
.Select(v =>
{
if(number++ == current)
return $"**⇒**`{number}.` {v.PrettyFullName}";
else
return $"`{number}.` {v.PrettyFullName}";
}));
desc = $"`🔊` {songs[current].PrettyFullName}\n\n" + desc;
var add = "";
if (mp.Stopped)
add += Format.Bold(GetText("queue_stopped", Format.Code(Prefix + "play"))) + "\n";
var mps = mp.MaxPlaytimeSeconds;
if (mps > 0)
add += Format.Bold(GetText("song_skips_after", TimeSpan.FromSeconds(mps).ToString("HH\\:mm\\:ss"))) + "\n";
if (mp.RepeatCurrentSong)
add += "🔂 " + GetText("repeating_cur_song") + "\n";
else if (mp.Shuffle)
add += "🔀 " + GetText("shuffling_playlist") + "\n";
else
{
if (mp.Autoplay)
add += "↪ " + GetText("autoplaying") + "\n";
if (mp.FairPlay && !mp.Autoplay)
add += " " + GetText("fairplay") + "\n";
else if (mp.RepeatPlaylist)
add += "🔁 " + GetText("repeating_playlist") + "\n";
}
if (!string.IsNullOrWhiteSpace(add))
desc = add + "\n" + desc;
var embed = new EmbedBuilder()
.WithAuthor(eab => eab.WithName(GetText("player_queue", curPage + 1, lastPage + 1))
.WithMusicIcon())
.WithDescription(desc)
.WithFooter(ef => ef.WithText($"{mp.PrettyVolume} | {songs.Length} " +
$"{("tracks".SnPl(songs.Length))} | {totalStr}"))
.WithOkColor();
return embed;
};
await Context.Channel.SendPaginatedConfirmAsync(_client, page, printAction, lastPage, false).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Next(int skipCount = 1)
{
if (skipCount < 1)
return;
var mp = await _service.GetOrCreatePlayer(Context);
mp.Next(skipCount);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Stop()
{
var mp = await _service.GetOrCreatePlayer(Context);
mp.Stop();
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Destroy()
{
await _service.DestroyPlayer(Context.Guild.Id);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Pause()
{
var mp = await _service.GetOrCreatePlayer(Context);
mp.TogglePause();
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Volume(int val)
{
var mp = await _service.GetOrCreatePlayer(Context);
if (val < 0 || val > 100)
{
await ReplyErrorLocalized("volume_input_invalid").ConfigureAwait(false);
return;
}
mp.SetVolume(val);
await ReplyConfirmLocalized("volume_set", val).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Defvol([Remainder] int val)
{
if (val < 0 || val > 100)
{
await ReplyErrorLocalized("volume_input_invalid").ConfigureAwait(false);
return;
}
using (var uow = _db.UnitOfWork)
{
uow.GuildConfigs.For(Context.Guild.Id, set => set).DefaultMusicVolume = val / 100.0f;
uow.Complete();
}
await ReplyConfirmLocalized("defvol_set", val).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public async Task SongRemove(int index)
{
if (index < 1)
{
await ReplyErrorLocalized("removed_song_error").ConfigureAwait(false);
return;
}
var mp = await _service.GetOrCreatePlayer(Context);
try
{
var song = mp.RemoveAt(index - 1);
var embed = new EmbedBuilder()
.WithAuthor(eab => eab.WithName(GetText("removed_song") + " #" + (index)).WithMusicIcon())
.WithDescription(song.PrettyName)
.WithFooter(ef => ef.WithText(song.PrettyInfo))
.WithErrorColor();
await mp.OutputTextChannel.EmbedAsync(embed).ConfigureAwait(false);
}
catch (ArgumentOutOfRangeException)
{
await ReplyErrorLocalized("removed_song_error").ConfigureAwait(false);
}
}
public enum All { All }
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public async Task SongRemove(All all)
{
var mp = _service.GetPlayerOrDefault(Context.Guild.Id);
if (mp == null)
return;
mp.Stop(true);
await ReplyConfirmLocalized("queue_cleared").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Playlists([Remainder] int num = 1)
{
if (num <= 0)
return;
List<MusicPlaylist> playlists;
using (var uow = _db.UnitOfWork)
{
playlists = uow.MusicPlaylists.GetPlaylistsOnPage(num);
}
var embed = new EmbedBuilder()
.WithAuthor(eab => eab.WithName(GetText("playlists_page", num)).WithMusicIcon())
.WithDescription(string.Join("\n", playlists.Select(r =>
GetText("playlists", r.Id, r.Name, r.Author, r.Songs.Count))))
.WithOkColor();
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task DeletePlaylist([Remainder] int id)
{
var success = false;
try
{
using (var uow = _db.UnitOfWork)
{
var pl = uow.MusicPlaylists.Get(id);
if (pl != null)
{
if (_creds.IsOwner(Context.User) || pl.AuthorId == Context.User.Id)
{
uow.MusicPlaylists.Remove(pl);
await uow.CompleteAsync().ConfigureAwait(false);
success = true;
}
}
}
if (!success)
await ReplyErrorLocalized("playlist_delete_fail").ConfigureAwait(false);
else
await ReplyConfirmLocalized("playlist_deleted").ConfigureAwait(false);
}
catch (Exception ex)
{
_log.Warn(ex);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Save([Remainder] string name)
{
var mp = await _service.GetOrCreatePlayer(Context);
var songs = mp.QueueArray().Songs
.Select(s => new PlaylistSong()
{
Provider = s.Provider,
ProviderType = s.ProviderType,
Title = s.Title,
Query = s.Query,
}).ToList();
MusicPlaylist playlist;
using (var uow = _db.UnitOfWork)
{
playlist = new MusicPlaylist
{
Name = name,
Author = Context.User.Username,
AuthorId = Context.User.Id,
Songs = songs.ToList(),
};
uow.MusicPlaylists.Add(playlist);
await uow.CompleteAsync().ConfigureAwait(false);
}
await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithTitle(GetText("playlist_saved"))
.AddField(efb => efb.WithName(GetText("name")).WithValue(name))
.AddField(efb => efb.WithName(GetText("id")).WithValue(playlist.Id.ToString())));
}
private static readonly ConcurrentHashSet<ulong> PlaylistLoadBlacklist = new ConcurrentHashSet<ulong>();
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Load([Remainder] int id)
{
if (!PlaylistLoadBlacklist.Add(Context.Guild.Id))
return;
try
{
var mp = await _service.GetOrCreatePlayer(Context);
MusicPlaylist mpl;
using (var uow = _db.UnitOfWork)
{
mpl = uow.MusicPlaylists.GetWithSongs(id);
}
if (mpl == null)
{
await ReplyErrorLocalized("playlist_id_not_found").ConfigureAwait(false);
return;
}
IUserMessage msg = null;
try { msg = await Context.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(mpl.Songs.Count.ToString()))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); }
foreach (var item in mpl.Songs)
{
try
{
await Task.Yield();
await Task.WhenAll(Task.Delay(1000), InternalQueue(mp, await _service.ResolveSong(item.Query, Context.User.ToString(), item.ProviderType), true)).ConfigureAwait(false);
}
catch (SongNotFoundException) { }
catch { break; }
}
if (msg != null)
await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false);
}
finally
{
PlaylistLoadBlacklist.TryRemove(Context.Guild.Id);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Fairplay()
{
var mp = await _service.GetOrCreatePlayer(Context);
var val = mp.FairPlay = !mp.FairPlay;
if (val)
{
await ReplyConfirmLocalized("fp_enabled").ConfigureAwait(false);
}
else
{
await ReplyConfirmLocalized("fp_disabled").ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task SongAutoDelete()
{
var mp = await _service.GetOrCreatePlayer(Context);
var val = mp.AutoDelete = !mp.AutoDelete;
if (val)
{
await ReplyConfirmLocalized("sad_enabled").ConfigureAwait(false);
}
else
{
await ReplyConfirmLocalized("sad_disabled").ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task SoundCloudQueue([Remainder] string query)
{
var mp = await _service.GetOrCreatePlayer(Context);
var song = await _service.ResolveSong(query, Context.User.ToString(), MusicType.Soundcloud);
await InternalQueue(mp, song, false).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task SoundCloudPl([Remainder] string pl)
{
pl = pl?.Trim();
if (string.IsNullOrWhiteSpace(pl))
return;
var mp = await _service.GetOrCreatePlayer(Context);
using (var http = new HttpClient())
{
var scvids = JObject.Parse(await http.GetStringAsync($"https://scapi.nadekobot.me/resolve?url={pl}").ConfigureAwait(false))["tracks"].ToObject<SoundCloudVideo[]>();
IUserMessage msg = null;
try { msg = await Context.Channel.SendMessageAsync(GetText("attempting_to_queue", Format.Bold(scvids.Length.ToString()))).ConfigureAwait(false); } catch { }
foreach (var svideo in scvids)
{
try
{
await Task.Yield();
var sinfo = await svideo.GetSongInfo();
sinfo.QueuerName = Context.User.ToString();
await InternalQueue(mp, sinfo, true);
}
catch (Exception ex)
{
_log.Warn(ex);
break;
}
}
if (msg != null)
await msg.ModifyAsync(m => m.Content = GetText("playlist_queue_complete")).ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task NowPlaying()
{
var mp = await _service.GetOrCreatePlayer(Context);
var (_, currentSong) = mp.Current;
if (currentSong == null)
return;
try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { }
var embed = new EmbedBuilder().WithOkColor()
.WithAuthor(eab => eab.WithName(GetText("now_playing")).WithMusicIcon())
.WithDescription(currentSong.PrettyName)
.WithThumbnailUrl(currentSong.Thumbnail)
.WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + mp.PrettyFullTime + $" | {currentSong.PrettyProvider} | {currentSong.QueuerName}"));
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ShufflePlaylist()
{
var mp = await _service.GetOrCreatePlayer(Context);
var val = mp.ToggleShuffle();
if(val)
await ReplyConfirmLocalized("songs_shuffle_enable").ConfigureAwait(false);
else
await ReplyConfirmLocalized("songs_shuffle_disable").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Playlist([Remainder] string playlist)
{
if (string.IsNullOrWhiteSpace(playlist))
return;
var mp = await _service.GetOrCreatePlayer(Context);
var plId = (await _google.GetPlaylistIdsByKeywordsAsync(playlist).ConfigureAwait(false)).FirstOrDefault();
if (plId == null)
{
await ReplyErrorLocalized("no_search_results").ConfigureAwait(false);
return;
}
var ids = await _google.GetPlaylistTracksAsync(plId, 500).ConfigureAwait(false);
if (!ids.Any())
{
await ReplyErrorLocalized("no_search_results").ConfigureAwait(false);
return;
}
var count = ids.Count();
var msg = await Context.Channel.SendMessageAsync("🎵 " + GetText("attempting_to_queue",
Format.Bold(count.ToString()))).ConfigureAwait(false);
foreach (var song in ids)
{
try
{
if (mp.Exited)
return;
await Task.WhenAll(Task.Delay(150), InternalQueue(mp, await _service.ResolveSong(song, Context.User.ToString(), MusicType.YouTube), true));
}
catch (SongNotFoundException) { }
catch { break; }
}
await msg.ModifyAsync(m => m.Content = "✅ " + Format.Bold(GetText("playlist_queue_complete"))).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Radio(string radioLink)
{
var mp = await _service.GetOrCreatePlayer(Context);
var song = await _service.ResolveSong(radioLink, Context.User.ToString(), MusicType.Radio);
await InternalQueue(mp, song, false).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task Local([Remainder] string path)
{
var mp = await _service.GetOrCreatePlayer(Context);
var song = await _service.ResolveSong(path, Context.User.ToString(), MusicType.Local);
await InternalQueue(mp, song, false).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task LocalPl([Remainder] string dirPath)
{
if (string.IsNullOrWhiteSpace(dirPath))
return;
var mp = await _service.GetOrCreatePlayer(Context);
DirectoryInfo dir;
try { dir = new DirectoryInfo(dirPath); } catch { return; }
var fileEnum = dir.GetFiles("*", SearchOption.AllDirectories)
.Where(x => !x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System) && x.Extension != ".jpg" && x.Extension != ".png");
foreach (var file in fileEnum)
{
try
{
await Task.Yield();
var song = await _service.ResolveSong(file.FullName, Context.User.ToString(), MusicType.Local);
await InternalQueue(mp, song, true).ConfigureAwait(false);
}
catch (QueueFullException)
{
break;
}
catch (Exception ex)
{
_log.Warn(ex);
break;
}
}
await ReplyConfirmLocalized("dir_queue_complete").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Move()
{
var vch = ((IGuildUser)Context.User).VoiceChannel;
if (vch == null)
return;
var mp = _service.GetPlayerOrDefault(Context.Guild.Id);
if (mp == null)
return;
await mp.SetVoiceChannel(vch);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task MoveSong([Remainder] string fromto)
{
if (string.IsNullOrWhiteSpace(fromto))
return;
MusicPlayer mp = _service.GetPlayerOrDefault(Context.Guild.Id);
if (mp == null)
return;
fromto = fromto?.Trim();
var fromtoArr = fromto.Split('>');
SongInfo s;
if (fromtoArr.Length != 2 || !int.TryParse(fromtoArr[0], out var n1) ||
!int.TryParse(fromtoArr[1], out var n2) || n1 < 1 || n2 < 1 || n1 == n2
|| (s = mp.MoveSong(--n1, --n2)) == null)
{
await ReplyConfirmLocalized("invalid_input").ConfigureAwait(false);
return;
}
var embed = new EmbedBuilder()
.WithTitle(s.Title.TrimTo(65))
.WithUrl(s.SongUrl)
.WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png"))
.AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{n1 + 1}").WithIsInline(true))
.AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{n2 + 1}").WithIsInline(true))
.WithColor(NadekoBot.OkColor);
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task SetMaxQueue(uint size = 0)
{
if (size < 0)
return;
var mp = await _service.GetOrCreatePlayer(Context);
mp.MaxQueueSize = size;
if (size == 0)
await ReplyConfirmLocalized("max_queue_unlimited").ConfigureAwait(false);
else
await ReplyConfirmLocalized("max_queue_x", size).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task SetMaxPlaytime(uint seconds)
{
if (seconds < 15 && seconds != 0)
return;
var mp = await _service.GetOrCreatePlayer(Context);
mp.MaxPlaytimeSeconds = seconds;
if (seconds == 0)
await ReplyConfirmLocalized("max_playtime_none").ConfigureAwait(false);
else
await ReplyConfirmLocalized("max_playtime_set", seconds).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ReptCurSong()
{
var mp = await _service.GetOrCreatePlayer(Context);
var (_, currentSong) = mp.Current;
if (currentSong == null)
return;
var currentValue = mp.ToggleRepeatSong();
if (currentValue)
await Context.Channel.EmbedAsync(new EmbedBuilder()
.WithOkColor()
.WithAuthor(eab => eab.WithMusicIcon().WithName("🔂 " + GetText("repeating_track")))
.WithDescription(currentSong.PrettyName)
.WithFooter(ef => ef.WithText(currentSong.PrettyInfo))).ConfigureAwait(false);
else
await Context.Channel.SendConfirmAsync("🔂 " + GetText("repeating_track_stopped"))
.ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task RepeatPl()
{
var mp = await _service.GetOrCreatePlayer(Context);
var currentValue = mp.ToggleRepeatPlaylist();
if (currentValue)
await ReplyConfirmLocalized("rpl_enabled").ConfigureAwait(false);
else
await ReplyConfirmLocalized("rpl_disabled").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Autoplay()
{
var mp = await _service.GetOrCreatePlayer(Context);
if (!mp.ToggleAutoplay())
await ReplyConfirmLocalized("autoplay_disabled").ConfigureAwait(false);
else
await ReplyConfirmLocalized("autoplay_enabled").ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[RequireUserPermission(GuildPermission.ManageMessages)]
public async Task SetMusicChannel()
{
var mp = await _service.GetOrCreatePlayer(Context);
mp.OutputTextChannel = (ITextChannel)Context.Channel;
await ReplyConfirmLocalized("set_music_channel").ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,239 @@
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using NadekoBot.Extensions;
using NadekoBot.Core.Services.Database.Models;
using NLog;
using System.IO;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Common;
using NadekoBot.Core.Services.Impl;
using NadekoBot.Core.Services;
using NadekoBot.Modules.Music.Common;
using NadekoBot.Modules.Music.Common.Exceptions;
using NadekoBot.Modules.Music.Common.SongResolver;
namespace NadekoBot.Modules.Music.Services
{
public class MusicService : INService, IUnloadableService
{
public const string MusicDataPath = "data/musicdata";
private readonly IGoogleApiService _google;
private readonly NadekoStrings _strings;
private readonly ILocalization _localization;
private readonly DbService _db;
private readonly Logger _log;
private readonly SoundCloudApiService _sc;
private readonly IBotCredentials _creds;
private readonly ConcurrentDictionary<ulong, float> _defaultVolumes;
private readonly DiscordSocketClient _client;
public ConcurrentDictionary<ulong, MusicPlayer> MusicPlayers { get; } = new ConcurrentDictionary<ulong, MusicPlayer>();
public MusicService(DiscordSocketClient client, IGoogleApiService google,
NadekoStrings strings, ILocalization localization, DbService db,
SoundCloudApiService sc, IBotCredentials creds, NadekoBot bot)
{
_client = client;
_google = google;
_strings = strings;
_localization = localization;
_db = db;
_sc = sc;
_creds = creds;
_log = LogManager.GetCurrentClassLogger();
_client.LeftGuild += _client_LeftGuild;
try { Directory.Delete(MusicDataPath, true); } catch { }
_defaultVolumes = new ConcurrentDictionary<ulong, float>(
bot.AllGuildConfigs
.ToDictionary(x => x.GuildId, x => x.DefaultMusicVolume));
Directory.CreateDirectory(MusicDataPath);
}
public Task Unload()
{
_client.LeftGuild -= _client_LeftGuild;
return Task.CompletedTask;
}
private Task _client_LeftGuild(SocketGuild arg)
{
var t = DestroyPlayer(arg.Id);
return Task.CompletedTask;
}
public float GetDefaultVolume(ulong guildId)
{
return _defaultVolumes.GetOrAdd(guildId, (id) =>
{
using (var uow = _db.UnitOfWork)
{
return uow.GuildConfigs.For(guildId, set => set).DefaultMusicVolume;
}
});
}
public Task<MusicPlayer> GetOrCreatePlayer(ICommandContext context)
{
var gUsr = (IGuildUser)context.User;
var txtCh = (ITextChannel)context.Channel;
var vCh = gUsr.VoiceChannel;
return GetOrCreatePlayer(context.Guild.Id, vCh, txtCh);
}
public async Task<MusicPlayer> GetOrCreatePlayer(ulong guildId, IVoiceChannel voiceCh, ITextChannel textCh)
{
string GetText(string text, params object[] replacements) =>
_strings.GetText(text, _localization.GetCultureInfo(textCh.Guild), "Music".ToLowerInvariant(), replacements);
_log.Info("Checks");
if (voiceCh == null || voiceCh.Guild != textCh.Guild)
{
if (textCh != null)
{
await textCh.SendErrorAsync(GetText("must_be_in_voice")).ConfigureAwait(false);
}
throw new NotInVoiceChannelException();
}
_log.Info("Get or add");
return MusicPlayers.GetOrAdd(guildId, _ =>
{
_log.Info("Getting default volume");
var vol = GetDefaultVolume(guildId);
_log.Info("Creating musicplayer instance");
var mp = new MusicPlayer(this, _google, voiceCh, textCh, vol);
IUserMessage playingMessage = null;
IUserMessage lastFinishedMessage = null;
_log.Info("Subscribing");
mp.OnCompleted += async (s, song) =>
{
try
{
lastFinishedMessage?.DeleteAfter(0);
try
{
lastFinishedMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithAuthor(eab => eab.WithName(GetText("finished_song")).WithMusicIcon())
.WithDescription(song.PrettyName)
.WithFooter(ef => ef.WithText(song.PrettyInfo)))
.ConfigureAwait(false);
}
catch
{
// ignored
}
}
catch
{
// ignored
}
};
mp.OnStarted += async (player, song) =>
{
//try { await mp.UpdateSongDurationsAsync().ConfigureAwait(false); }
//catch
//{
// // ignored
//}
var sender = player;
if (sender == null)
return;
try
{
playingMessage?.DeleteAfter(0);
playingMessage = await mp.OutputTextChannel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithAuthor(eab => eab.WithName(GetText("playing_song", song.Index + 1)).WithMusicIcon())
.WithDescription(song.Song.PrettyName)
.WithFooter(ef => ef.WithText(mp.PrettyVolume + " | " + song.Song.PrettyInfo)))
.ConfigureAwait(false);
}
catch
{
// ignored
}
};
mp.OnPauseChanged += async (player, paused) =>
{
try
{
IUserMessage msg;
if (paused)
msg = await mp.OutputTextChannel.SendConfirmAsync(GetText("paused")).ConfigureAwait(false);
else
msg = await mp.OutputTextChannel.SendConfirmAsync(GetText("resumed")).ConfigureAwait(false);
msg?.DeleteAfter(10);
}
catch
{
// ignored
}
};
_log.Info("Done creating");
return mp;
});
}
public MusicPlayer GetPlayerOrDefault(ulong guildId)
{
if (MusicPlayers.TryGetValue(guildId, out var mp))
return mp;
else
return null;
}
public async Task TryQueueRelatedSongAsync(SongInfo song, ITextChannel txtCh, IVoiceChannel vch)
{
var related = (await _google.GetRelatedVideosAsync(song.VideoId, 4)).ToArray();
if (!related.Any())
return;
var si = await ResolveSong(related[new NadekoRandom().Next(related.Length)], _client.CurrentUser.ToString(), MusicType.YouTube);
if (si == null)
throw new SongNotFoundException();
var mp = await GetOrCreatePlayer(txtCh.GuildId, vch, txtCh);
mp.Enqueue(si);
}
public async Task<SongInfo> ResolveSong(string query, string queuerName, MusicType? musicType = null)
{
query.ThrowIfNull(nameof(query));
ISongResolverFactory resolverFactory = new SongResolverFactory(_sc);
var strategy = await resolverFactory.GetResolveStrategy(query, musicType);
var sinfo = await strategy.ResolveSong(query);
if (sinfo == null)
return null;
sinfo.QueuerName = queuerName;
return sinfo;
}
public async Task DestroyAllPlayers()
{
foreach (var key in MusicPlayers.Keys)
{
await DestroyPlayer(key);
}
}
public async Task DestroyPlayer(ulong id)
{
if (MusicPlayers.TryRemove(id, out var mp))
await mp.Destroy();
}
}
}

View File

@ -0,0 +1,5 @@
using System;
namespace NadekoBot.Modules.NSFW.Exceptions
{
}

View File

@ -0,0 +1,354 @@
using Discord;
using Discord.Commands;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Extensions;
using System.Threading;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Common.Collections;
using NadekoBot.Modules.Searches.Common;
using NadekoBot.Modules.Searches.Services;
using NadekoBot.Modules.NSFW.Exceptions;
using NadekoBot.Modules.Searches.Exceptions;
namespace NadekoBot.Modules.NSFW
{
// thanks to halitalf for adding autoboob and autobutt features :D
public class NSFW : NadekoTopLevelModule<SearchesService>
{
private static readonly ConcurrentHashSet<ulong> _hentaiBombBlacklist = new ConcurrentHashSet<ulong>();
private async Task InternalHentai(IMessageChannel channel, string tag, bool noError)
{
var rng = new NadekoRandom();
var arr = Enum.GetValues(typeof(DapiSearchType));
var type = (DapiSearchType)arr.GetValue(new NadekoRandom().Next(2, arr.Length));
ImageCacherObject img;
try
{
img = await _service.DapiSearch(tag, type, Context.Guild?.Id, true).ConfigureAwait(false);
}
catch (TagBlacklistedException)
{
await ReplyErrorLocalized("blacklisted_tag").ConfigureAwait(false);
return;
}
if (img == null)
{
if (!noError)
await ReplyErrorLocalized("not_found").ConfigureAwait(false);
return;
}
await channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithImageUrl(img.FileUrl)
.WithDescription($"[{GetText("tag")}: {tag}]({img})"))
.ConfigureAwait(false);
}
private async Task InternalBoobs(IMessageChannel Channel)
{
try
{
JToken obj;
obj = JArray.Parse(await _service.Http.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 10330)}").ConfigureAwait(false))[0];
await Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false);
}
catch (Exception ex)
{
await Channel.SendErrorAsync(ex.Message).ConfigureAwait(false);
}
}
private async Task InternalButts(IMessageChannel Channel)
{
try
{
JToken obj;
obj = JArray.Parse(await _service.Http.GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 4335)}").ConfigureAwait(false))[0];
await Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false);
}
catch (Exception ex)
{
await Channel.SendErrorAsync(ex.Message).ConfigureAwait(false);
}
}
#if !GLOBAL_NADEKO
[NadekoCommand, Usage, Description, Aliases]
[RequireUserPermission(ChannelPermission.ManageMessages)]
public async Task AutoHentai(int interval = 0, string tags = null)
{
Timer t;
if (interval == 0)
{
if (!_service.AutoHentaiTimers.TryRemove(Context.Channel.Id, out t)) return;
t.Change(Timeout.Infinite, Timeout.Infinite); //proper way to disable the timer
await ReplyConfirmLocalized("stopped").ConfigureAwait(false);
return;
}
if (interval < 20)
return;
var tagsArr = tags?.Split('|');
t = new Timer(async (state) =>
{
try
{
if (tagsArr == null || tagsArr.Length == 0)
await InternalHentai(Context.Channel, null, true).ConfigureAwait(false);
else
await InternalHentai(Context.Channel, tagsArr[new NadekoRandom().Next(0, tagsArr.Length)], true).ConfigureAwait(false);
}
catch
{
// ignored
}
}, null, interval * 1000, interval * 1000);
_service.AutoHentaiTimers.AddOrUpdate(Context.Channel.Id, t, (key, old) =>
{
old.Change(Timeout.Infinite, Timeout.Infinite);
return t;
});
await ReplyConfirmLocalized("autohentai_started",
interval,
string.Join(", ", tagsArr)).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireUserPermission(ChannelPermission.ManageMessages)]
public async Task AutoBoobs(int interval = 0)
{
Timer t;
if (interval == 0)
{
if (!_service.AutoBoobTimers.TryRemove(Context.Channel.Id, out t)) return;
t.Change(Timeout.Infinite, Timeout.Infinite); //proper way to disable the timer
await ReplyConfirmLocalized("stopped").ConfigureAwait(false);
return;
}
if (interval < 20)
return;
t = new Timer(async (state) =>
{
try
{
await InternalBoobs(Context.Channel).ConfigureAwait(false);
}
catch
{
// ignored
}
}, null, interval * 1000, interval * 1000);
_service.AutoBoobTimers.AddOrUpdate(Context.Channel.Id, t, (key, old) =>
{
old.Change(Timeout.Infinite, Timeout.Infinite);
return t;
});
await ReplyConfirmLocalized("started", interval).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireUserPermission(ChannelPermission.ManageMessages)]
public async Task AutoButts(int interval = 0)
{
Timer t;
if (interval == 0)
{
if (!_service.AutoButtTimers.TryRemove(Context.Channel.Id, out t)) return;
t.Change(Timeout.Infinite, Timeout.Infinite); //proper way to disable the timer
await ReplyConfirmLocalized("stopped").ConfigureAwait(false);
return;
}
if (interval < 20)
return;
t = new Timer(async (state) =>
{
try
{
await InternalButts(Context.Channel).ConfigureAwait(false);
}
catch
{
// ignored
}
}, null, interval * 1000, interval * 1000);
_service.AutoButtTimers.AddOrUpdate(Context.Channel.Id, t, (key, old) =>
{
old.Change(Timeout.Infinite, Timeout.Infinite);
return t;
});
await ReplyConfirmLocalized("started", interval).ConfigureAwait(false);
}
#endif
[NadekoCommand, Usage, Description, Aliases]
public Task Hentai([Remainder] string tag = null) =>
InternalHentai(Context.Channel, tag, false);
[NadekoCommand, Usage, Description, Aliases]
public async Task HentaiBomb([Remainder] string tag = null)
{
if (!_hentaiBombBlacklist.Add(Context.Guild?.Id ?? Context.User.Id))
return;
try
{
var images = await Task.WhenAll(_service.DapiSearch(tag, DapiSearchType.Gelbooru, Context.Guild?.Id, true),
_service.DapiSearch(tag, DapiSearchType.Danbooru, Context.Guild?.Id, true),
_service.DapiSearch(tag, DapiSearchType.Konachan, Context.Guild?.Id, true),
_service.DapiSearch(tag, DapiSearchType.Yandere, Context.Guild?.Id, true)).ConfigureAwait(false);
var linksEnum = images?.Where(l => l != null).ToArray();
if (images == null || !linksEnum.Any())
{
await ReplyErrorLocalized("not_found").ConfigureAwait(false);
return;
}
await Context.Channel.SendMessageAsync(string.Join("\n\n", linksEnum.Select(x => x.FileUrl))).ConfigureAwait(false);
}
finally
{
_hentaiBombBlacklist.TryRemove(Context.Guild?.Id ?? Context.User.Id);
}
}
[NadekoCommand, Usage, Description, Aliases]
public Task Yandere([Remainder] string tag = null)
=> InternalDapiCommand(tag, DapiSearchType.Yandere, false);
[NadekoCommand, Usage, Description, Aliases]
public Task Konachan([Remainder] string tag = null)
=> InternalDapiCommand(tag, DapiSearchType.Konachan, false);
[NadekoCommand, Usage, Description, Aliases]
public Task E621([Remainder] string tag = null)
=> InternalDapiCommand(tag, DapiSearchType.E621, false);
[NadekoCommand, Usage, Description, Aliases]
public Task Rule34([Remainder] string tag = null)
=> InternalDapiCommand(tag, DapiSearchType.Rule34, false);
[NadekoCommand, Usage, Description, Aliases]
public Task Danbooru([Remainder] string tag = null)
=> InternalDapiCommand(tag, DapiSearchType.Danbooru, false);
[NadekoCommand, Usage, Description, Aliases]
public Task Gelbooru([Remainder] string tag = null)
=> InternalDapiCommand(tag, DapiSearchType.Gelbooru, false);
[NadekoCommand, Usage, Description, Aliases]
public async Task Boobs()
{
try
{
JToken obj;
obj = JArray.Parse(await _service.Http.GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 10330)}").ConfigureAwait(false))[0];
await Context.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false);
}
catch (Exception ex)
{
await Context.Channel.SendErrorAsync(ex.Message).ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
public async Task Butts()
{
try
{
JToken obj;
obj = JArray.Parse(await _service.Http.GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 4335)}").ConfigureAwait(false))[0];
await Context.Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false);
}
catch (Exception ex)
{
await Context.Channel.SendErrorAsync(ex.Message).ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task NsfwTagBlacklist([Remainder] string tag = null)
{
if (string.IsNullOrWhiteSpace(tag))
{
var blTags = _service.GetBlacklistedTags(Context.Guild.Id);
await Context.Channel.SendConfirmAsync(GetText("blacklisted_tag_list"),
blTags.Any()
? string.Join(", ", blTags)
: "-").ConfigureAwait(false);
}
else
{
tag = tag.Trim().ToLowerInvariant();
var added = _service.ToggleBlacklistedTag(Context.Guild.Id, tag);
if (added)
await ReplyConfirmLocalized("blacklisted_tag_add", tag).ConfigureAwait(false);
else
await ReplyConfirmLocalized("blacklisted_tag_remove", tag).ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public Task NsfwClearCache()
{
_service.ClearCache();
return Context.Channel.SendConfirmAsync("👌");
}
public async Task InternalDapiCommand(string tag, DapiSearchType type, bool forceExplicit)
{
ImageCacherObject imgObj;
try
{
imgObj = await _service.DapiSearch(tag, type, Context.Guild?.Id, forceExplicit).ConfigureAwait(false);
}
catch (TagBlacklistedException)
{
await ReplyErrorLocalized("blacklisted_tag").ConfigureAwait(false);
return;
}
if (imgObj == null)
await ReplyErrorLocalized("not_found").ConfigureAwait(false);
else
{
var embed = new EmbedBuilder().WithOkColor()
.WithDescription($"{Context.User} [{tag ?? "url"}]({imgObj}) ")
.WithFooter(efb => efb.WithText(type.ToString()));
if (Uri.IsWellFormedUriString(imgObj.FileUrl, UriKind.Absolute))
embed.WithImageUrl(imgObj.FileUrl);
else
_log.Error($"Image link from {type} is not a proper Url: {imgObj.FileUrl}");
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
}
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace NadekoBot.Modules.Pokemon.Common
{
public class PokeStats
{
//Health left
public int Hp { get; set; } = 500;
public int MaxHp { get; } = 500;
//Amount of moves made since last time attacked
public int MovesMade { get; set; } = 0;
//Last people attacked
public List<ulong> LastAttacked { get; set; } = new List<ulong>();
}
}

View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
namespace NadekoBot.Modules.Pokemon.Common
{
public class PokemonType
{
public PokemonType(string n, string i, string[] m, List<PokemonMultiplier> multi)
{
Name = n;
Icon = i;
Moves = m;
Multipliers = multi;
}
public string Name { get; set; }
public List<PokemonMultiplier> Multipliers { get; set; }
public string Icon { get; set; }
public string[] Moves { get; set; }
public override string ToString() =>
Icon + "**" + Name.ToLowerInvariant() + "**" + Icon;
}
public class PokemonMultiplier
{
public PokemonMultiplier(string t, double m)
{
Type = t;
Multiplication = m;
}
public string Type { get; set; }
public double Multiplication { get; set; }
}
}

View File

@ -0,0 +1,340 @@
using Discord.Commands;
using NadekoBot.Extensions;
using System.Linq;
using NadekoBot.Core.Services;
using NadekoBot.Core.Services.Database.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
using Discord;
using System;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Pokemon.Common;
using NadekoBot.Modules.Pokemon.Services;
namespace NadekoBot.Modules.Pokemon
{
public class Pokemon : NadekoTopLevelModule<PokemonService>
{
private readonly DbService _db;
private readonly IBotConfigProvider _bc;
private readonly CurrencyService _cs;
public Pokemon(DbService db, IBotConfigProvider bc, CurrencyService cs)
{
_db = db;
_bc = bc;
_cs = cs;
}
private int GetDamage(PokemonType usertype, PokemonType targetType)
{
var rng = new Random();
var damage = rng.Next(40, 60);
foreach (var multiplierObj in usertype.Multipliers)
{
if (multiplierObj.Type != targetType.Name) continue;
damage = (int)(damage * multiplierObj.Multiplication);
}
return damage;
}
private PokemonType GetPokeType(ulong id)
{
Dictionary<ulong, string> setTypes;
using (var uow = _db.UnitOfWork)
{
setTypes = uow.PokeGame.GetAll().ToDictionary(x => x.UserId, y => y.type);
}
if (setTypes.ContainsKey(id))
{
return StringToPokemonType(setTypes[id]);
}
var count = _service.PokemonTypes.Count;
var remainder = Math.Abs((int)(id % (ulong)count));
return _service.PokemonTypes[remainder];
}
private PokemonType StringToPokemonType(string v)
{
var str = v?.ToUpperInvariant();
var list = _service.PokemonTypes;
foreach (var p in list)
{
if (str == p.Name)
{
return p;
}
}
return null;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Attack(string move, IGuildUser targetUser = null)
{
IGuildUser user = (IGuildUser)Context.User;
if (string.IsNullOrWhiteSpace(move)) {
return;
}
if (targetUser == null)
{
await ReplyErrorLocalized("user_not_found").ConfigureAwait(false);
return;
}
if (targetUser == user)
{
await ReplyErrorLocalized("cant_attack_yourself").ConfigureAwait(false);
return;
}
// Checking stats first, then move
//Set up the userstats
var userStats = _service.Stats.GetOrAdd(user.Id, new PokeStats());
//Check if able to move
//User not able if HP < 0, has made more than 4 attacks
if (userStats.Hp < 0)
{
await ReplyErrorLocalized("you_fainted").ConfigureAwait(false);
return;
}
if (userStats.MovesMade >= 5)
{
await ReplyErrorLocalized("too_many_moves").ConfigureAwait(false);
return;
}
if (userStats.LastAttacked.Contains(targetUser.Id))
{
await ReplyErrorLocalized("cant_attack_again").ConfigureAwait(false);
return;
}
//get target stats
var targetStats = _service.Stats.GetOrAdd(targetUser.Id, new PokeStats());
//If target's HP is below 0, no use attacking
if (targetStats.Hp <= 0)
{
await ReplyErrorLocalized("too_many_moves", targetUser).ConfigureAwait(false);
return;
}
//Check whether move can be used
PokemonType userType = GetPokeType(user.Id);
var enabledMoves = userType.Moves;
if (!enabledMoves.Contains(move.ToLowerInvariant()))
{
await ReplyErrorLocalized("invalid_move", Format.Bold(move), Prefix).ConfigureAwait(false);
return;
}
//get target type
PokemonType targetType = GetPokeType(targetUser.Id);
//generate damage
int damage = GetDamage(userType, targetType);
//apply damage to target
targetStats.Hp -= damage;
var response = GetText("attack", Format.Bold(move), userType.Icon, Format.Bold(targetUser.ToString()), targetType.Icon, Format.Bold(damage.ToString()));
//Damage type
if (damage < 40)
{
response += "\n" + GetText("not_effective");
}
else if (damage > 60)
{
response += "\n" + GetText("super_effective");
}
else
{
response += "\n" + GetText("somewhat_effective");
}
//check fainted
if (targetStats.Hp <= 0)
{
response += "\n" + GetText("fainted", Format.Bold(targetUser.ToString()));
}
else
{
response += "\n" + GetText("hp_remaining", Format.Bold(targetUser.ToString()), targetStats.Hp);
}
//update other stats
userStats.LastAttacked.Add(targetUser.Id);
userStats.MovesMade++;
targetStats.MovesMade = 0;
if (targetStats.LastAttacked.Contains(user.Id))
{
targetStats.LastAttacked.Remove(user.Id);
}
//update dictionary
//This can stay the same right?
_service.Stats[user.Id] = userStats;
_service.Stats[targetUser.Id] = targetStats;
await Context.Channel.SendConfirmAsync(Context.User.Mention + " " + response).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Movelist()
{
IGuildUser user = (IGuildUser)Context.User;
var userType = GetPokeType(user.Id);
var movesList = userType.Moves;
var embed = new EmbedBuilder().WithOkColor()
.WithTitle(GetText("moves", userType))
.WithDescription(string.Join("\n", movesList.Select(m => userType.Icon + " " + m)));
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Heal(IGuildUser targetUser = null)
{
IGuildUser user = (IGuildUser)Context.User;
if (targetUser == null)
{
await ReplyErrorLocalized("user_not_found").ConfigureAwait(false);
return;
}
if (_service.Stats.ContainsKey(targetUser.Id))
{
var targetStats = _service.Stats[targetUser.Id];
if (targetStats.Hp == targetStats.MaxHp)
{
await ReplyErrorLocalized("already_full", Format.Bold(targetUser.ToString())).ConfigureAwait(false);
return;
}
//Payment~
var amount = 1;
var target = (targetUser.Id == user.Id) ? "yourself" : targetUser.Mention;
if (amount > 0)
{
if (!await _cs.RemoveAsync(user, $"Poke-Heal {target}", amount, true).ConfigureAwait(false))
{
await ReplyErrorLocalized("no_currency", _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
}
//healing
targetStats.Hp = targetStats.MaxHp;
if (targetStats.Hp < 0)
{
//Could heal only for half HP?
_service.Stats[targetUser.Id].Hp = (targetStats.MaxHp / 2);
if (target == "yourself")
{
await ReplyConfirmLocalized("revive_yourself", _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
await ReplyConfirmLocalized("revive_other", Format.Bold(targetUser.ToString()), _bc.BotConfig.CurrencySign).ConfigureAwait(false);
}
await ReplyConfirmLocalized("healed", Format.Bold(targetUser.ToString()), _bc.BotConfig.CurrencySign).ConfigureAwait(false);
}
else
{
await ErrorLocalized("already_full", Format.Bold(targetUser.ToString()));
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Type(IGuildUser targetUser = null)
{
targetUser = targetUser ?? (IGuildUser)Context.User;
var pType = GetPokeType(targetUser.Id);
await ReplyConfirmLocalized("type_of_user", Format.Bold(targetUser.ToString()), pType).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Settype([Remainder] string typeTargeted = null)
{
IGuildUser user = (IGuildUser)Context.User;
var targetType = StringToPokemonType(typeTargeted);
if (targetType == null)
{
await Context.Channel.EmbedAsync(_service.PokemonTypes.Aggregate(new EmbedBuilder().WithDescription("List of the available types:"),
(eb, pt) => eb.AddField(efb => efb.WithName(pt.Name)
.WithValue(pt.Icon)
.WithIsInline(true)))
.WithColor(NadekoBot.OkColor)).ConfigureAwait(false);
return;
}
if (targetType == GetPokeType(user.Id))
{
await ReplyErrorLocalized("already_that_type", targetType).ConfigureAwait(false);
return;
}
//Payment~
var amount = 1;
if (amount > 0)
{
if (!await _cs.RemoveAsync(user, $"{user} change type to {typeTargeted}", amount, true).ConfigureAwait(false))
{
await ReplyErrorLocalized("no_currency", _bc.BotConfig.CurrencySign).ConfigureAwait(false);
return;
}
}
//Actually changing the type here
using (var uow = _db.UnitOfWork)
{
var pokeUsers = uow.PokeGame.GetAll().ToArray();
var setTypes = pokeUsers.ToDictionary(x => x.UserId, y => y.type);
var pt = new UserPokeTypes
{
UserId = user.Id,
type = targetType.Name,
};
if (!setTypes.ContainsKey(user.Id))
{
//create user in db
uow.PokeGame.Add(pt);
}
else
{
//update user in db
var pokeUserCmd = pokeUsers.FirstOrDefault(p => p.UserId == user.Id);
pokeUserCmd.type = targetType.Name;
uow.PokeGame.Update(pokeUserCmd);
}
await uow.CompleteAsync();
}
//Now for the response
await ReplyConfirmLocalized("settype_success",
targetType,
_bc.BotConfig.CurrencySign).ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,34 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using NadekoBot.Modules.Pokemon.Common;
using NadekoBot.Core.Services;
using Newtonsoft.Json;
using NLog;
namespace NadekoBot.Modules.Pokemon.Services
{
public class PokemonService : INService
{
public readonly List<PokemonType> PokemonTypes = new List<PokemonType>();
public readonly ConcurrentDictionary<ulong, PokeStats> Stats = new ConcurrentDictionary<ulong, PokeStats>();
public const string PokemonTypesFile = "data/pokemon_types.json";
private Logger _log { get; }
public PokemonService()
{
_log = LogManager.GetCurrentClassLogger();
if (File.Exists(PokemonTypesFile))
{
PokemonTypes = JsonConvert.DeserializeObject<List<PokemonType>>(File.ReadAllText(PokemonTypesFile));
}
else
{
PokemonTypes = new List<PokemonType>();
_log.Warn(PokemonTypesFile + " is missing. Pokemon types not loaded.");
}
}
}
}

View File

@ -0,0 +1,176 @@
using AngleSharp;
using AngleSharp.Dom.Html;
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using NadekoBot.Modules.Searches.Services;
using System;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
[Group]
public class AnimeSearchCommands : NadekoSubmodule<AnimeSearchService>
{
[NadekoCommand, Usage, Description, Aliases]
[Priority(0)]
public async Task Mal([Remainder] string name)
{
if (string.IsNullOrWhiteSpace(name))
return;
var fullQueryLink = "https://myanimelist.net/profile/" + name;
var config = Configuration.Default.WithDefaultLoader();
var document = await BrowsingContext.New(config).OpenAsync(fullQueryLink);
var imageElem = document.QuerySelector("body > div#myanimelist > div.wrapper > div#contentWrapper > div#content > div.content-container > div.container-left > div.user-profile > div.user-image > img");
var imageUrl = ((IHtmlImageElement)imageElem)?.Source ?? "http://icecream.me/uploads/870b03f36b59cc16ebfe314ef2dde781.png";
var stats = document.QuerySelectorAll("body > div#myanimelist > div.wrapper > div#contentWrapper > div#content > div.content-container > div.container-right > div#statistics > div.user-statistics-stats > div.stats > div.clearfix > ul.stats-status > li > span").Select(x => x.InnerHtml).ToList();
var favorites = document.QuerySelectorAll("div.user-favorites > div.di-tc");
var favAnime = GetText("anime_no_fav");
if (favorites[0].QuerySelector("p") == null)
favAnime = string.Join("\n", favorites[0].QuerySelectorAll("ul > li > div.di-tc.va-t > a")
.Shuffle()
.Take(3)
.Select(x =>
{
var elem = (IHtmlAnchorElement)x;
return $"[{elem.InnerHtml}]({elem.Href})";
}));
var info = document.QuerySelectorAll("ul.user-status:nth-child(3) > li.clearfix")
.Select(x => Tuple.Create(x.Children[0].InnerHtml, x.Children[1].InnerHtml))
.ToList();
var daysAndMean = document.QuerySelectorAll("div.anime:nth-child(1) > div:nth-child(2) > div")
.Select(x => x.TextContent.Split(':').Select(y => y.Trim()).ToArray())
.ToArray();
var embed = new EmbedBuilder()
.WithOkColor()
.WithTitle(GetText("mal_profile", name))
.AddField(efb => efb.WithName("💚 " + GetText("watching")).WithValue(stats[0]).WithIsInline(true))
.AddField(efb => efb.WithName("💙 " + GetText("completed")).WithValue(stats[1]).WithIsInline(true));
if (info.Count < 3)
embed.AddField(efb => efb.WithName("💛 " + GetText("on_hold")).WithValue(stats[2]).WithIsInline(true));
embed
.AddField(efb => efb.WithName("💔 " + GetText("dropped")).WithValue(stats[3]).WithIsInline(true))
.AddField(efb => efb.WithName("⚪ " + GetText("plan_to_watch")).WithValue(stats[4]).WithIsInline(true))
.AddField(efb => efb.WithName("🕐 " + daysAndMean[0][0]).WithValue(daysAndMean[0][1]).WithIsInline(true))
.AddField(efb => efb.WithName("📊 " + daysAndMean[1][0]).WithValue(daysAndMean[1][1]).WithIsInline(true))
.AddField(efb => efb.WithName(MalInfoToEmoji(info[0].Item1) + " " + info[0].Item1).WithValue(info[0].Item2.TrimTo(20)).WithIsInline(true))
.AddField(efb => efb.WithName(MalInfoToEmoji(info[1].Item1) + " " + info[1].Item1).WithValue(info[1].Item2.TrimTo(20)).WithIsInline(true));
if (info.Count > 2)
embed.AddField(efb => efb.WithName(MalInfoToEmoji(info[2].Item1) + " " + info[2].Item1).WithValue(info[2].Item2.TrimTo(20)).WithIsInline(true));
//if(info.Count > 3)
// embed.AddField(efb => efb.WithName(MalInfoToEmoji(info[3].Item1) + " " + info[3].Item1).WithValue(info[3].Item2).WithIsInline(true))
embed
.WithDescription($@"
** https://myanimelist.net/animelist/{ name } **
**{GetText("top_3_fav_anime")}**
{favAnime}"
//**[Manga List](https://myanimelist.net/mangalist/{name})**
//💚`Reading:` {stats[5]}
//💙`Completed:` {stats[6]}
//💔`Dropped:` {stats[8]}
//⚪`Plan to read:` {stats[9]}
//**Top 3 Favorite Manga:**
//{favManga}"
)
.WithUrl(fullQueryLink)
.WithImageUrl(imageUrl);
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
private static string MalInfoToEmoji(string info)
{
info = info.Trim().ToLowerInvariant();
switch (info)
{
case "gender":
return "🚁";
case "location":
return "🗺";
case "last online":
return "👥";
case "birthday":
return "📆";
default:
return "❔";
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public Task Mal(IGuildUser usr) => Mal(usr.Username);
[NadekoCommand, Usage, Description, Aliases]
public async Task Anime([Remainder] string query)
{
if (string.IsNullOrWhiteSpace(query))
return;
var animeData = await _service.GetAnimeData(query).ConfigureAwait(false);
if (animeData == null)
{
await ReplyErrorLocalized("failed_finding_anime").ConfigureAwait(false);
return;
}
var embed = new EmbedBuilder().WithColor(NadekoBot.OkColor)
.WithDescription(animeData.Synopsis.Replace("<br>", Environment.NewLine))
.WithTitle(animeData.title_english)
.WithUrl(animeData.Link)
.WithImageUrl(animeData.image_url_lge)
.AddField(efb => efb.WithName(GetText("episodes")).WithValue(animeData.total_episodes.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("status")).WithValue(animeData.AiringStatus.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("genres")).WithValue(string.Join(",\n", animeData.Genres.Any() ? animeData.Genres : new[] { "none" })).WithIsInline(true))
.WithFooter(efb => efb.WithText(GetText("score") + " " + animeData.average_score + " / 100"));
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Manga([Remainder] string query)
{
if (string.IsNullOrWhiteSpace(query))
return;
var mangaData = await _service.GetMangaData(query).ConfigureAwait(false);
if (mangaData == null)
{
await ReplyErrorLocalized("failed_finding_manga").ConfigureAwait(false);
return;
}
var embed = new EmbedBuilder().WithColor(NadekoBot.OkColor)
.WithDescription(mangaData.Synopsis.Replace("<br>", Environment.NewLine))
.WithTitle(mangaData.title_english)
.WithUrl(mangaData.Link)
.WithImageUrl(mangaData.image_url_lge)
.AddField(efb => efb.WithName(GetText("chapters")).WithValue(mangaData.total_chapters.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("status")).WithValue(mangaData.publishing_status.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("genres")).WithValue(string.Join(",\n", mangaData.Genres.Any() ? mangaData.Genres : new[] { "none" })).WithIsInline(true))
.WithFooter(efb => efb.WithText(GetText("score") + " " + mangaData.average_score + " / 100"));
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
}
}
}

View File

@ -0,0 +1,20 @@
using NadekoBot.Extensions;
namespace NadekoBot.Modules.Searches.Common
{
public class AnimeResult
{
public int id;
public string AiringStatus => airing_status.ToTitleCase();
public string airing_status;
public string title_english;
public int total_episodes;
public string description;
public string image_url_lge;
public string[] Genres;
public string average_score;
public string Link => "http://anilist.co/anime/" + id;
public string Synopsis => description?.Substring(0, description.Length > 500 ? 500 : description.Length) + "...";
}
}

View File

@ -0,0 +1,39 @@
using System.Collections.Generic;
namespace NadekoBot.Modules.Searches.Common
{
public class Audio
{
public string url { get; set; }
}
public class Example
{
public List<Audio> audio { get; set; }
public string text { get; set; }
}
public class GramaticalInfo
{
public string type { get; set; }
}
public class Sens
{
public object Definition { get; set; }
public List<Example> Examples { get; set; }
public GramaticalInfo Gramatical_info { get; set; }
}
public class Result
{
public string Part_of_speech { get; set; }
public List<Sens> Senses { get; set; }
public string Url { get; set; }
}
public class DefineModel
{
public List<Result> Results { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System;
namespace NadekoBot.Modules.Searches.Common.Exceptions
{
public class StreamNotFoundException : Exception
{
public StreamNotFoundException(string message) : base($"Stream '{message}' not found.")
{
}
}
}

View File

@ -0,0 +1,16 @@
namespace NadekoBot.Modules.Searches.Common
{
public struct GoogleSearchResult
{
public string Title { get; }
public string Link { get; }
public string Text { get; }
public GoogleSearchResult(string title, string link, string text)
{
this.Title = title;
this.Link = link;
this.Text = text;
}
}
}

View File

@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Searches.Common
{
public class MagicItem
{
public string Name { get; set; }
public string Description { get; set; }
}
}

View File

@ -0,0 +1,17 @@
namespace NadekoBot.Modules.Searches.Common
{
public class MangaResult
{
public int id;
public string publishing_status;
public string image_url_lge;
public string title_english;
public int total_chapters;
public int total_volumes;
public string description;
public string[] Genres;
public string average_score;
public string Link => "http://anilist.co/manga/" + id;
public string Synopsis => description?.Substring(0, description.Length > 500 ? 500 : description.Length) + "...";
}
}

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