diff --git a/MinecraftClient/ChatBots/Map.cs b/MinecraftClient/ChatBots/Map.cs new file mode 100644 index 00000000..0969daba --- /dev/null +++ b/MinecraftClient/ChatBots/Map.cs @@ -0,0 +1,310 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MinecraftClient.Mapping; +using System.Drawing; +using MinecraftClient.Protocol.Handlers; + +namespace MinecraftClient.ChatBots +{ + class Map : ChatBot + { + private string baseDirectory = @"Rendered_Maps"; + + private Dictionary cachedMaps = new(); + private bool shouldResize = true; + private int resizeTo = 256; + private bool autoRenderOnUpdate = true; + private bool deleteAllOnExit = true; + private bool notifyOnFirstUpdate = true; + + public override void Initialize() + { + if (!Directory.Exists(baseDirectory)) + Directory.CreateDirectory(baseDirectory); + + shouldResize = Settings.Map_Should_Resize; + resizeTo = Settings.Map_Resize_To; + + if (resizeTo < 128) + resizeTo = 128; + + autoRenderOnUpdate = Settings.Map_Auto_Render_On_Update; + deleteAllOnExit = Settings.Map_Delete_All_On_Unload; + notifyOnFirstUpdate = Settings.Map_Notify_On_First_Update; + + RegisterChatBotCommand("maps", "bot.map.cmd.desc", "maps > | maps >", OnMapCommand); + } + + public override void OnUnload() + { + if (deleteAllOnExit) + { + DirectoryInfo di = new DirectoryInfo(baseDirectory); + FileInfo[] files = di.GetFiles(); + + foreach (FileInfo file in files) + file.Delete(); + } + } + + public string OnMapCommand(string command, string[] args) + { + if (args.Length == 0 || (args.Length == 1 && (args[0].ToLower().Equals("list") || args[0].ToLower().Equals("l")))) + { + if (cachedMaps.Count == 0) + return Translations.TryGet("bot.map.no_maps"); + + LogToConsoleTranslated("bot.map.received"); + + foreach (var (key, value) in new SortedDictionary(cachedMaps)) + LogToConsoleTranslated("bot.map.list_item", key, value.LastUpdated); + + return ""; + } + + if (args.Length > 1) + { + if (args[0].ToLower().Equals("render") || args[0].ToLower().Equals("r")) + { + if (args.Length < 2) + return "maps > | maps >"; + + if (int.TryParse(args[1], out int mapId)) + { + if (!cachedMaps.ContainsKey(mapId)) + return Translations.TryGet("bot.map.cmd.not_found", mapId); + + try + { + McMap map = cachedMaps[mapId]; + GenerateMapImage(map); + } + catch (Exception e) + { + LogDebugToConsole(e.StackTrace!); + return Translations.TryGet("bot.map.failed_to_render", mapId); + } + + return ""; + } + + return Translations.TryGet("bot.map.cmd.invalid_id"); + } + } + + return ""; + } + + public override void OnMapData(int mapid, byte scale, bool trackingPosition, bool locked, List icons, byte columnsUpdated, byte rowsUpdated, byte mapCoulmnX, byte mapRowZ, byte[]? colors) + { + if (columnsUpdated == 0 && cachedMaps.ContainsKey(mapid)) + return; + + McMap map = new McMap(); + + map.MapId = mapid; + map.Scale = scale; + map.TrackingPosition = trackingPosition; + map.Locked = locked; + map.MapIcons = icons; + map.Width = rowsUpdated; + map.Height = columnsUpdated; + map.X = mapCoulmnX; + map.Z = mapRowZ; + map.Colors = colors; + map.LastUpdated = DateTime.Now; + + if (!cachedMaps.ContainsKey(mapid)) + { + cachedMaps.Add(mapid, map); + + if (notifyOnFirstUpdate) + LogToConsoleTranslated("bot.map.received_map", map.MapId); + } + else + { + cachedMaps.Remove(mapid); + cachedMaps.Add(mapid, map); + } + + if (autoRenderOnUpdate) + GenerateMapImage(map); + } + + private void GenerateMapImage(McMap map) + { + string fileName = baseDirectory + "/Map_" + map.MapId + ".jpg"; + + if (File.Exists(fileName)) + File.Delete(fileName); + + Bitmap image = new Bitmap(map.Width, map.Height); + + for (int x = 0; x < map.Width; ++x) + { + for (int y = 0; y < map.Height; ++y) + { + byte inputColor = map.Colors![x + y * map.Width]; + ColorRGBA color = MapColors.ColorByteToRGBA(inputColor); + + if (color.Unknown) + { + string hexCode = new DataTypes(GetProtocolVersion()).ByteArrayToString(new byte[] { inputColor }); + LogDebugToConsole("Unknown color encountered: " + inputColor + " (Hex: " + hexCode + "), using: RGB(248, 0, 248)"); + } + + image.SetPixel(x, y, Color.FromArgb(color.A, color.R, color.G, color.B)); + } + } + + // Resize, double the image + + if (shouldResize) + image = ResizeBitmap(image, resizeTo, resizeTo); + + image.Save(fileName); + LogToConsole(Translations.TryGet("bot.map.rendered", map.MapId, fileName)); + } + private Bitmap ResizeBitmap(Bitmap sourceBMP, int width, int height) + { + Bitmap result = new Bitmap(width, height); + using (Graphics g = Graphics.FromImage(result)) + g.DrawImage(sourceBMP, 0, 0, width, height); + return result; + } + } + + internal class McMap + { + public int MapId { get; set; } + public byte Scale { get; set; } + public bool TrackingPosition { get; set; } + public bool Locked { get; set; } + public List? MapIcons { get; set; } + public byte Width { get; set; } // rows + public byte Height { get; set; } // columns + public byte X { get; set; } + public byte Z { get; set; } + public byte[]? Colors; + public DateTime LastUpdated { get; set; } + } + + class ColorRGBA + { + public byte R { get; set; } + public byte G { get; set; } + public byte B { get; set; } + public byte A { get; set; } + public bool Unknown { get; set; } = false; + } + + class MapColors + { + // When colors are updated in a new update, you can get them using the game code: net\minecraft\world\level\material\MaterialColor.java + public static Dictionary Colors = new() + { + //Color ID R G B + {0, new byte[]{0, 0, 0}}, + {1, new byte[]{127, 178, 56}}, + {2, new byte[]{247, 233, 163}}, + {3, new byte[]{199, 199, 199}}, + {4, new byte[]{255, 0, 0}}, + {5, new byte[]{160, 160, 255}}, + {6, new byte[]{167, 167, 167}}, + {7, new byte[]{0, 124, 0}}, + {8, new byte[]{255, 255, 255}}, + {9, new byte[]{164, 168, 184}}, + {10, new byte[]{151, 109, 77}}, + {11, new byte[]{112, 112, 112}}, + {12, new byte[]{64, 64, 255}}, + {13, new byte[]{143, 119, 72}}, + {14, new byte[]{255, 252, 245}}, + {15, new byte[]{216, 127, 51}}, + {16, new byte[]{178, 76, 216}}, + {17, new byte[]{102, 153, 216}}, + {18, new byte[]{229, 229, 51}}, + {19, new byte[]{127, 204, 25}}, + {20, new byte[]{242, 127, 165}}, + {21, new byte[]{76, 76, 76}}, + {22, new byte[]{153, 153, 153}}, + {23, new byte[]{76, 127, 153}}, + {24, new byte[]{127, 63, 178}}, + {25, new byte[]{51, 76, 178}}, + {26, new byte[]{102, 76, 51}}, + {27, new byte[]{102, 127, 51}}, + {28, new byte[]{153, 51, 51}}, + {29, new byte[]{25, 25, 25}}, + {30, new byte[]{250, 238, 77}}, + {31, new byte[]{92, 219, 213}}, + {32, new byte[]{74, 128, 255}}, + {33, new byte[]{0, 217, 58}}, + {34, new byte[]{129, 86, 49}}, + {35, new byte[]{112, 2, 0}}, + {36, new byte[]{209, 177, 161}}, + {37, new byte[]{159, 82, 36}}, + {38, new byte[]{149, 87, 108}}, + {39, new byte[]{112, 108, 138}}, + {40, new byte[]{186, 133, 36}}, + {41, new byte[]{103, 117, 53}}, + {42, new byte[]{160, 77, 78}}, + {43, new byte[]{57, 41, 35}}, + {44, new byte[]{135, 107, 98}}, + {45, new byte[]{87, 92, 92}}, + {46, new byte[]{122, 73, 88}}, + {47, new byte[]{76, 62, 92}}, + {48, new byte[]{76, 50, 35}}, + {49, new byte[]{76, 82, 42}}, + {50, new byte[]{142, 60, 46}}, + {51, new byte[]{37, 22, 16}}, + {52, new byte[]{189, 48, 49}}, + {53, new byte[]{148, 63, 97}}, + {54, new byte[]{92, 25, 29}}, + {55, new byte[]{22, 126, 134}}, + {56, new byte[]{58, 142, 140}}, + {57, new byte[]{86, 44, 62}}, + {58, new byte[]{20, 180, 133}}, + {59, new byte[]{100, 100, 100}}, + {60, new byte[]{216, 175, 147}}, + {61, new byte[]{127, 167, 150}} + }; + + public static ColorRGBA ColorByteToRGBA(byte receivedColorId) + { + // Divide received color id by 4 to get the base color id + // Much thanks to DevBobcorn + byte baseColorId = (byte)(receivedColorId >> 2); + + // Any new colors that we haven't added will be purple like in the missing CS: Source Texture + if (!Colors.ContainsKey(baseColorId)) + return new ColorRGBA { R = 248, G = 0, B = 248, A = 255, Unknown = true }; + + byte shadeId = (byte)(receivedColorId % 4); + byte shadeMultiplier = 255; + + switch (shadeId) + { + case 0: + shadeMultiplier = 180; + break; + + case 1: + shadeMultiplier = 220; + break; + + case 3: + // NOTE: If we ever add map support below 1.8, this needs to be 220 before 1.8 + shadeMultiplier = 135; + break; + } + + return new ColorRGBA + { + R = (byte)((Colors[baseColorId][0] * shadeMultiplier) / 255), + G = (byte)((Colors[baseColorId][1] * shadeMultiplier) / 255), + B = (byte)((Colors[baseColorId][2] * shadeMultiplier) / 255), + A = 255 + }; + } + } +} diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 2b9e419d..f5305d2e 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -288,6 +288,7 @@ namespace MinecraftClient if (Settings.AutoDrop_Enabled) { BotLoad(new AutoDrop(Settings.AutoDrop_Mode, Settings.AutoDrop_items)); } if (Settings.ReplayMod_Enabled && reload) { BotLoad(new ReplayCapture(Settings.ReplayMod_BackupInterval)); } if (Settings.FollowPlayer_Enabled) { BotLoad(new FollowPlayer(Settings.FollowPlayer_UpdateLimit, Settings.FollowPlayer_UpdateLimit)); } + if (Settings.Map_Enabled) { BotLoad(new Map()); } //Add your ChatBot here by uncommenting and adapting //BotLoad(new ChatBots.YourBot()); diff --git a/MinecraftClient/Resources/config/MinecraftClient.ini b/MinecraftClient/Resources/config/MinecraftClient.ini index 1fdb59bb..55562ea7 100644 --- a/MinecraftClient/Resources/config/MinecraftClient.ini +++ b/MinecraftClient/Resources/config/MinecraftClient.ini @@ -290,4 +290,20 @@ stop_at_distance=3 # Do not follow the player if he is in the ran # Log the list of players periodically into a textual file enabled=false log_file=playerlog.txt -log_delay=600 # 10 = 1s \ No newline at end of file +log_delay=600 # 10 = 1s + +[Map] +# Allows you to render maps into .jpg images +# This is useful for solving captchas which use maps +# NOTE: This is a new feature, we could not find the proper color mappings, but we are continuing with the search +# The colors are not like in minecraft and might look ugly +# This feature is currently only useful for solving captchas, which is it's primary purpose for the time being. +# If some servers have a very short time for solving captchas, enabe auto_render_on_update and prepare to open the file quickly. +# On linux you can use FTP to access generated files. +enabled=false +resize_map=false # Should the map be resized? (Default one is small 128x128) +resize_to=256 # The size to resize the map to (Note: the bigger it is, the lower the quallity is) +auto_render_on_update=false # Automatically render the map once it's received or updated from/by the server +delete_rendered_on_unload=true # Delete all rendered maps on unload or exit +notify_on_first_update=false # Get a notification when you have gotten a map from the server for the first time + # Note: Will be printed for each map in vicinity, could cause spam if there are a lot of maps \ No newline at end of file diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 0ee5163d..4b80c088 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -431,7 +431,6 @@ cmd.useitem.use=Used an item bot.autoAttack.mode=Unknown attack mode: {0}. Using single mode as default. bot.autoAttack.priority=Unknown priority: {0}. Using distance priority as default. bot.autoAttack.invalidcooldown=Attack cooldown value cannot be smaller than 0. Using auto as default -bot.autoAttack.invalidlist=Invalid list type provided, using the default list mode of: 'blacklist' # AutoCraft bot.autoCraft.cmd=Auto-crafting ChatBot command @@ -556,6 +555,17 @@ bot.mailer.cmd.ignore.removed=Removed {0} from the ignore list! bot.mailer.cmd.ignore.invalid=Missing or invalid name. Usage: {0} bot.mailer.cmd.help=See usage +# Maps +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! +bot.map.received=The list of received maps from the server: +bot.map.no_maps=No maps received! +bot.map.received_map=Received a new Map, with Id: {0} +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}) + # ReplayCapture bot.replayCapture.cmd=replay command bot.replayCapture.created=Replay file created. diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 4704661a..29011834 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -255,6 +255,14 @@ namespace MinecraftClient public static int FollowPlayer_UpdateLimit = 10; public static int FollowPlayer_StopAtDistance = 3; + // Map + public static bool Map_Enabled = false; + public static bool Map_Should_Resize = false; + public static int Map_Resize_To = 256; + public static bool Map_Auto_Render_On_Update = false; + public static bool Map_Delete_All_On_Unload = true; + public static bool Map_Notify_On_First_Update = true; + //Custom app variables and Minecraft accounts private static readonly Dictionary AppVars = new Dictionary(); private static readonly Dictionary> Accounts = new Dictionary>(); @@ -264,7 +272,7 @@ namespace MinecraftClient private static string ServerAliasTemp = null; //Mapping for settings sections in the INI file - private enum Section { Default, Main, AppVars, Proxy, MCSettings, AntiAFK, Hangman, Alerts, ChatLog, AutoRelog, ScriptScheduler, RemoteControl, ChatFormat, AutoRespond, AutoAttack, AutoFishing, AutoEat, AutoCraft, AutoDrop, Mailer, ReplayMod, FollowPlayer, PlayerListLogger, Logging, Signature }; + private enum Section { Default, Main, AppVars, Proxy, MCSettings, AntiAFK, Hangman, Alerts, ChatLog, AutoRelog, ScriptScheduler, RemoteControl, ChatFormat, AutoRespond, AutoAttack, AutoFishing, AutoEat, AutoCraft, AutoDrop, Mailer, ReplayMod, FollowPlayer, PlayerListLogger, Map, Logging, Signature }; /// /// Get settings section from name @@ -880,6 +888,17 @@ namespace MinecraftClient case "log_delay": PlayerLog_Delay = str2int(argValue); return true; } break; + case Section.Map: + switch (ToLowerIfNeed(argName)) + { + case "enabled": Map_Enabled = str2bool(argValue); return true; + case "resize_map": Map_Should_Resize = str2bool(argValue); return true; + case "resize_to": Map_Resize_To = str2int(argValue); return true; + case "auto_render_on_update": Map_Auto_Render_On_Update = str2bool(argValue); return true; + case "delete_rendered_on_unload": Map_Delete_All_On_Unload = str2bool(argValue); return true; + case "notify_on_first_update": Map_Notify_On_First_Update = str2bool(argValue); return true; + } + break; } return false; }