diff --git a/patches/server/0002-Sakura-Utils.patch b/patches/server/0002-Sakura-Utils.patch index 31cac87..2a4f551 100644 --- a/patches/server/0002-Sakura-Utils.patch +++ b/patches/server/0002-Sakura-Utils.patch @@ -4,37 +4,6 @@ Date: Tue, 23 May 2023 23:07:20 +0100 Subject: [PATCH] Sakura Utils -diff --git a/src/main/java/me/samsuik/sakura/utils/collections/EntityTable.java b/src/main/java/me/samsuik/sakura/utils/collections/EntityTable.java -new file mode 100644 -index 0000000000000000000000000000000000000000..54292c693f89ee1444f8b22f4c1488e95ef6bcde ---- /dev/null -+++ b/src/main/java/me/samsuik/sakura/utils/collections/EntityTable.java -@@ -0,0 +1,25 @@ -+package me.samsuik.sakura.utils.collections; -+ -+import it.unimi.dsi.fastutil.HashCommon; -+import net.minecraft.world.entity.Entity; -+ -+public final class EntityTable { -+ -+ private final Entity[] entities; -+ private final int mask; -+ -+ public EntityTable(int size) { -+ int n = HashCommon.nextPowerOfTwo(size - 1); -+ entities = new Entity[n]; -+ mask = n - 1; -+ } -+ -+ public Entity locate(Entity entity) { -+ int pos = entity.blockPosition().hashCode(); -+ int key = pos & mask; -+ Entity found = entities[key]; -+ entities[key] = entity; -+ return found; -+ } -+ -+} diff --git a/src/main/java/me/samsuik/sakura/utils/collections/FixedSizeCustomObjectTable.java b/src/main/java/me/samsuik/sakura/utils/collections/FixedSizeCustomObjectTable.java new file mode 100644 index 0000000000000000000000000000000000000000..e97f3cc00945f79026af894685d6104dfc920a35 @@ -97,10 +66,10 @@ index 0000000000000000000000000000000000000000..e97f3cc00945f79026af894685d6104d +} diff --git a/src/main/java/me/samsuik/sakura/utils/collections/OrderedComparatorList.java b/src/main/java/me/samsuik/sakura/utils/collections/OrderedComparatorList.java new file mode 100644 -index 0000000000000000000000000000000000000000..239fc8823b32ae5c8f6e3bfd6ecdde0ccd1e5a8b +index 0000000000000000000000000000000000000000..edbbf9dea7c6659c2053407dc55b5a2a1cfcb0dd --- /dev/null +++ b/src/main/java/me/samsuik/sakura/utils/collections/OrderedComparatorList.java -@@ -0,0 +1,51 @@ +@@ -0,0 +1,49 @@ +package me.samsuik.sakura.utils.collections; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; @@ -109,7 +78,6 @@ index 0000000000000000000000000000000000000000..239fc8823b32ae5c8f6e3bfd6ecdde0c +import java.util.Comparator; + +public final class OrderedComparatorList extends ObjectArrayList { -+ + private final Comparator comparator; + private boolean binarySearch = true; + @@ -123,41 +91,40 @@ index 0000000000000000000000000000000000000000..239fc8823b32ae5c8f6e3bfd6ecdde0c + } + + private void validateBounds(int index, T t, boolean up) { -+ if (index != 0 && comparator.compare(get(index - 1), t) > 0) { -+ binarySearch = false; -+ } else if (up && index < size() - 1 && comparator.compare(get(index + 1), t) < 0) { -+ binarySearch = false; ++ if (index != 0 && this.comparator.compare(get(index - 1), t) > 0) { ++ this.binarySearch = false; ++ } else if (up && index < size() - 1 && this.comparator.compare(get(index + 1), t) < 0) { ++ this.binarySearch = false; + } + } + + @Override + public boolean add(T t) { -+ validateBounds(size(), t, false); ++ this.validateBounds(size(), t, false); + return super.add(t); + } + + @Override + public void add(int index, T t) { -+ validateBounds(index, t, true); ++ this.validateBounds(index, t, true); + super.add(index, t); + } + + @Override + public int indexOf(final Object k) { -+ if (binarySearch) { -+ return Math.max(Arrays.binarySearch(a, (T) k, comparator), -1); ++ if (this.binarySearch) { ++ return Math.max(Arrays.binarySearch(this.a, (T) k, this.comparator), -1); + } else { + return super.indexOf(k); + } + } -+ +} diff --git a/src/main/java/me/samsuik/sakura/utils/collections/TrackedEntityChunkMap.java b/src/main/java/me/samsuik/sakura/utils/collections/TrackedEntityChunkMap.java new file mode 100644 -index 0000000000000000000000000000000000000000..267db86c5d12a804d2f9c868df996a3391910cbd +index 0000000000000000000000000000000000000000..d112b559eaff82c32e943edf34c616565cb1e189 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/utils/collections/TrackedEntityChunkMap.java -@@ -0,0 +1,34 @@ +@@ -0,0 +1,32 @@ +package me.samsuik.sakura.utils.collections; + +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; @@ -166,7 +133,6 @@ index 0000000000000000000000000000000000000000..267db86c5d12a804d2f9c868df996a33 +import net.minecraft.server.level.ChunkMap; + +public final class TrackedEntityChunkMap extends Int2ObjectOpenHashMap { -+ + private final ObjectArrayList entityList = new UnorderedIndexedList<>(); + + @Override @@ -174,54 +140,50 @@ index 0000000000000000000000000000000000000000..267db86c5d12a804d2f9c868df996a33 + ChunkMap.TrackedEntity tracked = super.put(k, trackedEntity); + // bad plugins may replace entries + if (tracked != null) -+ entityList.remove(trackedEntity); -+ entityList.add(trackedEntity); ++ this.entityList.remove(trackedEntity); ++ this.entityList.add(trackedEntity); + return tracked; + } + + @Override + public ChunkMap.TrackedEntity remove(int k) { + ChunkMap.TrackedEntity tracked = super.remove(k); -+ entityList.remove(tracked); ++ this.entityList.remove(tracked); + return tracked; + } + + @Override + public ObjectCollection values() { -+ return entityList; ++ return this.entityList; + } -+ +} diff --git a/src/main/java/me/samsuik/sakura/utils/collections/UnorderedIndexedList.java b/src/main/java/me/samsuik/sakura/utils/collections/UnorderedIndexedList.java new file mode 100644 -index 0000000000000000000000000000000000000000..4ca3bf6d6c7aec3a1b31e6ef4f863fa5c34888bd +index 0000000000000000000000000000000000000000..131dc69cdd2c121975199324022f942194d345c8 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/utils/collections/UnorderedIndexedList.java -@@ -0,0 +1,65 @@ +@@ -0,0 +1,61 @@ +package me.samsuik.sakura.utils.collections; + +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +public final class UnorderedIndexedList extends ObjectArrayList { ++ private final Int2IntOpenHashMap elementToIndex; + -+ private final Int2IntOpenHashMap elementToIndex = new Int2IntOpenHashMap(); -+ -+ { -+ elementToIndex.defaultReturnValue(-1); ++ public UnorderedIndexedList() { ++ this(DEFAULT_INITIAL_CAPACITY); + } + + public UnorderedIndexedList(int capacity) { + super(capacity); -+ } -+ -+ public UnorderedIndexedList() { -+ super(); ++ this.elementToIndex = new Int2IntOpenHashMap(); ++ this.elementToIndex.defaultReturnValue(-1); + } + + @Override + public boolean add(final T t) { -+ elementToIndex.put(t.hashCode(), size()); ++ this.elementToIndex.put(t.hashCode(), size()); + return super.add(t); + } + @@ -233,20 +195,20 @@ index 0000000000000000000000000000000000000000..4ca3bf6d6c7aec3a1b31e6ef4f863fa5 + if (index != tail) { + final T tailObj = a[tail]; + if (tailObj != null) -+ elementToIndex.put(tailObj.hashCode(), index); -+ a[index] = tailObj; ++ this.elementToIndex.put(tailObj.hashCode(), index); ++ this.a[index] = tailObj; + } + + if (at != null) -+ elementToIndex.remove(at.hashCode()); -+ a[tail] = null; -+ size = tail; ++ this.elementToIndex.remove(at.hashCode()); ++ this.a[tail] = null; ++ this.size = tail; + return at; + } + + @Override + public void clear() { -+ elementToIndex.clear(); ++ this.elementToIndex.clear(); + super.clear(); + } + @@ -254,25 +216,23 @@ index 0000000000000000000000000000000000000000..4ca3bf6d6c7aec3a1b31e6ef4f863fa5 + public int indexOf(final Object k) { + if (k == null) return -1; + // entities uses their id as a hashcode -+ return elementToIndex.get(k.hashCode()); ++ return this.elementToIndex.get(k.hashCode()); + } + + @Override + public void add(final int index, final T t) { + throw new UnsupportedOperationException(); + } -+ +} diff --git a/src/main/java/me/samsuik/sakura/utils/objects/Expiry.java b/src/main/java/me/samsuik/sakura/utils/objects/Expiry.java new file mode 100644 -index 0000000000000000000000000000000000000000..93a5655d9dc355d0596c86ea7b592d14ff941476 +index 0000000000000000000000000000000000000000..65e3c06062f529fd3e70eedccfe38b93c6c66c60 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/utils/objects/Expiry.java -@@ -0,0 +1,21 @@ +@@ -0,0 +1,19 @@ +package me.samsuik.sakura.utils.objects; + +public final class Expiry { -+ + private long expireAt; + private final int inc; + @@ -288,5 +248,4 @@ index 0000000000000000000000000000000000000000..93a5655d9dc355d0596c86ea7b592d14 + public boolean isExpired(long tick) { + return tick >= this.expireAt; + } -+ +} diff --git a/patches/server/0003-Sakura-Configuration-Files.patch b/patches/server/0003-Sakura-Configuration-Files.patch index 9fc9323..57c1d8f 100644 --- a/patches/server/0003-Sakura-Configuration-Files.patch +++ b/patches/server/0003-Sakura-Configuration-Files.patch @@ -92,7 +92,7 @@ index 0000000000000000000000000000000000000000..87d8fd1fe60dfea4ce697048a34d9bb8 +} diff --git a/src/main/java/me/samsuik/sakura/command/SakuraCommand.java b/src/main/java/me/samsuik/sakura/command/SakuraCommand.java new file mode 100644 -index 0000000000000000000000000000000000000000..18c74437018ff4c0935242f6002083edb1282a01 +index 0000000000000000000000000000000000000000..f46883c6206b56af1b9f125b9b361e7efc666b49 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/command/SakuraCommand.java @@ -0,0 +1,86 @@ @@ -117,7 +117,7 @@ index 0000000000000000000000000000000000000000..18c74437018ff4c0935242f6002083ed + +@DefaultQualifier(NonNull.class) +public final class SakuraCommand extends Command { -+ private static final Component INFORMATION_MESSAGE = MiniMessage.miniMessage().deserialize(""" ++ private static final Component HEADER_MESSAGE = MiniMessage.miniMessage().deserialize(""" + . + | This is the main command for Sakura. + | All exclusive commands are listed below.""" @@ -155,7 +155,7 @@ index 0000000000000000000000000000000000000000..18c74437018ff4c0935242f6002083ed + } + + private void sendHelpMessage(CommandSender sender) { -+ sender.sendMessage(INFORMATION_MESSAGE); ++ sender.sendMessage(HEADER_MESSAGE); + + Stream uniqueCommands = SakuraCommands.COMMANDS.values() + .stream() diff --git a/patches/server/0004-Local-Config-and-Value-Storage-API.patch b/patches/server/0004-Local-Config-and-Value-Storage-API.patch index 4d29225..8b5c8dc 100644 --- a/patches/server/0004-Local-Config-and-Value-Storage-API.patch +++ b/patches/server/0004-Local-Config-and-Value-Storage-API.patch @@ -6,10 +6,10 @@ Subject: [PATCH] Local Config and Value Storage API diff --git a/src/main/java/me/samsuik/sakura/local/config/LocalConfigManager.java b/src/main/java/me/samsuik/sakura/local/config/LocalConfigManager.java new file mode 100644 -index 0000000000000000000000000000000000000000..a3a09b8d58589883c7c465597bc64502bbfa0d88 +index 0000000000000000000000000000000000000000..00953ca239c677ece37715b759a1f02608e252be --- /dev/null +++ b/src/main/java/me/samsuik/sakura/local/config/LocalConfigManager.java -@@ -0,0 +1,143 @@ +@@ -0,0 +1,141 @@ +package me.samsuik.sakura.local.config; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; @@ -32,7 +32,6 @@ index 0000000000000000000000000000000000000000..a3a09b8d58589883c7c465597bc64502 +import java.util.Map; + +public final class LocalConfigManager implements LocalStorageHandler { -+ + private final Map storageMap = new Object2ObjectOpenHashMap<>(); + // tree is a tree. it may not be correct but it works. + private final Long2ObjectRBTreeMap regionTree = new Long2ObjectRBTreeMap<>(); @@ -151,14 +150,13 @@ index 0000000000000000000000000000000000000000..a3a09b8d58589883c7c465597bc64502 + } + } + } -+ +} diff --git a/src/main/java/me/samsuik/sakura/local/config/LocalValueConfig.java b/src/main/java/me/samsuik/sakura/local/config/LocalValueConfig.java new file mode 100644 -index 0000000000000000000000000000000000000000..3f518f3f1241d3dc1f76fab42e9fd789fab4d53e +index 0000000000000000000000000000000000000000..5b3e2cca7ee16bc6ecfa0f29438fa6588fa39a99 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/local/config/LocalValueConfig.java -@@ -0,0 +1,71 @@ +@@ -0,0 +1,69 @@ +package me.samsuik.sakura.local.config; + +import io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation; @@ -175,7 +173,6 @@ index 0000000000000000000000000000000000000000..3f518f3f1241d3dc1f76fab42e9fd789 +import java.util.Map; + +public final class LocalValueConfig { -+ + private final Expiry expiry; + public Map durableMaterials; + public PhysicsVersion physicsVersion; @@ -226,9 +223,8 @@ index 0000000000000000000000000000000000000000..3f518f3f1241d3dc1f76fab42e9fd789 + } + + Expiry expiry() { -+ return expiry; ++ return this.expiry; + } -+ +} diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 741ca90ca91c77f3cc58a7020169165b20250225..39cc9aba054696c063133d529e79594a5ce2c0cd 100644 diff --git a/patches/server/0015-Store-Entity-Data-State.patch b/patches/server/0015-Store-Entity-Data-State.patch index fdd0cf9..c81165e 100644 --- a/patches/server/0015-Store-Entity-Data-State.patch +++ b/patches/server/0015-Store-Entity-Data-State.patch @@ -6,10 +6,10 @@ Subject: [PATCH] Store Entity Data/State diff --git a/src/main/java/me/samsuik/sakura/entity/EntityState.java b/src/main/java/me/samsuik/sakura/entity/EntityState.java new file mode 100644 -index 0000000000000000000000000000000000000000..c9f2c5ae57878283e8c8bc3847fe63b98f4e8d10 +index 0000000000000000000000000000000000000000..10630c7e04a137ce766f4a45cb0f2e51bd967c98 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/entity/EntityState.java -@@ -0,0 +1,41 @@ +@@ -0,0 +1,37 @@ +package me.samsuik.sakura.entity; + +import net.minecraft.core.BlockPos; @@ -20,36 +20,32 @@ index 0000000000000000000000000000000000000000..c9f2c5ae57878283e8c8bc3847fe63b9 +import java.util.Optional; + +public record EntityState(Vec3 position, Vec3 momentum, AABB bb, Vec3 stuckSpeed, Optional supportingPos, boolean onGround, float fallDistance) { -+ + public static EntityState of(Entity entity) { + return new EntityState(entity.position(), entity.getDeltaMovement(), entity.getBoundingBox(), entity.stuckSpeedMultiplier(), entity.mainSupportingBlockPos, entity.onGround(), entity.fallDistance); + } + + public void apply(Entity entity) { -+ entity.setPos(position); -+ entity.setDeltaMovement(momentum); -+ entity.setBoundingBox(bb); ++ entity.setPos(this.position); ++ entity.setDeltaMovement(this.momentum); ++ entity.setBoundingBox(this.bb); + // null here is only safe for our use case (tnt and sand) + //noinspection DataFlowIssue -+ entity.makeStuckInBlock(null, stuckSpeed); -+ entity.onGround = onGround; -+ entity.mainSupportingBlockPos = supportingPos; -+ entity.fallDistance = fallDistance; ++ entity.makeStuckInBlock(null, this.stuckSpeed); ++ entity.onGround = this.onGround; ++ entity.mainSupportingBlockPos = this.supportingPos; ++ entity.fallDistance = this.fallDistance; + } + + public void position(Entity entity) { -+ entity.setPos(position); -+ entity.setBoundingBox(bb); ++ entity.setPos(this.position); ++ entity.setBoundingBox(this.bb); + } + + public boolean isCurrentState(Entity entity) { -+ return entity.position().equals(position) -+ && entity.getDeltaMovement().equals(momentum); -+ // 1.14+ versions seem to correct morphed bounding boxes after a gametick. -+ // If there are any related issues uncomment this line of code. -+ // && entity.getBoundingBox().equals(bb); ++ return entity.position().equals(this.position) ++ && entity.getDeltaMovement().equals(this.momentum); ++ // && entity.getBoundingBox().equals(bb); // uncomment if there are issues with morphed bounding boxes + } -+ +} diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java index f375728a95465bbcbb8581cb43e48fc0e6966858..4435e0cd1370f3720102734ac3647674d5f46730 100644 diff --git a/patches/server/0029-Explosion-Durable-Blocks.patch b/patches/server/0029-Explosion-Durable-Blocks.patch index 0746366..1d3f8e0 100644 --- a/patches/server/0029-Explosion-Durable-Blocks.patch +++ b/patches/server/0029-Explosion-Durable-Blocks.patch @@ -6,35 +6,33 @@ Subject: [PATCH] Explosion Durable Blocks diff --git a/src/main/java/me/samsuik/sakura/explosion/durable/DurableBlockManager.java b/src/main/java/me/samsuik/sakura/explosion/durable/DurableBlockManager.java new file mode 100644 -index 0000000000000000000000000000000000000000..c58e52f7cc012babf4235e405e5fb5015c6e95d9 +index 0000000000000000000000000000000000000000..c5eb573f946d24d2a44a46e0f395e00e4dc29bd6 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/explosion/durable/DurableBlockManager.java -@@ -0,0 +1,63 @@ +@@ -0,0 +1,61 @@ +package me.samsuik.sakura.explosion.durable; + +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import me.samsuik.sakura.utils.objects.Expiry; +import net.minecraft.core.BlockPos; -+import net.minecraft.server.MinecraftServer; + +public final class DurableBlockManager { -+ + private final Long2ObjectOpenHashMap blocks = new Long2ObjectOpenHashMap<>(); ++ private long serverTick; + + public boolean damage(BlockPos pos, DurableMaterial material) { + long packed = pos.asLong(); -+ ++ // Expires after 1200 ticks (1 minute) without any changes. + DurableBlock block = this.blocks.computeIfAbsent(packed, k -> new DurableBlock( -+ material.durability(), -+ // expire after 1 minute -+ new Expiry(MinecraftServer.currentTickLong, 1200) ++ new Expiry(this.serverTick, 1200), ++ material.durability() + )); + + if (block.damage()) { + this.blocks.remove(packed); + return true; + } else { -+ block.getExpiry().refresh(MinecraftServer.currentTickLong); ++ block.getExpiry().refresh(this.serverTick); + return false; + } + } @@ -48,30 +46,30 @@ index 0000000000000000000000000000000000000000..c58e52f7cc012babf4235e405e5fb501 + if (tick % 200 == 0) { + this.blocks.values().removeIf(block -> block.getExpiry().isExpired(tick)); + } ++ this.serverTick = tick; + } + -+ private static class DurableBlock { -+ private int durability; ++ private static final class DurableBlock { + private final Expiry expiry; ++ private int durability; + -+ public DurableBlock(int durability, Expiry expiry) { -+ this.durability = durability; ++ public DurableBlock(Expiry expiry, int durability) { + this.expiry = expiry; ++ this.durability = durability; + } + + public Expiry getExpiry() { -+ return expiry; ++ return this.expiry; + } + + public int getDurability() { -+ return durability; ++ return this.durability; + } + + public boolean damage() { + return --this.durability <= 0; + } + } -+ +} diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index af3bcb2dff44d9a734cab9610abbe5d7430c5d57..479e10d3b431ce1292c97b4c2af18c79433e43e8 100644 @@ -124,7 +122,7 @@ index a8008c7550488be34b51f4280f5569170b1ebd1d..2e5a46b9d27b930870c68dbde93d8731 public String getDescriptionId() { return this.getOrCreateDescriptionId(); diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java -index d3ddf3c99cb6338cea6e1cad3d96c931885e5a20..da1dd876b5e64d1f5fefd0701f380ff20b965e0f 100644 +index fef6966070e0b539f246293652140fb9504c06bd..1c57725cef4bb0613b4bb539814c914aed5c179d 100644 --- a/src/main/java/net/minecraft/world/level/Explosion.java +++ b/src/main/java/net/minecraft/world/level/Explosion.java @@ -144,7 +144,7 @@ public class Explosion { diff --git a/patches/server/0066-Legacy-player-combat-mechanics.patch b/patches/server/0066-Legacy-player-combat-mechanics.patch index 9f0e405..f34181b 100644 --- a/patches/server/0066-Legacy-player-combat-mechanics.patch +++ b/patches/server/0066-Legacy-player-combat-mechanics.patch @@ -6,13 +6,14 @@ Subject: [PATCH] Legacy player combat mechanics diff --git a/src/main/java/me/samsuik/sakura/utils/CombatUtil.java b/src/main/java/me/samsuik/sakura/utils/CombatUtil.java new file mode 100644 -index 0000000000000000000000000000000000000000..b1c55ef758f61914e6df9b2c8096bce639b26353 +index 0000000000000000000000000000000000000000..babeceb7c6063933bf10bcc4bbfc6ea424ffb574 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/utils/CombatUtil.java -@@ -0,0 +1,39 @@ +@@ -0,0 +1,40 @@ +package me.samsuik.sakura.utils; + -+import it.unimi.dsi.fastutil.objects.*; ++import it.unimi.dsi.fastutil.objects.Reference2FloatMap; ++import it.unimi.dsi.fastutil.objects.Reference2FloatOpenHashMap; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.*; + @@ -50,7 +51,7 @@ index 0000000000000000000000000000000000000000..b1c55ef758f61914e6df9b2c8096bce6 + } +} diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 7acb574bf1ca8de1ecd023e96ef9c0013118091d..72e435537f9b2cb3bd15deba0e5e9c49c8b19d3f 100644 +index bfa7725cc59c393047c4fb0173453128d0d6e161..377e862bb6b926f73bab3d5cecce41a57fe9b73d 100644 --- a/src/main/java/net/minecraft/world/entity/LivingEntity.java +++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java @@ -299,6 +299,79 @@ public abstract class LivingEntity extends Entity implements Attackable {