2022-01-11 20:32:25 +01:00
using Discord ;
using Discord.WebSocket ;
using Fleck ;
using Newtonsoft.Json ;
using System.Diagnostics ;
2022-01-15 21:26:32 +01:00
using System.Runtime.Serialization ;
2022-01-11 20:32:25 +01:00
using System.Text ;
namespace MinecraftDiscordBot ;
2022-01-15 21:26:32 +01:00
public delegate Task < TResponse > HandleCommandDelegate < TResponse > ( 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 ; }
2022-01-16 15:58:35 +01:00
public string? HelpText { get ; init ; }
2022-01-15 21:26:32 +01:00
}
2022-01-16 15:58:35 +01:00
public class RefinedStorageService : CommandRouter {
private readonly ITaskWaitSource _taskSource ;
public override string HelpTextPrefix = > "!rs " ;
public RefinedStorageService ( ITaskWaitSource taskSource ) : base ( ) = > _taskSource = taskSource ;
public override Task < ResponseType > FallbackHandler ( SocketUserMessage message , string method , string [ ] parameters , CancellationToken ct )
= > Task . FromResult ( ResponseType . AsString ( $"The RS system has no command '{method}'!" ) ) ;
public override Task < ResponseType > RootAnswer ( SocketUserMessage message , CancellationToken ct )
= > Task . FromResult ( ResponseType . AsString ( "The RS system is online!" ) ) ;
2022-01-11 20:32:25 +01:00
2022-01-16 21:31:07 +01:00
private async Task < T > Method < T > ( string methodName , Func < string , T > parser , CancellationToken ct , Dictionary < string , object > ? parameters = null ) {
2022-01-16 15:58:35 +01:00
var waiter = _taskSource . GetWaiter ( parser , ct ) ;
2022-01-16 21:31:07 +01:00
await _taskSource . Send ( new RequestMessage ( waiter . ID , methodName , parameters ) ) ;
2022-01-16 15:58:35 +01:00
return await waiter . Task ;
2022-01-11 20:32:25 +01:00
}
private const string CmdEnergyUsage = "energyusage" ;
private const string CmdEnergyStorage = "energystorage" ;
private const string CmdListItems = "listitems" ;
private const string CmdItemName = "itemname" ;
private const string CmdListFluids = "listfluids" ;
2022-01-16 21:31:07 +01:00
private const string CmdCraftItem = "craft" ;
2022-01-11 20:32:25 +01:00
2022-01-16 15:58:35 +01:00
public async Task < int > GetEnergyUsageAsync ( CancellationToken ct ) = > await Method ( CmdEnergyUsage , int . Parse , ct ) ;
public async Task < int > GetEnergyStorageAsync ( CancellationToken ct ) = > await Method ( CmdEnergyStorage , int . Parse , ct ) ;
public async Task < IEnumerable < Item > > ListItemsAsync ( CancellationToken ct ) = > await Method ( CmdListItems , ConnectedComputer . Deserialize < IEnumerable < Item > > ( ) , ct ) ;
public async Task < IEnumerable < Fluid > > ListFluidsAsync ( CancellationToken ct ) = > await Method ( CmdListFluids , ConnectedComputer . Deserialize < IEnumerable < Fluid > > ( ) , ct ) ;
2022-01-16 21:31:07 +01:00
public async Task < bool > CraftItem ( string itemid , int amount , CancellationToken ct ) = > await Method ( CmdCraftItem , ConnectedComputer . Deserialize < bool > ( ) , ct , new ( ) {
["name"] = itemid ,
["count"] = amount
} ) ;
2022-01-16 15:58:35 +01:00
private Task < IEnumerable < Item > > FilterItems ( SocketUserMessage message , IEnumerable < string > filters , CancellationToken ct )
= > FilterItems ( message , filters . Select ( ItemFilter . Parse ) , ct ) ;
private async Task < IEnumerable < Item > > FilterItems ( SocketUserMessage message , IEnumerable < ItemFilter > filters , CancellationToken ct ) {
var items = Items ? . ToList ( ) . AsEnumerable ( ) ;
if ( items is null ) items = ( await RefreshItemList ( ct ) ) . ToList ( ) ;
foreach ( var filter in filters )
items = items . Where ( filter . MatchItem ) ;
return items . ToList ( ) ;
2022-01-11 20:32:25 +01:00
}
2022-01-16 15:58:35 +01:00
private async Task < List < Item > > RefreshItemList ( CancellationToken ct ) {
var response = await ListItemsAsync ( ct ) ;
lock ( _itemLock ) {
Items = response . OrderByDescending ( i = > i . Amount ) . ToList ( ) ;
return Items ;
}
2022-01-11 20:32:25 +01:00
}
2022-01-16 15:58:35 +01:00
private List < Item > ? Items ;
private readonly object _itemLock = new ( ) ;
[CommandHandler(CmdEnergyStorage, HelpText = "Get the amount of energy stored in the RS system.")]
2022-01-15 21:26:32 +01:00
public async Task < ResponseType > HandleEnergyStorage ( SocketUserMessage message , string [ ] parameters , CancellationToken ct )
= > ResponseType . AsString ( $"Refined Storage system stores {await GetEnergyStorageAsync(ct)} RF/t" ) ;
2022-01-16 15:58:35 +01:00
[CommandHandler(CmdEnergyUsage, HelpText = "Get the amount of energy used by the RS system.")]
2022-01-15 21:26:32 +01:00
public async Task < ResponseType > HandleEnergyUsage ( SocketUserMessage message , string [ ] parameters , CancellationToken ct )
= > ResponseType . AsString ( $"Refined Storage system currently uses {await GetEnergyUsageAsync(ct)} RF/t" ) ;
2022-01-16 21:31:07 +01:00
[CommandHandler(CmdCraftItem, HelpText = "Craft a specific item given an item ID and optionally an amount.")]
public async Task < ResponseType > HandleCraftItem ( SocketUserMessage message , string [ ] parameters , CancellationToken ct ) {
var amount = 1 ;
string itemid ;
if ( parameters . Length is 1 or 2 ) {
itemid = parameters [ 0 ] ;
if ( parameters . Length is 2 )
if ( int . TryParse ( parameters [ 1 ] , out var value ) ) amount = value ;
else return ResponseType . AsString ( $"I expected an amount to craft, not '{parameters[1]}'!" ) ;
} else return parameters . Length is < 1
? ResponseType . AsString ( "You have to give me at least an item name!" )
: parameters . Length is > 2
? ResponseType . AsString ( "Yo, those are way too many arguments! I want only item name and maybe an amount!" )
: throw new InvalidOperationException ( $"Forgot to match parameter length {parameters.Length}!" ) ;
return await CraftItem ( itemid , amount , ct )
? ResponseType . AsString ( $"Alright, I'm starting to craft {amount} {itemid}." )
: ResponseType . AsString ( $"Nope, that somehow doesn't work!" ) ;
}
2022-01-16 15:58:35 +01:00
[CommandHandler(CmdItemName, HelpText = "Filter items by name.")]
2022-01-15 21:26:32 +01:00
public async Task < ResponseType > HandleItemName ( SocketUserMessage message , string [ ] parameters , CancellationToken ct ) {
if ( parameters . Length < 2 ) return ResponseType . AsString ( $"Usage: {CmdItemName} filters..." ) ;
2022-01-11 20:32:25 +01:00
else {
var items = await FilterItems ( message , parameters [ 1. . ] , ct ) ;
var sb = new StringBuilder ( ) ;
sb . AppendLine ( "Did you mean:" ) ;
sb . AppendJoin ( "\n" , items . Select ( i = > i . ToString ( ) ) ) ;
2022-01-15 21:26:32 +01:00
return ResponseType . AsString ( sb . ToString ( ) ) ;
2022-01-11 20:32:25 +01:00
}
}
2022-01-16 15:58:35 +01:00
[CommandHandler(CmdListFluids, HelpText = "Gets a list of fluids that are currently stored in the RS system.")]
2022-01-15 21:26:32 +01:00
public async Task < ResponseType > HandleFluidListing ( SocketUserMessage message , string [ ] parameters , CancellationToken ct ) {
2022-01-11 20:32:25 +01:00
var sb = new StringBuilder ( ) ;
sb . Append ( "The Refined Storage system stores those fluids:" ) ;
var fluids = await ListFluidsAsync ( ct ) ;
foreach ( var fluid in fluids . OrderByDescending ( i = > i . Amount ) )
if ( fluid . Amount > 10000 ) sb . AppendFormat ( "\n{0:n2} B of {1}" , fluid . Amount / 1000.0f , fluid . DisplayName ) ;
else sb . AppendFormat ( "\n{0:n0} mB of {1}" , fluid . Amount , fluid . DisplayName ) ;
2022-01-15 21:26:32 +01:00
return ResponseType . AsString ( sb . ToString ( ) ) ;
2022-01-11 20:32:25 +01:00
}
2022-01-16 15:58:35 +01:00
[CommandHandler(CmdListItems, HelpText = "Gets a list of items that are currently stored in the RS system.")]
2022-01-15 21:26:32 +01:00
public async Task < ResponseType > HandleItemListing ( SocketUserMessage message , string [ ] parameters , CancellationToken ct ) {
2022-01-11 20:32:25 +01:00
var sb = new StringBuilder ( ) ;
sb . Append ( "The Refined Storage system currently stores these items:" ) ;
var items = await RefreshItemList ( ct ) ;
lock ( _itemLock ) {
int taken = 0 ;
foreach ( var item in items ) {
if ( sb . Length > 500 ) break ;
sb . AppendFormat ( "\n{0:n0}x {1}" , item . Amount , item . DisplayName ) ;
taken + + ;
}
if ( items . Count > taken ) sb . AppendFormat ( "\nand {0} more items." , items . Skip ( taken ) . Sum ( i = > i . Amount ) ) ;
}
2022-01-15 21:26:32 +01:00
return ResponseType . AsString ( sb . ToString ( ) ) ;
2022-01-11 20:32:25 +01:00
}
2022-01-16 15:58:35 +01:00
}
2022-01-11 20:32:25 +01:00
2022-01-16 15:58:35 +01:00
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 < ReplyMessage > ( 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 ;
}
2022-01-11 20:32:25 +01:00
}
2022-01-16 15:58:35 +01:00
if ( ! msg . Success ) waiter . SetUnsuccessful ( ) ;
waiter . AddChunk ( msg . Chunk , msg . Total , msg . Result ) ;
if ( waiter . Finished | | waiter . IsCancellationRequested )
lock ( _syncRoot )
_waits . Remove ( waiter . ID ) ;
2022-01-11 20:32:25 +01:00
}
2022-01-15 21:26:32 +01:00
2022-01-16 15:58:35 +01:00
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 < int , IChunkWaiter > _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 < T > GetWaiter < T > ( Func < string , T > resultParser , CancellationToken ct ) {
ChunkWaiter < T > waiter ;
lock ( _syncRoot ) {
waiter = new ChunkWaiter < T > ( GetFreeId ( ) , resultParser , ct ) ;
_waits . Add ( waiter . ID , waiter ) ;
}
return waiter ;
}
private readonly ICommandHandler < ResponseType > _rs ;
2022-01-16 21:31:07 +01:00
[CommandHandler("rs", HelpText = "Provides some commands for interacting with the Refined Storage system.")]
2022-01-16 15:58:35 +01:00
public Task < ResponseType > RefinedStorageHandler ( SocketUserMessage message , string [ ] parameters , CancellationToken ct )
= > _rs . HandleCommand ( message , parameters , ct ) ;
public static Func < string , T > Deserialize < T > ( ) = > msg
= > JsonConvert . DeserializeObject < T > ( msg ) ? ? throw new InvalidProgramException ( "Empty response!" ) ;
2022-01-15 21:26:32 +01:00
public override Task < ResponseType > RootAnswer ( SocketUserMessage message , CancellationToken ct )
= > Task . FromResult ( ResponseType . AsString ( "The Minecraft server is connected!" ) ) ;
public override Task < ResponseType > FallbackHandler ( SocketUserMessage message , string method , string [ ] parameters , CancellationToken ct )
= > Task . FromResult ( ResponseType . AsString ( $"What the fuck do you mean by '{method}'?" ) ) ;
}
2022-01-16 15:58:35 +01:00
public interface ITaskWaitSource {
ChunkWaiter < T > GetWaiter < T > ( Func < string , T > resultParser , CancellationToken ct ) ;
Task Send ( Message requestMessage ) ;
}
2022-01-15 21:26:32 +01:00
[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 ) { }
2022-01-11 20:32:25 +01:00
}
[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}" ;
}
[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 < ModItemId > {
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 < Md5Hash ? > {
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 < Md5Hash > {
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 ( ) ) ;
}
}
}