diff --git a/MinecraftDiscordBot/BotConfiguration.cs b/MinecraftDiscordBot/BotConfiguration.cs index 881f039..0922222 100644 --- a/MinecraftDiscordBot/BotConfiguration.cs +++ b/MinecraftDiscordBot/BotConfiguration.cs @@ -20,6 +20,9 @@ public class BotConfiguration : IBotConfiguration, IBotConfigurator { [JsonProperty("prefix", Required = Required.DisallowNull)] [Option("prefix", Default = DEFAULT_PREFIX, HelpText = "The Discord bot command prefix")] public string Prefix { get; init; } = DEFAULT_PREFIX; + [JsonProperty("host", Required = Required.Always)] + [Option("host", Default = DEFAULT_PREFIX, HelpText = "The Discord bot command prefix", Required = true)] + public string SocketHost { get; init; } = default!; [JsonIgnore] public BotConfiguration Config => this; } diff --git a/MinecraftDiscordBot/ChunkWaiter.cs b/MinecraftDiscordBot/ChunkWaiter.cs index a422090..184ce33 100644 --- a/MinecraftDiscordBot/ChunkWaiter.cs +++ b/MinecraftDiscordBot/ChunkWaiter.cs @@ -1,4 +1,6 @@ -namespace MinecraftDiscordBot; +using MinecraftDiscordBot.Services; + +namespace MinecraftDiscordBot; public class ChunkWaiter : IChunkWaiter { public int ID { get; } diff --git a/MinecraftDiscordBot/ClientScript.lua b/MinecraftDiscordBot/ClientScript.lua index c0e20eb..18408a6 100644 --- a/MinecraftDiscordBot/ClientScript.lua +++ b/MinecraftDiscordBot/ClientScript.lua @@ -1,5 +1,5 @@ local secretToken = "$TOKEN" -local connectionUri = "ws://ws.cnml.de:8081" +local connectionUri = "$HOST" local waitSeconds = 5 local function chunkString(value, chunkSize) diff --git a/MinecraftDiscordBot/Commands/CommandHandlerAttribute.cs b/MinecraftDiscordBot/Commands/CommandHandlerAttribute.cs new file mode 100644 index 0000000..e919c9b --- /dev/null +++ b/MinecraftDiscordBot/Commands/CommandHandlerAttribute.cs @@ -0,0 +1,8 @@ +namespace MinecraftDiscordBot.Commands; + +[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)] +public sealed class CommandHandlerAttribute : Attribute { + public CommandHandlerAttribute(string commandName) => CommandName = commandName; + public string CommandName { get; } + public string? HelpText { get; init; } +} diff --git a/MinecraftDiscordBot/CommandRouter.cs b/MinecraftDiscordBot/Commands/CommandRouter.cs similarity index 91% rename from MinecraftDiscordBot/CommandRouter.cs rename to MinecraftDiscordBot/Commands/CommandRouter.cs index 4ca4572..858b6a7 100644 --- a/MinecraftDiscordBot/CommandRouter.cs +++ b/MinecraftDiscordBot/Commands/CommandRouter.cs @@ -1,11 +1,12 @@ -using Discord; -using Discord.WebSocket; +using Discord.WebSocket; +using MinecraftDiscordBot.Services; using System.Reflection; using System.Text; -namespace MinecraftDiscordBot; +namespace MinecraftDiscordBot.Commands; public abstract class CommandRouter : ICommandHandler { + public record struct HandlerStruct(HandleCommandDelegate Delegate, CommandHandlerAttribute Attribute); private readonly Dictionary _handlers = new(); public abstract string HelpTextPrefix { get; } public CommandRouter() { @@ -42,6 +43,4 @@ public abstract class CommandRouter : ICommandHandler { } return sb.ToString(); } -} - -public record struct HandlerStruct(HandleCommandDelegate Delegate, CommandHandlerAttribute Attribute); \ No newline at end of file +} \ No newline at end of file diff --git a/MinecraftDiscordBot/ICommandHandler.cs b/MinecraftDiscordBot/Commands/ICommandHandler.cs similarity index 88% rename from MinecraftDiscordBot/ICommandHandler.cs rename to MinecraftDiscordBot/Commands/ICommandHandler.cs index 3ffffd0..835f077 100644 --- a/MinecraftDiscordBot/ICommandHandler.cs +++ b/MinecraftDiscordBot/Commands/ICommandHandler.cs @@ -1,6 +1,6 @@ using Discord.WebSocket; -namespace MinecraftDiscordBot; +namespace MinecraftDiscordBot.Commands; public interface ICommandHandler { Task HandleCommand(SocketUserMessage message, string[] parameters, CancellationToken ct); diff --git a/MinecraftDiscordBot/ConnectedComputer.cs b/MinecraftDiscordBot/ConnectedComputer.cs deleted file mode 100644 index 708be79..0000000 --- a/MinecraftDiscordBot/ConnectedComputer.cs +++ /dev/null @@ -1,179 +0,0 @@ -using Discord; -using Discord.WebSocket; -using Fleck; -using Newtonsoft.Json; -using System.Diagnostics; -using System.Runtime.Serialization; - -namespace MinecraftDiscordBot; - -public delegate Task HandleCommandDelegate(SocketUserMessage message, string[] parameters, CancellationToken ct); -public delegate Task HandleCommandDelegate(SocketUserMessage message, string[] parameters, CancellationToken ct); - -[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)] -public sealed class CommandHandlerAttribute : Attribute { - public CommandHandlerAttribute(string commandName) => CommandName = commandName; - public string CommandName { get; } - public string? HelpText { get; init; } -} - -public class ConnectedComputer : CommandRouter, ITaskWaitSource { - protected readonly IWebSocketConnection _socket; - public override string HelpTextPrefix => "!"; - public ConnectedComputer(IWebSocketConnection socket) : base() { - socket.OnMessage = OnMessage; - _socket = socket; - _rs = new RefinedStorageService(this); - } - - private void OnMessage(string message) { - if (JsonConvert.DeserializeObject(message) is not ReplyMessage msg) return; - IChunkWaiter? waiter; - lock (_syncRoot) { - if (!_waits.TryGetValue(msg.AnswerId, out waiter)) { - Program.LogWarningAsync("Socket", $"Invalid wait id '{msg.AnswerId}'!"); - return; - } - } - if (!msg.Success) waiter.SetUnsuccessful(); - waiter.AddChunk(msg.Chunk, msg.Total, msg.Result); - if (waiter.Finished || waiter.IsCancellationRequested) - lock (_syncRoot) - _waits.Remove(waiter.ID); - } - - public Task Send(string message) => _socket.Send(message); - public Task Send(Message message) => Send(JsonConvert.SerializeObject(message)); - private readonly object _syncRoot = new(); - private readonly Dictionary _waits = new(); - private readonly Random _rnd = new(); - public IWebSocketConnectionInfo ConnectionInfo => _socket.ConnectionInfo; - - private int GetFreeId() { - var attempts = 0; - while (true) { - var id = _rnd.Next(); - if (!_waits.ContainsKey(id)) - return id; - Program.LogWarningAsync(Program.WebSocketSource, $"Could not get a free ID after {++attempts} attempts!"); - } - } - - public ChunkWaiter GetWaiter(Func resultParser, CancellationToken ct) { - ChunkWaiter waiter; - lock (_syncRoot) { - waiter = new ChunkWaiter(GetFreeId(), resultParser, ct); - _waits.Add(waiter.ID, waiter); - } - return waiter; - } - - private readonly ICommandHandler _rs; - [CommandHandler("rs", HelpText = "Provides some commands for interacting with the Refined Storage system.")] - public Task RefinedStorageHandler(SocketUserMessage message, string[] parameters, CancellationToken ct) - => _rs.HandleCommand(message, parameters, ct); - - public static Func Deserialize() => msg - => JsonConvert.DeserializeObject(msg) ?? throw new InvalidProgramException("Empty response!"); - public override Task RootAnswer(SocketUserMessage message, CancellationToken ct) - => Task.FromResult(ResponseType.AsString("The Minecraft server is connected!")); - public override Task FallbackHandler(SocketUserMessage message, string method, string[] parameters, CancellationToken ct) - => throw new ReplyException($"What the fuck do you mean by '{method}'?"); -} - -public interface ITaskWaitSource { - ChunkWaiter GetWaiter(Func resultParser, CancellationToken ct); - Task Send(Message requestMessage); -} - -[Serializable] -public class ReplyException : Exception { - public ReplyException() { } - public ReplyException(string message) : base(message) { } - public ReplyException(string message, Exception inner) : base(message, inner) { } - protected ReplyException(SerializationInfo info, StreamingContext context) : base(info, context) { } -} - -[JsonObject(MemberSerialization.OptIn, Description = "Describes an item in a Refined Storage system.", MissingMemberHandling = MissingMemberHandling.Ignore)] -[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] -public class Item : Fluid { - [JsonProperty("fingerprint", Required = Required.Always)] - public Md5Hash Fingerprint { get; set; } = default!; - [JsonProperty("nbt", Required = Required.DisallowNull)] - public dynamic? NBT { get; set; } - public override string ToString() => $"{Amount:n0}x {DisplayName}"; -} - -[JsonObject(MemberSerialization.OptIn, Description = "Describes a fluid in a Refined Storage system.", MissingMemberHandling = MissingMemberHandling.Ignore)] -[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] -public class Fluid { - [JsonProperty("amount", Required = Required.Always)] - public int Amount { get; set; } - [JsonProperty("displayName", Required = Required.Always)] - public string DisplayName { get; set; } = default!; - [JsonProperty("tags", Required = Required.DisallowNull)] - public string[]? Tags { get; set; } = default; - [JsonProperty("name", Required = Required.Always)] - public ModItemId ItemId { get; set; } = default!; - public override string ToString() => Amount > 10000 - ? $"{Amount / 1000.0f:n2} B of {DisplayName}" - : $"{Amount:n0} mB of {DisplayName}"; - [JsonIgnore] - public string CleanDisplayName => DisplayName[1..^2]; -} - -[JsonConverter(typeof(ModItemIdJsonConverter))] -[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] -public class ModItemId { - public ModItemId(string name) { - var colon = name.IndexOf(':'); - if (colon < 0) throw new ArgumentException("Invalid mod item id!", nameof(name)); - ModName = name[..colon]; - ModItem = name[(colon + 1)..]; - if (ToString() != name) throw new InvalidProgramException("Bad Parsing!"); - } - public override string ToString() => $"{ModName}:{ModItem}"; - public string ModName { get; } - public string ModItem { get; } - - public class ModItemIdJsonConverter : JsonConverter { - public override ModItemId? ReadJson(JsonReader reader, Type objectType, ModItemId? existingValue, bool hasExistingValue, JsonSerializer serializer) - => reader.Value is string value - ? new(value) - : throw new JsonException($"Could not parse mod name with token '{reader.Value}'"); - public override void WriteJson(JsonWriter writer, ModItemId? value, JsonSerializer serializer) { - if (value is null) writer.WriteNull(); - else writer.WriteValue(value.ToString()); - } - } -} - -[JsonConverter(typeof(Md5JsonConverter))] -[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] -public class Md5Hash : IEquatable { - private readonly byte[] _hash; - public Md5Hash(string hash) : this(Convert.FromHexString(hash)) { } - public Md5Hash(byte[] hash) { - if (hash is not { Length: 16 }) throw new ArgumentException("Invalid digest size!", nameof(hash)); - _hash = hash; - } - public override bool Equals(object? obj) => Equals(obj as Md5Hash); - public bool Equals(Md5Hash? other) => other != null && _hash.SequenceEqual(other._hash); - public override int GetHashCode() { - var hashCode = new HashCode(); - hashCode.AddBytes(_hash); - return hashCode.ToHashCode(); - } - public override string ToString() => Convert.ToHexString(_hash); - - public class Md5JsonConverter : JsonConverter { - public override Md5Hash? ReadJson(JsonReader reader, Type objectType, Md5Hash? existingValue, bool hasExistingValue, JsonSerializer serializer) - => reader.Value is string { Length: 32 } value - ? new(value) - : throw new JsonException($"Could not parse MD5 hash with token '{reader.Value}'"); - public override void WriteJson(JsonWriter writer, Md5Hash? value, JsonSerializer serializer) { - if (value is null) writer.WriteNull(); - else writer.WriteValue(value.ToString()); - } - } -} \ No newline at end of file diff --git a/MinecraftDiscordBot/ItemFilter.cs b/MinecraftDiscordBot/ItemFilter.cs index f229a01..f0a497b 100644 --- a/MinecraftDiscordBot/ItemFilter.cs +++ b/MinecraftDiscordBot/ItemFilter.cs @@ -1,4 +1,6 @@ -namespace MinecraftDiscordBot; +using MinecraftDiscordBot.Models; + +namespace MinecraftDiscordBot; public abstract class ItemFilter { public abstract bool Match(Fluid item); diff --git a/MinecraftDiscordBot/Models/Fluid.cs b/MinecraftDiscordBot/Models/Fluid.cs new file mode 100644 index 0000000..07c4556 --- /dev/null +++ b/MinecraftDiscordBot/Models/Fluid.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; +using System.Diagnostics; + +namespace MinecraftDiscordBot.Models; + +[JsonObject(MemberSerialization.OptIn, Description = "Describes a fluid in a Refined Storage system.", MissingMemberHandling = MissingMemberHandling.Ignore)] +[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] +public class Fluid { + [JsonProperty("amount", Required = Required.Always)] + public int Amount { get; set; } + [JsonProperty("displayName", Required = Required.Always)] + public string DisplayName { get; set; } = default!; + [JsonProperty("tags", Required = Required.DisallowNull)] + public string[]? Tags { get; set; } = default; + [JsonProperty("name", Required = Required.Always)] + public ModItemId ItemId { get; set; } = default!; + public override string ToString() => Amount > 10000 + ? $"{Amount / 1000.0f:n2} B of {DisplayName}" + : $"{Amount:n0} mB of {DisplayName}"; + [JsonIgnore] + public string CleanDisplayName => DisplayName[1..^1]; +} diff --git a/MinecraftDiscordBot/Models/Item.cs b/MinecraftDiscordBot/Models/Item.cs new file mode 100644 index 0000000..70e5298 --- /dev/null +++ b/MinecraftDiscordBot/Models/Item.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; +using System.Diagnostics; + +namespace MinecraftDiscordBot.Models; + +[JsonObject(MemberSerialization.OptIn, Description = "Describes an item in a Refined Storage system.", MissingMemberHandling = MissingMemberHandling.Ignore)] +[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] +public class Item : Fluid { + [JsonProperty("fingerprint", Required = Required.Always)] + public Md5Hash Fingerprint { get; set; } = default!; + [JsonProperty("nbt", Required = Required.DisallowNull)] + public dynamic? NBT { get; set; } + public override string ToString() => $"{Amount:n0}x {DisplayName}"; +} diff --git a/MinecraftDiscordBot/Models/Md5Hash.cs b/MinecraftDiscordBot/Models/Md5Hash.cs new file mode 100644 index 0000000..d858b8b --- /dev/null +++ b/MinecraftDiscordBot/Models/Md5Hash.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; +using System.Diagnostics; + +namespace MinecraftDiscordBot.Models; + +[JsonConverter(typeof(Md5JsonConverter))] +[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] +public class Md5Hash : IEquatable { + private readonly byte[] _hash; + public Md5Hash(string hash) : this(Convert.FromHexString(hash)) { } + public Md5Hash(byte[] hash) { + if (hash is not { Length: 16 }) throw new ArgumentException("Invalid digest size!", nameof(hash)); + _hash = hash; + } + public override bool Equals(object? obj) => Equals(obj as Md5Hash); + public bool Equals(Md5Hash? other) => other != null && _hash.SequenceEqual(other._hash); + public override int GetHashCode() { + var hashCode = new HashCode(); + hashCode.AddBytes(_hash); + return hashCode.ToHashCode(); + } + public override string ToString() => Convert.ToHexString(_hash); + + public class Md5JsonConverter : JsonConverter { + public override Md5Hash? ReadJson(JsonReader reader, Type objectType, Md5Hash? existingValue, bool hasExistingValue, JsonSerializer serializer) + => reader.Value is string { Length: 32 } value + ? new(value) + : throw new JsonException($"Could not parse MD5 hash with token '{reader.Value}'"); + public override void WriteJson(JsonWriter writer, Md5Hash? value, JsonSerializer serializer) { + if (value is null) writer.WriteNull(); + else writer.WriteValue(value.ToString()); + } + } +} \ No newline at end of file diff --git a/MinecraftDiscordBot/Message.cs b/MinecraftDiscordBot/Models/Message.cs similarity index 98% rename from MinecraftDiscordBot/Message.cs rename to MinecraftDiscordBot/Models/Message.cs index 2db0dd7..ef41d50 100644 --- a/MinecraftDiscordBot/Message.cs +++ b/MinecraftDiscordBot/Models/Message.cs @@ -1,7 +1,7 @@ using Discord.WebSocket; using Newtonsoft.Json; -namespace MinecraftDiscordBot; +namespace MinecraftDiscordBot.Models; public abstract class Message { [JsonProperty("type")] diff --git a/MinecraftDiscordBot/Models/ModItemId.cs b/MinecraftDiscordBot/Models/ModItemId.cs new file mode 100644 index 0000000..f1c2f92 --- /dev/null +++ b/MinecraftDiscordBot/Models/ModItemId.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; +using System.Diagnostics; + +namespace MinecraftDiscordBot.Models; + +[JsonConverter(typeof(ModItemIdJsonConverter))] +[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] +public class ModItemId { + public ModItemId(string name) { + var colon = name.IndexOf(':'); + if (colon < 0) throw new ArgumentException("Invalid mod item id!", nameof(name)); + ModName = name[..colon]; + ModItem = name[(colon + 1)..]; + if (ToString() != name) throw new InvalidProgramException("Bad Parsing!"); + } + public override string ToString() => $"{ModName}:{ModItem}"; + public string ModName { get; } + public string ModItem { get; } + + public class ModItemIdJsonConverter : JsonConverter { + public override ModItemId? ReadJson(JsonReader reader, Type objectType, ModItemId? existingValue, bool hasExistingValue, JsonSerializer serializer) + => reader.Value is string value + ? new(value) + : throw new JsonException($"Could not parse mod name with token '{reader.Value}'"); + public override void WriteJson(JsonWriter writer, ModItemId? value, JsonSerializer serializer) { + if (value is null) writer.WriteNull(); + else writer.WriteValue(value.ToString()); + } + } +} diff --git a/MinecraftDiscordBot/Program.cs b/MinecraftDiscordBot/Program.cs index 0f51f01..f779ffb 100644 --- a/MinecraftDiscordBot/Program.cs +++ b/MinecraftDiscordBot/Program.cs @@ -4,6 +4,8 @@ using Discord.Commands; using Discord.Rest; using Discord.WebSocket; using Fleck; +using MinecraftDiscordBot.Commands; +using MinecraftDiscordBot.Services; using System.Collections.Concurrent; using System.Reflection; using System.Runtime.CompilerServices; @@ -22,26 +24,29 @@ public class Program : IDisposable, ICommandHandler { private readonly WebSocketServer _wssv; private readonly BotConfiguration _config; private readonly HashSet _whitelistedChannels; - private readonly ConcurrentDictionary _connections = new(); + private readonly ConcurrentDictionary _connections = new(); private static readonly char[] WhiteSpace = new char[] { '\t', '\n', ' ', '\r' }; public ITextChannel[] _channels = Array.Empty(); - private ConnectedComputer? _rsSystem = null; + private RootCommandService? _rsSystem = null; private bool disposedValue; public static bool OnlineNotifications => false; - public static readonly string ClientScript = GetClientScript(); + private const string ClientScriptName = "MinecraftDiscordBot.ClientScript.lua"; + public readonly string ClientScript; private readonly ITokenProvider _tokenProvider = new TimeoutTokenProvider(InstanceId, 10); private static readonly int InstanceId = new Random().Next(); - private string GetVerifiedClientScript() => ClientScript.Replace("$TOKEN", _tokenProvider.GenerateToken()); + private string GetVerifiedClientScript() => ClientScript + .Replace("$TOKEN", _tokenProvider.GenerateToken()); - private static string GetClientScript() { - using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MinecraftDiscordBot.ClientScript.lua"); + private string GetClientScript(BotConfiguration config) { + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(ClientScriptName); if (stream is null) throw new FileNotFoundException("Client script could not be loaded!"); using var sr = new StreamReader(stream); - return sr.ReadToEnd(); + return sr.ReadToEnd() + .Replace("$HOST", $"ws://{config.SocketHost}:{config.Port}"); } - public ConnectedComputer? Computer { + public RootCommandService? Computer { get => _rsSystem; set { if (_rsSystem != value) { _rsSystem = value; @@ -56,6 +61,7 @@ public class Program : IDisposable, ICommandHandler { private async Task Broadcast(Func> message) => _ = await Task.WhenAll(_channels.Select(message)); public Program(BotConfiguration config) { _config = config; + ClientScript = GetClientScript(config); _client.Log += LogAsync; _client.MessageReceived += (msg) => DiscordMessageReceived(msg); _client.ReactionAdded += DiscordReactionAdded; @@ -159,7 +165,7 @@ public class Program : IDisposable, ICommandHandler { await LogInfoAsync(WebSocketSource, $"[{socket.ConnectionInfo.Id}] Script sent to client!"); } - private void AddComputerSocket(IWebSocketConnection socket, ConnectedComputer pc) => Computer = pc; + private void AddComputerSocket(IWebSocketConnection socket, RootCommandService pc) => Computer = pc; private void RemoveComputerSocket(IWebSocketConnection socket) { if (Computer is { ConnectionInfo.Id: Guid id } && id == socket.ConnectionInfo.Id) Computer = null; diff --git a/MinecraftDiscordBot/RefinedStorageService.cs b/MinecraftDiscordBot/Services/RefinedStorageService.cs similarity index 93% rename from MinecraftDiscordBot/RefinedStorageService.cs rename to MinecraftDiscordBot/Services/RefinedStorageService.cs index f2a895f..1d7dd52 100644 --- a/MinecraftDiscordBot/RefinedStorageService.cs +++ b/MinecraftDiscordBot/Services/RefinedStorageService.cs @@ -1,7 +1,9 @@ using Discord.WebSocket; +using MinecraftDiscordBot.Commands; +using MinecraftDiscordBot.Models; using System.Text; -namespace MinecraftDiscordBot; +namespace MinecraftDiscordBot.Services; public class RefinedStorageService : CommandRouter { private readonly ITaskWaitSource _taskSource; @@ -28,12 +30,12 @@ public class RefinedStorageService : CommandRouter { public async Task GetEnergyUsageAsync(CancellationToken ct) => await Method(CmdEnergyUsage, int.Parse, ct); public async Task GetEnergyStorageAsync(CancellationToken ct) => await Method(CmdEnergyStorage, int.Parse, ct); - public async Task> ListItemsAsync(CancellationToken ct) => await Method(CmdListItems, ConnectedComputer.Deserialize>(), ct); - public async Task> ListFluidsAsync(CancellationToken ct) => await Method(CmdListFluids, ConnectedComputer.Deserialize>(), ct); - public async Task GetItemData(string itemid, CancellationToken ct) => await Method(CmdGetItem, ConnectedComputer.Deserialize(), ct, new() { + public async Task> ListItemsAsync(CancellationToken ct) => await Method(CmdListItems, RootCommandService.Deserialize>(), ct); + public async Task> ListFluidsAsync(CancellationToken ct) => await Method(CmdListFluids, RootCommandService.Deserialize>(), ct); + public async Task GetItemData(string itemid, CancellationToken ct) => await Method(CmdGetItem, RootCommandService.Deserialize(), ct, new() { ["name"] = itemid }); - public async Task CraftItem(string itemid, int amount, CancellationToken ct) => await Method(CmdCraftItem, ConnectedComputer.Deserialize(), ct, new() { + public async Task CraftItem(string itemid, int amount, CancellationToken ct) => await Method(CmdCraftItem, RootCommandService.Deserialize(), ct, new() { ["name"] = itemid, ["count"] = amount }); @@ -104,7 +106,7 @@ public class RefinedStorageService : CommandRouter { sb.Append($"We currently have {item.Amount:n0} {item.CleanDisplayName}!"); if (item.Tags is not null and var tags) { sb.AppendLine("\nThis item has the following tags:"); - sb.AppendJoin('\n',tags.Select(tag => $"- {tag}")); + sb.AppendJoin('\n', tags.Select(tag => $"- {tag}")); } sb.Append($"\nRefer to this item with fingerprint {item.Fingerprint}"); return ResponseType.AsString(sb.ToString()); @@ -138,7 +140,7 @@ public class RefinedStorageService : CommandRouter { sb.Append("The Refined Storage system currently stores these items:"); var items = await RefreshItemList(ct); lock (_itemLock) { - int taken = 0; + var taken = 0; foreach (var item in items) { if (sb.Length > 500) break; sb.AppendFormat("\n{0:n0}x {1}", item.Amount, item.DisplayName); diff --git a/MinecraftDiscordBot/Services/ReplyException.cs b/MinecraftDiscordBot/Services/ReplyException.cs new file mode 100644 index 0000000..bc12443 --- /dev/null +++ b/MinecraftDiscordBot/Services/ReplyException.cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; + +namespace MinecraftDiscordBot.Services; + +[Serializable] +public class ReplyException : Exception { + public ReplyException() { } + public ReplyException(string message) : base(message) { } + public ReplyException(string message, Exception inner) : base(message, inner) { } + protected ReplyException(SerializationInfo info, StreamingContext context) : base(info, context) { } +} diff --git a/MinecraftDiscordBot/Services/RootCommandService.cs b/MinecraftDiscordBot/Services/RootCommandService.cs new file mode 100644 index 0000000..bb05307 --- /dev/null +++ b/MinecraftDiscordBot/Services/RootCommandService.cs @@ -0,0 +1,77 @@ +using Discord.WebSocket; +using Fleck; +using MinecraftDiscordBot.Commands; +using MinecraftDiscordBot.Models; +using Newtonsoft.Json; + +namespace MinecraftDiscordBot.Services; + +public delegate Task HandleCommandDelegate(SocketUserMessage message, string[] parameters, CancellationToken ct); +public delegate Task HandleCommandDelegate(SocketUserMessage message, string[] parameters, CancellationToken ct); + +public class RootCommandService : CommandRouter, ITaskWaitSource { + protected readonly IWebSocketConnection _socket; + public override string HelpTextPrefix => "!"; + public RootCommandService(IWebSocketConnection socket) : base() { + socket.OnMessage = OnMessage; + _socket = socket; + _rs = new RefinedStorageService(this); + } + + private void OnMessage(string message) { + if (JsonConvert.DeserializeObject(message) is not ReplyMessage msg) return; + IChunkWaiter? waiter; + lock (_syncRoot) if (!_waits.TryGetValue(msg.AnswerId, out waiter)) { + Program.LogWarningAsync("Socket", $"Invalid wait id '{msg.AnswerId}'!"); + return; + } + if (!msg.Success) waiter.SetUnsuccessful(); + waiter.AddChunk(msg.Chunk, msg.Total, msg.Result); + if (waiter.Finished || waiter.IsCancellationRequested) + lock (_syncRoot) + _waits.Remove(waiter.ID); + } + + public Task Send(string message) => _socket.Send(message); + public Task Send(Message message) => Send(JsonConvert.SerializeObject(message)); + private readonly object _syncRoot = new(); + private readonly Dictionary _waits = new(); + private readonly Random _rnd = new(); + public IWebSocketConnectionInfo ConnectionInfo => _socket.ConnectionInfo; + + private int GetFreeId() { + var attempts = 0; + while (true) { + var id = _rnd.Next(); + if (!_waits.ContainsKey(id)) + return id; + Program.LogWarningAsync(Program.WebSocketSource, $"Could not get a free ID after {++attempts} attempts!"); + } + } + + public ChunkWaiter GetWaiter(Func resultParser, CancellationToken ct) { + ChunkWaiter waiter; + lock (_syncRoot) { + waiter = new ChunkWaiter(GetFreeId(), resultParser, ct); + _waits.Add(waiter.ID, waiter); + } + return waiter; + } + + private readonly ICommandHandler _rs; + [CommandHandler("rs", HelpText = "Provides some commands for interacting with the Refined Storage system.")] + public Task RefinedStorageHandler(SocketUserMessage message, string[] parameters, CancellationToken ct) + => _rs.HandleCommand(message, parameters, ct); + + public static Func Deserialize() => msg + => JsonConvert.DeserializeObject(msg) ?? throw new InvalidProgramException("Empty response!"); + public override Task RootAnswer(SocketUserMessage message, CancellationToken ct) + => Task.FromResult(ResponseType.AsString("The Minecraft server is connected!")); + public override Task FallbackHandler(SocketUserMessage message, string method, string[] parameters, CancellationToken ct) + => throw new ReplyException($"What the fuck do you mean by '{method}'?"); +} + +public interface ITaskWaitSource { + ChunkWaiter GetWaiter(Func resultParser, CancellationToken ct); + Task Send(Message requestMessage); +}