From c9b0913c1a7fbff36c640a78614eed1e77f26990 Mon Sep 17 00:00:00 2001 From: Anon Date: Tue, 31 Mar 2026 00:18:31 +0200 Subject: [PATCH] feat(autofishing): add velocity and sound bite detection --- MinecraftClient/ChatBots/AutoFishing.cs | 100 ++++++++++++++++-- MinecraftClient/McClient.cs | 38 +++++++ .../Protocol/Handlers/DataTypes.cs | 42 ++++++-- .../Protocol/Handlers/Protocol18.cs | 97 +++++++++++++++++ .../Protocol/IMinecraftComHandler.cs | 21 ++++ .../ConfigComments/ConfigComments.resx | 15 +++ MinecraftClient/Scripting/ChatBot.cs | 23 ++++ docs/guide/chat-bots.md | 61 +++++++++++ 8 files changed, 380 insertions(+), 17 deletions(-) diff --git a/MinecraftClient/ChatBots/AutoFishing.cs b/MinecraftClient/ChatBots/AutoFishing.cs index 9711b86c..cf381561 100644 --- a/MinecraftClient/ChatBots/AutoFishing.cs +++ b/MinecraftClient/ChatBots/AutoFishing.cs @@ -62,6 +62,21 @@ namespace MinecraftClient.ChatBots [TomlInlineComment("$ChatBot.AutoFishing.Hook_Threshold$")] public double Hook_Threshold = 0.2; + [TomlInlineComment("$ChatBot.AutoFishing.Enable_Velocity_Detection$")] + public bool Enable_Velocity_Detection = true; + + [TomlInlineComment("$ChatBot.AutoFishing.Velocity_Hook_Threshold$")] + public double Velocity_Hook_Threshold = -0.2; + + [TomlInlineComment("$ChatBot.AutoFishing.Enable_Sound_Detection$")] + public bool Enable_Sound_Detection = true; + + [TomlInlineComment("$ChatBot.AutoFishing.Sound_Distance$")] + public double Sound_Distance = 5.0; + + [TomlInlineComment("$ChatBot.AutoFishing.Detection_Warmup$")] + public double Detection_Warmup = 1.0; + [TomlInlineComment("$ChatBot.AutoFishing.Log_Fish_Bobber$")] public bool Log_Fish_Bobber = false; @@ -97,6 +112,15 @@ namespace MinecraftClient.ChatBots if (Hook_Threshold < 0) Hook_Threshold = -Hook_Threshold; + + if (Velocity_Hook_Threshold > 0) + Velocity_Hook_Threshold = -Velocity_Hook_Threshold; + + if (Sound_Distance < 0) + Sound_Distance = -Sound_Distance; + + if (Detection_Warmup < 0) + Detection_Warmup = 0; } public struct LocationConfig @@ -171,6 +195,7 @@ namespace MinecraftClient.ChatBots private Entity? fishingBobber; private Location LastPos = Location.Zero; private DateTime CaughtTime = DateTime.Now; + private DateTime BobberSpawnTime = DateTime.MinValue; private int fishItemCounter = 15; private Dictionary fishItemCnt = new(); private Entity fishItem = new(-1, EntityType.Item, Location.Zero); @@ -464,6 +489,7 @@ namespace MinecraftClient.ChatBots fishingBobber = entity; LastPos = entity.Location; isFishing = true; + BobberSpawnTime = DateTime.Now; castTimeout = 24; counter = 0; @@ -500,7 +526,7 @@ namespace MinecraftClient.ChatBots public override void OnEntityMove(Entity entity) { if (isFishing && entity is not null && fishingBobber!.ID == entity.ID && - (state == FishingState.WaitingFishToBite || state == FishingState.WaitingFishingBobber)) + state == FishingState.WaitingFishToBite) { Location Pos = entity.Location; double Dx = LastPos.X - Pos.X; @@ -515,13 +541,7 @@ namespace MinecraftClient.ChatBots Math.Abs(Dz) < Math.Abs(Config.Stationary_Threshold) && Math.Abs(Dy) > Math.Abs(Config.Hook_Threshold)) { - // prevent triggering multiple time - if ((DateTime.Now - CaughtTime).TotalSeconds > 1) - { - isFishing = false; - CaughtTime = DateTime.Now; - OnCaughtFish(); - } + TryCatchFish(); } } } @@ -540,6 +560,38 @@ namespace MinecraftClient.ChatBots } } + public override void OnEntityVelocity(Entity entity, double velocityX, double velocityY, double velocityZ) + { + if (!Config.Enable_Velocity_Detection || !CanUseAdvancedDetection()) + return; + + if (fishingBobber is null || entity.ID != fishingBobber.ID) + return; + + if (velocityY <= Config.Velocity_Hook_Threshold) + TryCatchFish(); + } + + public override void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, + Entity? sourceEntity) + { + if (!Config.Enable_Sound_Detection || !CanUseAdvancedDetection()) + return; + + if (!IsFishingBobberSplashSound(soundName)) + return; + + Location? soundLocation = location; + if (soundLocation is null && sourceEntity is not null) + soundLocation = sourceEntity.Location; + + if (soundLocation is null || fishingBobber is null) + return; + + if (soundLocation.Value.Distance(fishingBobber.Location) <= Config.Sound_Distance) + TryCatchFish(); + } + public override void AfterGameJoined() { StartFishing(); @@ -562,10 +614,42 @@ namespace MinecraftClient.ChatBots fishingBobber = null; LastPos = Location.Zero; CaughtTime = DateTime.Now; + BobberSpawnTime = DateTime.MinValue; return base.OnDisconnect(reason, message); } + private bool CanUseAdvancedDetection() + { + if (!isFishing || fishingBobber is null || state != FishingState.WaitingFishToBite) + return false; + + return (DateTime.Now - BobberSpawnTime).TotalSeconds >= Config.Detection_Warmup; + } + + private void TryCatchFish() + { + if (!CanUseAdvancedDetection()) + return; + + // Prevent repeated catches from multiple packets of the same bite. + if ((DateTime.Now - CaughtTime).TotalSeconds <= 1) + return; + + isFishing = false; + CaughtTime = DateTime.Now; + OnCaughtFish(); + } + + private static bool IsFishingBobberSplashSound(string? soundName) + { + return string.Equals(soundName, "minecraft:entity.fishing_bobber.splash", + StringComparison.OrdinalIgnoreCase) + || string.Equals(soundName, "entity.fishing_bobber.splash", StringComparison.OrdinalIgnoreCase) + || string.Equals(soundName, "minecraft:entity.bobber.splash", StringComparison.OrdinalIgnoreCase) + || string.Equals(soundName, "entity.bobber.splash", StringComparison.OrdinalIgnoreCase); + } + /// /// Called when detected a fish is caught /// diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index bd84d562..e1079c3c 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -3798,6 +3798,44 @@ namespace MinecraftClient } } + /// + /// Called when an entity velocity update is received. + /// + /// Entity ID + /// Velocity on X axis (blocks/tick) + /// Velocity on Y axis (blocks/tick) + /// Velocity on Z axis (blocks/tick) + public void OnEntityVelocity(int entityID, double velocityX, double velocityY, double velocityZ) + { + if (entities.TryGetValue(entityID, out Entity? entity)) + DispatchBotEvent(bot => bot.OnEntityVelocity(entity, velocityX, velocityY, velocityZ)); + } + + /// + /// Called when a sound packet is received. + /// + /// Sound key when available, otherwise null + /// Sound location when available + /// Sound category id from packet + /// Sound volume + /// Sound pitch + /// Source entity id for entity sound packets, if any + public void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, + int? entityID) + { + Entity? sourceEntity = null; + Location? resolvedLocation = location; + + if (entityID is int id && entities.TryGetValue(id, out Entity? entity)) + { + sourceEntity = entity; + resolvedLocation ??= entity.Location; + } + + DispatchBotEvent(bot => bot.OnSoundEffect(soundName, resolvedLocation, category, volume, pitch, + sourceEntity)); + } + /// /// Called when received entity properties from server. /// diff --git a/MinecraftClient/Protocol/Handlers/DataTypes.cs b/MinecraftClient/Protocol/Handlers/DataTypes.cs index 6a0d27aa..bdbdecea 100644 --- a/MinecraftClient/Protocol/Handlers/DataTypes.cs +++ b/MinecraftClient/Protocol/Handlers/DataTypes.cs @@ -1054,20 +1054,44 @@ namespace MinecraftClient.Protocol.Handlers } } + private static bool HasLpVec3Continuation(int firstByte) => (firstByte & 4) == 4; + + private static double UnpackLpVec3(long packedAxis) + { + return Math.Min((double)(packedAxis & 32767L), 32766.0) * 2.0 / 32766.0 - 1.0; + } + /// - /// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+). - /// Variable-length encoding: first byte 0 = zero vector; otherwise - /// 2 bytes + 4 bytes (6 total), plus an optional VarInt continuation. + /// Read and decode an LpVec3 (low-precision vec3) from the cache (1.21.9+). + /// Returned vector is expressed in blocks per tick. /// - public void ReadNextLpVec3(Queue cache) + public (double X, double Y, double Z) ReadNextLpVec3Values(Queue cache) { int first = ReadNextByte(cache); if (first == 0) - return; - ReadNextByte(cache); // second byte - ReadData(4, cache); // uint32 - if ((first & 4) == 4) // continuation bit set - ReadNextVarInt(cache); + return (0.0, 0.0, 0.0); + + int second = ReadNextByte(cache); + uint high = (uint)ReadNextInt(cache); + long packed = ((long)high << 16) | (long)(second << 8) | (uint)first; + + long scale = first & 3; + if (HasLpVec3Continuation(first)) + scale |= ((long)ReadNextVarInt(cache) & 0xFFFFFFFFL) << 2; + + return ( + UnpackLpVec3(packed >> 3) * scale, + UnpackLpVec3(packed >> 18) * scale, + UnpackLpVec3(packed >> 33) * scale + ); + } + + /// + /// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+) and discard it. + /// + public void ReadNextLpVec3(Queue cache) + { + ReadNextLpVec3Values(cache); } /// diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 88c26690..a27c4b0a 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -2642,6 +2642,27 @@ namespace MinecraftClient.Protocol.Handlers handler.OnEntityRotation(entityId, yaw, pitch, isOnGround); } + break; + case PacketTypesIn.EntityVelocity: + if (handler.GetEntityHandlingEnabled()) + { + var entityId = dataTypes.ReadNextVarInt(packetData); + double velocityX, velocityY, velocityZ; + + if (protocolVersion >= MC_1_21_9_Version) + { + (velocityX, velocityY, velocityZ) = dataTypes.ReadNextLpVec3Values(packetData); + } + else + { + velocityX = dataTypes.ReadNextShort(packetData) / 8000.0D; + velocityY = dataTypes.ReadNextShort(packetData) / 8000.0D; + velocityZ = dataTypes.ReadNextShort(packetData) / 8000.0D; + } + + handler.OnEntityVelocity(entityId, velocityX, velocityY, velocityZ); + } + break; case PacketTypesIn.EntityProperties: if (handler.GetEntityHandlingEnabled()) @@ -2892,6 +2913,65 @@ namespace MinecraftClient.Protocol.Handlers handler.OnExplosion(explosionLocation, explosionStrength, explosionBlockCount); break; + case PacketTypesIn.NamedSoundEffect: + { + string? soundName = dataTypes.ReadNextString(packetData); + int category = dataTypes.ReadNextVarInt(packetData); + double x = dataTypes.ReadNextInt(packetData) / 8.0D; + double y = dataTypes.ReadNextInt(packetData) / 8.0D; + double z = dataTypes.ReadNextInt(packetData) / 8.0D; + float volume = dataTypes.ReadNextFloat(packetData); + float pitch = dataTypes.ReadNextFloat(packetData); + + handler.OnSoundEffect(soundName, new Location(x, y, z), category, volume, pitch, null); + break; + } + case PacketTypesIn.SoundEffect: + { + string? soundName; + if (protocolVersion >= MC_1_19_Version) + soundName = ReadSoundEventHolderName(packetData); + else + { + dataTypes.ReadNextVarInt(packetData); // Sound id + soundName = null; + } + + int category = dataTypes.ReadNextVarInt(packetData); + double x = dataTypes.ReadNextInt(packetData) / 8.0D; + double y = dataTypes.ReadNextInt(packetData) / 8.0D; + double z = dataTypes.ReadNextInt(packetData) / 8.0D; + float volume = dataTypes.ReadNextFloat(packetData); + float pitch = dataTypes.ReadNextFloat(packetData); + + if (protocolVersion >= MC_1_19_Version) + dataTypes.ReadNextLong(packetData); // Seed + + handler.OnSoundEffect(soundName, new Location(x, y, z), category, volume, pitch, null); + break; + } + case PacketTypesIn.EntitySoundEffect: + { + string? soundName; + if (protocolVersion >= MC_1_19_Version) + soundName = ReadSoundEventHolderName(packetData); + else + { + dataTypes.ReadNextVarInt(packetData); // Sound id + soundName = null; + } + + int category = dataTypes.ReadNextVarInt(packetData); + int entityId = dataTypes.ReadNextVarInt(packetData); + float volume = dataTypes.ReadNextFloat(packetData); + float pitch = dataTypes.ReadNextFloat(packetData); + + if (protocolVersion >= MC_1_19_Version) + dataTypes.ReadNextLong(packetData); // Seed + + handler.OnSoundEffect(soundName, null, category, volume, pitch, entityId); + break; + } case PacketTypesIn.HeldItemChange: case PacketTypesIn.SetHeldSlot: handler.OnHeldItemChange(dataTypes.ReadNextByte(packetData)); // Slot @@ -3154,6 +3234,23 @@ namespace MinecraftClient.Protocol.Handlers return true; //Packet processed } + /// + /// Read a Holder<SoundEvent> from packet data and return its key when inline. + /// Returns null when the holder is a registry reference. + /// + private string? ReadSoundEventHolderName(Queue packetData) + { + int soundHolderId = dataTypes.ReadNextVarInt(packetData); + if (soundHolderId != 0) + return null; + + string soundName = dataTypes.ReadNextString(packetData); + bool hasFixedRange = dataTypes.ReadNextBool(packetData); + if (hasFixedRange) + dataTypes.ReadNextFloat(packetData); + return soundName; + } + /// /// Handle the Statistics packet for pre-1.12 legacy achievements. /// diff --git a/MinecraftClient/Protocol/IMinecraftComHandler.cs b/MinecraftClient/Protocol/IMinecraftComHandler.cs index 9bfa44e8..13f5a628 100644 --- a/MinecraftClient/Protocol/IMinecraftComHandler.cs +++ b/MinecraftClient/Protocol/IMinecraftComHandler.cs @@ -295,6 +295,16 @@ namespace MinecraftClient.Protocol /// TRUE if on ground void OnEntityTeleport(int entityID, Double x, Double y, Double z, bool onGround); + /// + /// Called when an entity velocity update packet is received. + /// Velocity values are in blocks per tick. + /// + /// Entity ID + /// Velocity X + /// Velocity Y + /// Velocity Z + void OnEntityVelocity(int entityID, double velocityX, double velocityY, double velocityZ); + /// /// Called when additional properties have been received for an entity /// @@ -371,6 +381,17 @@ namespace MinecraftClient.Protocol /// Amount of affected blocks void OnExplosion(Location location, float strength, int affectedBlocks); + /// + /// Called when a sound packet is received. + /// + /// Sound key if available, otherwise null + /// Sound location for world sounds, or null if unavailable + /// Sound category id + /// Sound volume + /// Sound pitch + /// Source entity id for entity-sound packets, if any + void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, int? entityID); + /// /// Called when a player's game mode has changed /// diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index 8f3e4964..825c7e76 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -311,6 +311,21 @@ You can use "/fish" to control the bot manually. A "stationary" hook that moves above this threshold in the Y-axis will be considered to have caught a fish. + + Enable fish bite detection using fishing bobber velocity packets. + + + Velocity Y threshold (blocks/tick). Values below this are treated as a bite. Keep this value negative. + + + Enable fish bite detection using splash sounds near the fishing bobber. + + + Maximum distance (blocks) between splash sound and bobber to treat it as a bite. + + + Delay (seconds) after bobber spawn before bite detection starts. Helps ignore cast-entry splash/motion. + Used to adjust the above two thresholds, which when enabled will print the change in the position of the fishhook entity upon receipt of its movement packet. diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index 2776cda9..c5950334 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -199,6 +199,29 @@ namespace MinecraftClient.Scripting /// Entity with updated location public virtual void OnEntityMove(Entity entity) { } + /// + /// Called when a tracked entity receives a velocity update packet. + /// Velocity is expressed in blocks per tick. + /// + /// Entity with updated velocity + /// Velocity on X axis (blocks/tick) + /// Velocity on Y axis (blocks/tick) + /// Velocity on Z axis (blocks/tick) + public virtual void OnEntityVelocity(Entity entity, double velocityX, double velocityY, double velocityZ) { } + + /// + /// Called when a sound packet is received. + /// The sound name is null when the protocol provides only a registry id. + /// + /// Sound key when available, otherwise null + /// Sound position when available + /// Sound category id from packet + /// Sound volume + /// Sound pitch + /// Source entity for entity-sound packets when tracked + public virtual void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, + Entity? sourceEntity) { } + /// /// Called when an entity rotates /// diff --git a/docs/guide/chat-bots.md b/docs/guide/chat-bots.md index 512f1664..e8b6c711 100644 --- a/docs/guide/chat-bots.md +++ b/docs/guide/chat-bots.md @@ -927,6 +927,7 @@ redirectFrom: - **Description:** Automatically catch fish using a fishing rod. + Bite detection combines bobber movement, bobber velocity, and splash sounds.

Note

@@ -1103,6 +1104,66 @@ redirectFrom: - **Default:** `0.2` + #### `Enable_Velocity_Detection` + + - **Description:** + + Enables bite detection using the fishing bobber velocity packet. + + This improves reliability when bobber X/Z movement is constrained (for example by blocks near the water surface). + + - **Available values:** `true` and `false`. + + - **Type:** `boolean` + + - **Default:** `true` + + #### `Velocity_Hook_Threshold` + + - **Description:** + + Velocity Y threshold in blocks/tick for velocity-based bite detection. + + Values below this threshold are considered a bite. Keep this value negative. + + - **Type:** `float` + + - **Default:** `-0.2` + + #### `Enable_Sound_Detection` + + - **Description:** + + Enables bite detection using nearby splash sounds (`entity.fishing_bobber.splash`). + + - **Available values:** `true` and `false`. + + - **Type:** `boolean` + + - **Default:** `true` + + #### `Sound_Distance` + + - **Description:** + + Maximum distance in blocks between a splash sound and the tracked bobber to treat it as a bite. + + - **Type:** `float` + + - **Default:** `5.0` + + #### `Detection_Warmup` + + - **Description:** + + Delay in seconds after bobber spawn before bite detection starts. + + This helps ignore the initial cast-entry splash/motion. + + - **Type:** `float` + + - **Default:** `1.0` + #### `Log_Fish_Bobber` - **Description:**