diff --git a/anticheat/entity/entity.go b/anticheat/entity/entity.go index 2fec2180..c65967aa 100755 --- a/anticheat/entity/entity.go +++ b/anticheat/entity/entity.go @@ -2,6 +2,7 @@ package entity import ( "log/slog" + "maps" "github.com/ethaniccc/float32-cube/cube" "github.com/go-gl/mathgl/mgl32" @@ -15,6 +16,7 @@ const ( type Entity struct { RuntimeId uint64 + UniqueId int64 Metadata map[uint32]any Type string @@ -39,43 +41,64 @@ type Entity struct { Height float32 Scale float32 + NetworkOffset float32 + IsPlayer bool historySize int log **slog.Logger } +// Config ... +type Config struct { + RuntimeID uint64 + UniqueID int64 + + Type string + Metadata map[uint32]any + + NetworkPosition mgl32.Vec3 + HistorySize int + IsPlayer bool + + Width, Height, Scale float32 + + Log **slog.Logger +} + // New creates and returns a new Entity instance. -func New( - runtimeId uint64, - entType string, - metadata map[uint32]any, - pos mgl32.Vec3, - historySize int, - isPlayer bool, - width, height, scale float32, - log **slog.Logger, -) *Entity { +func New(c Config) *Entity { + metadata := maps.Clone(c.Metadata) + if metadata == nil { + metadata = make(map[uint32]any) + } + offset := networkOffset(c.Type, metadata) + pos := c.NetworkPosition + pos[1] -= offset + e := &Entity{ - RuntimeId: runtimeId, + RuntimeId: c.RuntimeID, + UniqueId: c.UniqueID, - Type: entType, + Type: c.Type, Metadata: metadata, Position: pos, PrevPosition: pos, RecvPosition: pos, - Width: width, - Height: height, - Scale: scale, + Width: c.Width, + Height: c.Height, + Scale: c.Scale, + + NetworkOffset: offset, - PositionHistory: utils.NewCircularQueue(historySize, func() (hp HistoricalPosition) { return }), + PositionHistory: utils.NewCircularQueue(c.HistorySize, func() (hp HistoricalPosition) { return }), - IsPlayer: isPlayer, + IsPlayer: c.IsPlayer, - log: log, - historySize: historySize, + log: c.Log, + historySize: c.HistorySize, } /* e.InterpolationTicks = EntityMobInterpolationTicks if isPlayer { @@ -86,16 +109,16 @@ func New( } // ReceivePosition updates the position of the entity, and adds the previous position to its position history. -func (e *Entity) ReceivePosition(hp HistoricalPosition) { +func (e *Entity) ReceivePosition(pos mgl32.Vec3, teleport bool) { e.PrevRecvPosition = e.RecvPosition - e.RecvPosition = hp.Position + e.RecvPosition = pos e.InterpolationTicks = EntityMobInterpolationTicks if e.IsPlayer { e.InterpolationTicks = EntityPlayerInterpolationTicks } - if hp.Teleport { + if teleport { e.TicksSinceTeleport = 0 e.InterpolationTicks = 1 } @@ -120,6 +143,15 @@ func (e *Entity) UpdateVelocity(vel mgl32.Vec3) { e.RecvVelocity = vel } +// UpdateMetadata ... +func (e *Entity) UpdateMetadata(metadata map[uint32]any) { + if e.Metadata == nil { + e.Metadata = make(map[uint32]any, len(metadata)) + } + maps.Copy(e.Metadata, metadata) + e.NetworkOffset = networkOffset(e.Type, e.Metadata) +} + // Box returns the entity's bounding box. func (e *Entity) Box(pos mgl32.Vec3) cube.BBox { w := (e.Width * e.Scale) / 2 diff --git a/anticheat/entity/metadata.go b/anticheat/entity/metadata.go index ceac6088..8329aa72 100755 --- a/anticheat/entity/metadata.go +++ b/anticheat/entity/metadata.go @@ -5,9 +5,17 @@ const ( DataKeyOwnerID = 5 DataKeyTargetID = 6 DataKeyFireworkMetadata = 16 + DataKeyPlayerFlags = 26 DataKeyScale = 38 DataKeyBoundingBoxWidth = 53 DataKeyBoundingBoxHeight = 54 + DataKeyFlagsTwo = 92 +) + +// DataPlayerFlag ... +const ( + DataPlayerFlagSleep = 1 + DataPlayerFlagDead = 2 ) const ( diff --git a/anticheat/entity/network_offset.go b/anticheat/entity/network_offset.go new file mode 100644 index 00000000..78f73c0e --- /dev/null +++ b/anticheat/entity/network_offset.go @@ -0,0 +1,25 @@ +package entity + +import ( + "github.com/oomph-ac/oomph/anticheat/game" + "github.com/oomph-ac/oomph/anticheat/utils" +) + +// networkOffset ... +func networkOffset(entityType string, metadata map[uint32]any) float32 { + switch entityType { + case TypePlayer: + if utils.HasMetadataFlag[byte](metadata, DataKeyPlayerFlags, DataPlayerFlagSleep) || utils.HasMetadataFlag[int64](metadata, DataKeyFlagsTwo, DataFlagSleeping) { + return game.SleepingPlayerHeightOffset + } + return game.DefaultPlayerHeightOffset + case TypeItem, TypeFallingBlock, TypeMinecart, TypeChestMinecart, TypeCommandBlockMinecart, TypeHopperMinecart, TypeTNTMinecart: + return game.ItemAndMinecartNetworkOffset + case TypeBoat, TypeChestBoat: + return game.BoatNetworkOffset + case TypeTNT: + return game.PrimedTNTNetworkOffset + default: + return 0 + } +} diff --git a/anticheat/entity/type.go b/anticheat/entity/type.go index 760f788d..4be54457 100644 --- a/anticheat/entity/type.go +++ b/anticheat/entity/type.go @@ -2,4 +2,16 @@ package entity const ( TypeFireworksRocket = "minecraft:fireworks_rocket" + + TypeBoat = "minecraft:boat" + TypeChestBoat = "minecraft:chest_boat" + TypeChestMinecart = "minecraft:chest_minecart" + TypeCommandBlockMinecart = "minecraft:command_block_minecart" + TypeFallingBlock = "minecraft:falling_block" + TypeHopperMinecart = "minecraft:hopper_minecart" + TypeItem = "minecraft:item" + TypeMinecart = "minecraft:minecart" + TypePlayer = "minecraft:player" + TypeTNT = "minecraft:tnt" + TypeTNTMinecart = "minecraft:tnt_minecart" ) diff --git a/anticheat/game/movement.go b/anticheat/game/movement.go index 6964db05..b7196308 100755 --- a/anticheat/game/movement.go +++ b/anticheat/game/movement.go @@ -18,8 +18,13 @@ const ( MaxSneakImpulse = float32(0.3) MaxNormalizedImpulse = float32(0.70710678118) // 1/sqrt(2) - DefaultPlayerHeightOffset = float32(1.62) + DefaultPlayerHeightOffset = float32(1.62001) SneakingPlayerHeightOffset = float32(1.27) + SleepingPlayerHeightOffset = float32(0.2) + + ItemAndMinecartNetworkOffset = float32(0.5) + BoatNetworkOffset = float32(0.375) + PrimedTNTNetworkOffset = float32(0.49) JumpDelayTicks = 10 GlideBoostTicks = 20 diff --git a/anticheat/player/component/acknowledgement/entities.go b/anticheat/player/component/acknowledgement/entities.go index fdb33e1d..03a01b44 100644 --- a/anticheat/player/component/acknowledgement/entities.go +++ b/anticheat/player/component/acknowledgement/entities.go @@ -4,18 +4,21 @@ import ( "github.com/go-gl/mathgl/mgl32" "github.com/oomph-ac/oomph/anticheat/entity" "github.com/oomph-ac/oomph/anticheat/player" + "github.com/sandertv/gophertunnel/minecraft/protocol" ) -type EntitySize struct { - mEntity *entity.Entity - width float32 - height float32 - scale float32 +type EntityData struct { + mEntity *entity.Entity + metadata map[uint32]any + width float32 + height float32 + scale float32 } -func NewEntitySizeACK(e *entity.Entity, width, height, scale float32) *EntitySize { - return &EntitySize{ - mEntity: e, +func NewEntityDataACK(e *entity.Entity, metadata map[uint32]any, width, height, scale float32) *EntityData { + return &EntityData{ + mEntity: e, + metadata: metadata, width: width, height: height, @@ -23,7 +26,8 @@ func NewEntitySizeACK(e *entity.Entity, width, height, scale float32) *EntitySiz } } -func (ack *EntitySize) Run() { +func (ack *EntityData) Run() { + ack.mEntity.UpdateMetadata(ack.metadata) ack.mEntity.Width = ack.width ack.mEntity.Height = ack.height ack.mEntity.Scale = ack.scale @@ -56,3 +60,36 @@ func (ack *EntityPosition) Run() { ) ack.mPlayer = nil } + +type EntityDeltaPosition struct { + mPlayer *player.Player + + positionX protocol.Optional[float32] + positionY protocol.Optional[float32] + positionZ protocol.Optional[float32] + runtimeID uint64 + teleport bool +} + +func NewEntityDeltaPositionACK(p *player.Player, posX, posY, posZ protocol.Optional[float32], rid uint64, teleport bool) *EntityDeltaPosition { + return &EntityDeltaPosition{ + mPlayer: p, + positionX: posX, + positionY: posY, + positionZ: posZ, + runtimeID: rid, + teleport: teleport, + } +} + +func (ack *EntityDeltaPosition) Run() { + ack.mPlayer.ClientEntityTracker().MoveEntityDelta( + ack.runtimeID, + 0, + ack.positionX, + ack.positionY, + ack.positionZ, + ack.teleport, + ) + ack.mPlayer = nil +} diff --git a/anticheat/player/component/combat.go b/anticheat/player/component/combat.go index 1a5b372b..cb379c87 100644 --- a/anticheat/player/component/combat.go +++ b/anticheat/player/component/combat.go @@ -489,6 +489,9 @@ func (c *AuthoritativeCombatComponent) checkForMispredictedEntity() bool { rewTick := c.mPlayer.ClientTick - 1 attackPos := c.endAttackPos for rid, e := range c.entityTracker().All() { + if e.Type == entity.TypeItem { + continue + } rewind, ok := e.Rewind(rewTick) if !ok { continue diff --git a/anticheat/player/component/entities.go b/anticheat/player/component/entities.go index 19954827..f65b3c71 100644 --- a/anticheat/player/component/entities.go +++ b/anticheat/player/component/entities.go @@ -3,26 +3,28 @@ package component import ( "github.com/go-gl/mathgl/mgl32" "github.com/oomph-ac/oomph/anticheat/entity" - "github.com/oomph-ac/oomph/anticheat/game" "github.com/oomph-ac/oomph/anticheat/player" "github.com/oomph-ac/oomph/anticheat/player/component/acknowledgement" "github.com/oomph-ac/oomph/anticheat/utils" + "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) // EntityTrackerComponent is a component that handles entities that the member player is // viewing on their screen. type EntityTrackerComponent struct { - mPlayer *player.Player - entities map[uint64]*entity.Entity + mPlayer *player.Player + entities map[uint64]*entity.Entity + runtimeIDs map[int64]uint64 isClientTracker bool } func NewEntityTrackerComponent(p *player.Player, clientTracker bool) *EntityTrackerComponent { return &EntityTrackerComponent{ - mPlayer: p, - entities: make(map[uint64]*entity.Entity), + mPlayer: p, + entities: make(map[uint64]*entity.Entity), + runtimeIDs: make(map[int64]uint64), isClientTracker: clientTracker, } @@ -30,12 +32,28 @@ func NewEntityTrackerComponent(p *player.Player, clientTracker bool) *EntityTrac // AddEntity adds an entity to the entity tracker component. func (c *EntityTrackerComponent) AddEntity(rid uint64, ent *entity.Entity) { + if previousRID, ok := c.runtimeIDs[ent.UniqueId]; ok && previousRID != rid { + c.RemoveEntity(previousRID) + } + c.RemoveEntity(rid) c.entities[rid] = ent + c.runtimeIDs[ent.UniqueId] = rid } // RemoveEntity removes an entity from the entity tracker component. func (c *EntityTrackerComponent) RemoveEntity(rid uint64) { - delete(c.entities, rid) + if e, ok := c.entities[rid]; ok { + delete(c.runtimeIDs, e.UniqueId) + delete(c.entities, rid) + } +} + +// RemoveEntityByUniqueID ... +func (c *EntityTrackerComponent) RemoveEntityByUniqueID(uniqueID int64) { + if rid, ok := c.runtimeIDs[uniqueID]; ok { + delete(c.runtimeIDs, uniqueID) + delete(c.entities, rid) + } } // FindEntity searches for an entity in the entity tracker component from the given runtime ID. @@ -51,16 +69,31 @@ func (c *EntityTrackerComponent) All() map[uint64]*entity.Entity { // MoveEntity moves an entity to the given position func (c *EntityTrackerComponent) MoveEntity(rid uint64, tick int64, pos mgl32.Vec3, teleport bool) { if e, ok := c.entities[rid]; ok { - if e.IsPlayer { - pos[1] -= game.DefaultPlayerHeightOffset - } - e.ReceivePosition(entity.HistoricalPosition{ - Position: pos, - PrevPosition: e.RecvPosition, - Teleport: teleport, - Tick: tick, - }) + pos[1] -= e.NetworkOffset + e.ReceivePosition(pos, teleport) + } +} + +// MoveEntityDelta ... +func (c *EntityTrackerComponent) MoveEntityDelta(rid uint64, tick int64, posX, posY, posZ protocol.Optional[float32], teleport bool) { + e, ok := c.entities[rid] + if !ok { + return + } + pos, moved := e.RecvPosition, false + if x, has := posX.Value(); has { + pos[0], moved = x, true } + if y, has := posY.Value(); has { + pos[1], moved = y-e.NetworkOffset, true + } + if z, has := posZ.Value(); has { + pos[2], moved = z, true + } + if !moved { + return + } + e.ReceivePosition(pos, teleport) } // HandleMovePlayer is a function that handles entity position updates sent with MovePlayerPacket. @@ -91,13 +124,30 @@ func (c *EntityTrackerComponent) HandleMoveActorAbsolute(pk *packet.MoveActorAbs )) } +// HandleMoveActorDelta ... +func (c *EntityTrackerComponent) HandleMoveActorDelta(pk *packet.MoveActorDelta) { + if !c.isClientTracker { + c.MoveEntityDelta(pk.EntityRuntimeID, c.mPlayer.ServerTick, pk.PositionX, pk.PositionY, pk.PositionZ, pk.ForceMove) + return + } + c.mPlayer.ACKs().Add(acknowledgement.NewEntityDeltaPositionACK( + c.mPlayer, + pk.PositionX, + pk.PositionY, + pk.PositionZ, + pk.EntityRuntimeID, + pk.ForceMove, + )) +} + // HandleSetActorData is a function that handles entity data updates sent with SetActorDataPacket. func (c *EntityTrackerComponent) HandleSetActorData(pk *packet.SetActorData) { if e := c.FindEntity(pk.EntityRuntimeID); e != nil { width, height, scale := calculateBBSize(pk.EntityMetadata, e.Width, e.Height, e.Scale) if c.isClientTracker { - c.mPlayer.ACKs().Add(acknowledgement.NewEntitySizeACK(e, width, height, scale)) + c.mPlayer.ACKs().Add(acknowledgement.NewEntityDataACK(e, pk.EntityMetadata, width, height, scale)) } else { + e.UpdateMetadata(pk.EntityMetadata) e.Width, e.Height, e.Scale = width, height, scale } } diff --git a/anticheat/player/entities.go b/anticheat/player/entities.go index b689aef9..317b3999 100644 --- a/anticheat/player/entities.go +++ b/anticheat/player/entities.go @@ -3,6 +3,7 @@ package player import ( "github.com/go-gl/mathgl/mgl32" "github.com/oomph-ac/oomph/anticheat/entity" + "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -13,17 +14,23 @@ type EntityTrackerComponent interface { AddEntity(rid uint64, ent *entity.Entity) // RemoveEntity removes an entity from the entity tracker component. RemoveEntity(rid uint64) + // RemoveEntityByUniqueID ... + RemoveEntityByUniqueID(uniqueID int64) // FindEntity searches for an entity in the entity tracker component from the given runtime ID. FindEntity(rid uint64) *entity.Entity // All returns all the entities the entity tracker component is tracking. All() map[uint64]*entity.Entity // MoveEntity moves an entity to the given position. MoveEntity(rid uint64, tick int64, pos mgl32.Vec3, teleport bool) + // MoveEntityDelta ... + MoveEntityDelta(rid uint64, tick int64, posX, posY, posZ protocol.Optional[float32], teleport bool) // HandleMovePlayer is a function that handles entity position updates sent with MovePlayerPacket. HandleMovePlayer(pk *packet.MovePlayer) // HandleMoveActorAbsolute is a function that handles entity position updates sent with MoveActorAbsolutePacket. HandleMoveActorAbsolute(pk *packet.MoveActorAbsolute) + // HandleMoveActorDelta ... + HandleMoveActorDelta(pk *packet.MoveActorDelta) // HandleSetActorData is a function that handles entity data updates sent with SetActorDataPacket. HandleSetActorData(pk *packet.SetActorData) diff --git a/anticheat/player/packet.go b/anticheat/player/packet.go index 06db703f..7dae38ed 100644 --- a/anticheat/player/packet.go +++ b/anticheat/player/packet.go @@ -36,6 +36,7 @@ var ClientDecode = []uint32{ var ServerDecode = []uint32{ packet.IDAddActor, + packet.IDAddItemActor, packet.IDAddPlayer, packet.IDChunkRadiusUpdated, packet.IDInventorySlot, @@ -44,6 +45,7 @@ var ServerDecode = []uint32{ packet.IDLevelChunk, packet.IDMobEffect, packet.IDMoveActorAbsolute, + packet.IDMoveActorDelta, packet.IDMovePlayer, packet.IDRemoveActor, packet.IDSetActorData, @@ -370,56 +372,42 @@ func (p *Player) handleServerPacket(ctx *context.HandlePacketContext) { p.initOomphCommand(pk) case *packet.AddActor: width, height, scale := calculateBBSize(pk.EntityMetadata, 0.6, 1.8, 1.0) - p.entTracker.AddEntity(pk.EntityRuntimeID, entity.New( - pk.EntityRuntimeID, - pk.EntityType, - pk.EntityMetadata, - pk.Position, - p.Opts().Network.MaxEntityRewind, - false, - width, - height, - scale, - &p.log, - )) - p.clientEntTracker.AddEntity(pk.EntityRuntimeID, entity.New( - pk.EntityRuntimeID, - pk.EntityType, - pk.EntityMetadata, - pk.Position, - p.Opts().Network.MaxEntityRewind, - false, - width, - height, - scale, - &p.log, - )) + p.trackEntity(entity.Config{ + RuntimeID: pk.EntityRuntimeID, + UniqueID: pk.EntityUniqueID, + Type: pk.EntityType, + Metadata: pk.EntityMetadata, + NetworkPosition: pk.Position, + Width: width, + Height: height, + Scale: scale, + }) + case *packet.AddItemActor: + const itemEntitySize = 0.25 + width, height, scale := calculateBBSize(pk.EntityMetadata, itemEntitySize, itemEntitySize, 1.0) + p.trackEntity(entity.Config{ + RuntimeID: pk.EntityRuntimeID, + UniqueID: pk.EntityUniqueID, + Type: entity.TypeItem, + Metadata: pk.EntityMetadata, + NetworkPosition: pk.Position, + Width: width, + Height: height, + Scale: scale, + }) case *packet.AddPlayer: width, height, scale := calculateBBSize(pk.EntityMetadata, 0.6, 1.8, 1.0) - p.entTracker.AddEntity(pk.EntityRuntimeID, entity.New( - pk.EntityRuntimeID, - "", - pk.EntityMetadata, - pk.Position, - p.Opts().Network.MaxEntityRewind, - true, - width, - height, - scale, - &p.log, - )) - p.clientEntTracker.AddEntity(pk.EntityRuntimeID, entity.New( - pk.EntityRuntimeID, - "", - pk.EntityMetadata, - pk.Position, - p.Opts().Network.MaxEntityRewind, - true, - width, - height, - scale, - &p.log, - )) + p.trackEntity(entity.Config{ + RuntimeID: pk.EntityRuntimeID, + UniqueID: pk.AbilityData.EntityUniqueID, + Type: entity.TypePlayer, + Metadata: pk.EntityMetadata, + NetworkPosition: pk.Position, + IsPlayer: true, + Width: width, + Height: height, + Scale: scale, + }) case *packet.ChunkRadiusUpdated: p.worldUpdater.SetServerChunkRadius(pk.ChunkRadius + 4) case *packet.InventorySlot: @@ -452,6 +440,13 @@ func (p *Player) handleServerPacket(ctx *context.HandlePacketContext) { } else { p.movement.ServerUpdate(pk) } + case *packet.MoveActorDelta: + if pk.EntityRuntimeID != p.RuntimeId { + p.entTracker.HandleMoveActorDelta(pk) + if p.opts.Combat.EnableClientEntityTracking { + p.clientEntTracker.HandleMoveActorDelta(pk) + } + } case *packet.MovePlayer: pk.Tick = 0 ctx.SetModified() @@ -465,8 +460,8 @@ func (p *Player) handleServerPacket(ctx *context.HandlePacketContext) { p.movement.ServerUpdate(pk) } case *packet.RemoveActor: - p.entTracker.RemoveEntity(uint64(pk.EntityUniqueID)) - p.clientEntTracker.RemoveEntity(uint64(pk.EntityUniqueID)) + p.entTracker.RemoveEntityByUniqueID(pk.EntityUniqueID) + p.clientEntTracker.RemoveEntityByUniqueID(pk.EntityUniqueID) case *packet.SetActorData: pk.Tick = 0 ctx.SetModified() @@ -555,3 +550,12 @@ func (p *Player) handleServerPacket(ctx *context.HandlePacketContext) { } } } + +// trackEntity ... +func (p *Player) trackEntity(c entity.Config) { + c.HistorySize = p.Opts().Network.MaxEntityRewind + c.Log = &p.log + + p.entTracker.AddEntity(c.RuntimeID, entity.New(c)) + p.clientEntTracker.AddEntity(c.RuntimeID, entity.New(c)) +} diff --git a/anticheat/utils/bit.go b/anticheat/utils/bit.go index 7a295cdf..59d67118 100755 --- a/anticheat/utils/bit.go +++ b/anticheat/utils/bit.go @@ -28,6 +28,12 @@ func HasDataFlag(flag uint64, data int64) bool { return (data & (1 << (flag % 64))) > 0 } +// HasMetadataFlag ... +func HasMetadataFlag[T byte | int64](metadata map[uint32]any, key uint32, flag uint64) bool { + v, ok := metadata[key].(T) + return ok && HasDataFlag(flag, int64(v)) +} + // RemoveDataFlag removes the specified flag from the flags. func RemoveDataFlag(flags int64, flag uint64) int64 { return flags &^ (1 << (flag % 64))