diff --git a/MinecraftClient/ChatBots/AutoFishing.cs b/MinecraftClient/ChatBots/AutoFishing.cs index 961a9527..b767cdc2 100644 --- a/MinecraftClient/ChatBots/AutoFishing.cs +++ b/MinecraftClient/ChatBots/AutoFishing.cs @@ -13,61 +13,237 @@ namespace MinecraftClient.ChatBots /// class AutoFishing : ChatBot { - private Entity fishingRod; - private Double fishingHookThreshold = 0.2; - private Location LastPos = new Location(); - private DateTime CaughtTime = DateTime.Now; + private int fishCount = 0; private bool inventoryEnabled; - private bool isFishing = false; - private int useItemCounter = 0; + private int castTimeout = 12; + + private bool isFishing = false, isWaitingRod = false; + private Entity? fishingBobber; + private Location LastPos = Location.Zero; + private DateTime CaughtTime = DateTime.Now; + + private int counter = 0; + private readonly object stateLock = new(); + private FishingState state = FishingState.WaitJoinGame; + + private int curLocationIdx = 0, moveDir = 1; + float nextYaw = 0, nextPitch = 0; + + private enum FishingState + { + WaitJoinGame, + WaitingToCast, + CastingRod, + WaitingFishingBobber, + WaitingFishToBite, + StartMove, + WaitingMovement, + DurabilityCheck, + Stopping, + } public override void Initialize() { if (!GetEntityHandlingEnabled()) { LogToConsoleTranslated("extra.entity_required"); - LogToConsoleTranslated("general.bot_unload"); - UnloadBot(); + state = FishingState.WaitJoinGame; } inventoryEnabled = GetInventoryEnabled(); + if (!inventoryEnabled) + LogToConsoleTranslated("bot.autoFish.no_inv_handle"); + } + + /// + /// Update settings when reloaded + /// + public /* override */ void OnSettingsReload() + { + if (Settings.AutoFishing_Enabled) + { + if (!GetEntityHandlingEnabled()) + { + LogToConsoleTranslated("extra.entity_required"); + state = FishingState.WaitJoinGame; + } + inventoryEnabled = GetInventoryEnabled(); + if (!inventoryEnabled) + LogToConsoleTranslated("bot.autoFish.no_inv_handle"); + } + else + { + UnloadBot(); + return; + } + } + + private void StartFishing() + { + isFishing = false; + if (Settings.AutoFishing_AutoStart) + { + double delay = Settings.AutoFishing_FishingDelay; + LogToConsole(Translations.Get("bot.autoFish.start", delay)); + lock (stateLock) + { + counter = (int)(delay * 10); + state = FishingState.StartMove; + } + } + else + { + lock (stateLock) + { + state = FishingState.WaitJoinGame; + } + } + } + + private void StopFishing() + { + isFishing = false; + lock (stateLock) + { + state = FishingState.Stopping; + } + } + + private void UseFishRod() + { + if (Settings.AutoFishing_Mainhand) + UseItemInHand(); + else + UseItemInLeftHand(); } public override void Update() { - if (useItemCounter > 0) + lock (stateLock) { - useItemCounter--; - if (useItemCounter <= 0) + switch (state) { - UseItemInHand(); + case FishingState.WaitJoinGame: + break; + case FishingState.WaitingToCast: + if (AutoEat.Eating) + counter = (int)(Settings.AutoFishing_CastDelay * 10); + else if (--counter < 0) + state = FishingState.CastingRod; + break; + case FishingState.CastingRod: + UseFishRod(); + counter = 0; + state = FishingState.WaitingFishingBobber; + break; + case FishingState.WaitingFishingBobber: + if (++counter > castTimeout) + { + if (castTimeout < 6000) + castTimeout *= 2; // Exponential backoff + LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.cast_timeout", castTimeout / 10.0)); + + counter = (int)(Settings.AutoFishing_CastDelay * 10); + state = FishingState.WaitingToCast; + } + break; + case FishingState.WaitingFishToBite: + if (++counter > (int)(Settings.AutoFishing_FishingTimeout * 10)) + { + LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.fishing_timeout")); + + counter = (int)(Settings.AutoFishing_CastDelay * 10); + state = FishingState.WaitingToCast; + } + break; + case FishingState.StartMove: + if (--counter < 0) + { + double[,]? locationList = Settings.AutoFishing_Location; + if (locationList != null) + { + if (GetTerrainEnabled()) + { + UpdateLocation(locationList); + state = FishingState.WaitingMovement; + } + else + { + LogToConsole(Translations.Get("extra.terrainandmovement_required")); + state = FishingState.WaitJoinGame; + } + } + else + { + counter = (int)(Settings.AutoFishing_CastDelay * 10); + state = FishingState.DurabilityCheck; + goto case FishingState.DurabilityCheck; + } + } + break; + case FishingState.WaitingMovement: + if (!ClientIsMoving()) + { + LookAtLocation(nextYaw, nextPitch); + LogToConsole(Translations.Get("bot.autoFish.update_lookat", nextYaw, nextPitch)); + + state = FishingState.DurabilityCheck; + goto case FishingState.DurabilityCheck; + } + break; + case FishingState.DurabilityCheck: + if (DurabilityCheck()) + { + counter = (int)(Settings.AutoFishing_CastDelay * 10); + state = FishingState.WaitingToCast; + } + break; + case FishingState.Stopping: + break; } } } public override void OnEntitySpawn(Entity entity) { - if (entity.Type == EntityType.FishingBobber) + if (entity.Type == EntityType.FishingBobber && entity.ObjectData == GetPlayerEntityID()) { - if (GetCurrentLocation().Distance(entity.Location) < 2 && !isFishing) + if (Settings.AutoFishing_LogFishingBobber) + LogToConsole(string.Format("FishingBobber spawn at {0}, distance = {1:0.00}", entity.Location, GetCurrentLocation().Distance(entity.Location))); + + LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.throw")); + lock (stateLock) { - LogToConsoleTranslated("bot.autoFish.throw"); - fishingRod = entity; + fishingBobber = entity; LastPos = entity.Location; isFishing = true; + + castTimeout = 24; + counter = 0; + state = FishingState.WaitingFishToBite; } } } public override void OnEntityDespawn(Entity entity) { - if (entity.Type == EntityType.FishingBobber) + if (entity.Type == EntityType.FishingBobber && entity.ID == fishingBobber!.ID) { - if(entity.ID == fishingRod.ID) + if (Settings.AutoFishing_LogFishingBobber) + LogToConsole(string.Format("FishingBobber despawn at {0}", entity.Location)); + + if (isFishing) { isFishing = false; + if (Settings.AutoFishing_Antidespawn) { - useItemCounter = 5; // 500ms + LogToConsoleTranslated("bot.autoFish.despawn"); + + lock (stateLock) + { + counter = (int)(Settings.AutoFishing_CastDelay * 10); + state = FishingState.WaitingToCast; + } } } } @@ -75,41 +251,55 @@ namespace MinecraftClient.ChatBots public override void OnEntityMove(Entity entity) { - if (isFishing) + if (isFishing && fishingBobber!.ID == entity.ID) { - if (fishingRod.ID == entity.ID) + Location Pos = entity.Location; + double Dx = LastPos.X - Pos.X; + double Dy = LastPos.Y - Pos.Y; + double Dz = LastPos.Z - Pos.Z; + LastPos = Pos; + + if (Settings.AutoFishing_LogFishingBobber) + LogToConsole(string.Format("FishingBobber {0} Dx={1:0.000000} Dy={2:0.000000} Dz={3:0.000000}", Pos, Dx, Math.Abs(Dy), Dz)); + + if (Math.Abs(Dx) < Math.Abs(Settings.AutoFishing_StationaryThreshold) && + Math.Abs(Dz) < Math.Abs(Settings.AutoFishing_StationaryThreshold) && + Math.Abs(Dy) > Math.Abs(Settings.AutoFishing_HookThreshold)) { - Location Pos = entity.Location; - Double Dx = LastPos.X - Pos.X; - Double Dy = LastPos.Y - Pos.Y; - Double Dz = LastPos.Z - Pos.Z; - LastPos = Pos; - // check if fishing hook is stationary - if (Dx == 0 && Dz == 0) + // prevent triggering multiple time + if ((DateTime.Now - CaughtTime).TotalSeconds > 1) { - if (Math.Abs(Dy) > fishingHookThreshold) - { - // caught - // prevent triggering multiple time - if ((DateTime.Now - CaughtTime).TotalSeconds > 1) - { - OnCaughtFish(); - CaughtTime = DateTime.Now; - } - } + isFishing = false; + CaughtTime = DateTime.Now; + OnCaughtFish(); } - fishingRod = entity; } } } + public override void AfterGameJoined() + { + StartFishing(); + } + + public override void OnRespawn() + { + StartFishing(); + } + + public override void OnDeath() + { + StopFishing(); + } + public override bool OnDisconnect(DisconnectReason reason, string message) { - fishingRod = null; - LastPos = new Location(); + StopFishing(); + + fishingBobber = null; + LastPos = Location.Zero; CaughtTime = DateTime.Now; - isFishing = false; - useItemCounter = 0; + return base.OnDisconnect(reason, message); } @@ -118,43 +308,124 @@ namespace MinecraftClient.ChatBots /// public void OnCaughtFish() { - LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.caught")); - // retract fishing rod - UseItemInHand(); - if (inventoryEnabled) + ++fishCount; + if (Settings.AutoFishing_Location != null) + LogToConsole(GetTimestamp() + ": " + 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)); + + lock (stateLock) { - if (!hasFishingRod()) - { - LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.no_rod")); - return; - } + UseFishRod(); + + counter = 0; + state = FishingState.StartMove; } - // thread-safe - useItemCounter = 8; // 800ms } - /// - /// Check whether the player has a fishing rod in inventory - /// - /// TRUE if the player has a fishing rod - public bool hasFishingRod() + private void UpdateLocation(double[,] locationList) { - if (!inventoryEnabled) - return false; - int start = 36; - int end = 44; - Inventory.Container container = GetPlayerInventory(); - - foreach (KeyValuePair a in container.Items) + if (curLocationIdx >= locationList.GetLength(0)) { - if (a.Key < start || a.Key > end) - continue; - - if (a.Value.Type == ItemType.FishingRod) - return true; + curLocationIdx = Math.Max(0, locationList.GetLength(0) - 2); + moveDir = -1; + } + else if (curLocationIdx < 0) + { + curLocationIdx = Math.Min(locationList.GetLength(0) - 1, 1); + moveDir = 1; } - return false; + int locationType = locationList.GetLength(1); + + if (locationType == 2) + { + nextYaw = (float)locationList[curLocationIdx, 0]; + nextPitch = (float)locationList[curLocationIdx, 1]; + } + else if (locationType == 3) + { + nextYaw = GetYaw(); + nextPitch = GetPitch(); + } + else if (locationType == 5) + { + nextYaw = (float)locationList[curLocationIdx, 3]; + nextPitch = (float)locationList[curLocationIdx, 4]; + } + + if (locationType == 3 || locationType == 5) + { + Location current = GetCurrentLocation(); + Location goal = new(locationList[curLocationIdx, 0], locationList[curLocationIdx, 1], locationList[curLocationIdx, 2]); + + bool isMoveSuccessed; + if (!Movement.CheckChunkLoading(GetWorld(), current, goal)) + { + LogToConsole(Translations.Get("cmd.move.chunk_not_loaded", goal.X, goal.Y, goal.Z)); + isMoveSuccessed = false; + } + else + { + isMoveSuccessed = MoveToLocation(goal, allowUnsafe: false, allowDirectTeleport: false); + } + + if (!isMoveSuccessed) + { + nextYaw = GetYaw(); + nextPitch = GetPitch(); + LogToConsole(Translations.Get("cmd.move.fail", goal)); + } + else + { + LogToConsole(Translations.Get("cmd.move.walk", goal, current)); + } + } + + curLocationIdx += moveDir; + } + + private bool DurabilityCheck() + { + if (!inventoryEnabled) + return true; + + bool useMainHand = Settings.AutoFishing_Mainhand; + Container container = GetPlayerInventory(); + + int itemSolt = useMainHand ? GetCurrentSlot() + 36 : 45; + + if (container.Items.TryGetValue(itemSolt, out Item? handItem) && + handItem.Type == ItemType.FishingRod && (64 - handItem.Damage) >= Settings.AutoFishing_DurabilityLimit) + { + isWaitingRod = false; + return true; + } + else + { + if (!isWaitingRod) + LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.no_rod")); + + if (Settings.AutoFishing_AutoRodSwitch) + { + foreach ((int slot, Item item) in container.Items) + { + if (item.Type == ItemType.FishingRod && (64 - item.Damage) >= Settings.AutoFishing_DurabilityLimit) + { + WindowAction(0, slot, WindowActionType.LeftClick); + WindowAction(0, itemSolt, WindowActionType.LeftClick); + WindowAction(0, slot, WindowActionType.LeftClick); + LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.switch", slot, (64 - item.Damage))); + isWaitingRod = false; + return true; + } + } + } + + isWaitingRod = true; + return false; + } } } } diff --git a/MinecraftClient/Commands/Move.cs b/MinecraftClient/Commands/Move.cs index 1b4f85ee..da58e76b 100644 --- a/MinecraftClient/Commands/Move.cs +++ b/MinecraftClient/Commands/Move.cs @@ -74,8 +74,7 @@ namespace MinecraftClient.Commands Location goal = Movement.Move(handler.GetCurrentLocation(), direction); - ChunkColumn? chunkColumn = handler.GetWorld().GetChunkColumn(goal); - if (chunkColumn == null || chunkColumn.FullyLoaded == false) + if (!Movement.CheckChunkLoading(handler.GetWorld(), handler.GetCurrentLocation(), goal)) return Translations.Get("cmd.move.chunk_not_loaded", goal.X, goal.Y, goal.Z); if (Movement.CanMove(handler.GetWorld(), handler.GetCurrentLocation(), direction)) @@ -98,8 +97,7 @@ namespace MinecraftClient.Commands double z = args[2].StartsWith('~') ? current.Z + (args[2].Length > 1 ? double.Parse(args[2][1..]) : 0) : double.Parse(args[2]); Location goal = new(x, y, z); - ChunkColumn? chunkColumn = handler.GetWorld().GetChunkColumn(goal); - if (chunkColumn == null || chunkColumn.FullyLoaded == false) + if (!Movement.CheckChunkLoading(handler.GetWorld(), current, goal)) return Translations.Get("cmd.move.chunk_not_loaded", x, y, z); if (takeRisk || Movement.PlayerFitsHere(handler.GetWorld(), goal)) diff --git a/MinecraftClient/Mapping/Movement.cs b/MinecraftClient/Mapping/Movement.cs index 8389cdd7..21811c8d 100644 --- a/MinecraftClient/Mapping/Movement.cs +++ b/MinecraftClient/Mapping/Movement.cs @@ -643,5 +643,25 @@ namespace MinecraftClient.Mapping throw new ArgumentException("Unknown direction", "direction"); } } + + /// + /// Check that the chunks at both the start and destination locations have been loaded + /// + /// Current world + /// Start location + /// Destination location + /// Is loading complete + public static bool CheckChunkLoading(World world, Location start, Location dest) + { + ChunkColumn? chunkColumn = world.GetChunkColumn(dest); + if (chunkColumn == null || chunkColumn.FullyLoaded == false) + return false; + + chunkColumn = world.GetChunkColumn(start); + if (chunkColumn == null || chunkColumn.FullyLoaded == false) + return false; + + return true; + } } } diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 3ef7fbf1..8ba106fa 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -1277,6 +1277,15 @@ namespace MinecraftClient return InvokeOnMainThread(() => handler.SendUseItem(0, this.sequenceId)); } + /// + /// Use the item currently in the player's left hand + /// + /// TRUE if the item was successfully used + public bool UseItemOnLeftHand() + { + return InvokeOnMainThread(() => handler.SendUseItem(1, this.sequenceId)); + } + /// /// Try to merge a slot /// diff --git a/MinecraftClient/Resources/config/MinecraftClient.ini b/MinecraftClient/Resources/config/MinecraftClient.ini index 332c00be..634004f8 100644 --- a/MinecraftClient/Resources/config/MinecraftClient.ini +++ b/MinecraftClient/Resources/config/MinecraftClient.ini @@ -207,6 +207,21 @@ interaction=Attack # Possible values: Interact, Attack (default) # /!\ Make sure server rules allow automated farming before using this bot enabled=false antidespawn=false +main_hand=true # Use the main hand or the second hand to hold the rod. +auto_start=true # Whether to start fishing automatically after entering a world. +cast_delay=0.4 # How soon to re-cast after successful fishing. +fishing_delay=3.0 # How long after entering the game to start fishing (seconds). +fishing_timeout=300.0 # Fishing timeout (seconds). Timeout will trigger a re-cast. +durability_limit=2 # Will not use rods with less durability than this (full durability is 64). Set to zero to disable this feature. +auto_rod_switch=true # Switch to a new rod from inventory after the current rod is unavailable. +stationary_threshold=0.001 # Hooks moving in the X and Z axes below this threshold will be considered stationary. +hook_threshold=0.2 # A stationary hook moving on the Y-axis above this threshold will be considered to have caught a fish. +log_fishing_bobber=false # For debugging purposes, you can use this log to adjust the two thresholds mentioned above. +location= # Some plugins do not allow the player to fish in one place. This allows the player to change position/angle after each fish caught. + # Floating point numbers can be used for both coordinates and angles. Leave blank to disable this function. + # Change the angle only (recommended): location=yaw_1, pitch_1; yaw_2, pitch_2; ...; yaw_n, pitch_n + # Change position only: location=x1, y1, z1; x2, y2, z2; ...; xn, yn, zn + # Change both angle and position: location=x1, y1, z1, yaw_1, pitch_1; x2, y2, z2, yaw_2, pitch_2; ... ;xn, yn, zn, yaw_n, pitch_n [AutoEat] # Automatically eat food when your Hunger value is low diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 1d0cc4ad..637c40bd 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -88,6 +88,8 @@ error.connection_timeout=§8A timeout occured while attempting to connect to thi error.forge=§8Forge Login Handshake did not complete successfully error.forge_encrypt=§8Forge StartEncryption Handshake did not complete successfully error.setting.str2int=Failed to convert '{0}' into an integer. Please check your settings. +error.setting.str2locationList.convert_fail=Failed to convert '{0}' to a floating point number. Please check your settings. +error.setting.str2locationList.format_err=Wrong format, can't parse '{0}' into position data.. Please check your settings. error.setting.argument_syntax={0}: Invalid syntax, expecting --argname=value or --section.argname=value error.setting.unknown_section={0}: Unknown setting section '{1}' error.setting.unknown_or_invalid={0}: Unknown setting or invalid value @@ -447,9 +449,17 @@ bot.autoDrop.no_mode=Cannot read drop mode from config. Using include mode. bot.autoDrop.no_inventory=Cannot find inventory {0}! # AutoFish -bot.autoFish.throw=Threw a fishing rod -bot.autoFish.caught=Caught a fish! -bot.autoFish.no_rod=No Fishing Rod on hand. Maybe broken? +bot.autoFish.no_inv_handle=Inventory handling is not enabled. Cannot check rod durability and switch rods. +bot.autoFish.start=Fishing will start in {0:0.0} second(s). +bot.autoFish.throw=Casting successfully. +bot.autoFish.caught=Caught a fish! (Count: {0}) +bot.autoFish.caught_at=Caught a fish at ({0:0.0},{1:0.0},{2:0.0})! (Count: {3}) +bot.autoFish.no_rod=Current fishing rod is not available. Maybe broken or low durability? +bot.autoFish.despawn=Fish floating despawn, will re-cast. +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. # AutoRelog bot.autoRelog.launch=Launching with {0} reconnection attempts diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index 4fc59e1e..4d4ac0ac 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -1012,6 +1012,16 @@ namespace MinecraftClient Handler.UpdateLocation(Handler.GetCurrentLocation(), location); } + /// + /// Look at the specified location + /// + /// Yaw to look at + /// Pitch to look at + protected void LookAtLocation(float yaw, float pitch) + { + Handler.UpdateLocation(Handler.GetCurrentLocation(), yaw, pitch); + } + /// /// Get a Y-M-D h:m:s timestamp representing the current system date and time /// @@ -1113,6 +1123,15 @@ namespace MinecraftClient return Handler.GetUserUuidStr(); } + /// + /// Return the EntityID of the current player + /// + /// EntityID of the current player + protected int GetPlayerEntityID() + { + return Handler.GetPlayerEntityID(); + } + /// /// Return the list of currently online players /// @@ -1253,6 +1272,15 @@ namespace MinecraftClient return Handler.UseItemOnHand(); } + /// + /// Use item currently in the player's hand (active inventory bar slot) + /// + /// TRUE if successful + protected bool UseItemInLeftHand() + { + return Handler.UseItemOnLeftHand(); + } + /// /// Check inventory handling enable status /// diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index db493953..254f7c31 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -203,6 +203,17 @@ namespace MinecraftClient //Auto Fishing public static bool AutoFishing_Enabled = false; public static bool AutoFishing_Antidespawn = false; + public static bool AutoFishing_Mainhand = true; + public static bool AutoFishing_AutoStart = true; + public static double AutoFishing_CastDelay = 0.4; + public static double AutoFishing_FishingDelay = 3.0; + public static double AutoFishing_FishingTimeout = 300.0; + public static double AutoFishing_DurabilityLimit = 2; + public static bool AutoFishing_AutoRodSwitch = true; + public static double AutoFishing_StationaryThreshold = 0.001; + public static double AutoFishing_HookThreshold = 0.2; + public static bool AutoFishing_LogFishingBobber = false; + public static double[,]? AutoFishing_Location = null; //Auto Eating public static bool AutoEat_Enabled = false; @@ -711,6 +722,17 @@ namespace MinecraftClient { case "enabled": AutoFishing_Enabled = str2bool(argValue); return true; case "antidespawn": AutoFishing_Antidespawn = str2bool(argValue); return true; + case "main_hand": AutoFishing_Mainhand = str2bool(argValue); return true; + case "auto_start": AutoFishing_AutoStart = str2bool(argValue); return true; + case "cast_delay": AutoFishing_CastDelay = str2double(argValue); return true; + case "fishing_delay": AutoFishing_FishingDelay = str2double(argValue); return true; + case "fishing_timeout": AutoFishing_FishingTimeout = str2double(argValue); return true; + case "durability_limit": AutoFishing_DurabilityLimit = str2int(argValue); return true; + case "auto_rod_switch": AutoFishing_AutoRodSwitch = str2bool(argValue); return true; + case "stationary_threshold": AutoFishing_StationaryThreshold = str2double(argValue); return true; + case "hook_threshold": AutoFishing_HookThreshold = str2double(argValue); return true; + case "log_fishing_bobber": AutoFishing_LogFishingBobber = str2bool(argValue); return true; + case "location": AutoFishing_Location = str2locationList(argValue); return true; } break; @@ -863,9 +885,24 @@ namespace MinecraftClient /// Float number public static float str2float(string str) { - float f; - if (float.TryParse(str.Trim(), out f)) - return f; + if (float.TryParse(str.Trim(), out float num)) + return num; + else + { + ConsoleIO.WriteLogLine(Translations.Get("error.setting.str2int", str)); + return 0; + } + } + + /// + /// Convert the specified string to a double number, defaulting to zero if invalid argument + /// + /// String to parse as a float number + /// Double number + public static double str2double(string str) + { + if (double.TryParse(str.Trim(), out double num)) + return num; else { ConsoleIO.WriteLogLine(Translations.Get("error.setting.str2int", str)); @@ -886,6 +923,46 @@ namespace MinecraftClient return str == "true" || str == "1"; } + /// + /// Convert the specified string to a list of location, returning null if invalid argument + /// + /// String to parse as a location list + /// Location list (null or double[*,5] or double[*,3] or double[*,2]) + public static double[,]? str2locationList(string str) + { + string[] locationStrList = str.Split(';', StringSplitOptions.RemoveEmptyEntries); + double[,]? res = null; + int codLen = 0; + for (int i = 0; i < locationStrList.Length; ++i) + { + string[] coordinates_str_list = locationStrList[i].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + int curCodLen = coordinates_str_list.Length; + if ((curCodLen == 2 || curCodLen == 3 || curCodLen == 5) && (i == 0 || curCodLen == codLen)) + { + if (i == 0) + { + res = new double[locationStrList.Length, curCodLen]; + codLen = curCodLen; + } + + for (int j = 0; j < curCodLen; ++j) + { + if (!double.TryParse(coordinates_str_list[j], out res![i, j])) + { + ConsoleIO.WriteLogLine(Translations.Get("error.setting.str2locationList.convert_fail", coordinates_str_list[j])); + return null; + } + } + } + else + { + ConsoleIO.WriteLogLine(Translations.Get("error.setting.str2locationList.format_err", locationStrList[i])); + return null; + } + } + return res; + } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public static string ToLowerIfNeed(string str) {