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 <ORelio@users.noreply.github.com>
This commit is contained in:
ReinforceZwei 2020-08-15 21:32:46 +08:00 committed by GitHub
parent 526dabd1e7
commit 2c8ec4aa4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 191 additions and 2 deletions

View file

@ -325,6 +325,13 @@ namespace MinecraftClient
/// </summary>
public virtual void OnRespawn() { }
/// <summary>
/// Called when the health of an entity changed
/// </summary>
/// <param name="entityID">Entity ID</param>
/// <param name="health">The health of the entity</param>
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. */

View file

@ -112,7 +112,7 @@ namespace MinecraftClient.ChatBots
/// <returns>If the entity should be attacked</returns>
public bool handleEntity(Entity entity)
{
if (!entity.Type.IsHostile())
if (!entity.Type.IsHostile() || entity.Health <= 0)
return false;
bool isBeingAttacked = entitiesToAttack.ContainsKey(entity.ID);

View file

@ -33,6 +33,11 @@ namespace MinecraftClient.Mapping
/// </summary>
public Location Location;
/// <summary>
/// Health of the entity
/// </summary>
public float Health;
/// <summary>
/// Create a new entity based on Entity ID, Entity Type and location
/// </summary>
@ -44,6 +49,7 @@ namespace MinecraftClient.Mapping
this.ID = ID;
this.Type = type;
this.Location = location;
this.Health = 1.0f;
}
/// <summary>
/// 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;
}
}
}

View file

@ -2111,6 +2111,20 @@ namespace MinecraftClient
{
DispatchBotEvent(bot => bot.OnUpdateScore(entityname, action, objectivename, value));
}
/// <summary>
/// Called when the health of an entity changed
/// </summary>
/// <param name="entityID">Entity ID</param>
/// <param name="health">The health of the entity</param>
public void OnEntityHealth(int entityID, float health)
{
if (entities.ContainsKey(entityID))
{
entities[entityID].Health = health;
DispatchBotEvent(bot => bot.OnEntityHealth(entityID, health));
}
}
#endregion
}
}

View file

@ -494,6 +494,142 @@ namespace MinecraftClient.Protocol.Handlers
}
}
public Dictionary<int, object> ReadNextMetadata(Queue<byte> cache)
{
Dictionary<int, object> data = new Dictionary<int, object>();
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<float> t = new List<float>();
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<int> d = new List<int>();
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;
}
/// <summary>
/// Build an uncompressed Named Binary Tag blob for sending over the network
/// </summary>

View file

@ -47,6 +47,7 @@ namespace MinecraftClient.Protocol.Handlers
EntityEquipment,
EntityVelocity,
EntityEffect,
EntityMetadata,
TimeUpdate,
UpdateHealth,
SetExperience,

View file

@ -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<int, object> 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);

View file

@ -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;

View file

@ -301,5 +301,12 @@ namespace MinecraftClient.Protocol
/// <param name="objectivename">The name of the objective the score belongs to</param>
/// <param name="value">he score to be displayed next to the entry. Only sent when Action does not equal 1.</param>
void OnUpdateScore(string entityname, byte action, string objectivename, int value);
/// <summary>
/// Called when the health of an entity changed
/// </summary>
/// <param name="entityID">Entity ID</param>
/// <param name="health">The health of the entity</param>
void OnEntityHealth(int entityID, float health);
}
}