diff --git a/MinecraftClient/ChatBots/AutoFishing.cs b/MinecraftClient/ChatBots/AutoFishing.cs index 3849f161..8e53992a 100644 --- a/MinecraftClient/ChatBots/AutoFishing.cs +++ b/MinecraftClient/ChatBots/AutoFishing.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; using MinecraftClient.Inventory; using MinecraftClient.Mapping; using Tomlet.Attributes; @@ -143,7 +145,8 @@ namespace MinecraftClient.ChatBots private Entity? fishingBobber; private Location LastPos = Location.Zero; private DateTime CaughtTime = DateTime.Now; - private int fishItemCounter = 10; + private int fishItemCounter = 15; + private Dictionary fishItemCnt = new(); private Entity fishItem = new(-1, EntityType.Item, Location.Zero); private int counter = 0; @@ -182,7 +185,7 @@ namespace MinecraftClient.ChatBots public string CommandHandler(string cmd, string[] args) { - if (args.Length > 0) + if (args.Length >= 1) { switch (args[0]) { @@ -206,13 +209,47 @@ namespace MinecraftClient.ChatBots } StopFishing(); return Translations.Get("bot.autoFish.stop"); + case "status": + if (args.Length >= 2) + { + if (args[1] == "clear") + { + fishItemCnt = new(); + return Translations.Get("bot.autoFish.status_clear"); + } + else + { + return GetCommandHelp("status"); + } + } + else + { + if (fishItemCnt.Count == 0) + return Translations.Get("bot.autoFish.status_info"); + + List> orderedList = fishItemCnt.OrderBy(x => x.Value).ToList(); + int maxLen = orderedList[^1].Value.ToString().Length; + StringBuilder sb = new(); + sb.Append(Translations.Get("bot.autoFish.status_info")); + foreach ((ItemType type, uint cnt) in orderedList) + { + sb.Append(Environment.NewLine); + + string cntStr = cnt.ToString(); + sb.Append(' ', maxLen - cntStr.Length).Append(cntStr); + sb.Append(" x "); + sb.Append(Item.GetTypeString(type)); + } + return sb.ToString(); + } case "help": return GetCommandHelp(args.Length >= 2 ? args[1] : ""); default: return GetHelp(); } } - else return GetHelp(); + else + return GetHelp(); } private void StartFishing() @@ -246,7 +283,7 @@ namespace MinecraftClient.ChatBots isFishing = false; state = FishingState.Stopping; } - fishItemCounter = 10; + fishItemCounter = 15; } private void UseFishRod() @@ -259,8 +296,9 @@ namespace MinecraftClient.ChatBots public override void Update() { - if (fishItemCounter < 10) + if (fishItemCounter < 15) ++fishItemCounter; + lock (stateLock) { switch (state) @@ -283,7 +321,7 @@ namespace MinecraftClient.ChatBots { if (castTimeout < 6000) castTimeout *= 2; // Exponential backoff - LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.cast_timeout", castTimeout / 10.0)); + LogToConsole(GetShortTimestamp() + ": " + Translations.Get("bot.autoFish.cast_timeout", castTimeout / 10.0)); counter = Settings.DoubleToTick(Config.Cast_Delay); state = FishingState.WaitingToCast; @@ -292,7 +330,7 @@ namespace MinecraftClient.ChatBots case FishingState.WaitingFishToBite: if (++counter > Settings.DoubleToTick(Config.Fishing_Timeout)) { - LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.fishing_timeout")); + LogToConsole(GetShortTimestamp() + ": " + Translations.Get("bot.autoFish.fishing_timeout")); counter = Settings.DoubleToTick(Config.Cast_Delay); state = FishingState.WaitingToCast; @@ -347,8 +385,8 @@ namespace MinecraftClient.ChatBots public override void OnEntitySpawn(Entity entity) { - if (fishItemCounter < 10 && entity.Type == EntityType.Item && Math.Abs(entity.Location.Y - LastPos.Y) < 2.0 && - Math.Abs(entity.Location.X - LastPos.X) < 0.1 && Math.Abs(entity.Location.Z - LastPos.Z) < 0.1) + if (fishItemCounter < 15 && entity.Type == EntityType.Item && Math.Abs(entity.Location.Y - LastPos.Y) < 2.2 && + Math.Abs(entity.Location.X - LastPos.X) < 0.12 && Math.Abs(entity.Location.Z - LastPos.Z) < 0.12) { if (Config.Log_Fish_Bobber) LogToConsole(string.Format("Item ({0}) spawn at {1}, distance = {2:0.00}", entity.ID, entity.Location, entity.Location.Distance(LastPos))); @@ -359,9 +397,9 @@ namespace MinecraftClient.ChatBots if (Config.Log_Fish_Bobber) LogToConsole(string.Format("FishingBobber spawn at {0}, distance = {1:0.00}", entity.Location, GetCurrentLocation().Distance(entity.Location))); - fishItemCounter = 10; + fishItemCounter = 15; - LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.throw")); + LogToConsole(GetShortTimestamp() + ": " + Translations.Get("bot.autoFish.throw")); lock (stateLock) { fishingBobber = entity; @@ -377,7 +415,7 @@ namespace MinecraftClient.ChatBots public override void OnEntityDespawn(Entity entity) { - if (entity != null && entity.Type == EntityType.FishingBobber && entity.ID == fishingBobber!.ID) + if (entity != null && fishingBobber != null && entity.Type == EntityType.FishingBobber && entity.ID == fishingBobber!.ID) { if (Config.Log_Fish_Bobber) LogToConsole(string.Format("FishingBobber despawn at {0}", entity.Location)); @@ -431,10 +469,15 @@ namespace MinecraftClient.ChatBots public override void OnEntityMetadata(Entity entity, Dictionary metadata) { - if (fishItemCounter < 10 && entity.ID == fishItem.ID && metadata.TryGetValue(8, out object? item)) + if (fishItemCounter < 15 && entity.ID == fishItem.ID && metadata.TryGetValue(8, out object? itemObj)) { - LogToConsole(Translations.Get("bot.autoFish.got", ((Item)item!).ToFullString())); - fishItemCounter = 10; + fishItemCounter = 15; + Item item = (Item)itemObj!; + LogToConsole(Translations.Get("bot.autoFish.got", item.ToFullString())); + if (fishItemCnt.ContainsKey(item.Type)) + fishItemCnt[item.Type] += (uint)item.Count; + else + fishItemCnt.Add(item.Type, (uint)item.Count); } } @@ -471,10 +514,10 @@ namespace MinecraftClient.ChatBots { ++fishCount; if (Config.Enable_Move && Config.Movements.Length > 0) - LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.caught_at", + LogToConsole(GetShortTimestamp() + ": " + Translations.Get("bot.autoFish.caught_at", fishingBobber!.Location.X, fishingBobber!.Location.Y, fishingBobber!.Location.Z, fishCount)); else - LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.caught", fishCount)); + LogToConsole(GetShortTimestamp() + ": " + Translations.Get("bot.autoFish.caught", fishCount)); lock (stateLock) { @@ -580,7 +623,7 @@ namespace MinecraftClient.ChatBots private static string GetHelp() { - return Translations.Get("bot.autoFish.available_cmd", "start, stop, help"); + return Translations.Get("bot.autoFish.available_cmd", "start, stop, status, help"); } private string GetCommandHelp(string cmd) @@ -590,8 +633,9 @@ namespace MinecraftClient.ChatBots #pragma warning disable format // @formatter:off "start" => Translations.Get("bot.autoFish.help.start"), "stop" => Translations.Get("bot.autoFish.help.stop"), + "status" => Translations.Get("bot.autoFish.help.status"), "help" => Translations.Get("bot.autoFish.help.help"), - _ => GetHelp(), + _ => GetHelp(), #pragma warning restore format // @formatter:on }; } diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs new file mode 100644 index 00000000..c7d327b9 --- /dev/null +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -0,0 +1,414 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using DSharpPlus; +using DSharpPlus.Entities; +using DSharpPlus.Exceptions; +using Microsoft.Extensions.Logging; +using Tomlet.Attributes; + +namespace MinecraftClient.ChatBots +{ + public class DiscordBridge : ChatBot + { + private enum BridgeDirection + { + Both = 0, + Minecraft, + Discord + } + + private static DiscordBridge? instance = null; + public bool IsConnected { get; private set; } + + private DiscordClient? discordBotClient; + private DiscordChannel? discordChannel; + private BridgeDirection bridgeDirection = BridgeDirection.Both; + + public static Configs Config = new(); + + [TomlDoNotInlineObject] + public class Configs + { + [NonSerialized] + private const string BotName = "DiscordBridge"; + + public bool Enabled = false; + + [TomlInlineComment("$config.ChatBot.DiscordBridge.Token$")] + public string Token = "your bot token here"; + + [TomlInlineComment("$config.ChatBot.DiscordBridge.GuildId$")] + public ulong GuildId = 1018553894831403028L; + + [TomlInlineComment("$config.ChatBot.DiscordBridge.ChannelId$")] + public ulong ChannelId = 1018565295654326364L; + + [TomlInlineComment("$config.ChatBot.DiscordBridge.OwnersIds$")] + public ulong[] OwnersIds = new[] { 978757810781323276UL }; + + [TomlInlineComment("$config.ChatBot.DiscordBridge.MessageSendTimeout$")] + public int Message_Send_Timeout = 3; + + [TomlPrecedingComment("$config.ChatBot.DiscordBridge.Formats$")] + public string PrivateMessageFormat = "**[Private Message]** {username}: {message}"; + public string PublicMessageFormat = "{username}: {message}"; + public string TeleportRequestMessageFormat = "A new Teleport Request from **{username}**!"; + + public void OnSettingUpdate() + { + Message_Send_Timeout = Message_Send_Timeout <= 0 ? 3 : Message_Send_Timeout; + } + } + + public DiscordBridge() + { + instance = this; + } + + public override void Initialize() + { + RegisterChatBotCommand("dscbridge", "bot.DiscordBridge.desc", "dscbridge direction ", OnDscCommand); + + Task.Run(async () => await MainAsync()); + } + + ~DiscordBridge() + { + Disconnect(); + } + + public override void OnUnload() + { + Disconnect(); + } + + private void Disconnect() + { + if (discordBotClient != null) + { + try + { + if (discordChannel != null) + discordBotClient.SendMessageAsync(discordChannel, new DiscordEmbedBuilder + { + Description = Translations.TryGet("bot.DiscordBridge.disconnected"), + Color = new DiscordColor(0xFF0000) + }).Wait(Config.Message_Send_Timeout * 1000); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.DiscordBridge.canceled_sending")); + LogDebugToConsole(e); + } + + discordBotClient.DisconnectAsync().Wait(); + IsConnected = false; + } + } + + public static DiscordBridge? GetInstance() + { + return instance; + } + + private string OnDscCommand(string cmd, string[] args) + { + if (args.Length == 2) + { + if (args[0].ToLower().Equals("direction")) + { + string direction = args[1].ToLower().Trim(); + + string? bridgeName = ""; + + switch (direction) + { + case "b": + case "both": + bridgeName = "bot.DiscordBridge.direction.both"; + bridgeDirection = BridgeDirection.Both; + break; + + case "mc": + case "minecraft": + bridgeName = "bot.DiscordBridge.direction.minecraft"; + bridgeDirection = BridgeDirection.Minecraft; + break; + + case "d": + case "dcs": + case "discord": + bridgeName = "bot.DiscordBridge.direction.discord"; + bridgeDirection = BridgeDirection.Discord; + break; + + default: + return Translations.TryGet("bot.DiscordBridge.invalid_direction"); + } + + return Translations.TryGet("bot.DiscordBridge.direction", Translations.TryGet(bridgeName)); + }; + } + + return "dscbridge direction "; + } + + public override void GetText(string text) + { + if (!CanSendMessages()) + return; + + text = GetVerbatim(text).Trim(); + + // Stop the crash when an empty text is recived somehow + if (string.IsNullOrEmpty(text)) + return; + + string message = ""; + string username = ""; + bool teleportRequest = false; + + if (IsPrivateMessage(text, ref message, ref username)) + message = Config.PrivateMessageFormat.Replace("{username}", username).Replace("{message}", message).Replace("{timestamp}", GetTimestamp()).Trim(); + else if (IsChatMessage(text, ref message, ref username)) + message = Config.PublicMessageFormat.Replace("{username}", username).Replace("{message}", message).Replace("{timestamp}", GetTimestamp()).Trim(); + else if (IsTeleportRequest(text, ref username)) + { + message = Config.TeleportRequestMessageFormat.Replace("{username}", username).Replace("{timestamp}", GetTimestamp()).Trim(); + teleportRequest = true; + } + else message = text; + + if (teleportRequest) + { + var messageBuilder = new DiscordMessageBuilder() + .WithEmbed(new DiscordEmbedBuilder + { + Description = message, + Color = new DiscordColor(0x3399FF) + }) + .AddComponents(new DiscordComponent[]{ + new DiscordButtonComponent(ButtonStyle.Success, "accept_teleport", "Accept"), + new DiscordButtonComponent(ButtonStyle.Danger, "deny_teleport", "Deny") + }); + + SendMessage(messageBuilder); + return; + } + else SendMessage(message); + } + + public void SendMessage(string message) + { + if (!CanSendMessages() || string.IsNullOrEmpty(message)) + return; + + try + { + discordBotClient!.SendMessageAsync(discordChannel, message).Wait(Config.Message_Send_Timeout * 1000); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.DiscordBridge.canceled_sending")); + LogDebugToConsole(e); + } + } + + public void SendMessage(DiscordMessageBuilder builder) + { + if (!CanSendMessages()) + return; + + try + { + discordBotClient!.SendMessageAsync(discordChannel, builder).Wait(Config.Message_Send_Timeout * 1000); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.DiscordBridge.canceled_sending")); + LogDebugToConsole(e); + } + } + + public void SendMessage(DiscordEmbedBuilder embedBuilder) + { + if (!CanSendMessages()) + return; + + try + { + discordBotClient!.SendMessageAsync(discordChannel, embedBuilder).Wait(Config.Message_Send_Timeout * 1000); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.DiscordBridge.canceled_sending")); + LogDebugToConsole(e); + } + } + public void SendImage(string filePath, string? text = null) + { + if (!CanSendMessages()) + return; + + try + { + using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + { + filePath = filePath[(filePath.IndexOf(Path.DirectorySeparatorChar) + 1)..]; + var messageBuilder = new DiscordMessageBuilder(); + + if (text != null) + messageBuilder.WithContent(text); + + messageBuilder.WithFiles(new Dictionary() { { $"attachment://{filePath}", fs } }); + + discordBotClient!.SendMessageAsync(discordChannel, messageBuilder).Wait(Config.Message_Send_Timeout * 1000); + } + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.DiscordBridge.canceled_sending")); + LogDebugToConsole(e); + } + } + + public void SendFile(FileStream fileStream) + { + if (!CanSendMessages()) + return; + + SendMessage(new DiscordMessageBuilder().WithFile(fileStream)); + } + + private bool CanSendMessages() + { + return discordBotClient == null || discordChannel == null || bridgeDirection == BridgeDirection.Minecraft ? false : true; + } + + async Task MainAsync() + { + try + { + if (string.IsNullOrEmpty(Config.Token.Trim())) + { + LogToConsole(Translations.TryGet("bot.DiscordBridge.missing_token")); + UnloadBot(); + return; + } + + discordBotClient = new DiscordClient(new DiscordConfiguration() + { + Token = Config.Token.Trim(), + TokenType = TokenType.Bot, + AutoReconnect = true, + Intents = DiscordIntents.All, + MinimumLogLevel = Settings.Config.Logging.DebugMessages ? + (LogLevel.Trace | LogLevel.Information | LogLevel.Debug | LogLevel.Critical | LogLevel.Error | LogLevel.Warning) : LogLevel.None + }); + + try + { + await discordBotClient.GetGuildAsync(Config.GuildId); + } + catch (Exception e) + { + if (e is NotFoundException) + { + LogToConsole(Translations.TryGet("bot.DiscordBridge.guild_not_found", Config.GuildId)); + UnloadBot(); + return; + } + + LogDebugToConsole("Exception when trying to find the guild:"); + LogDebugToConsole(e); + } + + try + { + discordChannel = await discordBotClient.GetChannelAsync(Config.ChannelId); + } + catch (Exception e) + { + if (e is NotFoundException) + { + LogToConsole(Translations.TryGet("bot.DiscordBridge.channel_not_found", Config.ChannelId)); + UnloadBot(); + return; + } + + LogDebugToConsole("Exception when trying to find the channel:"); + LogDebugToConsole(e); + } + + discordBotClient.MessageCreated += async (source, e) => + { + if (e.Guild.Id != Config.GuildId) + return; + + if (e.Channel.Id != Config.ChannelId) + return; + + if (!Config.OwnersIds.Contains(e.Author.Id)) + return; + + string message = e.Message.Content.Trim(); + + if (string.IsNullOrEmpty(message) || string.IsNullOrWhiteSpace(message)) + return; + + if (bridgeDirection == BridgeDirection.Discord) + { + if (!message.StartsWith(".dscbridge")) + return; + } + + if (message.StartsWith(".")) + { + message = message[1..]; + await e.Message.CreateReactionAsync(DiscordEmoji.FromName(discordBotClient, ":gear:")); + + string? result = ""; + PerformInternalCommand(message, ref result); + result = string.IsNullOrEmpty(result) ? "-" : result; + + await e.Message.DeleteOwnReactionAsync(DiscordEmoji.FromName(discordBotClient, ":gear:")); + await e.Message.CreateReactionAsync(DiscordEmoji.FromName(discordBotClient, ":white_check_mark:")); + await e.Message.RespondAsync($"{Translations.TryGet("bot.DiscordBridge.command_executed")}:\n```{result}```"); + } + else SendText(message); + }; + + discordBotClient.ComponentInteractionCreated += async (s, e) => + { + if (!(e.Id.Equals("accept_teleport") || e.Id.Equals("deny_teleport"))) + return; + + string result = e.Id.Equals("accept_teleport") ? "Accepted :white_check_mark:" : "Denied :x:"; + SendText(e.Id.Equals("accept_teleport") ? "/tpaccept" : "/tpdeny"); + await e.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().WithContent(result)); + }; + + await discordBotClient.ConnectAsync(); + + await discordBotClient.SendMessageAsync(discordChannel, new DiscordEmbedBuilder + { + Description = Translations.TryGet("bot.DiscordBridge.connected"), + Color = new DiscordColor(0x00FF00) + }); + + IsConnected = true; + LogToConsole("§y§l§f" + Translations.TryGet("bot.DiscordBridge.connected")); + await Task.Delay(-1); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.DiscordBridge.unknown_error")); + LogToConsole(e); + return; + } + } + } +} diff --git a/MinecraftClient/ChatBots/Map.cs b/MinecraftClient/ChatBots/Map.cs index 2b994c16..a004b82f 100644 --- a/MinecraftClient/ChatBots/Map.cs +++ b/MinecraftClient/ChatBots/Map.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; +using System.Threading.Tasks; +using ImageMagick; using MinecraftClient.Mapping; using Tomlet.Attributes; @@ -12,6 +15,12 @@ namespace MinecraftClient.ChatBots { public static Configs Config = new(); + public struct QueuedMap + { + public string FileName; + public int MapId; + } + [TomlDoNotInlineObject] public class Configs { @@ -35,22 +44,45 @@ namespace MinecraftClient.ChatBots [TomlInlineComment("$config.ChatBot.Map.Notify_On_First_Update$")] public bool Notify_On_First_Update = true; - public void OnSettingUpdate() { } + [TomlInlineComment("$config.ChatBot.Map.Rasize_Rendered_Image$")] + public bool Rasize_Rendered_Image = false; + + [TomlInlineComment("$config.ChatBot.Map.Resize_To$")] + public int Resize_To = 512; + + [TomlPrecedingComment("$config.ChatBot.Map.Send_Rendered_To_Bridges$")] + public bool Send_Rendered_To_Discord = false; + public bool Send_Rendered_To_Telegram = false; + + public void OnSettingUpdate() + { + if (Resize_To <= 0) + Resize_To = 128; + } } private readonly string baseDirectory = @"Rendered_Maps"; private readonly Dictionary cachedMaps = new(); + private readonly Queue discordQueue = new(); + public override void Initialize() { if (!Directory.Exists(baseDirectory)) Directory.CreateDirectory(baseDirectory); + DeleteRenderedMaps(); + RegisterChatBotCommand("maps", "bot.map.cmd.desc", "maps list|render or maps l|r ", OnMapCommand); } public override void OnUnload() + { + DeleteRenderedMaps(); + } + + private void DeleteRenderedMaps() { if (Config.Delete_All_On_Unload) { @@ -199,7 +231,93 @@ namespace MinecraftClient.ChatBots } } file.Close(); + LogToConsole(Translations.TryGet("bot.map.rendered", map.MapId, fileName)); + + if (Config.Rasize_Rendered_Image) + { + using (var image = new MagickImage(fileName)) + { + var size = new MagickGeometry(Config.Resize_To, Config.Resize_To); + size.IgnoreAspectRatio = true; + + image.Resize(size); + image.Write(fileName); + LogToConsole(Translations.TryGet("bot.map.resized_rendered_image", map.MapId, Config.Resize_To)); + } + } + + if (Config.Send_Rendered_To_Discord || Config.Send_Rendered_To_Telegram) + { + // We need to queue up images because Discord/Telegram Bridge is not ready immediatelly + if (DiscordBridge.Config.Enabled || TelegramBridge.Config.Enabled) + discordQueue.Enqueue(new QueuedMap { FileName = fileName, MapId = map.MapId }); + } + } + + public override void Update() + { + DiscordBridge? discordBridge = DiscordBridge.GetInstance(); + TelegramBridge? telegramBridge = TelegramBridge.GetInstance(); + + if (Config.Send_Rendered_To_Discord) + { + if (discordBridge == null || (discordBridge != null && !discordBridge.IsConnected)) + return; + } + + if (Config.Send_Rendered_To_Telegram) + { + if (telegramBridge == null || (telegramBridge != null && !telegramBridge.IsConnected)) + return; + } + + if (discordQueue.Count > 0) + { + QueuedMap map = discordQueue.Dequeue(); + string fileName = map.FileName; + + // We must convert to a PNG in order to send to Discord, BMP does not work + string newFileName = fileName.Replace(".bmp", ".png"); + using (var image = new MagickImage(fileName)) + { + image.Write(newFileName); + + if (Config.Send_Rendered_To_Discord) + discordBridge!.SendImage(newFileName, $"> A render of the map with an id: **{map.MapId}**"); + + if (Config.Send_Rendered_To_Telegram) + telegramBridge!.SendImage(newFileName, $"A render of the map with an id: *{map.MapId}*"); + + newFileName = Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + newFileName; + + if (Config.Send_Rendered_To_Discord) + LogToConsole(Translations.TryGet("bot.map.sent_to_discord", map.MapId)); + + if (Config.Send_Rendered_To_Telegram) + LogToConsole(Translations.TryGet("bot.map.sent_to_telegram", map.MapId)); + + // Wait for 2 seconds and then try until file is free for deletion + // 10 seconds timeout + Task.Run(async () => + { + await Task.Delay(2000); + + var time = Stopwatch.StartNew(); + + while (time.ElapsedMilliseconds < 10000) // 10 seconds + { + try + { + // Delete the temporary file + if (File.Exists(newFileName)) + File.Delete(newFileName); + } + catch (IOException e) { } + } + }); + } + } } private void RenderInConsole(McMap map) @@ -389,8 +507,8 @@ namespace MinecraftClient.ChatBots return new( r: (byte)((Colors[baseColorId][0] * shadeMultiplier) / 255), - g: (byte)((Colors[baseColorId][1] * shadeMultiplier) / 255), - b: (byte)((Colors[baseColorId][2] * shadeMultiplier) / 255), + g: (byte)((Colors[baseColorId][1] * shadeMultiplier) / 255), + b: (byte)((Colors[baseColorId][2] * shadeMultiplier) / 255), a: 255 ); } diff --git a/MinecraftClient/ChatBots/Script.cs b/MinecraftClient/ChatBots/Script.cs index 3fc1acaf..c72be5a2 100644 --- a/MinecraftClient/ChatBots/Script.cs +++ b/MinecraftClient/ChatBots/Script.cs @@ -158,7 +158,7 @@ namespace MinecraftClient.ChatBots { try { - CSharpRunner.Run(this, lines, args, localVars); + CSharpRunner.Run(this, lines, args, localVars, scriptName: file!); } catch (CSharpException e) { diff --git a/MinecraftClient/ChatBots/TelegramBridge.cs b/MinecraftClient/ChatBots/TelegramBridge.cs new file mode 100644 index 00000000..7b9f56d9 --- /dev/null +++ b/MinecraftClient/ChatBots/TelegramBridge.cs @@ -0,0 +1,358 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.Entities; +using Telegram.Bot; +using Telegram.Bot.Exceptions; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegram.Bot.Types.InputFiles; +using Tomlet.Attributes; +using File = System.IO.File; + +namespace MinecraftClient.ChatBots +{ + public class TelegramBridge : ChatBot + { + private enum BridgeDirection + { + Both = 0, + Minecraft, + Telegram + } + + private static TelegramBridge? instance = null; + public bool IsConnected { get; private set; } + + private TelegramBotClient? botClient; + private CancellationTokenSource? cancellationToken; + private BridgeDirection bridgeDirection = BridgeDirection.Both; + + public static Configs Config = new(); + + [TomlDoNotInlineObject] + public class Configs + { + [NonSerialized] + private const string BotName = "TelegramBridge"; + + public bool Enabled = false; + + [TomlInlineComment("$config.ChatBot.TelegramBridge.Token$")] + public string Token = "your bot token here"; + + [TomlInlineComment("$config.ChatBot.TelegramBridge.ChannelId$")] + public string ChannelId = ""; + + [TomlInlineComment("$config.ChatBot.TelegramBridge.Authorized_Chat_Ids$")] + public long[] Authorized_Chat_Ids = Array.Empty(); + + [TomlInlineComment("$config.ChatBot.TelegramBridge.MessageSendTimeout$")] + public int Message_Send_Timeout = 3; + + [TomlPrecedingComment("$config.ChatBot.TelegramBridge.Formats$")] + public string PrivateMessageFormat = "*(Private Message)* {username}: {message}"; + public string PublicMessageFormat = "{username}: {message}"; + public string TeleportRequestMessageFormat = "A new Teleport Request from **{username}**!"; + + public void OnSettingUpdate() + { + Message_Send_Timeout = Message_Send_Timeout <= 0 ? 3 : Message_Send_Timeout; + } + } + + public TelegramBridge() + { + instance = this; + } + + public override void Initialize() + { + RegisterChatBotCommand("tgbridge", "bot.TelegramBridge.desc", "tgbridge direction ", OnTgCommand); + + Task.Run(async () => await MainAsync()); + } + + ~TelegramBridge() + { + Disconnect(); + } + + public override void OnUnload() + { + Disconnect(); + } + + private void Disconnect() + { + if (botClient != null) + { + try + { + SendMessage(Translations.TryGet("bot.TelegramBridge.disconnected")); + cancellationToken?.Cancel(); + botClient = null; + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.TelegramBridge.canceled_sending")); + LogDebugToConsole(e); + } + + IsConnected = false; + } + } + + public static TelegramBridge? GetInstance() + { + return instance; + } + + private string OnTgCommand(string cmd, string[] args) + { + if (args.Length == 2) + { + if (args[0].ToLower().Equals("direction")) + { + string direction = args[1].ToLower().Trim(); + + string? bridgeName = ""; + + switch (direction) + { + case "b": + case "both": + bridgeName = "bot.TelegramBridge.direction.both"; + bridgeDirection = BridgeDirection.Both; + break; + + case "mc": + case "minecraft": + bridgeName = "bot.TelegramBridge.direction.minecraft"; + bridgeDirection = BridgeDirection.Minecraft; + break; + + case "t": + case "tg": + case "telegram": + bridgeName = "bot.TelegramBridge.direction.discord"; + bridgeDirection = BridgeDirection.Telegram; + break; + + default: + return Translations.TryGet("bot.TelegramBridge.invalid_direction"); + } + + return Translations.TryGet("bot.TelegramBridge.direction", Translations.TryGet(bridgeName)); + }; + } + + return "dscbridge direction "; + } + + public override void GetText(string text) + { + if (!CanSendMessages()) + return; + + text = GetVerbatim(text).Trim(); + + // Stop the crash when an empty text is recived somehow + if (string.IsNullOrEmpty(text)) + return; + + string message = ""; + string username = ""; + + if (IsPrivateMessage(text, ref message, ref username)) + message = Config.PrivateMessageFormat.Replace("{username}", username).Replace("{message}", message).Replace("{timestamp}", GetTimestamp()).Trim(); + else if (IsChatMessage(text, ref message, ref username)) + message = Config.PublicMessageFormat.Replace("{username}", username).Replace("{message}", message).Replace("{timestamp}", GetTimestamp()).Trim(); + else if (IsTeleportRequest(text, ref username)) + message = Config.TeleportRequestMessageFormat.Replace("{username}", username).Replace("{timestamp}", GetTimestamp()).Trim(); + + else message = text; + + SendMessage(message); + } + + public void SendMessage(string message) + { + if (!CanSendMessages() || string.IsNullOrEmpty(message)) + return; + + try + { + botClient!.SendTextMessageAsync(Config.ChannelId.Trim(), message, ParseMode.Markdown).Wait(Config.Message_Send_Timeout); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.TelegramBridge.canceled_sending")); + LogDebugToConsole(e); + } + } + + public void SendImage(string filePath, string? text = null) + { + if (!CanSendMessages()) + return; + + try + { + string fileName = filePath[(filePath.IndexOf(Path.DirectorySeparatorChar) + 1)..]; + + Stream stream = File.OpenRead(filePath); + botClient!.SendDocumentAsync( + Config.ChannelId.Trim(), + document: new InputOnlineFile(content: stream, fileName), + caption: text, + parseMode: ParseMode.Markdown).Wait(Config.Message_Send_Timeout * 1000); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.TelegramBridge.canceled_sending")); + LogDebugToConsole(e); + } + } + + private bool CanSendMessages() + { + return botClient != null && !string.IsNullOrEmpty(Config.ChannelId.Trim()) && bridgeDirection != BridgeDirection.Minecraft; + } + + async Task MainAsync() + { + try + { + if (string.IsNullOrEmpty(Config.Token.Trim())) + { + LogToConsole(Translations.TryGet("bot.TelegramBridge.missing_token")); + UnloadBot(); + return; + } + + if (string.IsNullOrEmpty(Config.ChannelId.Trim())) + LogToConsole("§w§l§f" + Translations.TryGet("bot.TelegramBridge.missing_channel_id")); + + botClient = new TelegramBotClient(Config.Token.Trim()); + cancellationToken = new CancellationTokenSource(); + + botClient.StartReceiving( + updateHandler: HandleUpdateAsync, + pollingErrorHandler: HandlePollingErrorAsync, + receiverOptions: new ReceiverOptions + { + // receive all update types + AllowedUpdates = Array.Empty() + }, + cancellationToken: cancellationToken.Token + ); + + IsConnected = true; + + SendMessage("✅ " + Translations.TryGet("bot.TelegramBridge.connected")); + LogToConsole("§y§l§f" + Translations.TryGet("bot.TelegramBridge.connected")); + + if (Config.Authorized_Chat_Ids.Length == 0) + { + SendMessage("⚠️ *" + Translations.TryGet("bot.TelegramBridge.missing_authorized_channels") + "* ⚠️"); + LogToConsole("§w§l§f" + Translations.TryGet("bot.TelegramBridge.missing_authorized_channels")); + return; + } + + await Task.Delay(-1); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.TelegramBridge.unknown_error")); + LogToConsole(e); + return; + } + } + + private async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken _cancellationToken) + { + // Only process Message updates: https://core.telegram.org/bots/api#message + if (update.Message is not { } message) + return; + + // Only process text messages + if (message.Text is not { } messageText) + return; + + var chatId = message.Chat.Id; + var text = message.Text; + + if (string.IsNullOrEmpty(text) || string.IsNullOrWhiteSpace(text)) + return; + + if (text.ToLower().Contains("/start")) + return; + + if (text.ToLower().Contains(".chatid")) + { + await botClient.SendTextMessageAsync(chatId: chatId, + replyToMessageId: message.MessageId, + text: $"Chat ID: {chatId}", + cancellationToken: _cancellationToken, + parseMode: ParseMode.Markdown); + return; + } + + if (Config.Authorized_Chat_Ids.Length > 0 && !Config.Authorized_Chat_Ids.Contains(chatId)) + { + LogDebugToConsole($"Unauthorized message '{messageText}' received in a chat with with an ID: {chatId} !"); + await botClient.SendTextMessageAsync( + chatId: chatId, + replyToMessageId: message.MessageId, + text: Translations.TryGet("bot.TelegramBridge.unauthorized"), + cancellationToken: _cancellationToken, + parseMode: ParseMode.Markdown); + return; + } + + LogDebugToConsole($"Received a '{messageText}' message in a chat with with an ID: {chatId} ."); + + if (bridgeDirection == BridgeDirection.Telegram) + { + if (!text.StartsWith(".dscbridge")) + return; + } + + if (text.StartsWith(".")) + { + var command = text[1..]; + + string? result = ""; + PerformInternalCommand(command, ref result); + result = string.IsNullOrEmpty(result) ? "-" : result; + + await botClient.SendTextMessageAsync( + chatId: chatId, + replyToMessageId: + message.MessageId, + text: $"{Translations.TryGet("bot.TelegramBridge.command_executed")}:\n\n{result}", + cancellationToken: _cancellationToken, + parseMode: ParseMode.Markdown); + } + else SendText(text); + } + + private Task HandlePollingErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken _cancellationToken) + { + var ErrorMessage = exception switch + { + ApiRequestException apiRequestException + => $"Telegram API Error:\n[{apiRequestException.ErrorCode}]\n{apiRequestException.Message}", + _ => exception.ToString() + }; + + LogToConsole("§w§l§f" + ErrorMessage); + return Task.CompletedTask; + } + } +} diff --git a/MinecraftClient/Inventory/Item.cs b/MinecraftClient/Inventory/Item.cs index fff591e5..5b4b7941 100644 --- a/MinecraftClient/Inventory/Item.cs +++ b/MinecraftClient/Inventory/Item.cs @@ -113,17 +113,22 @@ namespace MinecraftClient.Inventory } } - public string GetTypeString() + public static string GetTypeString(ItemType type) { - string type = Type.ToString(); - string type_renamed = type.ToUnderscoreCase(); + string type_str = type.ToString(); + string type_renamed = type_str.ToUnderscoreCase(); string? res1 = Protocol.ChatParser.TranslateString("item.minecraft." + type_renamed); if (!string.IsNullOrEmpty(res1)) return res1; string? res2 = Protocol.ChatParser.TranslateString("block.minecraft." + type_renamed); if (!string.IsNullOrEmpty(res2)) return res2; - return type; + return type_str; + } + + public string GetTypeString() + { + return GetTypeString(Type); } public string ToFullString() diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index fafc781f..5fefa0b1 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -264,6 +264,7 @@ namespace MinecraftClient if (Config.ChatBot.AutoRelog.Enabled) { BotLoad(new AutoRelog()); } if (Config.ChatBot.AutoRespond.Enabled) { BotLoad(new AutoRespond()); } if (Config.ChatBot.ChatLog.Enabled) { BotLoad(new ChatLog()); } + if (Config.ChatBot.DiscordBridge.Enabled) { BotLoad(new DiscordBridge()); } if (Config.ChatBot.Farmer.Enabled) { BotLoad(new Farmer()); } if (Config.ChatBot.FollowPlayer.Enabled) { BotLoad(new FollowPlayer()); } if (Config.ChatBot.HangmanGame.Enabled) { BotLoad(new HangmanGame()); } @@ -273,7 +274,9 @@ namespace MinecraftClient if (Config.ChatBot.RemoteControl.Enabled) { BotLoad(new RemoteControl()); } if (Config.ChatBot.ReplayCapture.Enabled && reload) { BotLoad(new ReplayCapture()); } if (Config.ChatBot.ScriptScheduler.Enabled) { BotLoad(new ScriptScheduler()); } + if (Config.ChatBot.TelegramBridge.Enabled) { BotLoad(new TelegramBridge()); } if (Config.ChatBot.WebSocketBot.Enabled) { BotLoad(new WebSocketBot()); } + //Add your ChatBot here by uncommenting and adapting //BotLoad(new ChatBots.YourBot()); } diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index fb2e19b8..39fbc06d 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -35,7 +35,9 @@ + + @@ -46,6 +48,7 @@ NU1701 + diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 0eabdae0..5f7c5bf0 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -691,7 +691,7 @@ namespace MinecraftClient public static void ReloadSettings() { if(Settings.LoadFromFile(settingsIniPath).Item1) - ConsoleIO.WriteLine(Translations.TryGet("config.loading", settingsIniPath)); + ConsoleIO.WriteLine(Translations.TryGet("config.load", settingsIniPath)); } /// diff --git a/MinecraftClient/Resources/lang/de.ini b/MinecraftClient/Resources/lang/de.ini index cc7b0f4e..b8eb6789 100644 Binary files a/MinecraftClient/Resources/lang/de.ini and b/MinecraftClient/Resources/lang/de.ini differ diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 0c7fdbe0..b4d2d2c5 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -477,20 +477,24 @@ cmd.useitem.use=Used an item # ChatBots. Naming style: bot.. # Alerts +botname.Alerts=Alerts bot.alerts.start_rain=§cWeather change: It is raining now.§r bot.alerts.end_rain=§cWeather change: It is no longer raining.§r bot.alerts.start_thunderstorm=§cWeather change: It is a thunderstorm.§r bot.alerts.end_thunderstorm=§cWeather change: It is no longer a thunderstorm.§r # Anti AFK +botname.AntiAFK=AntiAFK bot.antiafk.not_using_terrain_handling=The terrain handling is not enabled in the settings of the client, enable it if you want to use it with this bot. Using alternative (command) method. bot.antiafk.swapping=The time range begins with a bigger value, swapped them around. bot.antiafk.invalid_walk_range=Invalid walk range provided, must be a positive integer greater than 0, using default value of 5! # AutoAttack +botname.AutoAttack=AutoAttack bot.autoAttack.invalidcooldown=Attack cooldown value cannot be smaller than 0. # AutoCraft +botname.AutoCraft=AutoCraft bot.autoCraft.cmd=Auto-crafting ChatBot command bot.autoCraft.alias=Auto-crafting ChatBot command alias bot.autoCraft.cmd.list=Total {0} recipes loaded: {1} @@ -523,6 +527,7 @@ bot.autocraft.invaild_slots=The number of slots does not match and has been adju bot.autocraft.invaild_invaild_result=Invalid result item! # AutoDig +botname.AutoDig=AutoDig bot.autodig.start_delay=Digging will start in {0:0.0} second(s). bot.autodig.dig_timeout=Digging block timeout, retry. bot.autodig.not_allow=The block currently pointed to is not in the allowed list. @@ -535,6 +540,7 @@ bot.autodig.help.stop=Deactivate the automatic digging bot. bot.autodig.help.help=Get the command description. Usage: /digbot help # AutoDrop +botname.AutoDrop=AutoDrop bot.autoDrop.cmd=AutoDrop ChatBot command bot.autoDrop.alias=AutoDrop ChatBot command alias bot.autoDrop.on=AutoDrop enabled @@ -550,7 +556,11 @@ bot.autoDrop.unknown_mode=Unknwon mode. Available modes: Include, Exclude, Every bot.autoDrop.no_mode=Cannot read drop mode from config. Using include mode. bot.autoDrop.no_inventory=Cannot find inventory {0}! +# AutoEat +botname.AutoEat=AutoEat + # AutoFish +botname.AutoFishing=AutoFishing bot.autoFish.no_inv_handle=Inventory handling is not enabled. Cannot check rod durability and switch rods. bot.autoFish.start_at=Fishing will start in {0:0.0} second(s). bot.autoFish.throw=Casting successfully. @@ -563,15 +573,20 @@ bot.autoFish.fishing_timeout=Fishing timeout, will soon re-cast. bot.autoFish.cast_timeout=Casting timeout and will soon retry. (Timeout increased to {0:0.0} sec). bot.autoFish.update_lookat=Update yaw = {0:0.00}, pitch = {1:0.00}. bot.autoFish.switch=Switch to the rod in slot {0}, durability {1}/64. +bot.autoFish.status_info=All items obtained from fishing (not entirely accurate): +# AutoFish cmd bot.autoFish.cmd=Auto-Fishing ChatBot command bot.autoFish.available_cmd=Available commands: {0}. Use /fish help for more information. bot.autoFish.start=Start auto-fishing. bot.autoFish.stop=Stop auto-fishing. +bot.autoFish.status_clear=The record of the obtained items has been cleared. bot.autoFish.help.start=Start auto-fishing. bot.autoFish.help.stop=Stop auto-fishing. +bot.autoFish.help.status=List all obtained items. Or use "/fish status clear" to clear the list. bot.autoFish.help.help=Get the command description. Usage: /fish help # AutoRelog +botname.AutoRelog=AutoRelog bot.autoRelog.launch=Launching with {0} reconnection attempts bot.autoRelog.no_kick_msg=Initializing without a kick message file bot.autoRelog.loading=Loading messages from file: {0} @@ -586,6 +601,7 @@ bot.autoRelog.reconnect_ignore=Message not containing any defined keywords. Igno bot.autoRelog.wait=Waiting {0:0.000} seconds before reconnecting... # AutoRespond +botname.AutoRespond=AutoRespond bot.autoRespond.loading=Loading matches from '{0}' bot.autoRespond.file_not_found=File not found: '{0}' bot.autoRespond.loaded_match=Loaded match:\n{0} @@ -595,9 +611,70 @@ bot.autoRespond.match_run=Running action: {0} bot.autoRespond.match=match: {0}\nregex: {1}\naction: {2}\nactionPrivate: {3}\nactionOther: {4}\nownersOnly: {5}\ncooldown: {6} # ChatLog +botname.ChatLog=ChatLog bot.chatLog.invalid_file=Path '{0}' contains invalid characters. +# DiscordBridge +botname.DiscordBridge=DiscordBridge +bot.DiscordBridge.command_executed=The command was executed with the result +bot.DiscordBridge.connected=Succesfully connected with MCC! +bot.DiscordBridge.missing_token=Please provide a valid token! +bot.DiscordBridge.guild_not_found=The provided guild/server with an id '{0}' has not been found! +bot.DiscordBridge.channel_not_found=The provided channel with an id '{0}' has not been found! +bot.DiscordBridge.unknown_error=An unknown error has occured! +bot.DiscordBridge.canceled_sending=Sending message to Discord was canceled due an error occuring. For more info enable Debug. +bot.DiscordBridge.desc=This command allows you to specify in the which direction the messages will be relayed via the Discord Bridge chat bot. +bot.DiscordBridge.invalid_direction=Invalid direction provided! Available directions: both|b, minecraft|mc, discord|dsc. Example: "dscbridge direction mc" +bot.DiscordBridge.direction=Direction of the Discord Brdige has been switched to '{0}'! +bot.DiscordBridge.direction.both=Both +bot.DiscordBridge.direction.minecraft=Minecraft +bot.DiscordBridge.direction.discord=Discord + +# Farmer +botname.Farmer=Farmer +bot.farmer.desc=Farming bot +bot.farmer.not_implemented=Not implemented bellow 1.13! +bot.farmer.already_stopped=The bot has already stopped farming! +bot.farmer.stopping=Stoping farming, this might take a second... +bot.farmer.stopped=Stopped farming! +bot.farmer.already_running=The bot is already farming! +bot.farmer.invalid_crop_type=Invalid crop type provided (Types which you can use: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat)! +bot.farmer.warining_invalid_parameter=Invalid parameter "{0}" provided (Use format: "key:value")! +bot.farmer.invalid_radius=Invalid radius provided, you must provide a valid integer number greater than 0! +bot.farmer.warining_force_unsafe=You have enabled un-safe movement, the bot might get hurt! +bot.farmer.warining_allow_teleport=You have enabled teleporting, this might get your bot account kicked and in the worst case scenario banned! Use with caution! +bot.farmer.started=Started farming! +bot.farmer.crop_type=Crop type +bot.farmer.radius=Radius +bot.farmer.needs_terrain=The Farmer bot needs Terrain Handling in order to work, please enable it! +bot.farmer.needs_inventory=The Farmer bot needs Inventory Handling in order to work, please enable it! + +# Follow player +botname.FollowPlayer=FollowPlayer +cmd.follow.desc=Makes the bot follow a specified player +cmd.follow.usage=follow [-f] (Use -f to enable un-safe walking) +cmd.follow.already_stopped=Already stopped +cmd.follow.stopping=Stopped following! +cmd.follow.invalid_name=Invalid or empty player name provided! +cmd.follow.invalid_player=The specified player is either not connected out out of the range! +cmd.follow.cant_reach_player=Can not reach the player, he is either in chunks that are not loaded, too far away or not reachable by a bot due to obstacles like gaps or water bodies! +cmd.follow.already_following=Already following {0}! +cmd.follow.switched=Switched to following {0}! +cmd.follow.started=Started following {0}! +cmd.follow.unsafe_enabled=Enabled us-safe walking (NOTE: The bot might die or get hurt!) +cmd.follow.note=NOTE: The bot is quite slow, you need to walk slowly and at a close distance for it to be able to keep up, kinda like when you make animals follow you by holding food in your hand. This is a limitation due to a pathfinding algorithm, we are working to get a better one. +cmd.follow.player_came_to_the_range=The player {0} came back to the range! +cmd.follow.resuming=Resuming to follow! +cmd.follow.player_left_the_range=The player {0} has left the range! +cmd.follow.pausing=Pausing! +cmd.follow.player_left=The player {0} left the server! +cmd.follow.stopping=Stopped! + +# HangmanGame +botname.HangmanGame=HangmanGame + # Mailer +botname.Mailer=Mailer bot.mailer.init=Initializing Mailer with settings: bot.mailer.init.db= - Database File: {0} bot.mailer.init.ignore= - Ignore List: {0} @@ -630,6 +707,7 @@ bot.mailer.cmd.ignore.invalid=Missing or invalid name. Usage: {0} bot.mailer.cmd.help=See usage # Maps +botname.Map=Map bot.map.cmd.desc=Render maps (item maps) bot.map.cmd.not_found=A map with id '{0}' does not exists! bot.map.cmd.invalid_id=Invalid ID provided, must be a number! @@ -640,58 +718,32 @@ bot.map.rendered=Succesfully rendered a map with id '{0}' to: '{1}' bot.map.failed_to_render=Failed to render the map with id: '{0}' bot.map.list_item=- Map id: {0} (Last Updated: {1}) bot.map.scale=The size of the map is reduced from ({0}x{1}) to ({2}x{3}) due to the size limitation of the current terminal. +bot.map.resized_rendered_image=Resized the rendered image of the map with id: '{0}' to {1}x{1}. +bot.map.sent_to_discord=Sent a rendered image of a map with an id '{0}' to the Discord via Discord Brdige chat bot! +bot.map.sent_to_telegram=Sent a rendered image of a map with an id '{0}' to the Telegram via Telegram Bridge chat bot! + +# PlayerListLogger +botname.PlayerListLogger=PlayerListLogger + +# RemoteControl +botname.RemoteControl=RemoteControl # ReplayCapture +botname.ReplayCapture=ReplayCapture bot.replayCapture.cmd=replay command bot.replayCapture.created=Replay file created. bot.replayCapture.stopped=Record stopped. bot.replayCapture.restart=Record was stopped. Restart the program to start another record. -# Farmer -bot.farmer.desc=Farming bot -bot.farmer.not_implemented=Not implemented bellow 1.13! -bot.farmer.already_stopped=The bot has already stopped farming! -bot.farmer.stopping=Stoping farming, this might take a second... -bot.farmer.stopped=Stopped farming! -bot.farmer.already_running=The bot is already farming! -bot.farmer.invalid_crop_type=Invalid crop type provided (Types which you can use: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat)! -bot.farmer.warining_invalid_parameter=Invalid parameter "{0}" provided (Use format: "key:value")! -bot.farmer.invalid_radius=Invalid radius provided, you must provide a valid integer number greater than 0! -bot.farmer.warining_force_unsafe=You have enabled un-safe movement, the bot might get hurt! -bot.farmer.warining_allow_teleport=You have enabled teleporting, this might get your bot account kicked and in the worst case scenario banned! Use with caution! -bot.farmer.started=Started farming! -bot.farmer.crop_type=Crop type -bot.farmer.radius=Radius -bot.farmer.needs_terrain=The Farmer bot needs Terrain Handling in order to work, please enable it! -bot.farmer.needs_inventory=The Farmer bot needs Inventory Handling in order to work, please enable it! - -# Follow player -cmd.follow.desc=Makes the bot follow a specified player -cmd.follow.usage=follow [-f] (Use -f to enable un-safe walking) -cmd.follow.already_stopped=Already stopped -cmd.follow.stopping=Stopped following! -cmd.follow.invalid_name=Invalid or empty player name provided! -cmd.follow.invalid_player=The specified player is either not connected out out of the range! -cmd.follow.cant_reach_player=Can not reach the player, he is either in chunks that are not loaded, too far away or not reachable by a bot due to obstacles like gaps or water bodies! -cmd.follow.already_following=Already following {0}! -cmd.follow.switched=Switched to following {0}! -cmd.follow.started=Started following {0}! -cmd.follow.unsafe_enabled=Enabled us-safe walking (NOTE: The bot might die or get hurt!) -cmd.follow.note=NOTE: The bot is quite slow, you need to walk slowly and at a close distance for it to be able to keep up, kinda like when you make animals follow you by holding food in your hand. This is a limitation due to a pathfinding algorithm, we are working to get a better one. -cmd.follow.player_came_to_the_range=The player {0} came back to the range! -cmd.follow.resuming=Resuming to follow! -cmd.follow.player_left_the_range=The player {0} has left the range! -cmd.follow.pausing=Pausing! -cmd.follow.player_left=The player {0} left the server! -cmd.follow.stopping=Stopped! - # Script +botname.Script=Script bot.script.not_found=§8[MCC] [{0}] Cannot find script file: {1} bot.script.file_not_found=File not found: '{0}' bot.script.fail=Script '{0}' failed to run ({1}). bot.script.pm.loaded=Script '{0}' loaded. # ScriptScheduler +botname.ScriptScheduler=ScriptScheduler bot.scriptScheduler.loaded_task=Loaded task:\n{0} bot.scriptScheduler.no_trigger=This task will never trigger:\n{0} bot.scriptScheduler.no_action=No action for task:\n{0} @@ -700,6 +752,24 @@ bot.scriptScheduler.running_inverval=Interval / Running action: {0} bot.scriptScheduler.running_login=Login / Running action: {0} bot.scriptScheduler.task=triggeronfirstlogin: {0}\n triggeronlogin: {1}\n triggerontime: {2}\n triggeroninterval: {3}\n timevalue: {4}\n timeinterval: {5}\n action: {6} +# TelegramBridge +botname.TelegramBridge=TelegramBridge +bot.TelegramBridge.command_executed=The command was executed with the result +bot.TelegramBridge.connected=Succesfully connected with the MCC! +bot.TelegramBridge.disconnected=Disconnected from from the MCC! +bot.TelegramBridge.missing_token=Please provide a valid bot token! +bot.TelegramBridge.missing_channel_id=[WARNING] You have not provided a Channel ID, you will ONLY get replies to commands sent from Telegram! +bot.TelegramBridge.missing_authorized_channels=[WARNING] You have not provided any Channel IDs, for "Authorized_Chat_Ids" field, anyone who finds your bot will be able to send messages and commands to it! +bot.TelegramBridge.unauthorized=**🛑 Unauthorized access! 🛑\n\nAdd the ID of this chat to "Authorized_Chat_Ids" field in the configuration file to gain access!** +bot.TelegramBridge.unknown_error=An unknown error has occured! +bot.TelegramBridge.canceled_sending=Sending message to Telegram was canceled due an error occuring. For more info enable Debug. +bot.TelegramBridge.desc=This command allows you to specify in the which direction the messages will be relayed via the Telegram Bridge chat bot. +bot.TelegramBridge.invalid_direction=Invalid direction provided! Available directions: both|b, minecraft|mc, telegram|tg|t. Example: "tgbridge direction mc" +bot.TelegramBridge.direction=Direction of the Telegram Brdige has been switched to '{0}'! +bot.TelegramBridge.direction.both=Both +bot.TelegramBridge.direction.minecraft=Minecraft +bot.TelegramBridge.direction.Telegram=Telegram + # WebSocketBot bot.WebSocketBot.session_id_changed=§bSession with an id §a{0}§b has been renamed to: §a{1}§b! bot.WebSocketBot.session_authenticated=§bSession with an id §a{0}§b has been succesfully authenticated! @@ -712,12 +782,12 @@ bot.WebSocketBot.new_session=§bNew session connected: §a{0} bot.WebSocketBot.session_disconnected=§bSession with an id §a{0}§b has disconnected! # TestBot +botname.TestBot=TestBot bot.testBot.told=Bot: {0} told me : {1} bot.testBot.said=Bot: {0} said : {1} [config] - config.load=Settings have been loaded from {0} config.load.fail=§cFailed to load settings:§r config.write.fail=§cFailed to write to settings file {0}§r @@ -915,6 +985,15 @@ config.ChatBot.AutoRespond.Match_Colors=Do not remove colors from text (Note: Yo # ChatBot.ChatLog config.ChatBot.ChatLog=Logs chat messages in a file on disk. +# ChatBot.DiscordBridge +config.ChatBot.DiscordBridge=This bot allows you to send and recieve messages and commands via a Discord channel.\n# For Setup you can either use the documentation or read here (Documentation has images).\n# Documentation: https://mccteam.github.io/guide/chat-bots.html#discord-bridge\n# Setup:\n# First you need to create a Bot on the Discord Developers Portal, here is a video tutorial: https://www.youtube.com/watch?v=2FgMnZViNPA .\n# /!\ IMPORTANT /!\: When creating a bot, you MUST ENABLE "Message Content Intent", "Server Members Intent" and "Presence Intent" in order for bot to work! Also follow along carefully do not miss any steps!\n# When making a bot, copy the generated token and paste it here in "Token" field (tokens are important, keep them safe).\n# Copy the "Application ID" and go to: https://bit.ly/2Spn2Q3 .\n# Paste the id you have copied and check the "Administrator" field in permissions, then click on the link at the bottom.\n# This will open an invitation menu with your servers, choose the server you want to invite the bot on and invite him.\n# Once you've invited the bot, go to your Discord client and go to Settings -> Advanced and Enable "Developer Mode".\n# Exit the settings and right click on a server you have invited the bot to in the server list, then click "Copy ID", and paste the id here in "GuildId".\n# Then right click on a channel where you want to interact with the bot and again right click -> "Copy ID", pase the copied id here in "ChannelId".\n# And for the end, send a message in the channel, right click on your nick and again right click -> "Copy ID", then paste the id here in "OwnersIds".\n# How to use:\n# To execute an MCC command, prefix it with a dot ".", example: ".move 143 64 735" .\n# To send a message, simply type it out and hit enter. +config.ChatBot.DiscordBridge.Token=Your Discord Bot token. +config.ChatBot.DiscordBridge.GuildId=The ID of a server/guild where you have invited the bot to. +config.ChatBot.DiscordBridge.ChannelId=The ID of a channel where you want to interact with the MCC using the bot. +config.ChatBot.DiscordBridge.OwnersIds=A list of IDs of people you want to be able to interact with the MCC using the bot. +config.ChatBot.DiscordBridge.MessageSendTimeout=How long to wait (in seconds) if a message can not be sent to discord before canceling the task (minimum 1 second). +config.ChatBot.DiscordBridge.Formats=Message formats\n# Words wrapped with { and } are going to be replaced during the code execution, do not change them!\n# For example. {message} is going to be replace with an actual message, {username} will be replaced with an username, {timestamp} with the current time.\n# For Discord message formatting, check the following: https://bit.ly/3F8CUCm + # ChatBot.Farmer config.ChatBot.Farmer=Automatically farms crops for you (plants, breaks and bonemeals them).\n# Crop types available: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat.\n# Usage: "/farmer start" command and "/farmer stop" command.\n# NOTE: This a newly added bot, it is not perfect and was only tested in 1.19.2, there are some minor issues like not being able to bonemeal carrots/potatoes sometimes.\n# or bot jumps onto the farm land and breaks it (this happens rarely but still happens). We are looking forward at improving this.\n# It is recommended to keep the farming area walled off and flat to avoid the bot jumping.\n# Also, if you have your farmland that is one block high, make it 2 or more blocks high so the bot does not fall through, as it can happen sometimes when the bot reconnects.\n# The bot also does not pickup all items if they fly off to the side, we have a plan to implement this option in the future as well as drop off and bonemeal refill chest(s). config.ChatBot.Farmer.Delay_Between_Tasks=Delay between tasks in seconds (Minimum 1 second) @@ -931,12 +1010,15 @@ config.ChatBot.HangmanGame=A small game to demonstrate chat interactions. Player config.ChatBot.Mailer=Relay messages between players and servers, like a mail plugin\n# This bot can store messages when the recipients are offline, and send them when they join the server\n# /!\ Server admins can spoof PMs (/tellraw, /nick) so enable this bot only if you trust server admins # ChatBot.Map -config.ChatBot.Map=Allows you to render maps into .jpg images\n# This is useful for solving captchas which use maps\n# The maps are rendered into Rendered_Maps folder.\n# NOTE:\n# This feature is currently only useful for solving captchas which use maps.\n# If some servers have a very short time for solving captchas, enabe Auto_Render_On_Update and prepare to open the file quickly.\n# On linux you can use FTP to access generated files.\n# In the future it might will be possible to display maps directly in the console with a separate command.\n# /!\ Make sure server rules allow bots to be used on the server, or you risk being punished. +config.ChatBot.Map=Allows you to render maps in the console and into images (which can be then sent to Discord using Discord Bridge Chat Bot)\n# This is useful for solving captchas which use maps\n# The maps are rendered into Rendered_Maps folder if the Save_To_File is enabled.\n# NOTE:\n# If some servers have a very short time for solving captchas, enabe Auto_Render_On_Update to see them immediatelly in the console.\n# /!\ Make sure server rules allow bots to be used on the server, or you risk being punished. config.ChatBot.Map.Render_In_Console=Whether to render the map in the console. -config.ChatBot.Map.Save_To_File=Whether to store the rendered map as a file. +config.ChatBot.Map.Save_To_File=Whether to store the rendered map as a file (You need this setting if you want to get a map on Discord using Discord Bridge). config.ChatBot.Map.Auto_Render_On_Update=Automatically render the map once it is received or updated from/by the server -config.ChatBot.Map.Delete_All_On_Unload=Delete all rendered maps on unload/reload (Does not delete the images if you exit the client) +config.ChatBot.Map.Delete_All_On_Unload=Delete all rendered maps on unload/reload or when you launch the MCC again. config.ChatBot.Map.Notify_On_First_Update=Get a notification when you have gotten a map from the server for the first time +config.ChatBot.Map.Resize_To=The size that a rendered image should be resized to, in pixels (eg. 512). +config.ChatBot.Map.Rasize_Rendered_Image=Resize an rendered image, this is useful when images that are rendered are small and when are being sent to Discord. +config.ChatBot.Map.Send_Rendered_To_Bridges=Send a rendered map (saved to a file) to a Discord or a Telegram channel via the Discord or Telegram Bride chat bot (The Discord/Telegram Bridge chat bot must be enabled and configured!)\n# You need to enable Save_To_File in order for this to work.\n# We also recommend turning on resizing. # ChatBot.PlayerListLogger config.ChatBot.PlayerListLogger=Log the list of players periodically into a textual file. @@ -952,6 +1034,14 @@ config.ChatBot.ReplayCapture.Backup_Interval=How long should replay file be auto # ChatBot.ScriptScheduler config.ChatBot.ScriptScheduler=Schedule commands and scripts to launch on various events such as server join, date/time or time interval\n# See https://mccteam.github.io/guide/chat-bots.html#script-scheduler for more info +# ChatBot.TelegramBridge +config.ChatBot.TelegramBridge=This bot allows you to send and receive messages and commands via a Telegram Bot DM or to receive messages in a Telegram channel.\n# /!\ NOTE: You can't send messages and commands from a group channel, you can only send them in the bot DM, but you can get the messages from the client in a group channel.\n#-----------------------------------------------------------\n# Setup:\n# First you need to create a Telegram bot and obtain an API key, to do so, go to Telegram and find @botfather\n# Click on "Start" button and read the bot reply, then type "/newbot", the Botfather will guide you through the bot creation.\n# Once you create the bot, copy the API key that you have gotten, and put it into the "Token" field of "ChatBot.TelegramBridge" section (this section).\n# /!\ Do not share this token with anyone else as it will give them the control over your bot. Save it securely.\n# Then launch the client and go to Telegram, find your newly created bot by searching for it with its username, and open a DM with it.\n# Click on "Start" button and type and send the following command ".chatid" to obtain the chat id. \n# Copy the chat id number (eg. 2627844670) and paste it in the "ChannelId" field and add it to the "Authorized_Chat_Ids" field (in this section) (an id in "Authorized_Chat_Ids" field is a number/long, not a string!), then save the file.\n# Now you can use the bot using it's DM.\n# /!\ If you do not add the id of your chat DM with the bot to the "Authorized_Chat_Ids" field, ayone who finds your bot via search will be able to execute commands and send messages!\n# /!\ An id pasted in to the "Authorized_Chat_Ids" should be a number/long, not a string!\n#-----------------------------------------------------------\n# NOTE: If you want to recieve messages to a group channel instead, make the channel temporarely public, invite the bot to it and make it an administrator, then set the channel to private if you want.\n# Then set the "ChannelId" field to the @ of your channel (you must include the @ in the settings, eg. "@mysupersecretchannel"), this is the username you can see in the invite link of the channel.\n# /!\ Only include the username with @ prefix, do not include the rest of the link. Example if you have "https://t.me/mysupersecretchannel", the "ChannelId" will be "@mysupersecretchannel".\n# /!\ Note that you will not be able to send messages to the client from a group channel!\n#-----------------------------------------------------------\n# How to use the bot:\n# To execute an MCC command, prefix it with a dot ".", example: ".move 143 64 735" .\n# To send a message, simply type it out and hit enter. +config.ChatBot.TelegramBridge.Token=Your Telegram Bot token. +config.ChatBot.TelegramBridge.ChannelId=An ID of a channel where you want to interact with the MCC using the bot. +config.ChatBot.TelegramBridge.Authorized_Chat_Ids=A list of Chat IDs that are allowed to send messages and execute commands. To get an id of your chat DM with the bot use ".chatid" bot command in Telegram. +config.ChatBot.TelegramBridge.MessageSendTimeout=How long to wait (in seconds) if a message can not be sent to Telegram before canceling the task (minimum 1 second). +config.ChatBot.TelegramBridge.Formats=Message formats\n# Words wrapped with { and } are going to be replaced during the code execution, do not change them!\n# For example. {message} is going to be replace with an actual message, {username} will be replaced with an username, {timestamp} with the current time.\n# For Telegram message formatting, check the following: https://sendpulse.com/blog/telegram-text-formatting + # ChatBot.WebSocketBot config.ChatBot.WebSocketBot=Remotely control the client using Web Sockets.\n# This is useful if you want to implement an application that can remotely and asynchronously execute procedures in MCC.\n# Example implementation written in JavaScript: https://github.com/milutinke/MCC.js.git\n# The protocol specification will be available in the documentation soon. config.ChatBot.WebSocketBot.Ip=The IP address that Websocket server will be bounded to. diff --git a/MinecraftClient/Resources/lang/fr.ini b/MinecraftClient/Resources/lang/fr.ini index 3ab0cab9..307b70fd 100644 Binary files a/MinecraftClient/Resources/lang/fr.ini and b/MinecraftClient/Resources/lang/fr.ini differ diff --git a/MinecraftClient/Resources/lang/ru.ini b/MinecraftClient/Resources/lang/ru.ini index a5cfb829..617a888e 100644 Binary files a/MinecraftClient/Resources/lang/ru.ini and b/MinecraftClient/Resources/lang/ru.ini differ diff --git a/MinecraftClient/Resources/lang/vi.ini b/MinecraftClient/Resources/lang/vi.ini index 0d10fdd6..d7a5164a 100644 Binary files a/MinecraftClient/Resources/lang/vi.ini and b/MinecraftClient/Resources/lang/vi.ini differ diff --git a/MinecraftClient/Resources/lang/zh-Hans.ini b/MinecraftClient/Resources/lang/zh-Hans.ini index fbef6878..a4ac3b53 100644 Binary files a/MinecraftClient/Resources/lang/zh-Hans.ini and b/MinecraftClient/Resources/lang/zh-Hans.ini differ diff --git a/MinecraftClient/Resources/lang/zh-Hant.ini b/MinecraftClient/Resources/lang/zh-Hant.ini index 29b3bf29..9ac26a7c 100644 Binary files a/MinecraftClient/Resources/lang/zh-Hant.ini and b/MinecraftClient/Resources/lang/zh-Hant.ini differ diff --git a/MinecraftClient/Scripting/AssemblyResolver.cs b/MinecraftClient/Scripting/AssemblyResolver.cs new file mode 100644 index 00000000..f9f970ea --- /dev/null +++ b/MinecraftClient/Scripting/AssemblyResolver.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace MinecraftClient.Scripting; + +public static class AssemblyResolver { + private static Dictionary ScriptAssemblies = new(); + static AssemblyResolver() { + // Manually resolve assemblies that .NET can't resolve automatically. + AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => + { + var asmReqName = new AssemblyName(args.Name); + + // Check the script-referenced assemblies if we have the DLL that is required. + foreach (var dll in ScriptAssemblies) + { + // If we have the assembly, load it. + if (asmReqName.FullName == dll.Key) + { + return Assembly.LoadFile(dll.Value); + } + } + + ConsoleIO.WriteLogLine($"[Script Error] Failed to resolve assembly {args.Name} (are you missing a DLL file?)"); + return null; + }; + } + + internal static void AddAssembly(string AssemblyFullName, string AssemblyPath) + { + if (ScriptAssemblies.ContainsKey(AssemblyFullName)) + return; + + ScriptAssemblies.Add(AssemblyFullName, AssemblyPath); + } +} \ No newline at end of file diff --git a/MinecraftClient/Scripting/CSharpRunner.cs b/MinecraftClient/Scripting/CSharpRunner.cs index c3dd20f5..87857b5f 100644 --- a/MinecraftClient/Scripting/CSharpRunner.cs +++ b/MinecraftClient/Scripting/CSharpRunner.cs @@ -26,7 +26,7 @@ namespace MinecraftClient /// Set to false to compile and cache the script without launching it /// Thrown if an error occured /// Result of the execution, returned by the script - public static object? Run(ChatBot apiHandler, string[] lines, string[] args, Dictionary? localVars, bool run = true) + public static object? Run(ChatBot apiHandler, string[] lines, string[] args, Dictionary? localVars, bool run = true, string scriptName = "Unknown Script") { //Script compatibility check for handling future versions differently if (lines.Length < 1 || lines[0] != "//MCCScript 1.0") @@ -102,13 +102,24 @@ namespace MinecraftClient "}}", }); + ConsoleIO.WriteLogLine($"[Script] Starting compilation for {scriptName}..."); + //Compile the C# class in memory using all the currently loaded assemblies - var result = compiler.Compile(code, Guid.NewGuid().ToString()); + var result = compiler.Compile(code, Guid.NewGuid().ToString(), dlls); //Process compile warnings and errors - if (result.Failures != null) - throw new CSharpException(CSErrorType.LoadError, - new InvalidOperationException(result.Failures[0].GetMessage())); + if (result.Failures != null) { + + ConsoleIO.WriteLogLine("[Script] Compilation failed with error(s):"); + + foreach (var failure in result.Failures) { + ConsoleIO.WriteLogLine($"[Script] Error in {scriptName}, line:col{failure.Location.GetMappedLineSpan()}: [{failure.Id}] {failure.GetMessage()}"); + } + + throw new CSharpException(CSErrorType.InvalidScript, new InvalidProgramException("Compilation failed due to error.")); + } + + ConsoleIO.WriteLogLine("[Script] Compilation done with no errors."); //Retrieve compiled assembly assembly = result.Assembly; @@ -385,7 +396,7 @@ namespace MinecraftClient { throw new CSharpException(CSErrorType.FileReadError, e); } - return CSharpRunner.Run(this, lines, args, localVars); + return CSharpRunner.Run(this, lines, args, localVars, scriptName: script); } } } \ No newline at end of file diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index 8ce7de16..f014676a 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -835,10 +835,11 @@ namespace MinecraftClient /// Log text to write protected void LogToConsole(object? text) { + string botName = Translations.GetOrNull("botname." + GetType().Name) ?? GetType().Name; if (_handler == null || master == null) - ConsoleIO.WriteLogLine(String.Format("[{0}] {1}", GetType().Name, text)); + ConsoleIO.WriteLogLine(String.Format("[{0}] {1}", botName, text)); else - Handler.Log.Info(String.Format("[{0}] {1}", GetType().Name, text)); + Handler.Log.Info(String.Format("[{0}] {1}", botName, text)); string logfile = Settings.Config.AppVar.ExpandVars(Config.Main.Advanced.ChatbotLogFile); if (!String.IsNullOrEmpty(logfile)) @@ -913,7 +914,10 @@ namespace MinecraftClient protected void ReconnectToTheServer(int ExtraAttempts = 3, int delaySeconds = 0) { if (Settings.Config.Logging.DebugMessages) - ConsoleIO.WriteLogLine(Translations.Get("chatbot.reconnect", GetType().Name)); + { + string botName = Translations.GetOrNull("botname." + GetType().Name) ?? GetType().Name; + ConsoleIO.WriteLogLine(Translations.Get("chatbot.reconnect", botName)); + } McClient.ReconnectionAttemptsLeft = ExtraAttempts; Program.Restart(delaySeconds); } @@ -1129,14 +1133,15 @@ namespace MinecraftClient /// protected static string GetTimestamp() { - DateTime time = DateTime.Now; - return String.Format("{0}-{1}-{2} {3}:{4}:{5}", - time.Year.ToString("0000"), - time.Month.ToString("00"), - time.Day.ToString("00"), - time.Hour.ToString("00"), - time.Minute.ToString("00"), - time.Second.ToString("00")); + return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + } + + /// + /// Get a h:m:s timestamp representing the current system time + /// + protected static string GetShortTimestamp() + { + return DateTime.Now.ToString("HH:mm:ss"); } /// diff --git a/MinecraftClient/Scripting/DynamicRun/Builder/CompileRunner.cs b/MinecraftClient/Scripting/DynamicRun/Builder/CompileRunner.cs index 555622fc..8a1e492c 100644 --- a/MinecraftClient/Scripting/DynamicRun/Builder/CompileRunner.cs +++ b/MinecraftClient/Scripting/DynamicRun/Builder/CompileRunner.cs @@ -24,7 +24,7 @@ namespace DynamicRun.Builder GC.WaitForPendingFinalizers(); } - ConsoleIO.WriteLogLine(assemblyLoadContextWeakRef.Item1.IsAlive ? "Script continues to run." : "Script finished!"); + ConsoleIO.WriteLogLine(assemblyLoadContextWeakRef.Item1.IsAlive ? "[Script] Script continues to run." : "[Script] Script finished!"); return assemblyLoadContextWeakRef.Item2; } diff --git a/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs b/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs index 0ade609f..4bb02b07 100644 --- a/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs +++ b/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs @@ -15,23 +15,20 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; using MinecraftClient; +using MinecraftClient.Scripting; using SingleFileExtractor.Core; namespace DynamicRun.Builder { internal class Compiler { - public CompileResult Compile(string filepath, string fileName) + public CompileResult Compile(string filepath, string fileName, List additionalAssemblies) { - ConsoleIO.WriteLogLine($"Starting compilation..."); - using var peStream = new MemoryStream(); - var result = GenerateCode(filepath, fileName).Emit(peStream); + var result = GenerateCode(filepath, fileName, additionalAssemblies).Emit(peStream); if (!result.Success) { - ConsoleIO.WriteLogLine("Compilation done with error."); - var failures = result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error); return new CompileResult() @@ -41,9 +38,7 @@ namespace DynamicRun.Builder Failures = failures.ToList() }; } - - ConsoleIO.WriteLogLine("Compilation done without any error."); - + peStream.Seek(0, SeekOrigin.Begin); return new CompileResult() @@ -54,24 +49,37 @@ namespace DynamicRun.Builder }; } - private static CSharpCompilation GenerateCode(string sourceCode, string fileName) + private static CSharpCompilation GenerateCode(string sourceCode, string fileName, List additionalAssemblies) { var codeString = SourceText.From(sourceCode); var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp9); var parsedSyntaxTree = SyntaxFactory.ParseSyntaxTree(codeString, options); + + var references = new List(); - var mods = Assembly.GetEntryAssembly()!.GetModules(); - + // Find if any additional assembly DLL exists in the base directory where the .exe exists. + foreach (var assembly in additionalAssemblies) + { + var dllPath = Path.Combine(AppContext.BaseDirectory, assembly); + if (File.Exists(dllPath)) + { + references.Add(MetadataReference.CreateFromFile(dllPath)); + // Store the reference in our Assembly Resolver for future reference. + AssemblyResolver.AddAssembly(Assembly.LoadFile(dllPath).FullName!, dllPath); + } + else + { + ConsoleIO.WriteLogLine($"[Script Error] {assembly} is defined in script, but cannot find DLL! Script may not run."); + } + } #pragma warning disable IL3000 // We determine if we are in a self-contained binary by checking specifically if the Assembly file path is null. var SystemPrivateCoreLib = typeof(object).Assembly.Location; // System.Private.CoreLib var SystemConsole = typeof(Console).Assembly.Location; // System.Console var MinecraftClientDll = typeof(Program).Assembly.Location; // The path to MinecraftClient.dll - - var references = new List(); - + // We're on a self-contained binary, so we need to extract the executable to get the assemblies. if (string.IsNullOrEmpty(MinecraftClientDll)) { diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 9671c235..b2749bfa 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -387,6 +387,7 @@ namespace MinecraftClient { string[] sip = General.Server.Host.Split(new[] { ":", ":" }, StringSplitOptions.None); General.Server.Host = sip[0]; + InternalConfig.ServerIP = General.Server.Host; if (sip.Length > 1) { @@ -395,7 +396,10 @@ namespace MinecraftClient } } - SetServerIP(General.Server, true); + if (General.Server.Port.HasValue) + InternalConfig.ServerPort = General.Server.Port.Value; + else + SetServerIP(General.Server, true); for (int i = 0; i < Advanced.BotOwners.Count; ++i) Advanced.BotOwners[i] = ToLowerIfNeed(Advanced.BotOwners[i]); @@ -1090,6 +1094,13 @@ namespace MinecraftClient set { ChatBots.ChatLog.Config = value; ChatBots.ChatLog.Config.OnSettingUpdate(); } } + [TomlPrecedingComment("$config.ChatBot.DiscordBridge$")] + public ChatBots.DiscordBridge.Configs DiscordBridge + { + get { return ChatBots.DiscordBridge.Config; } + set { ChatBots.DiscordBridge.Config = value; ChatBots.DiscordBridge.Config.OnSettingUpdate(); } + } + [TomlPrecedingComment("$config.ChatBot.Farmer$")] public ChatBots.Farmer.Configs Farmer { @@ -1153,6 +1164,13 @@ namespace MinecraftClient set { ChatBots.ScriptScheduler.Config = value; ChatBots.ScriptScheduler.Config.OnSettingUpdate(); } } + [TomlPrecedingComment("$config.ChatBot.TelegramBridge$")] + public ChatBots.TelegramBridge.Configs TelegramBridge + { + get { return ChatBots.TelegramBridge.Config; } + set { ChatBots.TelegramBridge.Config = value; ChatBots.TelegramBridge.Config.OnSettingUpdate(); } + } + [TomlPrecedingComment("$config.ChatBot.WebSocketBot$")] public ChatBots.WebSocketBot.Configs WebSocketBot { diff --git a/MinecraftClient/Translations.cs b/MinecraftClient/Translations.cs index c66554ac..16decad2 100644 --- a/MinecraftClient/Translations.cs +++ b/MinecraftClient/Translations.cs @@ -683,6 +683,7 @@ namespace MinecraftClient } StringBuilder sb = new(); int total = 0, translated = 0; + int total_char = 0, translated_char = 0; for (int i = 0; i < transEn.Length; ++i) { string line = transEn[i].Trim(); @@ -693,23 +694,22 @@ namespace MinecraftClient } else { + int en_value_len = line.Length - index; string key = line[..index]; sb.Append(key).Append('='); if (trans.TryGetValue(key, out string? value)) { sb.Append(value.Replace("\n", "\\n")); - ++total; ++translated; + translated_char += en_value_len; } - else - { - ++total; - } + ++total; + total_char += en_value_len; } sb.AppendLine(); } File.WriteAllText(fileName, sb.ToString(), Encoding.Unicode); - ConsoleIO.WriteLine(string.Format("Language {0}: Translated {1} of {2}, {3:0.00}%", lang, translated, total, 100.0 * (double)translated / total)); + ConsoleIO.WriteLine(string.Format("Language {0}: Translated {1} of {2}, {3:0.00}%", lang, translated, total, 100.0 * translated_char / total_char)); } } diff --git a/README.md b/README.md index 7d49547e..222b40a1 100644 --- a/README.md +++ b/README.md @@ -52,13 +52,13 @@ If you'd like to contribute to Minecraft Console Client, great, just fork the re Check out: [How to update or add translations for MCC](https://mccteam.github.io/guide/contibuting.html#translations). MCC now supports the following languages (Alphabetical order) : - * `de.ini` (51.34% translated) : Deutsch - German + * `de.ini` (30.21% translated) : Deutsch - German * `en.ini` : English - English - * `fr.ini` (51.34% translated) : Français (France) - French - * `ru.ini` (50.49% translated) : Русский (Russkiy) - Russian - * `vi.ini` (50.49% translated) : Tiếng Việt (Việt Nam) - Vietnamese - * `zh-Hans.ini` (95.50% translated) : 简体中文 - Chinese Simplified - * `zh-Hant.ini` (95.50% translated) : 繁體中文 - Chinese Traditional + * `fr.ini` (30.21% translated) : Français (France) - French + * `ru.ini` (29.65% translated) : Русский (Russkiy) - Russian + * `vi.ini` (29.65% translated) : Tiếng Việt (Việt Nam) - Vietnamese + * `zh-Hans.ini` (87.08% translated) : 简体中文 - Chinese Simplified + * `zh-Hant.ini` (87.08% translated) : 繁體中文 - Chinese Traditional ## Building from the source 🏗️