From 2c8ec4aa4ab7de611da9e200e3a98e1f0ef33fad Mon Sep 17 00:00:00 2001 From: ReinforceZwei <39955851+ReinforceZwei@users.noreply.github.com> Date: Sat, 15 Aug 2020 21:32:46 +0800 Subject: [PATCH] Implemented entity metadata to keep track of entity health (#1205) * Implement entity metadata protocol handling * Add health information for entity * Make AutoAttack check entity health * LivingEntity: Default health is 1.0 as per https://wiki.vg/Entity_metadata#Living_Entity * Fix entity metadata for lower MC versions * Fix commit 888297d (1.0f instead of 1.0) * Add OnEntityHealth ChatBot event (Remove protocol-dependant stuff from McClient (undo part of 85c32b9)) * Remove OnEntityMetadata in favor of OnEntityHealth Co-authored-by: ORelio --- MinecraftClient/ChatBot.cs | 7 + MinecraftClient/ChatBots/AutoAttack.cs | 2 +- MinecraftClient/Mapping/Entity.cs | 7 + MinecraftClient/McClient.cs | 14 ++ .../Protocol/Handlers/DataTypes.cs | 136 ++++++++++++++++++ .../Protocol/Handlers/PacketIncomingType.cs | 1 + .../Protocol/Handlers/Protocol18.cs | 10 ++ .../Handlers/Protocol18PacketTypes.cs | 9 +- .../Protocol/IMinecraftComHandler.cs | 7 + 9 files changed, 191 insertions(+), 2 deletions(-) diff --git a/MinecraftClient/ChatBot.cs b/MinecraftClient/ChatBot.cs index 1e7b59b4..790cbfcb 100644 --- a/MinecraftClient/ChatBot.cs +++ b/MinecraftClient/ChatBot.cs @@ -325,6 +325,13 @@ namespace MinecraftClient /// public virtual void OnRespawn() { } + /// + /// Called when the health of an entity changed + /// + /// Entity ID + /// The health of the entity + public virtual void OnEntityHealth(int entityID, float health) { } + /* =================================================================== */ /* ToolBox - Methods below might be useful while creating your bot. */ /* You should not need to interact with other classes of the program. */ diff --git a/MinecraftClient/ChatBots/AutoAttack.cs b/MinecraftClient/ChatBots/AutoAttack.cs index 095466f8..5f6965ae 100644 --- a/MinecraftClient/ChatBots/AutoAttack.cs +++ b/MinecraftClient/ChatBots/AutoAttack.cs @@ -112,7 +112,7 @@ namespace MinecraftClient.ChatBots /// If the entity should be attacked public bool handleEntity(Entity entity) { - if (!entity.Type.IsHostile()) + if (!entity.Type.IsHostile() || entity.Health <= 0) return false; bool isBeingAttacked = entitiesToAttack.ContainsKey(entity.ID); diff --git a/MinecraftClient/Mapping/Entity.cs b/MinecraftClient/Mapping/Entity.cs index 34ed4a3e..e1b86ee3 100644 --- a/MinecraftClient/Mapping/Entity.cs +++ b/MinecraftClient/Mapping/Entity.cs @@ -33,6 +33,11 @@ namespace MinecraftClient.Mapping /// public Location Location; + /// + /// Health of the entity + /// + public float Health; + /// /// Create a new entity based on Entity ID, Entity Type and location /// @@ -44,6 +49,7 @@ namespace MinecraftClient.Mapping this.ID = ID; this.Type = type; this.Location = location; + this.Health = 1.0f; } /// /// Create a new entity based on Entity ID, Entity Type, location, name and UUID @@ -60,6 +66,7 @@ namespace MinecraftClient.Mapping this.Location = location; this.UUID = uuid; this.Name = name; + this.Health = 1.0f; } } } diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 91b921c2..0e5fbb35 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -2111,6 +2111,20 @@ namespace MinecraftClient { DispatchBotEvent(bot => bot.OnUpdateScore(entityname, action, objectivename, value)); } + + /// + /// Called when the health of an entity changed + /// + /// Entity ID + /// The health of the entity + public void OnEntityHealth(int entityID, float health) + { + if (entities.ContainsKey(entityID)) + { + entities[entityID].Health = health; + DispatchBotEvent(bot => bot.OnEntityHealth(entityID, health)); + } + } #endregion } } diff --git a/MinecraftClient/Protocol/Handlers/DataTypes.cs b/MinecraftClient/Protocol/Handlers/DataTypes.cs index c66d3a17..df66bc61 100644 --- a/MinecraftClient/Protocol/Handlers/DataTypes.cs +++ b/MinecraftClient/Protocol/Handlers/DataTypes.cs @@ -494,6 +494,142 @@ namespace MinecraftClient.Protocol.Handlers } } + public Dictionary ReadNextMetadata(Queue cache) + { + Dictionary data = new Dictionary(); + byte Key = ReadNextByte(cache); + while (Key != 0xff) + { + int Type = ReadNextVarInt(cache); + + // starting from 1.13, Optional Chat is inserted as number 5 in 1.13 and IDs after 5 got shifted. + // Increase type ID by 1 if + // - below 1.13 + // - type ID larger than 4 + if (protocolversion < Protocol18Handler.MC113Version) + { + if (Type > 4) + { + Type += 1; + } + } + // Value's data type is depended on Type + object Value = null; + + // This is backward compatible since new type is appended to the end + // Version upgrade note + // - Check type ID got shifted or not + // - Add new type if any + switch (Type) + { + case 0: // byte + Value = ReadNextByte(cache); + break; + case 1: // VarInt + Value = ReadNextVarInt(cache); + break; + case 2: // Float + Value = ReadNextFloat(cache); + break; + case 3: // String + Value = ReadNextString(cache); + break; + case 4: // Chat + Value = ReadNextString(cache); + break; + case 5: // Optional Chat + if (ReadNextBool(cache)) + { + Value = ReadNextString(cache); + } + break; + case 6: // Slot + Value = ReadNextItemSlot(cache); + break; + case 7: // Boolean + Value = ReadNextBool(cache); + break; + case 8: // Rotation (3x floats) + List t = new List(); + t.Add(ReadNextFloat(cache)); + t.Add(ReadNextFloat(cache)); + t.Add(ReadNextFloat(cache)); + Value = t; + break; + case 9: // Position + Value = ReadNextLocation(cache); + break; + case 10: // Optional Position + if (ReadNextBool(cache)) + { + Value = ReadNextLocation(cache); + } + break; + case 11: // Direction (VarInt) + Value = ReadNextVarInt(cache); + break; + case 12: // Optional UUID + if (ReadNextBool(cache)) + { + Value = ReadNextUUID(cache); + } + break; + case 13: // Optional BlockID (VarInt) + if (ReadNextBool(cache)) + { + Value = ReadNextVarInt(cache); + } + break; + case 14: // NBT + Value = ReadNextNbt(cache); + break; + case 15: // Particle + // Currecutly not handled. Reading data only + int ParticleID = ReadNextVarInt(cache); + switch (ParticleID) + { + case 3: + ReadNextVarInt(cache); + break; + case 14: + ReadNextFloat(cache); + ReadNextFloat(cache); + ReadNextFloat(cache); + ReadNextFloat(cache); + break; + case 23: + ReadNextVarInt(cache); + break; + case 32: + ReadNextItemSlot(cache); + break; + } + break; + case 16: // Villager Data (3x VarInt) + List d = new List(); + d.Add(ReadNextVarInt(cache)); + d.Add(ReadNextVarInt(cache)); + d.Add(ReadNextVarInt(cache)); + Value = d; + break; + case 17: // Optional VarInt + if (ReadNextBool(cache)) + { + Value = ReadNextVarInt(cache); + } + break; + case 18: // Pose + Value = ReadNextVarInt(cache); + break; + default: + throw new System.IO.InvalidDataException("Unknown Metadata Type ID " + Type + ". Is this up to date for new MC Version?"); + } + data.Add(Key, Value); + Key = ReadNextByte(cache); + } + return data; + } + /// /// Build an uncompressed Named Binary Tag blob for sending over the network /// diff --git a/MinecraftClient/Protocol/Handlers/PacketIncomingType.cs b/MinecraftClient/Protocol/Handlers/PacketIncomingType.cs index a612a8f5..03d8da68 100644 --- a/MinecraftClient/Protocol/Handlers/PacketIncomingType.cs +++ b/MinecraftClient/Protocol/Handlers/PacketIncomingType.cs @@ -47,6 +47,7 @@ namespace MinecraftClient.Protocol.Handlers EntityEquipment, EntityVelocity, EntityEffect, + EntityMetadata, TimeUpdate, UpdateHealth, SetExperience, diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index e125a84b..056f9c31 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -861,6 +861,16 @@ namespace MinecraftClient.Protocol.Handlers handler.OnEntityProperties(EntityID, keys); } break; + case PacketIncomingType.EntityMetadata: + if (handler.GetEntityHandlingEnabled()) + { + int EntityID = dataTypes.ReadNextVarInt(packetData); + Dictionary metadata = dataTypes.ReadNextMetadata(packetData); + int healthField = protocolversion >= MC114Version ? 8 : 7; // Health is field no. 7 in 1.10+ and 8 in 1.14+ + if (metadata.ContainsKey(healthField) && metadata[healthField].GetType() == typeof(float)) + handler.OnEntityHealth(EntityID, (float)metadata[healthField]); + } + break; case PacketIncomingType.TimeUpdate: long WorldAge = dataTypes.ReadNextLong(packetData); long TimeOfday = dataTypes.ReadNextLong(packetData); diff --git a/MinecraftClient/Protocol/Handlers/Protocol18PacketTypes.cs b/MinecraftClient/Protocol/Handlers/Protocol18PacketTypes.cs index c958dbda..3e3c4ca3 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18PacketTypes.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18PacketTypes.cs @@ -109,6 +109,7 @@ namespace MinecraftClient.Protocol.Handlers case 0x3B: return PacketIncomingType.EntityVelocity; case 0x3C: return PacketIncomingType.EntityEquipment; case 0x4B: return PacketIncomingType.EntityEffect; + case 0x39: return PacketIncomingType.EntityMetadata; case 0x44: return PacketIncomingType.TimeUpdate; case 0x3E: return PacketIncomingType.UpdateHealth; case 0x3D: return PacketIncomingType.SetExperience; @@ -157,6 +158,7 @@ namespace MinecraftClient.Protocol.Handlers case 0x3D: return PacketIncomingType.EntityVelocity; case 0x3E: return PacketIncomingType.EntityEquipment; case 0x4E: return PacketIncomingType.EntityEffect; + case 0x3B: return PacketIncomingType.EntityMetadata; case 0x46: return PacketIncomingType.TimeUpdate; case 0x40: return PacketIncomingType.UpdateHealth; case 0x3F: return PacketIncomingType.SetExperience; @@ -205,6 +207,7 @@ namespace MinecraftClient.Protocol.Handlers case 0x3E: return PacketIncomingType.EntityVelocity; case 0x3F: return PacketIncomingType.EntityEquipment; case 0x4F: return PacketIncomingType.EntityEffect; + case 0x3C: return PacketIncomingType.EntityMetadata; case 0x47: return PacketIncomingType.TimeUpdate; case 0x41: return PacketIncomingType.UpdateHealth; case 0x40: return PacketIncomingType.SetExperience; @@ -253,6 +256,7 @@ namespace MinecraftClient.Protocol.Handlers case 0x41: return PacketIncomingType.EntityVelocity; case 0x42: return PacketIncomingType.EntityEquipment; case 0x53: return PacketIncomingType.EntityEffect; + case 0x3F: return PacketIncomingType.EntityMetadata; case 0x4A: return PacketIncomingType.TimeUpdate; case 0x44: return PacketIncomingType.UpdateHealth; case 0x43: return PacketIncomingType.SetExperience; @@ -301,6 +305,7 @@ namespace MinecraftClient.Protocol.Handlers case 0x41: return PacketIncomingType.EntityVelocity; case 0x46: return PacketIncomingType.EntityEquipment; case 0x59: return PacketIncomingType.EntityEffect; + case 0x43: return PacketIncomingType.EntityMetadata; case 0x4E: return PacketIncomingType.TimeUpdate; case 0x48: return PacketIncomingType.UpdateHealth; case 0x47: return PacketIncomingType.SetExperience; @@ -349,6 +354,7 @@ namespace MinecraftClient.Protocol.Handlers case 0x46: return PacketIncomingType.EntityVelocity; case 0x47: return PacketIncomingType.EntityEquipment; case 0x5A: return PacketIncomingType.EntityEffect; + case 0x44: return PacketIncomingType.EntityMetadata; case 0x4F: return PacketIncomingType.TimeUpdate; case 0x49: return PacketIncomingType.UpdateHealth; case 0x48: return PacketIncomingType.SetExperience; @@ -361,7 +367,7 @@ namespace MinecraftClient.Protocol.Handlers } } else { - switch (packetID) + switch (packetID) // MC 1.16+ { case 0x20: return PacketIncomingType.KeepAlive; case 0x25: return PacketIncomingType.JoinGame; @@ -396,6 +402,7 @@ namespace MinecraftClient.Protocol.Handlers case 0x46: return PacketIncomingType.EntityVelocity; case 0x47: return PacketIncomingType.EntityEquipment; case 0x59: return PacketIncomingType.EntityEffect; + case 0x44: return PacketIncomingType.EntityMetadata; case 0x4E: return PacketIncomingType.TimeUpdate; case 0x49: return PacketIncomingType.UpdateHealth; case 0x48: return PacketIncomingType.SetExperience; diff --git a/MinecraftClient/Protocol/IMinecraftComHandler.cs b/MinecraftClient/Protocol/IMinecraftComHandler.cs index 40ddcf81..3e6740f9 100644 --- a/MinecraftClient/Protocol/IMinecraftComHandler.cs +++ b/MinecraftClient/Protocol/IMinecraftComHandler.cs @@ -301,5 +301,12 @@ namespace MinecraftClient.Protocol /// The name of the objective the score belongs to /// he score to be displayed next to the entry. Only sent when Action does not equal 1. void OnUpdateScore(string entityname, byte action, string objectivename, int value); + + /// + /// Called when the health of an entity changed + /// + /// Entity ID + /// The health of the entity + void OnEntityHealth(int entityID, float health); } }