Fixed some crashes in response strings source generator, reorganized more submodules into their folders

This commit is contained in:
Kwoth
2022-01-02 03:49:54 +01:00
parent 9c590668df
commit 4b6af0e4ef
191 changed files with 120 additions and 80 deletions

View File

@@ -0,0 +1,102 @@
#nullable disable
using NadekoBot.Modules.Games.Common;
using NadekoBot.Modules.Games.Services;
namespace NadekoBot.Modules.Games;
public partial class Games
{
[Group]
public partial class SpeedTypingCommands : NadekoSubmodule<GamesService>
{
private readonly GamesService _games;
private readonly DiscordSocketClient _client;
public SpeedTypingCommands(DiscordSocketClient client, GamesService games)
{
_games = games;
_client = client;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NadekoOptionsAttribute(typeof(TypingGame.Options))]
public async partial Task TypeStart(params string[] args)
{
var (options, _) = OptionsParser.ParseFrom(new TypingGame.Options(), args);
var channel = (ITextChannel)ctx.Channel;
var game = _service.RunningContests.GetOrAdd(ctx.Guild.Id,
_ => new(_games, _client, channel, Prefix, options, _eb));
if (game.IsActive)
await SendErrorAsync($"Contest already running in {game.Channel.Mention} channel.");
else
await game.Start();
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async partial Task TypeStop()
{
if (_service.RunningContests.TryRemove(ctx.Guild.Id, out var game))
{
await game.Stop();
return;
}
await SendErrorAsync("No contest to stop on this channel.");
}
[Cmd]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async partial Task Typeadd([Leftover] string text)
{
if (string.IsNullOrWhiteSpace(text))
return;
_games.AddTypingArticle(ctx.User, text);
await SendConfirmAsync("Added new article for typing game.");
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async partial Task Typelist(int page = 1)
{
if (page < 1)
return;
var articles = _games.TypingArticles.Skip((page - 1) * 15).Take(15).ToArray();
if (!articles.Any())
{
await SendErrorAsync($"{ctx.User.Mention} `No articles found on that page.`");
return;
}
var i = (page - 1) * 15;
await SendConfirmAsync("List of articles for Type Race",
string.Join("\n", articles.Select(a => $"`#{++i}` - {a.Text.TrimTo(50)}")));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async partial Task Typedel(int index)
{
var removed = _service.RemoveTypingArticle(--index);
if (removed is null) return;
var embed = _eb.Create()
.WithTitle($"Removed typing article #{index + 1}")
.WithDescription(removed.Text.TrimTo(50))
.WithOkColor();
await ctx.Channel.EmbedAsync(embed);
}
}
}

View File

@@ -0,0 +1,9 @@
#nullable disable
namespace NadekoBot.Modules.Games.Common;
public class TypingArticle
{
public string Source { get; set; }
public string Extra { get; set; }
public string Text { get; set; }
}

View File

@@ -0,0 +1,180 @@
#nullable disable
using CommandLine;
using NadekoBot.Modules.Games.Services;
using System.Diagnostics;
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 readonly Options _options;
private readonly IEmbedBuilderService _eb;
public TypingGame(
GamesService games,
DiscordSocketClient client,
ITextChannel channel,
string prefix,
Options options,
IEmbedBuilderService eb)
{
_games = games;
_client = client;
_prefix = prefix;
_options = options;
_eb = eb;
Channel = channel;
IsActive = false;
sw = new();
finishedUserIds = new();
}
public async Task<bool> Stop()
{
if (!IsActive) return false;
_client.MessageReceived -= AnswerReceived;
finishedUserIds.Clear();
IsActive = false;
sw.Stop();
sw.Reset();
try
{
await Channel.SendConfirmAsync(_eb, "Typing contest stopped.");
}
catch (Exception ex)
{
Log.Warning(ex.ToString());
}
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(_eb,
$@":clock2: Next contest will last for {i} seconds. Type the bolded text as fast as you can.");
var time = _options.StartTime;
var msg = await Channel.SendMessageAsync($"Starting new typing contest in **{time}**...",
options: new() { RetryMode = RetryMode.AlwaysRetry });
do
{
await Task.Delay(2000);
time -= 2;
try { await msg.ModifyAsync(m => m.Content = $"Starting new typing contest in **{time}**.."); }
catch { }
} while (time > 2);
await msg.ModifyAsync(m =>
{
m.Content = CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture);
});
sw.Start();
HandleAnswers();
while (i > 0)
{
await Task.Delay(1000);
i--;
if (!IsActive)
return;
}
}
catch { }
finally
{
await Stop();
}
}
public string GetRandomSentence()
{
if (_games.TypingArticles.Any())
return _games.TypingArticles[new NadekoRandom().Next(0, _games.TypingArticles.Count)].Text;
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;
if (imsg is not SocketUserMessage msg)
return;
if (Channel is null || 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 Channel.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle($"{msg.Author} finished the race!")
.AddField("Place", $"#{finishedUserIds.Count}", true)
.AddField("WPM", $"{wpm:F1} *[{elapsed.TotalSeconds:F2}sec]*", true)
.AddField("Errors", distance.ToString(), true));
if (finishedUserIds.Count % 4 == 0)
await Channel.SendConfirmAsync(_eb,
":exclamation: A lot of people finished, here is the text for those still typing:"
+ $"\n\n**{Format.Sanitize(CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture)).SanitizeMentions(true)}**");
}
}
catch (Exception ex)
{
Log.Warning(ex.ToString());
}
});
return Task.CompletedTask;
}
private static bool Judge(int errors, int textLength)
=> errors <= textLength / 25;
public class Options : INadekoCommandOptions
{
[Option('s',
"start-time",
Default = 5,
Required = false,
HelpText = "How long does it take for the race to start. Default 5.")]
public int StartTime { get; set; } = 5;
public void NormalizeOptions()
{
if (StartTime is < 3 or > 30)
StartTime = 5;
}
}
}