diff --git a/build.gradle b/build.gradle index 48065fc07..ecb0f0f70 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { group 'fr.themode.minestom' version '1.0' -sourceCompatibility = 1.8 +sourceCompatibility = 1.11 repositories { mavenCentral() diff --git a/src/main/java/fr/themode/minestom/Main.java b/src/main/java/fr/themode/minestom/Main.java index 7b2d8c689..315f2aac5 100644 --- a/src/main/java/fr/themode/minestom/Main.java +++ b/src/main/java/fr/themode/minestom/Main.java @@ -22,13 +22,14 @@ import java.lang.reflect.InvocationTargetException; public class Main { // Thread number - public static final int THREAD_COUNT_PACKET_WRITER = 3; + public static final int THREAD_COUNT_PACKET_WRITER = 5; public static final int THREAD_COUNT_CHUNK_IO = 2; public static final int THREAD_COUNT_CHUNK_BATCH = 2; - public static final int THREAD_COUNT_ENTITIES = 2; - public static final int THREAD_COUNT_PLAYERS_ENTITIES = 2; + public static final int THREAD_COUNT_ENTITIES = 1; + public static final int THREAD_COUNT_PLAYERS_ENTITIES = 3; public static final int TICK_MS = 50; + public static final int TICK_PER_SECOND = 1000 / TICK_MS; // Networking private static ConnectionManager connectionManager; @@ -41,7 +42,7 @@ public class Main { private static BlockManager blockManager; private static EntityManager entityManager; - public static void main(String[] args) throws InterruptedException { + public static void main(String[] args) { connectionManager = new ConnectionManager(); packetProcessor = new PacketProcessor(); packetListenerManager = new PacketListenerManager(); @@ -95,7 +96,7 @@ public class Main { @Override public void onException(Server server, Connection connection, Throwable cause) { - cause.printStackTrace(); + // cause.printStackTrace(); } }); @@ -127,7 +128,11 @@ public class Main { //String perfMessage = "Online: " + getConnectionManager().getOnlinePlayers().size() + " Tick time: " + (TICK_MS - sleepTime) + " ms"; //getConnectionManager().getOnlinePlayers().forEach(player -> player.sendMessage(perfMessage)); - Thread.sleep(sleepTime); + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + e.printStackTrace(); + } } } diff --git a/src/main/java/fr/themode/minestom/Viewable.java b/src/main/java/fr/themode/minestom/Viewable.java index 19996c27d..0c9459023 100644 --- a/src/main/java/fr/themode/minestom/Viewable.java +++ b/src/main/java/fr/themode/minestom/Viewable.java @@ -23,9 +23,12 @@ public interface Viewable { return; PacketWriter.writeCallbackPacket(packet, buffer -> { + int size = getViewers().size(); + if (size == 0) + return; buffer.getData().retain(getViewers().size()).markReaderIndex(); getViewers().forEach(player -> { - player.getPlayerConnection().sendUnencodedPacket(buffer); + player.getPlayerConnection().writeUnencodedPacket(buffer); buffer.getData().resetReaderIndex(); }); }); @@ -39,7 +42,7 @@ public interface Viewable { PacketWriter.writeCallbackPacket(packet, buffer -> { buffer.getData().retain(getViewers().size()).markReaderIndex(); getViewers().forEach(player -> { - player.getPlayerConnection().sendUnencodedPacket(buffer); + player.getPlayerConnection().writeUnencodedPacket(buffer); buffer.getData().resetReaderIndex(); }); }); @@ -50,12 +53,12 @@ public interface Viewable { if (this instanceof Player) { PacketWriter.writeCallbackPacket(packet, buffer -> { buffer.getData().retain(getViewers().size() + 1).markReaderIndex(); - ((Player) this).getPlayerConnection().sendUnencodedPacket(buffer); + ((Player) this).getPlayerConnection().writeUnencodedPacket(buffer); buffer.getData().resetReaderIndex(); if (!getViewers().isEmpty()) { getViewers().forEach(player -> { buffer.getData().resetReaderIndex(); - player.getPlayerConnection().sendUnencodedPacket(buffer); + player.getPlayerConnection().writeUnencodedPacket(buffer); }); } }); diff --git a/src/main/java/fr/themode/minestom/entity/Entity.java b/src/main/java/fr/themode/minestom/entity/Entity.java index f44ada4dd..59c3ff273 100644 --- a/src/main/java/fr/themode/minestom/entity/Entity.java +++ b/src/main/java/fr/themode/minestom/entity/Entity.java @@ -13,6 +13,7 @@ import fr.themode.minestom.instance.Instance; import fr.themode.minestom.net.packet.server.play.*; import fr.themode.minestom.utils.Position; import fr.themode.minestom.utils.Utils; +import fr.themode.minestom.utils.Vector; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -36,6 +37,7 @@ public abstract class Entity implements Viewable, DataContainer { protected Instance instance; protected Position position; + protected boolean onGround; protected double lastX, lastY, lastZ; protected float lastYaw, lastPitch; private int id; @@ -53,6 +55,15 @@ public abstract class Entity implements Viewable, DataContainer { private int entityType; private long lastUpdate; + // Velocity + // TODO gravity implementation for entity other than players + protected float velocityX, velocityY, velocityZ; // Movement in block per second + protected long velocityTime; // Reset velocity to 0 after countdown + + // Synchronization + private long synchronizationDelay = 2000; // In ms + private long lastSynchronizationTime; + // Metadata protected boolean onFire; protected boolean crouched; @@ -92,6 +103,9 @@ public abstract class Entity implements Viewable, DataContainer { public abstract void update(); + // Called when entity a new instance is set + public abstract void spawn(); + public void teleport(Position position) { if (isChunkUnloaded(position.getX(), position.getZ())) return; @@ -101,13 +115,14 @@ public abstract class Entity implements Viewable, DataContainer { EntityTeleportPacket entityTeleportPacket = new EntityTeleportPacket(); entityTeleportPacket.entityId = getEntityId(); entityTeleportPacket.position = position; - entityTeleportPacket.onGround = true; + entityTeleportPacket.onGround = onGround; sendPacketToViewers(entityTeleportPacket); } @Override public void addViewer(Player player) { this.viewers.add(player); + player.getPlayerConnection().sendPacket(getVelocityPacket()); } @Override @@ -153,7 +168,36 @@ public abstract class Entity implements Viewable, DataContainer { remove(); return; } else if (shouldUpdate()) { + long time = System.currentTimeMillis(); + + // Velocity + if (velocityTime != 0) { + if (time >= velocityTime) { + // TODO send synchronization packet? + resetVelocity(); + } else { + if (this instanceof Player) { + sendPacketToViewersAndSelf(getVelocityPacket()); + } else { + float tps = Main.TICK_PER_SECOND; + refreshPosition(position.getX() + velocityX / tps, position.getY() + velocityY / tps, position.getZ() + velocityZ / tps); + if (this instanceof ObjectEntity) { + sendPacketToViewers(getVelocityPacket()); + } else if (this instanceof EntityCreature) { + teleport(getPosition()); + } + } + } + } + update(); + + // Synchronization + if (time - lastSynchronizationTime >= synchronizationDelay) { + lastSynchronizationTime = System.currentTimeMillis(); + sendPositionSynchronization(); + } + this.lastUpdate = System.currentTimeMillis(); } @@ -214,6 +258,21 @@ public abstract class Entity implements Viewable, DataContainer { this.isActive = true; this.instance = instance; instance.addEntity(this); + spawn(); + } + + public void setVelocity(Vector velocity, long velocityTime) { + this.velocityX = velocity.getX(); + this.velocityY = velocity.getY(); + this.velocityZ = velocity.getZ(); + this.velocityTime = System.currentTimeMillis() + velocityTime; + } + + public void resetVelocity() { + this.velocityX = 0; + this.velocityY = 0; + this.velocityZ = 0; + this.velocityTime = 0; } public float getDistance(Entity entity) { @@ -252,6 +311,11 @@ public abstract class Entity implements Viewable, DataContainer { sendMetadataIndex(0); } + public void setGlowing(boolean glowing) { + this.glowing = glowing; + sendMetadataIndex(0); + } + public void setNoGravity(boolean noGravity) { this.noGravity = noGravity; sendMetadataIndex(5); @@ -322,6 +386,16 @@ public abstract class Entity implements Viewable, DataContainer { this.scheduledRemoveTime = System.currentTimeMillis() + delay; } + protected EntityVelocityPacket getVelocityPacket() { + final float strength = 8000f / Main.TICK_PER_SECOND; + EntityVelocityPacket velocityPacket = new EntityVelocityPacket(); + velocityPacket.entityId = getEntityId(); + velocityPacket.velocityX = (short) (velocityX * strength); + velocityPacket.velocityY = (short) (velocityY * strength); + velocityPacket.velocityZ = (short) (velocityZ * strength); + return velocityPacket; + } + public EntityMetaDataPacket getMetadataPacket() { EntityMetaDataPacket metaDataPacket = new EntityMetaDataPacket(); metaDataPacket.entityId = getEntityId(); @@ -344,9 +418,11 @@ public abstract class Entity implements Viewable, DataContainer { EntityMetaDataPacket metaDataPacket = new EntityMetaDataPacket(); metaDataPacket.entityId = getEntityId(); metaDataPacket.data = buffer; - sendPacketToViewers(metaDataPacket); if (this instanceof Player) { - ((Player) this).getPlayerConnection().sendPacket(metaDataPacket); + Player player = (Player) this; + player.sendPacketToViewersAndSelf(metaDataPacket); + } else { + sendPacketToViewers(metaDataPacket); } } @@ -390,6 +466,14 @@ public abstract class Entity implements Viewable, DataContainer { buffer.putByte(index0); } + protected void sendPositionSynchronization() { + EntityTeleportPacket entityTeleportPacket = new EntityTeleportPacket(); + entityTeleportPacket.entityId = getEntityId(); + entityTeleportPacket.position = getPosition(); + entityTeleportPacket.onGround = onGround; + sendPacketToViewers(entityTeleportPacket); + } + private void fillAirTickMetaData(Buffer buffer) { buffer.putByte((byte) 1); buffer.putByte(METADATA_VARINT); diff --git a/src/main/java/fr/themode/minestom/entity/EntityCreature.java b/src/main/java/fr/themode/minestom/entity/EntityCreature.java index 5ee6c3c18..8b382f82c 100644 --- a/src/main/java/fr/themode/minestom/entity/EntityCreature.java +++ b/src/main/java/fr/themode/minestom/entity/EntityCreature.java @@ -9,13 +9,22 @@ import fr.themode.minestom.utils.Position; // TODO viewers synchronization each X ticks? public abstract class EntityCreature extends LivingEntity { - protected boolean isDead; - public EntityCreature(int entityType) { super(entityType); } + @Override + public void update() { + super.update(); + } + public void move(float x, float y, float z) { + if (velocityTime != 0) { + // Cancel movements if velocity is active + return; + } + + // TODO change yaw/pitch based on next position? Position position = getPosition(); float newX = position.getX() + x; float newY = position.getY() + y; @@ -35,6 +44,7 @@ public abstract class EntityCreature extends LivingEntity { refreshPosition(newX, newY, newZ); } + @Override public void kill() { this.isDead = true; triggerStatus((byte) 3); @@ -59,7 +69,4 @@ public abstract class EntityCreature extends LivingEntity { playerConnection.sendPacket(getMetadataPacket()); } - public boolean isDead() { - return isDead; - } } diff --git a/src/main/java/fr/themode/minestom/entity/EntityManager.java b/src/main/java/fr/themode/minestom/entity/EntityManager.java index 3ff99ea88..0e923e0a7 100644 --- a/src/main/java/fr/themode/minestom/entity/EntityManager.java +++ b/src/main/java/fr/themode/minestom/entity/EntityManager.java @@ -6,6 +6,7 @@ import fr.themode.minestom.event.PlayerSpawnPacket; import fr.themode.minestom.instance.Chunk; import fr.themode.minestom.instance.Instance; import fr.themode.minestom.instance.InstanceManager; +import fr.themode.minestom.utils.GroupedCollections; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; @@ -24,19 +25,21 @@ public class EntityManager { public void update() { waitingPlayersTick(); for (Instance instance : instanceManager.getInstances()) { - testTick2(instance); // TODO optimize update engine for when there are too many entities on one chunk + // TODO optimize update engine for when there are too many entities on one chunk + testTick1(instance); } } private void waitingPlayersTick() { - Player waitingPlayer = null; + Player waitingPlayer; while ((waitingPlayer = waitingPlayers.poll()) != null) { final Player playerCache = waitingPlayer; - playersPool.submit(() -> { + playersPool.execute(() -> { PlayerLoginEvent loginEvent = new PlayerLoginEvent(); playerCache.callEvent(PlayerLoginEvent.class, loginEvent); Instance spawningInstance = loginEvent.getSpawningInstance() == null ? instanceManager.createInstance() : loginEvent.getSpawningInstance(); + // TODO load multiple chunks around player (based on view distance) instead of only one spawningInstance.loadChunk(playerCache.getPosition(), chunk -> { playerCache.spawned = true; playerCache.setInstance(spawningInstance); @@ -55,7 +58,7 @@ public class EntityManager { Set players = chunk.getPlayers(); if (!creatures.isEmpty() || !objects.isEmpty()) { - entitiesPool.submit(() -> { + entitiesPool.execute(() -> { for (EntityCreature creature : creatures) { creature.tick(); } @@ -66,7 +69,7 @@ public class EntityManager { } if (!players.isEmpty()) { - playersPool.submit(() -> { + playersPool.execute(() -> { for (Player player : players) { player.tick(); } @@ -75,38 +78,33 @@ public class EntityManager { } } + private void testTick1(Instance instance) { + GroupedCollections objects = instance.getObjectEntities(); + GroupedCollections creatures = instance.getCreatures(); + GroupedCollections players = instance.getPlayers(); + + if (!creatures.isEmpty() || !objects.isEmpty()) { + entitiesPool.execute(() -> { + for (EntityCreature creature : creatures) { + creature.tick(); + } + for (ObjectEntity objectEntity : objects) { + objectEntity.tick(); + } + }); + } + + if (!players.isEmpty()) { + playersPool.execute(() -> { + for (Player player : players) { + player.tick(); + } + }); + } + } + public void addWaitingPlayer(Player player) { this.waitingPlayers.add(player); } - /*private void testTick(Instance instance) { - // Creatures - for (EntityCreature creature : instance.getCreatures()) { - creaturesPool.submit(() -> { - boolean shouldRemove = creature.shouldRemove(); - if (!shouldRemove) { - creature.tick(); - } - - if (creature.shouldRemove()) { - instance.removeEntity(creature); - } - }); - } - - // Players - for (Player player : instance.getPlayers()) { - playersPool.submit(() -> { - boolean shouldRemove = player.shouldRemove(); - if (!shouldRemove) { - player.tick(); - } - - if (player.shouldRemove()) { - instance.removeEntity(player); - } - }); - } - }*/ - } diff --git a/src/main/java/fr/themode/minestom/entity/ItemEntity.java b/src/main/java/fr/themode/minestom/entity/ItemEntity.java index dd6f98dbb..fe6508875 100644 --- a/src/main/java/fr/themode/minestom/entity/ItemEntity.java +++ b/src/main/java/fr/themode/minestom/entity/ItemEntity.java @@ -2,8 +2,8 @@ package fr.themode.minestom.entity; import fr.adamaq01.ozao.net.Buffer; import fr.themode.minestom.item.ItemStack; -import fr.themode.minestom.net.packet.server.play.EntityRelativeMovePacket; import fr.themode.minestom.utils.Utils; +import fr.themode.minestom.utils.Vector; public class ItemEntity extends ObjectEntity { @@ -17,11 +17,17 @@ public class ItemEntity extends ObjectEntity { @Override public void update() { - // TODO how to keep items at the same position? - EntityRelativeMovePacket entityRelativeMovePacket = new EntityRelativeMovePacket(); - entityRelativeMovePacket.entityId = getEntityId(); - entityRelativeMovePacket.onGround = false; - sendPacketToViewers(entityRelativeMovePacket); + + } + + @Override + public void spawn() { + setVelocity(new Vector(0, 1, 0), 5000); + } + + @Override + public void addViewer(Player player) { + super.addViewer(player); } @Override diff --git a/src/main/java/fr/themode/minestom/entity/LivingEntity.java b/src/main/java/fr/themode/minestom/entity/LivingEntity.java index 340c24a5b..0b1cc5a62 100644 --- a/src/main/java/fr/themode/minestom/entity/LivingEntity.java +++ b/src/main/java/fr/themode/minestom/entity/LivingEntity.java @@ -1,11 +1,18 @@ package fr.themode.minestom.entity; import fr.adamaq01.ozao.net.Buffer; +import fr.themode.minestom.event.PickupItemEvent; +import fr.themode.minestom.instance.Chunk; +import fr.themode.minestom.item.ItemStack; +import fr.themode.minestom.net.packet.server.play.CollectItemPacket; + +import java.util.Set; // TODO attributes https://wiki.vg/Protocol#Entity_Properties public abstract class LivingEntity extends Entity { - protected boolean onGround; + protected boolean canPickupItem; + protected boolean isDead; private boolean isHandActive; private boolean activeHand; @@ -15,6 +22,40 @@ public abstract class LivingEntity extends Entity { super(entityType); } + public abstract void kill(); + + @Override + public void update() { + if (canPickupItem) { + Chunk chunk = instance.getChunkAt(getPosition()); // TODO check surrounding chunks + Set objectEntities = chunk.getObjectEntities(); + for (ObjectEntity objectEntity : objectEntities) { + if (objectEntity instanceof ItemEntity) { + ItemEntity itemEntity = (ItemEntity) objectEntity; + if (!itemEntity.isPickable()) + continue; + float distance = getDistance(objectEntity); + if (distance <= 2.04) { + synchronized (itemEntity) { + if (itemEntity.shouldRemove()) + continue; + ItemStack item = itemEntity.getItemStack(); + PickupItemEvent pickupItemEvent = new PickupItemEvent(item); + callCancellableEvent(PickupItemEvent.class, pickupItemEvent, () -> { + CollectItemPacket collectItemPacket = new CollectItemPacket(); + collectItemPacket.collectedEntityId = itemEntity.getEntityId(); + collectItemPacket.collectorEntityId = getEntityId(); + collectItemPacket.pickupItemCount = item.getAmount(); + sendPacketToViewersAndSelf(collectItemPacket); + objectEntity.remove(); + }); + } + } + } + } + } + } + @Override public Buffer getMetadataBuffer() { Buffer buffer = super.getMetadataBuffer(); @@ -32,9 +73,25 @@ public abstract class LivingEntity extends Entity { return buffer; } + public boolean isDead() { + return isDead; + } + + public boolean canPickupItem() { + return canPickupItem; + } + + public void setCanPickupItem(boolean canPickupItem) { + this.canPickupItem = canPickupItem; + } + public void refreshActiveHand(boolean isHandActive, boolean offHand, boolean riptideSpinAttack) { this.isHandActive = isHandActive; this.activeHand = offHand; this.riptideSpinAttack = riptideSpinAttack; } + + public void refreshIsDead(boolean isDead) { + this.isDead = isDead; + } } diff --git a/src/main/java/fr/themode/minestom/entity/ObjectEntity.java b/src/main/java/fr/themode/minestom/entity/ObjectEntity.java index 7022519a7..6ce34140c 100644 --- a/src/main/java/fr/themode/minestom/entity/ObjectEntity.java +++ b/src/main/java/fr/themode/minestom/entity/ObjectEntity.java @@ -14,7 +14,6 @@ public abstract class ObjectEntity extends Entity { @Override public void addViewer(Player player) { - super.addViewer(player); PlayerConnection playerConnection = player.getPlayerConnection(); SpawnObjectPacket spawnObjectPacket = new SpawnObjectPacket(); @@ -25,6 +24,7 @@ public abstract class ObjectEntity extends Entity { spawnObjectPacket.data = getObjectData(); playerConnection.sendPacket(spawnObjectPacket); playerConnection.sendPacket(getMetadataPacket()); + super.addViewer(player); // Add player to viewers list and send velocity packet } @Override diff --git a/src/main/java/fr/themode/minestom/entity/Player.java b/src/main/java/fr/themode/minestom/entity/Player.java index ece09f7c7..f36eec030 100644 --- a/src/main/java/fr/themode/minestom/entity/Player.java +++ b/src/main/java/fr/themode/minestom/entity/Player.java @@ -5,24 +5,23 @@ import fr.themode.minestom.bossbar.BossBar; import fr.themode.minestom.chat.Chat; import fr.themode.minestom.entity.demo.ChickenCreature; import fr.themode.minestom.event.*; -import fr.themode.minestom.instance.Chunk; import fr.themode.minestom.instance.CustomBlock; import fr.themode.minestom.instance.Instance; import fr.themode.minestom.instance.demo.ChunkGeneratorDemo; import fr.themode.minestom.inventory.Inventory; import fr.themode.minestom.inventory.PlayerInventory; -import fr.themode.minestom.item.ItemStack; import fr.themode.minestom.net.packet.client.ClientPlayPacket; import fr.themode.minestom.net.packet.server.ServerPacket; import fr.themode.minestom.net.packet.server.play.*; import fr.themode.minestom.net.player.PlayerConnection; import fr.themode.minestom.utils.BlockPosition; import fr.themode.minestom.utils.Position; +import fr.themode.minestom.utils.Vector; import fr.themode.minestom.world.Dimension; import fr.themode.minestom.world.LevelType; -import java.io.File; import java.util.Collections; +import java.util.Random; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; @@ -39,8 +38,7 @@ public class Player extends LivingEntity { private Dimension dimension; private GameMode gameMode; private LevelType levelType; - // DEBUG - private static Instance instance; + private PlayerSettings settings; private PlayerInventory inventory; private short heldSlot; @@ -53,18 +51,16 @@ public class Player extends LivingEntity { private Set bossBars = new CopyOnWriteArraySet<>(); - // Synchronization - private long synchronizationDelay = 1000; // In ms - private long lastSynchronizationTime; - // Vehicle private float sideways; private float forward; + private static Instance instance; + static { ChunkGeneratorDemo chunkGeneratorDemo = new ChunkGeneratorDemo(); - instance = Main.getInstanceManager().createInstance(new File("C:\\Users\\themo\\OneDrive\\Bureau\\Minestom data")); - //instance = Main.getInstanceManager().createInstance(); + //instance = Main.getInstanceManager().createInstance(new File("C:\\Users\\themo\\OneDrive\\Bureau\\Minestom data")); + instance = Main.getInstanceManager().createInstance(); instance.setChunkGenerator(chunkGeneratorDemo); int loopStart = -2; int loopEnd = 2; @@ -84,32 +80,29 @@ public class Player extends LivingEntity { this.username = username; this.playerConnection = playerConnection; + this.settings = new PlayerSettings(); this.inventory = new PlayerInventory(this); - /*setEventCallback(PickupItemEvent.class, event -> { - sendMessage("Hey you're trying to pick an item!"); - event.setCancelled(true); - });*/ - - /*setEventCallback(StartDiggingEvent.class, event -> { - Random random = new Random(); - boolean cancel = random.nextBoolean(); - event.setCancelled(cancel); - sendMessage("Cancelled: " + cancel); - });*/ - - /*setEventCallback(BlockPlaceEvent.class, event -> { - event.setCancelled(true); - sendMessage("CANCELLED"); - });*/ + setCanPickupItem(true); // By default setEventCallback(AttackEvent.class, event -> { Entity entity = event.getTarget(); if (entity instanceof EntityCreature) { ((EntityCreature) entity).kill(); sendMessage("You killed an entity!"); + } else if (entity instanceof Player) { + Player player = (Player) entity; + Vector velocity = getPosition().clone().getDirection().multiply(6); + velocity.setY(3.5f); + player.setVelocity(velocity, 150); + + AnimationPacket animationPacket = new AnimationPacket(); + animationPacket.entityId = player.getEntityId(); + animationPacket.animation = AnimationPacket.Animation.TAKE_DAMAGE; + sendPacketToViewersAndSelf(animationPacket); + + sendMessage("ATTACK"); } - sendMessage("ATTACK"); /*UpdateHealthPacket updateHealthPacket = new UpdateHealthPacket(); updateHealthPacket.health = -1f; updateHealthPacket.food = 5; @@ -127,22 +120,27 @@ public class Player extends LivingEntity { if (event.getHand() != Hand.MAIN) return; - sendMessage("Save chunk data..."); + /*sendMessage("Save chunk data..."); long time = System.currentTimeMillis(); getInstance().saveToFolder(() -> { sendMessage("Saved in " + (System.currentTimeMillis() - time) + " ms"); - }); + });*/ + + for (Player player : Main.getConnectionManager().getOnlinePlayers()) { + player.teleport(getPosition()); + } + }); + + setEventCallback(PickupItemEvent.class, event -> { + event.setCancelled(!getInventory().addItemStack(event.getItemStack())); // Cancel event if player does not have enough inventory space }); - // TODO loginevent set instance setEventCallback(PlayerLoginEvent.class, event -> { - System.out.println("PLAYER LOGIN EVENT"); event.setSpawningInstance(instance); }); setEventCallback(PlayerSpawnPacket.class, event -> { - System.out.println("TELEPORT"); - setGameMode(GameMode.CREATIVE); + setGameMode(GameMode.SURVIVAL); teleport(new Position(0, 66, 0)); for (int cx = 0; cx < 4; cx++) for (int cz = 0; cz < 4; cz++) { @@ -152,16 +150,43 @@ public class Player extends LivingEntity { chickenCreature.setInstance(instance); //chickenCreature.addPassenger(player); } + + /*for (int ix = 0; ix < 4; ix++) + for (int iz = 0; iz < 4; iz++) { + ItemEntity itemEntity = new ItemEntity(new ItemStack(1, (byte) 32)); + itemEntity.refreshPosition(ix, 66, iz); + itemEntity.setNoGravity(true); + itemEntity.setInstance(instance); + //itemEntity.remove(); + }*/ + + TeamsPacket teamsPacket = new TeamsPacket(); + teamsPacket.teamName = "TEAMNAME" + new Random().nextInt(100); + teamsPacket.action = TeamsPacket.Action.CREATE_TEAM; + teamsPacket.teamDisplayName = Chat.rawText("WOWdisplay"); + teamsPacket.nameTagVisibility = "always"; + teamsPacket.teamColor = 2; + teamsPacket.teamPrefix = Chat.rawText("pre"); + teamsPacket.teamSuffix = Chat.rawText("suf"); + teamsPacket.collisionRule = "never"; + teamsPacket.entities = new String[]{getUsername()}; + System.out.println(getViewers().size()); + sendPacketToViewersAndSelf(teamsPacket); }); } @Override public void update() { - ClientPlayPacket packet = null; + + playerConnection.flush(); + + ClientPlayPacket packet; while ((packet = packets.poll()) != null) { packet.process(this); } + super.update(); // Super update (item pickup) + // Target block stage if (targetCustomBlock != null) { int timeBreak = targetCustomBlock.getBreakDelay(this); @@ -178,37 +203,6 @@ public class Player extends LivingEntity { } } - // Item pickup - Chunk chunk = instance.getChunkAt(getPosition()); // TODO check surrounding chunks - Set objectEntities = chunk.getObjectEntities(); - for (ObjectEntity objectEntity : objectEntities) { - if (objectEntity instanceof ItemEntity) { - ItemEntity itemEntity = (ItemEntity) objectEntity; - if (!itemEntity.isPickable()) - continue; - float distance = getDistance(objectEntity); - if (distance <= 2.04) { - synchronized (itemEntity) { - if (itemEntity.shouldRemove()) - continue; - ItemStack item = itemEntity.getItemStack(); - PickupItemEvent pickupItemEvent = new PickupItemEvent(item); - callCancellableEvent(PickupItemEvent.class, pickupItemEvent, () -> { - boolean result = getInventory().addItemStack(item); - if (result) { - CollectItemPacket collectItemPacket = new CollectItemPacket(); - collectItemPacket.collectedEntityId = itemEntity.getEntityId(); - collectItemPacket.collectorEntityId = getEntityId(); - collectItemPacket.pickupItemCount = item.getAmount(); - sendPacketToViewersAndSelf(collectItemPacket); - objectEntity.remove(); - } - }); - } - } - } - } - // Multiplayer sync Position position = getPosition(); @@ -226,17 +220,12 @@ public class Player extends LivingEntity { entityLookAndRelativeMovePacket.pitch = position.getPitch(); entityLookAndRelativeMovePacket.onGround = onGround; - EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket(); - entityHeadLookPacket.entityId = getEntityId(); - entityHeadLookPacket.yaw = position.getYaw(); - lastX = position.getX(); lastY = position.getY(); lastZ = position.getZ(); lastYaw = position.getYaw(); lastPitch = position.getPitch(); updatePacket = entityLookAndRelativeMovePacket; - optionalUpdatePacket = entityHeadLookPacket; } else if (positionChanged) { EntityRelativeMovePacket entityRelativeMovePacket = new EntityRelativeMovePacket(); entityRelativeMovePacket.entityId = getEntityId(); @@ -255,15 +244,18 @@ public class Player extends LivingEntity { entityLookPacket.pitch = position.getPitch(); entityLookPacket.onGround = onGround; - EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket(); - entityHeadLookPacket.entityId = getEntityId(); - entityHeadLookPacket.yaw = position.getYaw(); - lastYaw = position.getYaw(); lastPitch = position.getPitch(); updatePacket = entityLookPacket; + } + + if (viewChanged) { + EntityHeadLookPacket entityHeadLookPacket = new EntityHeadLookPacket(); + entityHeadLookPacket.entityId = getEntityId(); + entityHeadLookPacket.yaw = position.getYaw(); optionalUpdatePacket = entityHeadLookPacket; } + if (updatePacket != null) { if (optionalUpdatePacket != null) { sendPacketsToViewers(updatePacket, optionalUpdatePacket); @@ -271,20 +263,11 @@ public class Player extends LivingEntity { sendPacketToViewers(updatePacket); } } - playerConnection.sendPacket(new UpdateViewPositionPacket(instance.getChunkAt(getPosition()))); + } + + @Override + public void spawn() { - // Synchronization - long time = System.currentTimeMillis(); - if (time - lastSynchronizationTime >= synchronizationDelay) { - lastSynchronizationTime = System.currentTimeMillis(); - for (Player viewer : getViewers()) { - EntityTeleportPacket teleportPacket = new EntityTeleportPacket(); - teleportPacket.entityId = viewer.getEntityId(); - teleportPacket.position = viewer.getPosition(); - teleportPacket.onGround = viewer.onGround; - playerConnection.sendPacket(teleportPacket); - } - } } @Override @@ -305,6 +288,7 @@ public class Player extends LivingEntity { connection.sendPacket(pInfoPacket); connection.sendPacket(spawnPlayerPacket); + connection.sendPacket(getMetadataPacket()); for (EntityEquipmentPacket.Slot slot : EntityEquipmentPacket.Slot.values()) { syncEquipment(slot); // TODO only send packets to "player" and not all viewers @@ -327,6 +311,12 @@ public class Player extends LivingEntity { super.setInstance(instance); } + @Override + public void kill() { + this.isDead = true; + // TODO set health to -1 + } + public void sendBlockBreakAnimation(BlockPosition blockPosition, byte destroyStage) { BlockBreakAnimationPacket breakAnimationPacket = new BlockBreakAnimationPacket(); breakAnimationPacket.entityId = getEntityId() + 1; @@ -342,6 +332,7 @@ public class Player extends LivingEntity { @Override public void teleport(Position position) { + super.teleport(position); // Send new position to all viewers directly if (isChunkUnloaded(position.getX(), position.getZ())) return; diff --git a/src/main/java/fr/themode/minestom/entity/demo/ChickenCreature.java b/src/main/java/fr/themode/minestom/entity/demo/ChickenCreature.java index d7b6c1450..36ae3773c 100644 --- a/src/main/java/fr/themode/minestom/entity/demo/ChickenCreature.java +++ b/src/main/java/fr/themode/minestom/entity/demo/ChickenCreature.java @@ -1,6 +1,7 @@ package fr.themode.minestom.entity.demo; import fr.themode.minestom.entity.EntityCreature; +import fr.themode.minestom.utils.Vector; public class ChickenCreature extends EntityCreature { @@ -10,6 +11,7 @@ public class ChickenCreature extends EntityCreature { @Override public void update() { + super.update(); float speed = 0.05f; /*if (hasPassenger()) { @@ -42,6 +44,11 @@ public class ChickenCreature extends EntityCreature { } }*/ - move(0, 0, speed); + //move(0, 0, speed); + } + + @Override + public void spawn() { + setVelocity(new Vector(0, 1, 0), 1000); } } diff --git a/src/main/java/fr/themode/minestom/entity/demo/TestArrow.java b/src/main/java/fr/themode/minestom/entity/demo/TestArrow.java index 9bf90de88..6c47d0c94 100644 --- a/src/main/java/fr/themode/minestom/entity/demo/TestArrow.java +++ b/src/main/java/fr/themode/minestom/entity/demo/TestArrow.java @@ -12,13 +12,22 @@ public class TestArrow extends ObjectEntity { this.shooter = shooter; } + @Override + public void update() { + + } + + @Override + public void spawn() { + + } + @Override public int getObjectData() { return shooter.getEntityId() + 1; } - @Override - public void update() { - + public LivingEntity getShooter() { + return shooter; } } diff --git a/src/main/java/fr/themode/minestom/instance/BlockBatch.java b/src/main/java/fr/themode/minestom/instance/BlockBatch.java index 716ea8197..0c7dee002 100644 --- a/src/main/java/fr/themode/minestom/instance/BlockBatch.java +++ b/src/main/java/fr/themode/minestom/instance/BlockBatch.java @@ -11,11 +11,11 @@ public class BlockBatch implements BlockModifier { private static volatile ExecutorService batchesPool = Executors.newFixedThreadPool(2); - private Instance instance; + private InstanceContainer instance; private Map> data = new HashMap<>(); - public BlockBatch(Instance instance) { + public BlockBatch(InstanceContainer instance) { this.instance = instance; } @@ -55,7 +55,7 @@ public class BlockBatch implements BlockModifier { for (Map.Entry> entry : data.entrySet()) { Chunk chunk = entry.getKey(); List dataList = entry.getValue(); - batchesPool.submit(() -> { + batchesPool.execute(() -> { synchronized (chunk) { for (BlockData data : dataList) { data.apply(chunk); diff --git a/src/main/java/fr/themode/minestom/instance/Chunk.java b/src/main/java/fr/themode/minestom/instance/Chunk.java index 0ba6de150..4b07e3ec8 100644 --- a/src/main/java/fr/themode/minestom/instance/Chunk.java +++ b/src/main/java/fr/themode/minestom/instance/Chunk.java @@ -124,10 +124,6 @@ public class Chunk { } } - public short[] getBlocksId() { - return blocksId; - } - public Biome getBiome() { return biome; } @@ -176,7 +172,7 @@ public class Chunk { // TODO customblock id map (StringId -> short id) // TODO List of (sectionId;blockcount;blocktype;blockarray) - + // TODO block data for (byte x = 0; x < CHUNK_SIZE_X; x++) { for (byte y = -128; y < 127; y++) { for (byte z = 0; z < CHUNK_SIZE_Z; z++) { diff --git a/src/main/java/fr/themode/minestom/instance/ChunkBatch.java b/src/main/java/fr/themode/minestom/instance/ChunkBatch.java index c22854b66..05f8a7eb9 100644 --- a/src/main/java/fr/themode/minestom/instance/ChunkBatch.java +++ b/src/main/java/fr/themode/minestom/instance/ChunkBatch.java @@ -12,12 +12,12 @@ public class ChunkBatch implements BlockModifier { private static volatile ExecutorService batchesPool = Executors.newFixedThreadPool(Main.THREAD_COUNT_CHUNK_BATCH); - private Instance instance; + private InstanceContainer instance; private Chunk chunk; private List dataList = new ArrayList<>(); - public ChunkBatch(Instance instance, Chunk chunk) { + public ChunkBatch(InstanceContainer instance, Chunk chunk) { this.instance = instance; this.chunk = chunk; } @@ -46,7 +46,7 @@ public class ChunkBatch implements BlockModifier { public void flush(Consumer callback) { synchronized (chunk) { - batchesPool.submit(() -> { + batchesPool.execute(() -> { for (BlockData data : dataList) { data.apply(chunk); } diff --git a/src/main/java/fr/themode/minestom/instance/ChunkLoaderIO.java b/src/main/java/fr/themode/minestom/instance/ChunkLoaderIO.java index d396c74ca..c252155a1 100644 --- a/src/main/java/fr/themode/minestom/instance/ChunkLoaderIO.java +++ b/src/main/java/fr/themode/minestom/instance/ChunkLoaderIO.java @@ -25,7 +25,7 @@ public class ChunkLoaderIO { } protected void saveChunk(Chunk chunk, File folder, Runnable callback) { - chunkLoaderPool.submit(() -> { + chunkLoaderPool.execute(() -> { File chunkFile = getChunkFile(chunk.getChunkX(), chunk.getChunkZ(), folder); try (FileOutputStream fos = new FileOutputStream(chunkFile)) { byte[] data = chunk.getSerializedData(); @@ -49,7 +49,7 @@ public class ChunkLoaderIO { } protected void loadChunk(int chunkX, int chunkZ, Instance instance, Consumer callback) { - chunkLoaderPool.submit(() -> { + chunkLoaderPool.execute(() -> { File chunkFile = getChunkFile(chunkX, chunkZ, instance.getFolder()); if (!chunkFile.exists()) { instance.createChunk(chunkX, chunkZ, callback); // Chunk file does not exist, create new chunk @@ -67,8 +67,8 @@ public class ChunkLoaderIO { int decompressedLength = SerializerUtils.bytesToInt(array); - byte[] compressedChunkData = new byte[array.length - 4]; - System.arraycopy(array, 4, compressedChunkData, 0, compressedChunkData.length); // Remove the decompressed length from the array + byte[] compressedChunkData = new byte[array.length - Integer.BYTES]; + System.arraycopy(array, Integer.BYTES, compressedChunkData, 0, compressedChunkData.length); // Remove the decompressed length from the array byte[] decompressed = new byte[decompressedLength]; long result = Zstd.decompress(decompressed, compressedChunkData); // Decompressed in an array with the max size @@ -85,10 +85,10 @@ public class ChunkLoaderIO { chunk = new Chunk(biome, chunkX, chunkZ); while (true) { + // TODO block data int index = stream.readInt(); boolean isCustomBlock = stream.readBoolean(); short blockId = stream.readShort(); - //System.out.println("id: " + blockId); byte[] chunkPos = SerializerUtils.indexToChunkPosition(index); if (isCustomBlock) { diff --git a/src/main/java/fr/themode/minestom/instance/Instance.java b/src/main/java/fr/themode/minestom/instance/Instance.java index 9d0349c71..fb0d5407b 100644 --- a/src/main/java/fr/themode/minestom/instance/Instance.java +++ b/src/main/java/fr/themode/minestom/instance/Instance.java @@ -5,327 +5,126 @@ import fr.themode.minestom.entity.Entity; import fr.themode.minestom.entity.EntityCreature; import fr.themode.minestom.entity.ObjectEntity; import fr.themode.minestom.entity.Player; -import fr.themode.minestom.event.BlockBreakEvent; -import fr.themode.minestom.net.PacketWriter; -import fr.themode.minestom.net.packet.server.play.DestroyEntitiesPacket; -import fr.themode.minestom.net.packet.server.play.ParticlePacket; import fr.themode.minestom.utils.BlockPosition; import fr.themode.minestom.utils.GroupedCollections; import fr.themode.minestom.utils.Position; import java.io.File; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; +import java.util.Collection; +import java.util.UUID; import java.util.function.Consumer; -public class Instance implements BlockModifier { +public interface Instance extends BlockModifier { - private static ChunkLoaderIO chunkLoaderIO = new ChunkLoaderIO(); + ChunkLoaderIO CHUNK_LOADER_IO = new ChunkLoaderIO(); - private UUID uniqueId; - private File folder; + void breakBlock(Player player, BlockPosition blockPosition, short blockId); - private GroupedCollections objectEntities = new GroupedCollections<>(new CopyOnWriteArrayList<>()); - private GroupedCollections creatures = new GroupedCollections<>(new CopyOnWriteArrayList()); - private GroupedCollections players = new GroupedCollections<>(new CopyOnWriteArrayList()); + void loadChunk(int chunkX, int chunkZ, Consumer callback); - private ChunkGenerator chunkGenerator; - private Map chunks = new ConcurrentHashMap<>(); + Chunk getChunk(int chunkX, int chunkZ); - public Instance(UUID uniqueId, File folder) { - this.uniqueId = uniqueId; - this.folder = folder; - } + void saveToFolder(Runnable callback); - @Override - public synchronized void setBlock(int x, int y, int z, short blockId) { - Chunk chunk = getChunkAt(x, z); - synchronized (chunk) { - chunk.setBlock((byte) (x % 16), (byte) y, (byte) (z % 16), blockId); - chunk.refreshDataPacket(); - sendChunkUpdate(chunk); - /*PacketWriter.writeCallbackPacket(chunk.getFreshFullDataPacket(), buffer -> { - chunk.setFullDataPacket(buffer); - sendChunkUpdate(chunk); - });*/ - } - } + BlockBatch createBlockBatch(); - @Override - public synchronized void setBlock(int x, int y, int z, String blockId) { - Chunk chunk = getChunkAt(x, z); - synchronized (chunk) { - chunk.setBlock((byte) (x % 16), (byte) y, (byte) (z % 16), blockId); - chunk.refreshDataPacket(); - sendChunkUpdate(chunk); - /*PacketWriter.writeCallbackPacket(chunk.getFreshFullDataPacket(), buffer -> { - chunk.setFullDataPacket(buffer); - sendChunkUpdate(chunk); - });*/ - } - } + ChunkBatch createChunkBatch(Chunk chunk); - public void breakBlock(Player player, BlockPosition blockPosition, short blockId) { - BlockBreakEvent blockBreakEvent = new BlockBreakEvent(blockPosition); - player.callEvent(BlockBreakEvent.class, blockBreakEvent); - if (!blockBreakEvent.isCancelled()) { - // TODO blockbreak setBlock result - int x = blockPosition.getX(); - int y = blockPosition.getY(); - int z = blockPosition.getZ(); - setBlock(x, y, z, (short) 0); - ParticlePacket particlePacket = new ParticlePacket(); // TODO change to a proper particle API - particlePacket.particleId = 3; // Block particle - particlePacket.longDistance = false; - particlePacket.x = x + 0.5f; - particlePacket.y = y; - particlePacket.z = z + 0.5f; - particlePacket.offsetX = 0.4f; - particlePacket.offsetY = 0.6f; - particlePacket.offsetZ = 0.4f; - particlePacket.particleData = 0.3f; - particlePacket.particleCount = 75; - particlePacket.blockId = blockId; - player.getPlayerConnection().sendPacket(particlePacket); - player.sendPacketToViewers(particlePacket); - } else { - sendChunkUpdate(player, getChunkAt(blockPosition)); - } - } + void setChunkGenerator(ChunkGenerator chunkGenerator); - public void breakBlock(Player player, BlockPosition blockPosition, CustomBlock customBlock) { - breakBlock(player, blockPosition, customBlock.getType()); - } + Collection getChunks(); - public void loadChunk(int chunkX, int chunkZ, Consumer callback) { - Chunk chunk = getChunk(chunkX, chunkZ); - if (chunk != null) { - if (callback != null) - callback.accept(chunk); - } else { - retrieveChunk(chunkX, chunkZ, callback); - } - } + GroupedCollections getObjectEntities(); - public void loadChunk(int chunkX, int chunkZ) { - loadChunk(chunkX, chunkZ, null); - } + GroupedCollections getCreatures(); - public void loadChunk(Position position, Consumer callback) { - int chunkX = Math.floorDiv((int) position.getX(), 16); - int chunkZ = Math.floorDiv((int) position.getY(), 16); - loadChunk(chunkX, chunkZ, callback); - } + GroupedCollections getPlayers(); - public boolean isChunkLoaded(int chunkX, int chunkZ) { - return getChunk(chunkX, chunkZ) != null; - } + UUID getUniqueId(); - public short getBlockId(int x, int y, int z) { - Chunk chunk = getChunkAt(x, z); - return chunk.getBlockId((byte) (x % 16), (byte) y, (byte) (z % 16)); - } + File getFolder(); - public short getBlockId(BlockPosition blockPosition) { - return getBlockId(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ()); - } + void setFolder(File folder); - public CustomBlock getCustomBlock(int x, int y, int z) { - Chunk chunk = getChunkAt(x, z); - return chunk.getCustomBlock((byte) (x % 16), (byte) y, (byte) (z % 16)); - } + void sendChunkUpdate(Player player, Chunk chunk); - public BlockBatch createBlockBatch() { - return new BlockBatch(this); - } + void retrieveChunk(int chunkX, int chunkZ, Consumer callback); - public Chunk getChunk(int chunkX, int chunkZ) { - return chunks.get(getChunkKey(chunkX, chunkZ)); - } + void createChunk(int chunkX, int chunkZ, Consumer callback); - public Chunk getChunkAt(double x, double z) { - int chunkX = Math.floorDiv((int) x, 16); - int chunkZ = Math.floorDiv((int) z, 16); - return getChunk(chunkX, chunkZ); - } + void sendChunks(Player player); - public Chunk getChunkAt(BlockPosition blockPosition) { - return getChunkAt(blockPosition.getX(), blockPosition.getZ()); - } + SharedInstance createSharedInstance(); - public Chunk getChunkAt(Position position) { - return getChunkAt(position.getX(), position.getZ()); - } - - public void saveToFolder(Runnable callback) { - if (folder == null) - throw new UnsupportedOperationException("You cannot save an instance without specified folder."); - - Iterator chunks = getChunks().iterator(); - while (chunks.hasNext()) { - Chunk chunk = chunks.next(); - boolean isLast = !chunks.hasNext(); - chunkLoaderIO.saveChunk(chunk, getFolder(), isLast ? callback : null); - } - } - - public void saveToFolder() { - saveToFolder(null); - } - - - public void setChunkGenerator(ChunkGenerator chunkGenerator) { - this.chunkGenerator = chunkGenerator; - } - - public Collection getChunks() { - return Collections.unmodifiableCollection(chunks.values()); - } - - public void addEntity(Entity entity) { - Instance lastInstance = entity.getInstance(); - if (lastInstance != null && lastInstance != this) { - lastInstance.removeEntity(entity); - } - - // TODO based on distance with players - getPlayers().forEach(p -> entity.addViewer(p)); - - if (entity instanceof Player) { - Player player = (Player) entity; - sendChunks(player); - getObjectEntities().forEach(objectEntity -> objectEntity.addViewer(player)); - getCreatures().forEach(entityCreature -> entityCreature.addViewer(player)); - getPlayers().forEach(p -> p.addViewer(player)); - } - - Chunk chunk = getChunkAt(entity.getPosition()); - chunk.addEntity(entity); - } - - public void removeEntity(Entity entity) { - Instance entityInstance = entity.getInstance(); - if (entityInstance == null || entityInstance != this) - return; - - entity.getViewers().forEach(p -> entity.removeViewer(p)); - - if (!(entity instanceof Player)) { - DestroyEntitiesPacket destroyEntitiesPacket = new DestroyEntitiesPacket(); - destroyEntitiesPacket.entityIds = new int[]{entity.getEntityId()}; - - entity.getViewers().forEach(p -> p.getPlayerConnection().sendPacket(destroyEntitiesPacket)); // TODO destroy batch - } else { - // TODO optimize (cache all entities that the player see) - Player player = (Player) entity; - getObjectEntities().forEach(objectEntity -> objectEntity.removeViewer(player)); - getCreatures().forEach(entityCreature -> entityCreature.removeViewer(player)); - getPlayers().forEach(p -> p.removeViewer(player)); - - } - - Chunk chunk = getChunkAt(entity.getPosition()); - chunk.removeEntity(entity); - } - - public GroupedCollections getObjectEntities() { - return objectEntities; - } - - public GroupedCollections getCreatures() { - return creatures; - } - - public GroupedCollections getPlayers() { - return players; - } - - public UUID getUniqueId() { - return uniqueId; - } - - public File getFolder() { - return folder; - } - - public void setFolder(File folder) { - this.folder = folder; - } - - public void sendChunkUpdate(Player player, Chunk chunk) { - Buffer chunkData = chunk.getFullDataPacket(); - chunkData.getData().retain(1).markReaderIndex(); - player.getPlayerConnection().sendUnencodedPacket(chunkData); - chunkData.getData().resetReaderIndex(); - } - - protected void retrieveChunk(int chunkX, int chunkZ, Consumer callback) { - if (folder != null) { - // Load from file if possible - chunkLoaderIO.loadChunk(chunkX, chunkZ, this, chunk -> { - cacheChunk(chunk); - if (callback != null) - callback.accept(chunk); - }); - } else { - createChunk(chunkX, chunkZ, callback); - } - } - - public void createChunk(int chunkX, int chunkZ, Consumer callback) { - Biome biome = chunkGenerator != null ? chunkGenerator.getBiome(chunkX, chunkZ) : Biome.VOID; - Chunk chunk = new Chunk(biome, chunkX, chunkZ); - cacheChunk(chunk); - if (chunkGenerator != null) { - ChunkBatch chunkBatch = createChunkBatch(chunk); - chunkGenerator.generateChunkData(chunkBatch, chunkX, chunkZ); - chunkBatch.flush(callback); - } - } - - private void cacheChunk(Chunk chunk) { - this.objectEntities.addCollection(chunk.objectEntities); - this.creatures.addCollection(chunk.creatures); - this.players.addCollection(chunk.players); - this.chunks.put(getChunkKey(chunk.getChunkX(), chunk.getChunkZ()), chunk); - } - - protected ChunkBatch createChunkBatch(Chunk chunk) { - return new ChunkBatch(this, chunk); - } - - protected void sendChunkUpdate(Chunk chunk) { - if (getPlayers().isEmpty()) - return; + // + default void sendChunkUpdate(Iterable players, Chunk chunk) { Buffer chunkData = chunk.getFullDataPacket(); chunkData.getData().retain(getPlayers().size()).markReaderIndex(); - getPlayers().forEach(player -> { + players.forEach(player -> { player.getPlayerConnection().sendUnencodedPacket(chunkData); chunkData.getData().resetReaderIndex(); }); } - private void sendChunks(Player player) { - for (Chunk chunk : getChunks()) { - Buffer chunkData = chunk.getFullDataPacket(); - if (chunkData == null) { - PacketWriter.writeCallbackPacket(chunk.getFreshFullDataPacket(), buffer -> { - buffer.getData().retain(1).markReaderIndex(); - player.getPlayerConnection().sendUnencodedPacket(buffer); - buffer.getData().resetReaderIndex(); - chunk.setFullDataPacket(buffer); - }); - } else { - chunkData.getData().retain(1).markReaderIndex(); - player.getPlayerConnection().sendUnencodedPacket(chunkData); - chunkData.getData().resetReaderIndex(); - } - } + // + + default void breakBlock(Player player, BlockPosition blockPosition, CustomBlock customBlock) { + breakBlock(player, blockPosition, customBlock.getType()); } - private long getChunkKey(int chunkX, int chunkZ) { + default void loadChunk(int chunkX, int chunkZ) { + loadChunk(chunkX, chunkZ, null); + } + + default void loadChunk(Position position, Consumer callback) { + int chunkX = Math.floorDiv((int) position.getX(), 16); + int chunkZ = Math.floorDiv((int) position.getY(), 16); + loadChunk(chunkX, chunkZ, callback); + } + + default short getBlockId(int x, int y, int z) { + Chunk chunk = getChunkAt(x, z); + return chunk.getBlockId((byte) (x % 16), (byte) y, (byte) (z % 16)); + } + + default short getBlockId(BlockPosition blockPosition) { + return getBlockId(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ()); + } + + default CustomBlock getCustomBlock(int x, int y, int z) { + Chunk chunk = getChunkAt(x, z); + return chunk.getCustomBlock((byte) (x % 16), (byte) y, (byte) (z % 16)); + } + + default Chunk getChunkAt(double x, double z) { + int chunkX = Math.floorDiv((int) x, 16); + int chunkZ = Math.floorDiv((int) z, 16); + return getChunk(chunkX, chunkZ); + } + + default boolean isChunkLoaded(int chunkX, int chunkZ) { + return getChunk(chunkX, chunkZ) != null; + } + + default Chunk getChunkAt(BlockPosition blockPosition) { + return getChunkAt(blockPosition.getX(), blockPosition.getZ()); + } + + default Chunk getChunkAt(Position position) { + return getChunkAt(position.getX(), position.getZ()); + } + + default void saveToFolder() { + saveToFolder(null); + } + + default long getChunkKey(int chunkX, int chunkZ) { return (((long) chunkX) << 32) | (chunkZ & 0xffffffffL); } + + // UNSAFE METHODS + void addEntity(Entity entity); + + void removeEntity(Entity entity); } diff --git a/src/main/java/fr/themode/minestom/instance/InstanceContainer.java b/src/main/java/fr/themode/minestom/instance/InstanceContainer.java new file mode 100644 index 000000000..8b3871549 --- /dev/null +++ b/src/main/java/fr/themode/minestom/instance/InstanceContainer.java @@ -0,0 +1,294 @@ +package fr.themode.minestom.instance; + +import fr.adamaq01.ozao.net.Buffer; +import fr.themode.minestom.entity.Entity; +import fr.themode.minestom.entity.EntityCreature; +import fr.themode.minestom.entity.ObjectEntity; +import fr.themode.minestom.entity.Player; +import fr.themode.minestom.event.BlockBreakEvent; +import fr.themode.minestom.net.PacketWriter; +import fr.themode.minestom.net.packet.server.play.DestroyEntitiesPacket; +import fr.themode.minestom.net.packet.server.play.ParticlePacket; +import fr.themode.minestom.utils.BlockPosition; +import fr.themode.minestom.utils.GroupedCollections; + +import java.io.File; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +/** + * InstanceContainer is an instance that contains chunks in contrary to SharedInstance. + */ +public class InstanceContainer implements Instance { + + private UUID uniqueId; + private File folder; + + // Entities present in this instance + private GroupedCollections objectEntities = new GroupedCollections<>(new CopyOnWriteArrayList<>()); + private GroupedCollections creatures = new GroupedCollections<>(new CopyOnWriteArrayList()); + private GroupedCollections players = new GroupedCollections<>(new CopyOnWriteArrayList()); + + // Entities present in all shared instances referenced AND this instance + private GroupedCollections referencedObjectEntities = new GroupedCollections<>(new CopyOnWriteArrayList<>()); + private GroupedCollections referencedCreatures = new GroupedCollections<>(new CopyOnWriteArrayList()); + private GroupedCollections referencedPlayers = new GroupedCollections<>(new CopyOnWriteArrayList()); + + private ChunkGenerator chunkGenerator; + private Map chunks = new ConcurrentHashMap<>(); + + public InstanceContainer(UUID uniqueId, File folder) { + this.uniqueId = uniqueId; + this.folder = folder; + } + + @Override + public synchronized void setBlock(int x, int y, int z, short blockId) { + Chunk chunk = getChunkAt(x, z); + synchronized (chunk) { + chunk.setBlock((byte) (x % 16), (byte) y, (byte) (z % 16), blockId); + PacketWriter.writeCallbackPacket(chunk.getFreshFullDataPacket(), buffer -> { + chunk.setFullDataPacket(buffer); + sendChunkUpdate(chunk); + }); + } + } + + @Override + public synchronized void setBlock(int x, int y, int z, String blockId) { + Chunk chunk = getChunkAt(x, z); + synchronized (chunk) { + chunk.setBlock((byte) (x % 16), (byte) y, (byte) (z % 16), blockId); + PacketWriter.writeCallbackPacket(chunk.getFreshFullDataPacket(), buffer -> { + chunk.setFullDataPacket(buffer); + sendChunkUpdate(chunk); + }); + } + } + + // TODO deplace + @Override + public void breakBlock(Player player, BlockPosition blockPosition, short blockId) { + BlockBreakEvent blockBreakEvent = new BlockBreakEvent(blockPosition); + player.callEvent(BlockBreakEvent.class, blockBreakEvent); + if (!blockBreakEvent.isCancelled()) { + // TODO blockbreak setBlock result + int x = blockPosition.getX(); + int y = blockPosition.getY(); + int z = blockPosition.getZ(); + setBlock(x, y, z, (short) 0); + ParticlePacket particlePacket = new ParticlePacket(); // TODO change to a proper particle API + particlePacket.particleId = 3; // Block particle + particlePacket.longDistance = false; + particlePacket.x = x + 0.5f; + particlePacket.y = y; + particlePacket.z = z + 0.5f; + particlePacket.offsetX = 0.4f; + particlePacket.offsetY = 0.6f; + particlePacket.offsetZ = 0.4f; + particlePacket.particleData = 0.3f; + particlePacket.particleCount = 75; + particlePacket.blockId = blockId; + player.getPlayerConnection().sendPacket(particlePacket); + player.sendPacketToViewers(particlePacket); + } else { + sendChunkUpdate(player, getChunkAt(blockPosition)); + } + } + + @Override + public void loadChunk(int chunkX, int chunkZ, Consumer callback) { + Chunk chunk = getChunk(chunkX, chunkZ); + if (chunk != null) { + if (callback != null) + callback.accept(chunk); + } else { + retrieveChunk(chunkX, chunkZ, callback); + } + } + + @Override + public Chunk getChunk(int chunkX, int chunkZ) { + return chunks.get(getChunkKey(chunkX, chunkZ)); + } + + @Override + public void saveToFolder(Runnable callback) { + if (folder == null) + throw new UnsupportedOperationException("You cannot save an instance without setting a folder."); + + Iterator chunks = getChunks().iterator(); + while (chunks.hasNext()) { + Chunk chunk = chunks.next(); + boolean isLast = !chunks.hasNext(); + CHUNK_LOADER_IO.saveChunk(chunk, getFolder(), isLast ? callback : null); + } + } + + @Override + public BlockBatch createBlockBatch() { + return new BlockBatch(this); + } + + @Override + public ChunkBatch createChunkBatch(Chunk chunk) { + return new ChunkBatch(this, chunk); + } + + @Override + public void addEntity(Entity entity) { + Instance lastInstance = entity.getInstance(); + if (lastInstance != null && lastInstance != this) { + lastInstance.removeEntity(entity); + } + + // TODO based on distance with players + getPlayers().forEach(p -> entity.addViewer(p)); + + if (entity instanceof Player) { + Player player = (Player) entity; + sendChunks(player); + getObjectEntities().forEach(objectEntity -> objectEntity.addViewer(player)); + getCreatures().forEach(entityCreature -> entityCreature.addViewer(player)); + getPlayers().forEach(p -> p.addViewer(player)); + } + + Chunk chunk = getChunkAt(entity.getPosition()); + chunk.addEntity(entity); + } + + @Override + public void removeEntity(Entity entity) { + Instance entityInstance = entity.getInstance(); + if (entityInstance == null || entityInstance != this) + return; + + entity.getViewers().forEach(p -> entity.removeViewer(p)); + + if (!(entity instanceof Player)) { + DestroyEntitiesPacket destroyEntitiesPacket = new DestroyEntitiesPacket(); + destroyEntitiesPacket.entityIds = new int[]{entity.getEntityId()}; + + entity.getViewers().forEach(p -> p.getPlayerConnection().sendPacket(destroyEntitiesPacket)); // TODO destroy batch + } else { + // TODO optimize (cache all entities that the player see) + Player player = (Player) entity; + getObjectEntities().forEach(objectEntity -> objectEntity.removeViewer(player)); + getCreatures().forEach(entityCreature -> entityCreature.removeViewer(player)); + getPlayers().forEach(p -> p.removeViewer(player)); + + } + + Chunk chunk = getChunkAt(entity.getPosition()); + chunk.removeEntity(entity); + } + + @Override + public void sendChunkUpdate(Player player, Chunk chunk) { + Buffer chunkData = chunk.getFullDataPacket(); + chunkData.getData().retain(1).markReaderIndex(); + player.getPlayerConnection().sendUnencodedPacket(chunkData); + chunkData.getData().resetReaderIndex(); + } + + @Override + public void retrieveChunk(int chunkX, int chunkZ, Consumer callback) { + if (folder != null) { + // Load from file if possible + CHUNK_LOADER_IO.loadChunk(chunkX, chunkZ, this, chunk -> { + cacheChunk(chunk); + if (callback != null) + callback.accept(chunk); + }); + } else { + createChunk(chunkX, chunkZ, callback); + } + } + + @Override + public void createChunk(int chunkX, int chunkZ, Consumer callback) { + Biome biome = chunkGenerator != null ? chunkGenerator.getBiome(chunkX, chunkZ) : Biome.VOID; + Chunk chunk = new Chunk(biome, chunkX, chunkZ); + cacheChunk(chunk); + if (chunkGenerator != null) { + ChunkBatch chunkBatch = createChunkBatch(chunk); + chunkGenerator.generateChunkData(chunkBatch, chunkX, chunkZ); + chunkBatch.flush(callback); + } + } + + public void sendChunkUpdate(Chunk chunk) { // TODO work with referenced SharedInstances + if (getPlayers().isEmpty()) + return; + + sendChunkUpdate(getPlayers(), chunk); + } + + @Override + public void sendChunks(Player player) { + for (Chunk chunk : getChunks()) { + Buffer chunkData = chunk.getFullDataPacket(); + if (chunkData == null) { + PacketWriter.writeCallbackPacket(chunk.getFreshFullDataPacket(), buffer -> { + buffer.getData().retain(1).markReaderIndex(); + player.getPlayerConnection().sendUnencodedPacket(buffer); + buffer.getData().resetReaderIndex(); + chunk.setFullDataPacket(buffer); + }); + } else { + chunkData.getData().retain(1).markReaderIndex(); + player.getPlayerConnection().sendUnencodedPacket(chunkData); + chunkData.getData().resetReaderIndex(); + } + } + } + + @Override + public SharedInstance createSharedInstance() { + SharedInstance sharedInstance = new SharedInstance(this); + // TODO add list of entities + return sharedInstance; + } + + private void cacheChunk(Chunk chunk) { + this.objectEntities.addCollection(chunk.objectEntities); + this.creatures.addCollection(chunk.creatures); + this.players.addCollection(chunk.players); + this.chunks.put(getChunkKey(chunk.getChunkX(), chunk.getChunkZ()), chunk); + } + + public void setChunkGenerator(ChunkGenerator chunkGenerator) { + this.chunkGenerator = chunkGenerator; + } + + public Collection getChunks() { + return Collections.unmodifiableCollection(chunks.values()); + } + + public GroupedCollections getObjectEntities() { + return objectEntities; + } + + public GroupedCollections getCreatures() { + return creatures; + } + + public GroupedCollections getPlayers() { + return players; + } + + public UUID getUniqueId() { + return uniqueId; + } + + public File getFolder() { + return folder; + } + + public void setFolder(File folder) { + this.folder = folder; + } + +} diff --git a/src/main/java/fr/themode/minestom/instance/InstanceManager.java b/src/main/java/fr/themode/minestom/instance/InstanceManager.java index 9dc5b1bac..7fb9ab9dc 100644 --- a/src/main/java/fr/themode/minestom/instance/InstanceManager.java +++ b/src/main/java/fr/themode/minestom/instance/InstanceManager.java @@ -11,7 +11,7 @@ public class InstanceManager { private Set instances = Collections.synchronizedSet(new HashSet<>()); public Instance createInstance(File folder) { - Instance instance = new Instance(UUID.randomUUID(), folder); + Instance instance = new InstanceContainer(UUID.randomUUID(), folder); this.instances.add(instance); return instance; } diff --git a/src/main/java/fr/themode/minestom/instance/SharedInstance.java b/src/main/java/fr/themode/minestom/instance/SharedInstance.java new file mode 100644 index 000000000..d620a1ef5 --- /dev/null +++ b/src/main/java/fr/themode/minestom/instance/SharedInstance.java @@ -0,0 +1,145 @@ +package fr.themode.minestom.instance; + +import fr.themode.minestom.entity.Entity; +import fr.themode.minestom.entity.EntityCreature; +import fr.themode.minestom.entity.ObjectEntity; +import fr.themode.minestom.entity.Player; +import fr.themode.minestom.utils.BlockPosition; +import fr.themode.minestom.utils.GroupedCollections; + +import java.io.File; +import java.util.Collection; +import java.util.UUID; +import java.util.function.Consumer; + +/** + * Shared instance is an instance that share the same chunks as instanceContainer, entities are separated. + */ +public class SharedInstance implements Instance { + + private InstanceContainer instanceContainer; + + public SharedInstance(InstanceContainer instanceContainer) { + this.instanceContainer = instanceContainer; + } + + @Override + public void breakBlock(Player player, BlockPosition blockPosition, short blockId) { + instanceContainer.breakBlock(player, blockPosition, blockId); + } + + @Override + public void loadChunk(int chunkX, int chunkZ, Consumer callback) { + instanceContainer.loadChunk(chunkX, chunkZ, callback); + } + + @Override + public Chunk getChunk(int chunkX, int chunkZ) { + return instanceContainer.getChunk(chunkX, chunkZ); + } + + @Override + public void saveToFolder(Runnable callback) { + instanceContainer.saveToFolder(callback); + } + + @Override + public BlockBatch createBlockBatch() { + return instanceContainer.createBlockBatch(); + } + + @Override + public ChunkBatch createChunkBatch(Chunk chunk) { + return instanceContainer.createChunkBatch(chunk); + } + + @Override + public void setChunkGenerator(ChunkGenerator chunkGenerator) { + instanceContainer.setChunkGenerator(chunkGenerator); + } + + @Override + public Collection getChunks() { + return instanceContainer.getChunks(); + } + + @Override + public GroupedCollections getObjectEntities() { + return null; + } + + @Override + public GroupedCollections getCreatures() { + return null; + } + + @Override + public GroupedCollections getPlayers() { + return null; + } + + @Override + public UUID getUniqueId() { + // FIXME: share same UUID ? + return null; + } + + @Override + public File getFolder() { + return instanceContainer.getFolder(); + } + + @Override + public void setFolder(File folder) { + instanceContainer.setFolder(folder); + } + + @Override + public void sendChunkUpdate(Player player, Chunk chunk) { + instanceContainer.sendChunkUpdate(player, chunk); + } + + @Override + public void retrieveChunk(int chunkX, int chunkZ, Consumer callback) { + instanceContainer.retrieveChunk(chunkX, chunkZ, callback); + } + + @Override + public void createChunk(int chunkX, int chunkZ, Consumer callback) { + instanceContainer.createChunk(chunkX, chunkZ, callback); + } + + @Override + public void sendChunks(Player player) { + instanceContainer.sendChunks(player); + } + + @Override + public SharedInstance createSharedInstance() { + return new SharedInstance(instanceContainer); + } + + @Override + public void addEntity(Entity entity) { + + } + + @Override + public void removeEntity(Entity entity) { + + } + + @Override + public void setBlock(int x, int y, int z, short blockId) { + instanceContainer.setBlock(x, y, z, blockId); + } + + @Override + public void setBlock(int x, int y, int z, String blockId) { + instanceContainer.setBlock(x, y, z, blockId); + } + + public InstanceContainer getContainer() { + return instanceContainer; + } +} diff --git a/src/main/java/fr/themode/minestom/listener/PlayerPositionListener.java b/src/main/java/fr/themode/minestom/listener/PlayerPositionListener.java index 09a19ec9e..bde5b651d 100644 --- a/src/main/java/fr/themode/minestom/listener/PlayerPositionListener.java +++ b/src/main/java/fr/themode/minestom/listener/PlayerPositionListener.java @@ -41,6 +41,7 @@ public class PlayerPositionListener { } private static void processMovement(Player player, float x, float y, float z, Runnable runnable) { + //System.out.println("MOVEMENT PACKET " + Math.round(x) + ":" + Math.round(y) + ":" + Math.round(z)); boolean chunkTest = player.isChunkUnloaded(x, z); if (chunkTest) { player.teleport(player.getPosition()); diff --git a/src/main/java/fr/themode/minestom/listener/StatusListener.java b/src/main/java/fr/themode/minestom/listener/StatusListener.java index c4f8e0ece..d64256894 100644 --- a/src/main/java/fr/themode/minestom/listener/StatusListener.java +++ b/src/main/java/fr/themode/minestom/listener/StatusListener.java @@ -17,6 +17,7 @@ public class StatusListener { player.getPlayerConnection().sendPacket(respawnPacket); PlayerRespawnEvent respawnEvent = new PlayerRespawnEvent(player.getPosition()); player.callEvent(PlayerRespawnEvent.class, respawnEvent); + player.refreshIsDead(false); player.teleport(respawnEvent.getRespawnPosition()); player.getInventory().update(); break; diff --git a/src/main/java/fr/themode/minestom/net/PacketWriter.java b/src/main/java/fr/themode/minestom/net/PacketWriter.java index fbb5e5ba5..21a8c84df 100644 --- a/src/main/java/fr/themode/minestom/net/PacketWriter.java +++ b/src/main/java/fr/themode/minestom/net/PacketWriter.java @@ -15,7 +15,7 @@ public class PacketWriter { private static volatile ExecutorService batchesPool = Executors.newFixedThreadPool(Main.THREAD_COUNT_PACKET_WRITER); public static void writeCallbackPacket(ServerPacket serverPacket, Consumer consumer) { - batchesPool.submit(() -> { + batchesPool.execute(() -> { Packet p = PacketUtils.writePacket(serverPacket); consumer.accept(PacketUtils.encode(p)); // TODO accept in another thread? }); diff --git a/src/main/java/fr/themode/minestom/net/packet/server/handshake/ResponsePacket.java b/src/main/java/fr/themode/minestom/net/packet/server/handshake/ResponsePacket.java index 9fbcae91d..81259af55 100644 --- a/src/main/java/fr/themode/minestom/net/packet/server/handshake/ResponsePacket.java +++ b/src/main/java/fr/themode/minestom/net/packet/server/handshake/ResponsePacket.java @@ -22,7 +22,7 @@ public class ResponsePacket implements ServerPacket { " ]\n" + " },\t\n" + " \"description\": {\n" + - " \"text\": \"Wallah les cubes\"\n" + + " \"text\": \"Hey guys!\"\n" + " },\n" + " \"favicon\": \"data:image/png;base64,\"\n" + "}"; diff --git a/src/main/java/fr/themode/minestom/net/packet/server/play/TeamsPacket.java b/src/main/java/fr/themode/minestom/net/packet/server/play/TeamsPacket.java index 08247d06b..6ef1e2b9a 100644 --- a/src/main/java/fr/themode/minestom/net/packet/server/play/TeamsPacket.java +++ b/src/main/java/fr/themode/minestom/net/packet/server/play/TeamsPacket.java @@ -37,17 +37,15 @@ public class TeamsPacket implements ServerPacket { break; case REMOVE_TEAM: - break; - case ADD_PLAYERS_TEAM: - case REMOVE_PLAYERS_TEAM: - Utils.writeVarInt(buffer, entities.length); - for (String entity : entities) { - Utils.writeString(buffer, entity); - } break; } - if (action == Action.CREATE_TEAM) { + if (action == Action.CREATE_TEAM || action == Action.ADD_PLAYERS_TEAM || action == Action.REMOVE_PLAYERS_TEAM) { + if (entities == null) { + Utils.writeVarInt(buffer, 0); + return; + } + Utils.writeVarInt(buffer, entities.length); for (String entity : entities) { Utils.writeString(buffer, entity); diff --git a/src/main/java/fr/themode/minestom/net/player/PlayerConnection.java b/src/main/java/fr/themode/minestom/net/player/PlayerConnection.java index 3fab0186d..eeb592e64 100644 --- a/src/main/java/fr/themode/minestom/net/player/PlayerConnection.java +++ b/src/main/java/fr/themode/minestom/net/player/PlayerConnection.java @@ -48,14 +48,14 @@ public class PlayerConnection { getChannel().write(packet.getData()); } - public void flush() { - getChannel().flush(); - } - public void sendPacket(ServerPacket serverPacket) { sendPacket(PacketUtils.writePacket(serverPacket)); } + public void flush() { + getChannel().flush(); + } + public Connection getConnection() { return connection; } diff --git a/src/main/java/fr/themode/minestom/utils/MathUtils.java b/src/main/java/fr/themode/minestom/utils/MathUtils.java new file mode 100644 index 000000000..d0a052098 --- /dev/null +++ b/src/main/java/fr/themode/minestom/utils/MathUtils.java @@ -0,0 +1,9 @@ +package fr.themode.minestom.utils; + +public class MathUtils { + + public static float square(float num) { + return num * num; + } + +} diff --git a/src/main/java/fr/themode/minestom/utils/Position.java b/src/main/java/fr/themode/minestom/utils/Position.java index e2426c6c3..2a3cc8dff 100644 --- a/src/main/java/fr/themode/minestom/utils/Position.java +++ b/src/main/java/fr/themode/minestom/utils/Position.java @@ -28,7 +28,63 @@ public class Position { } public float getDistance(Position position) { - return (float) Math.sqrt(Math.pow(position.getX() - getX(), 2) + Math.pow(position.getY() - getY(), 2) + Math.pow(position.getZ() - getZ(), 2)); + return (float) Math.sqrt(MathUtils.square(position.getX() - getX()) + MathUtils.square(position.getY() - getY()) + MathUtils.square(position.getZ() - getZ())); + } + + /** + * Gets a unit-vector pointing in the direction that this Location is + * facing. + * + * @return a vector pointing the direction of this location's {@link + * #getPitch() pitch} and {@link #getYaw() yaw} + */ + public Vector getDirection() { + Vector vector = new Vector(); + + float rotX = this.getYaw(); + float rotY = this.getPitch(); + + vector.setY((float) -Math.sin(Math.toRadians(rotY))); + + double xz = Math.cos(Math.toRadians(rotY)); + + vector.setX((float) (-xz * Math.sin(Math.toRadians(rotX)))); + vector.setZ((float) (xz * Math.cos(Math.toRadians(rotX)))); + + return vector; + } + + /** + * Sets the {@link #getYaw() yaw} and {@link #getPitch() pitch} to point + * in the direction of the vector. + */ + public Position setDirection(Vector vector) { + /* + * Sin = Opp / Hyp + * Cos = Adj / Hyp + * Tan = Opp / Adj + * + * x = -Opp + * z = Adj + */ + final double _2PI = 2 * Math.PI; + final float x = vector.getX(); + final float z = vector.getZ(); + + if (x == 0 && z == 0) { + pitch = vector.getY() > 0 ? -90 : 90; + return this; + } + + double theta = Math.atan2(-x, z); + yaw = (float) Math.toDegrees((theta + _2PI) % _2PI); + + float x2 = MathUtils.square(x); + float z2 = MathUtils.square(z); + float xz = (float) Math.sqrt(x2 + z2); + pitch = (float) Math.toDegrees(Math.atan(-vector.getY() / xz)); + + return this; } public Position clone() { diff --git a/src/main/java/fr/themode/minestom/utils/Vector.java b/src/main/java/fr/themode/minestom/utils/Vector.java new file mode 100644 index 000000000..2bfc239be --- /dev/null +++ b/src/main/java/fr/themode/minestom/utils/Vector.java @@ -0,0 +1,239 @@ +package fr.themode.minestom.utils; + +public class Vector implements Cloneable { + + private static final double epsilon = 0.000001; + + protected float x, y, z; + + public Vector() { + this.x = 0; + this.y = 0; + this.z = 0; + } + + public Vector(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vector add(Vector vec) { + x += vec.x; + y += vec.y; + z += vec.z; + return this; + } + + /** + * Subtracts a vector from this one. + * + * @param vec The other vector + * @return the same vector + */ + public Vector subtract(Vector vec) { + x -= vec.x; + y -= vec.y; + z -= vec.z; + return this; + } + + /** + * Multiplies the vector by another. + * + * @param vec The other vector + * @return the same vector + */ + public Vector multiply(Vector vec) { + x *= vec.x; + y *= vec.y; + z *= vec.z; + return this; + } + + /** + * Divides the vector by another. + * + * @param vec The other vector + * @return the same vector + */ + public Vector divide(Vector vec) { + x /= vec.x; + y /= vec.y; + z /= vec.z; + return this; + } + + /** + * Copies another vector + * + * @param vec The other vector + * @return the same vector + */ + public Vector copy(Vector vec) { + x = vec.x; + y = vec.y; + z = vec.z; + return this; + } + + /** + * Gets the magnitude of the vector, defined as sqrt(x^2+y^2+z^2). The + * value of this method is not cached and uses a costly square-root + * function, so do not repeatedly call this method to get the vector's + * magnitude. NaN will be returned if the inner result of the sqrt() + * function overflows, which will be caused if the length is too long. + * + * @return the magnitude + */ + public double length() { + return Math.sqrt(MathUtils.square(x) + MathUtils.square(y) + MathUtils.square(z)); + } + + /** + * Get the distance between this vector and another. The value of this + * method is not cached and uses a costly square-root function, so do not + * repeatedly call this method to get the vector's magnitude. NaN will be + * returned if the inner result of the sqrt() function overflows, which + * will be caused if the distance is too long. + * + * @param o The other vector + * @return the distance + */ + public double distance(Vector o) { + return Math.sqrt(MathUtils.square(x - o.x) + MathUtils.square(y - o.y) + MathUtils.square(z - o.z)); + } + + /** + * Performs scalar multiplication, multiplying all components with a + * scalar. + * + * @param m The factor + * @return the same vector + */ + public Vector multiply(int m) { + x *= m; + y *= m; + z *= m; + return this; + } + + /** + * Performs scalar multiplication, multiplying all components with a + * scalar. + * + * @param m The factor + * @return the same vector + */ + public Vector multiply(double m) { + x *= m; + y *= m; + z *= m; + return this; + } + + /** + * Performs scalar multiplication, multiplying all components with a + * scalar. + * + * @param m The factor + * @return the same vector + */ + public Vector multiply(float m) { + x *= m; + y *= m; + z *= m; + return this; + } + + /** + * Converts this vector to a unit vector (a vector with length of 1). + * + * @return the same vector + */ + public Vector normalize() { + double length = length(); + + x /= length; + y /= length; + z /= length; + + return this; + } + + /** + * Zero this vector's components. + * + * @return the same vector + */ + public Vector zero() { + x = 0; + y = 0; + z = 0; + return this; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Vector)) { + return false; + } + + Vector other = (Vector) obj; + + return Math.abs(x - other.x) < epsilon && Math.abs(y - other.y) < epsilon && Math.abs(z - other.z) < epsilon && (this.getClass().equals(obj.getClass())); + } + + /** + * Returns a hash code for this vector + * + * @return hash code + */ + @Override + public int hashCode() { + int hash = 7; + + hash = 79 * hash + (int) (Double.doubleToLongBits(this.x) ^ (Double.doubleToLongBits(this.x) >>> 32)); + hash = 79 * hash + (int) (Double.doubleToLongBits(this.y) ^ (Double.doubleToLongBits(this.y) >>> 32)); + hash = 79 * hash + (int) (Double.doubleToLongBits(this.z) ^ (Double.doubleToLongBits(this.z) >>> 32)); + return hash; + } + + /** + * Get a new vector. + * + * @return vector + */ + @Override + public Vector clone() { + try { + return (Vector) super.clone(); + } catch (CloneNotSupportedException e) { + throw new Error(e); + } + } + + public float getX() { + return x; + } + + public void setX(float x) { + this.x = x; + } + + public float getY() { + return y; + } + + public void setY(float y) { + this.y = y; + } + + public float getZ() { + return z; + } + + public void setZ(float z) { + this.z = z; + } +}