feat(autofishing): add velocity and sound bite detection

This commit is contained in:
Anon 2026-03-31 00:18:31 +02:00
parent d5358d3d1a
commit c9b0913c1a
8 changed files with 380 additions and 17 deletions

View file

@ -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<ItemType, uint> 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);
}
/// <summary>
/// Called when detected a fish is caught
/// </summary>

View file

@ -3798,6 +3798,44 @@ namespace MinecraftClient
}
}
/// <summary>
/// Called when an entity velocity update is received.
/// </summary>
/// <param name="entityID">Entity ID</param>
/// <param name="velocityX">Velocity on X axis (blocks/tick)</param>
/// <param name="velocityY">Velocity on Y axis (blocks/tick)</param>
/// <param name="velocityZ">Velocity on Z axis (blocks/tick)</param>
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));
}
/// <summary>
/// Called when a sound packet is received.
/// </summary>
/// <param name="soundName">Sound key when available, otherwise null</param>
/// <param name="location">Sound location when available</param>
/// <param name="category">Sound category id from packet</param>
/// <param name="volume">Sound volume</param>
/// <param name="pitch">Sound pitch</param>
/// <param name="entityID">Source entity id for entity sound packets, if any</param>
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));
}
/// <summary>
/// Called when received entity properties from server.
/// </summary>

View file

@ -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;
}
/// <summary>
/// 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.
/// </summary>
public void ReadNextLpVec3(Queue<byte> cache)
public (double X, double Y, double Z) ReadNextLpVec3Values(Queue<byte> 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
);
}
/// <summary>
/// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+) and discard it.
/// </summary>
public void ReadNextLpVec3(Queue<byte> cache)
{
ReadNextLpVec3Values(cache);
}
/// <summary>

View file

@ -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
}
/// <summary>
/// Read a Holder&lt;SoundEvent&gt; from packet data and return its key when inline.
/// Returns null when the holder is a registry reference.
/// </summary>
private string? ReadSoundEventHolderName(Queue<byte> 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;
}
/// <summary>
/// Handle the Statistics packet for pre-1.12 legacy achievements.
/// </summary>

View file

@ -295,6 +295,16 @@ namespace MinecraftClient.Protocol
/// <param name="onGround">TRUE if on ground</param>
void OnEntityTeleport(int entityID, Double x, Double y, Double z, bool onGround);
/// <summary>
/// Called when an entity velocity update packet is received.
/// Velocity values are in blocks per tick.
/// </summary>
/// <param name="entityID">Entity ID</param>
/// <param name="velocityX">Velocity X</param>
/// <param name="velocityY">Velocity Y</param>
/// <param name="velocityZ">Velocity Z</param>
void OnEntityVelocity(int entityID, double velocityX, double velocityY, double velocityZ);
/// <summary>
/// Called when additional properties have been received for an entity
/// </summary>
@ -371,6 +381,17 @@ namespace MinecraftClient.Protocol
/// <param name="affectedBlocks">Amount of affected blocks</param>
void OnExplosion(Location location, float strength, int affectedBlocks);
/// <summary>
/// Called when a sound packet is received.
/// </summary>
/// <param name="soundName">Sound key if available, otherwise null</param>
/// <param name="location">Sound location for world sounds, or null if unavailable</param>
/// <param name="category">Sound category id</param>
/// <param name="volume">Sound volume</param>
/// <param name="pitch">Sound pitch</param>
/// <param name="entityID">Source entity id for entity-sound packets, if any</param>
void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, int? entityID);
/// <summary>
/// Called when a player's game mode has changed
/// </summary>

View file

@ -311,6 +311,21 @@ You can use "/fish" to control the bot manually.
<data name="ChatBot.AutoFishing.Hook_Threshold" xml:space="preserve">
<value>A "stationary" hook that moves above this threshold in the Y-axis will be considered to have caught a fish.</value>
</data>
<data name="ChatBot.AutoFishing.Enable_Velocity_Detection" xml:space="preserve">
<value>Enable fish bite detection using fishing bobber velocity packets.</value>
</data>
<data name="ChatBot.AutoFishing.Velocity_Hook_Threshold" xml:space="preserve">
<value>Velocity Y threshold (blocks/tick). Values below this are treated as a bite. Keep this value negative.</value>
</data>
<data name="ChatBot.AutoFishing.Enable_Sound_Detection" xml:space="preserve">
<value>Enable fish bite detection using splash sounds near the fishing bobber.</value>
</data>
<data name="ChatBot.AutoFishing.Sound_Distance" xml:space="preserve">
<value>Maximum distance (blocks) between splash sound and bobber to treat it as a bite.</value>
</data>
<data name="ChatBot.AutoFishing.Detection_Warmup" xml:space="preserve">
<value>Delay (seconds) after bobber spawn before bite detection starts. Helps ignore cast-entry splash/motion.</value>
</data>
<data name="ChatBot.AutoFishing.Log_Fish_Bobber" xml:space="preserve">
<value>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.</value>
</data>

View file

@ -199,6 +199,29 @@ namespace MinecraftClient.Scripting
/// <param name="entity">Entity with updated location</param>
public virtual void OnEntityMove(Entity entity) { }
/// <summary>
/// Called when a tracked entity receives a velocity update packet.
/// Velocity is expressed in blocks per tick.
/// </summary>
/// <param name="entity">Entity with updated velocity</param>
/// <param name="velocityX">Velocity on X axis (blocks/tick)</param>
/// <param name="velocityY">Velocity on Y axis (blocks/tick)</param>
/// <param name="velocityZ">Velocity on Z axis (blocks/tick)</param>
public virtual void OnEntityVelocity(Entity entity, double velocityX, double velocityY, double velocityZ) { }
/// <summary>
/// Called when a sound packet is received.
/// The sound name is null when the protocol provides only a registry id.
/// </summary>
/// <param name="soundName">Sound key when available, otherwise null</param>
/// <param name="location">Sound position when available</param>
/// <param name="category">Sound category id from packet</param>
/// <param name="volume">Sound volume</param>
/// <param name="pitch">Sound pitch</param>
/// <param name="sourceEntity">Source entity for entity-sound packets when tracked</param>
public virtual void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch,
Entity? sourceEntity) { }
/// <summary>
/// Called when an entity rotates
/// </summary>