Merge branch 'master' into rpc

This commit is contained in:
Anon 2022-10-24 20:45:40 +00:00 committed by GitHub
commit 477da50fe0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1230 additions and 116 deletions

View file

@ -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<ItemType, uint> 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<KeyValuePair<ItemType, uint>> 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<int, object?> 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,6 +633,7 @@ 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(),
#pragma warning restore format // @formatter:on

View file

@ -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 <both|mc|discord>", 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 <both|mc|discord>";
}
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<string, Stream>() { { $"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;
}
}
}
}

View file

@ -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<int, McMap> cachedMaps = new();
private readonly Queue<QueuedMap> discordQueue = new();
public override void Initialize()
{
if (!Directory.Exists(baseDirectory))
Directory.CreateDirectory(baseDirectory);
DeleteRenderedMaps();
RegisterChatBotCommand("maps", "bot.map.cmd.desc", "maps list|render <id> or maps l|r <id>", 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)

View file

@ -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)
{

View file

@ -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<long>();
[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 <both|mc|telegram>", 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 <both|mc|discord>";
}
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<UpdateType>()
},
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;
}
}
}

View file

@ -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()

View file

@ -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());
}

View file

@ -35,7 +35,9 @@
<ItemGroup>
<PackageReference Include="DnsClient" Version="1.6.1" />
<PackageReference Include="DotNetZip" Version="1.16.0" />
<PackageReference Include="DSharpPlus" Version="4.2.0" />
<PackageReference Include="DynamicExpresso.Core" Version="2.13.0" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="12.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="6.0.0" />
@ -46,6 +48,7 @@
<NoWarn>NU1701</NoWarn>
</PackageReference>
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageReference Include="Telegram.Bot" Version="18.0.0" />
</ItemGroup>
<ItemGroup>
<Compile Remove="config\ChatBots\AutoLook.cs" />

View file

@ -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));
}
/// <summary>

View file

@ -477,20 +477,24 @@ cmd.useitem.use=Used an item
# ChatBots. Naming style: bot.<className>.<msg...>
# 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 <command name>
# 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 <cmd name> 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 <command name>
# 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 <player name|stop> [-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} <username>
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 <player name|stop> [-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.

View file

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace MinecraftClient.Scripting;
public static class AssemblyResolver {
private static Dictionary<string, string> 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);
}
}

View file

@ -26,7 +26,7 @@ namespace MinecraftClient
/// <param name="run">Set to false to compile and cache the script without launching it</param>
/// <exception cref="CSharpException">Thrown if an error occured</exception>
/// <returns>Result of the execution, returned by the script</returns>
public static object? Run(ChatBot apiHandler, string[] lines, string[] args, Dictionary<string, object>? localVars, bool run = true)
public static object? Run(ChatBot apiHandler, string[] lines, string[] args, Dictionary<string, object>? 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);
}
}
}

View file

@ -835,10 +835,11 @@ namespace MinecraftClient
/// <param name="text">Log text to write</param>
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
/// </summary>
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");
}
/// <summary>
/// Get a h:m:s timestamp representing the current system time
/// </summary>
protected static string GetShortTimestamp()
{
return DateTime.Now.ToString("HH:mm:ss");
}
/// <summary>

View file

@ -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;
}

View file

@ -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<string> 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()
@ -42,8 +39,6 @@ namespace DynamicRun.Builder
};
}
ConsoleIO.WriteLogLine("Compilation done without any error.");
peStream.Seek(0, SeekOrigin.Begin);
return new CompileResult()
@ -54,15 +49,30 @@ namespace DynamicRun.Builder
};
}
private static CSharpCompilation GenerateCode(string sourceCode, string fileName)
private static CSharpCompilation GenerateCode(string sourceCode, string fileName, List<string> additionalAssemblies)
{
var codeString = SourceText.From(sourceCode);
var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp9);
var parsedSyntaxTree = SyntaxFactory.ParseSyntaxTree(codeString, options);
var mods = Assembly.GetEntryAssembly()!.GetModules();
var references = new List<MetadataReference>();
// 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.
@ -70,8 +80,6 @@ namespace DynamicRun.Builder
var SystemConsole = typeof(Console).Assembly.Location; // System.Console
var MinecraftClientDll = typeof(Program).Assembly.Location; // The path to MinecraftClient.dll
var references = new List<MetadataReference>();
// We're on a self-contained binary, so we need to extract the executable to get the assemblies.
if (string.IsNullOrEmpty(MinecraftClientDll))
{

View file

@ -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,6 +396,9 @@ namespace MinecraftClient
}
}
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)
@ -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
{

View file

@ -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_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));
}
}

View file

@ -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 🏗️