diff --git a/SpigotCore_14/src/de/steamwar/core/FlatteningWrapper14.java b/SpigotCore_14/src/de/steamwar/core/FlatteningWrapper14.java index 1844eba..daea6e1 100644 --- a/SpigotCore_14/src/de/steamwar/core/FlatteningWrapper14.java +++ b/SpigotCore_14/src/de/steamwar/core/FlatteningWrapper14.java @@ -294,4 +294,17 @@ public class FlatteningWrapper14 implements FlatteningWrapper.IFlatteningWrapper head.setItemMeta(headmeta); return head; } + + private static final Class entityPose = Reflection.getClass("{nms.world.entity}.EntityPose"); + private static final Object standing = entityPose.getEnumConstants()[0]; + private static final Object sneaking = entityPose.getEnumConstants()[5]; + @Override + public Object getPose(boolean isSneaking) { + return isSneaking ? sneaking : standing; + } + + @Override + public void setNamedSpawnPacketDataWatcher(Object packet) { + // field not present + } } diff --git a/SpigotCore_18/src/de/steamwar/core/ProtocolWrapper18.java b/SpigotCore_18/src/de/steamwar/core/ProtocolWrapper18.java new file mode 100644 index 0000000..a09d9e9 --- /dev/null +++ b/SpigotCore_18/src/de/steamwar/core/ProtocolWrapper18.java @@ -0,0 +1,63 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2022 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.core; + +import com.comphenix.tinyprotocol.Reflection; +import com.mojang.authlib.GameProfile; +import com.mojang.datafixers.util.Pair; +import net.minecraft.world.entity.EntityTypes; +import org.bukkit.entity.EntityType; + +import java.util.Collections; +import java.util.List; + +public class ProtocolWrapper18 implements ProtocolWrapper { + + private static final Reflection.FieldAccessor equipmentStack = Reflection.getField(equipmentPacket, List.class, 0); + @Override + public void setEquipmentPacketStack(Object packet, Object slot, Object stack) { + equipmentStack.set(packet, Collections.singletonList(new Pair<>(slot, stack))); + } + + private static final Reflection.FieldAccessor spawnType = Reflection.getField(ProtocolWrapper.spawnPacket, EntityTypes.class, 0); + @Override + public void setSpawnPacketType(Object packet, EntityType type) { + switch(type) { + case PRIMED_TNT: + spawnType.set(packet, EntityTypes.as); + break; + case ARROW: + spawnType.set(packet, EntityTypes.d); + break; + case FIREBALL: + spawnType.set(packet, EntityTypes.S); + break; + } + } + + private static final Class playerInfoDataClass = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutPlayerInfo$PlayerInfoData"); + private static final Class enumGamemode = Reflection.getClass(Core.getVersion() > 9 ? "{nms.world.level}.EnumGamemode" : "{nms}.WorldSettings$EnumGamemode"); + private static final Class iChatBaseComponent = Reflection.getClass("{nms.network.chat}.IChatBaseComponent"); + private static final Reflection.ConstructorInvoker playerInfoDataConstructor = Reflection.getConstructor(playerInfoDataClass, GameProfile.class, int.class, enumGamemode, iChatBaseComponent); + @Override + public Object playerInfoDataConstructor(Object packet, GameProfile profile, Object mode) { + return playerInfoDataConstructor.invoke(profile, 0, mode, null); + } +} diff --git a/SpigotCore_19/build.gradle b/SpigotCore_19/build.gradle index 6f8f9d6..3a3e5d2 100644 --- a/SpigotCore_19/build.gradle +++ b/SpigotCore_19/build.gradle @@ -52,5 +52,6 @@ dependencies { compileOnly 'org.spigotmc:spigot-api:1.19-R0.1-SNAPSHOT' compileOnly 'com.mojang:brigadier:1.0.18' compileOnly 'com.mojang:datafixerupper:4.0.26' + compileOnly 'com.mojang:authlib:1.5.25' compileOnly swdep("Spigot-1.19") } diff --git a/SpigotCore_19/src/de/steamwar/core/ProtocolWrapper19.java b/SpigotCore_19/src/de/steamwar/core/ProtocolWrapper19.java new file mode 100644 index 0000000..d672c97 --- /dev/null +++ b/SpigotCore_19/src/de/steamwar/core/ProtocolWrapper19.java @@ -0,0 +1,63 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2022 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.core; + +import com.comphenix.tinyprotocol.Reflection; +import com.mojang.authlib.GameProfile; +import com.mojang.datafixers.util.Pair; +import net.minecraft.world.entity.EntityTypes; +import org.bukkit.entity.EntityType; + +import java.util.Collections; +import java.util.List; + +public class ProtocolWrapper19 implements ProtocolWrapper { + + private static final Reflection.FieldAccessor equipmentStack = Reflection.getField(equipmentPacket, List.class, 0); + @Override + public void setEquipmentPacketStack(Object packet, Object slot, Object stack) { + equipmentStack.set(packet, Collections.singletonList(new Pair<>(slot, stack))); + } + + private static final Reflection.FieldAccessor spawnType = Reflection.getField(ProtocolWrapper.spawnPacket, EntityTypes.class, 0); + @Override + public void setSpawnPacketType(Object packet, EntityType type) { + switch(type) { + case PRIMED_TNT: + spawnType.set(packet, EntityTypes.av); + break; + case ARROW: + spawnType.set(packet, EntityTypes.e); + break; + case FIREBALL: + spawnType.set(packet, EntityTypes.V); + break; + } + } + + private static final Class playerInfoDataClass = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutPlayerInfo$PlayerInfoData"); + private static final Class enumGamemode = Reflection.getClass(Core.getVersion() > 9 ? "{nms.world.level}.EnumGamemode" : "{nms}.WorldSettings$EnumGamemode"); + private static final Class iChatBaseComponent = Reflection.getClass("{nms.network.chat}.IChatBaseComponent"); + private static final Reflection.ConstructorInvoker playerInfoDataConstructor = Reflection.getConstructor(playerInfoDataClass, GameProfile.class, int.class, enumGamemode, iChatBaseComponent, Reflection.getClass("net.minecraft.world.entity.player.ProfilePublicKey$a")); + @Override + public Object playerInfoDataConstructor(Object packet, GameProfile profile, Object mode) { + return playerInfoDataConstructor.invoke(profile, 0, mode, null, null); + } +} diff --git a/SpigotCore_8/src/de/steamwar/core/BountifulWrapper8.java b/SpigotCore_8/src/de/steamwar/core/BountifulWrapper8.java index 8c08082..0659c10 100644 --- a/SpigotCore_8/src/de/steamwar/core/BountifulWrapper8.java +++ b/SpigotCore_8/src/de/steamwar/core/BountifulWrapper8.java @@ -19,14 +19,20 @@ package de.steamwar.core; +import com.comphenix.tinyprotocol.Reflection; import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.chat.BaseComponent; import net.minecraft.server.v1_8_R3.ChatComponentText; +import net.minecraft.server.v1_8_R3.MathHelper; import net.minecraft.server.v1_8_R3.PacketPlayOutChat; import org.bukkit.Sound; import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer; import org.bukkit.entity.Player; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + public class BountifulWrapper8 implements BountifulWrapper.IBountifulWrapper { @Override @@ -38,4 +44,52 @@ public class BountifulWrapper8 implements BountifulWrapper.IBountifulWrapper { public void sendMessage(Player player, ChatMessageType type, BaseComponent... msg) { ((CraftPlayer) player).getHandle().playerConnection.sendPacket(new PacketPlayOutChat(new ChatComponentText(BaseComponent.toLegacyText(msg)), (byte)type.ordinal())); } + + @Override + public Object getDataWatcherObject(int index, Class type) { + return index; + } + + private static final Class watchableObject = Reflection.getClass("{nms}.DataWatcher$WatchableObject"); + private static final Reflection.ConstructorInvoker watchableObjectConstructor = Reflection.getConstructor(watchableObject, int.class, int.class, Object.class); + private static final Map, Integer> watchableDatatypes = new HashMap<>(); + static { + watchableDatatypes.put(byte.class, 0); + watchableDatatypes.put(short.class, 1); + watchableDatatypes.put(int.class, 2); + watchableDatatypes.put(float.class, 3); + watchableDatatypes.put(String.class, 4); + } + + @Override + public Object getDataWatcherItem(Object dwo, Object value) { + return watchableObjectConstructor.invoke(watchableDatatypes.get(value.getClass()), dwo, value); + } + + private static final Class teleportPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutEntityTeleport"); + private static final Reflection.FieldAccessor teleportX = Reflection.getField(teleportPacket, int.class, 1); + private static final Reflection.FieldAccessor teleportY = Reflection.getField(teleportPacket, int.class, 2); + private static final Reflection.FieldAccessor teleportZ = Reflection.getField(teleportPacket, int.class, 3); + @Override + public void setTeleportPacketPosition(Object packet, double x, double y, double z) { + teleportX.set(packet, MathHelper.floor(x * 32)); + teleportY.set(packet, MathHelper.floor(y * 32)); + teleportZ.set(packet, MathHelper.floor(z * 32)); + } + + @Override + public void setSpawnPacketUUID(Object packet, UUID uuid) { + // field not present + } + + private static final Class namedSpawnPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutNamedEntitySpawn"); + private static final Reflection.FieldAccessor namedSpawnX = Reflection.getField(namedSpawnPacket, int.class, 0); + private static final Reflection.FieldAccessor namedSpawnY = Reflection.getField(namedSpawnPacket, int.class, 1); + private static final Reflection.FieldAccessor namedSpawnZ = Reflection.getField(namedSpawnPacket, int.class, 2); + @Override + public void setNamedSpawnPosition(Object packet, double x, double y, double z) { + namedSpawnX.set(packet, MathHelper.floor(x * 32)); + namedSpawnY.set(packet, MathHelper.floor(y * 32)); + namedSpawnZ.set(packet, MathHelper.floor(z * 32)); + } } diff --git a/SpigotCore_8/src/de/steamwar/core/FlatteningWrapper8.java b/SpigotCore_8/src/de/steamwar/core/FlatteningWrapper8.java index ca0a636..14b605d 100644 --- a/SpigotCore_8/src/de/steamwar/core/FlatteningWrapper8.java +++ b/SpigotCore_8/src/de/steamwar/core/FlatteningWrapper8.java @@ -64,4 +64,19 @@ public class FlatteningWrapper8 implements FlatteningWrapper.IFlatteningWrapper head.setItemMeta(headmeta); return head; } + + @Override + public Object getPose(boolean sneaking) { + return Byte.valueOf((byte)(sneaking ? 2 : 0)); + } + + private static final Class dataWatcher = Reflection.getClass("{nms}.DataWatcher"); + private static final Class namedSpawnPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutNamedEntitySpawn"); + private static final Reflection.FieldAccessor namedSpawnDataWatcher = Reflection.getField(namedSpawnPacket, dataWatcher, 0); + private static final Class entity = Reflection.getClass("{nms}.Entity"); + private static final Reflection.ConstructorInvoker dataWatcherConstructor = Reflection.getConstructor(dataWatcher, entity); + @Override + public void setNamedSpawnPacketDataWatcher(Object packet) { + namedSpawnDataWatcher.set(packet, dataWatcherConstructor.invoke((Object) null)); + } } diff --git a/SpigotCore_8/src/de/steamwar/core/ProtocolWrapper8.java b/SpigotCore_8/src/de/steamwar/core/ProtocolWrapper8.java new file mode 100644 index 0000000..08123bf --- /dev/null +++ b/SpigotCore_8/src/de/steamwar/core/ProtocolWrapper8.java @@ -0,0 +1,87 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2022 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.core; + +import com.comphenix.tinyprotocol.Reflection; +import com.mojang.authlib.GameProfile; +import org.bukkit.entity.EntityType; + +public class ProtocolWrapper8 implements ProtocolWrapper { + + private static final Reflection.FieldAccessor equipmentSlot; + static { + if(Core.getVersion() == 8) { + equipmentSlot = Reflection.getField(equipmentPacket, int.class, 1); + } else { + Class enumItemSlot = Reflection.getClass("{nms.world.entity}.EnumItemSlot"); + equipmentSlot = Reflection.getField(equipmentPacket, enumItemSlot, 0); + } + } + + private static final Reflection.FieldAccessor equipmentStack = Reflection.getField(equipmentPacket, itemStack, 0); + @Override + public void setEquipmentPacketStack(Object packet, Object slot, Object stack) { + equipmentSlot.set(packet, slot); + equipmentStack.set(packet, stack); + } + + private static final Reflection.FieldAccessor spawnType; + private static final Object tnt; + private static final Object arrow; + private static final Object fireball; + static { + if(Core.getVersion() < 14) { + spawnType = Reflection.getField(ProtocolWrapper.spawnPacket, int.class, Core.getVersion() > 8 ? 6 : 9); + tnt = 50; + arrow = 60; + fireball = 63; + } else { + Class entityTypes = Reflection.getClass("{nms.world.entity}.EntityTypes"); + spawnType = Reflection.getField(ProtocolWrapper.spawnPacket, entityTypes, 0); + tnt = Reflection.getField(entityTypes, "TNT", entityTypes).get(null); + arrow = Reflection.getField(entityTypes, "ARROW", entityTypes).get(null); + fireball = Reflection.getField(entityTypes, "FIREBALL", entityTypes).get(null); + } + } + @Override + public void setSpawnPacketType(Object packet, EntityType type) { + switch(type) { + case PRIMED_TNT: + spawnType.set(packet, tnt); + break; + case ARROW: + spawnType.set(packet, arrow); + break; + case FIREBALL: + spawnType.set(packet, fireball); + break; + } + } + + private static final Class playerInfoDataClass = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutPlayerInfo$PlayerInfoData"); + private static final Class playerInfoPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutPlayerInfo"); + private static final Class enumGamemode = Reflection.getClass(Core.getVersion() > 9 ? "{nms.world.level}.EnumGamemode" : "{nms}.WorldSettings$EnumGamemode"); + private static final Class iChatBaseComponent = Reflection.getClass("{nms.network.chat}.IChatBaseComponent"); + private static final Reflection.ConstructorInvoker playerInfoDataConstructor = Reflection.getConstructor(playerInfoDataClass, playerInfoPacket, GameProfile.class, int.class, enumGamemode, iChatBaseComponent); + @Override + public Object playerInfoDataConstructor(Object packet, GameProfile profile, Object mode) { + return playerInfoDataConstructor.invoke(packet, profile, 0, mode, null); + } +} diff --git a/SpigotCore_9/src/de/steamwar/core/BountifulWrapper9.java b/SpigotCore_9/src/de/steamwar/core/BountifulWrapper9.java index d12a37d..c16300c 100644 --- a/SpigotCore_9/src/de/steamwar/core/BountifulWrapper9.java +++ b/SpigotCore_9/src/de/steamwar/core/BountifulWrapper9.java @@ -19,11 +19,14 @@ package de.steamwar.core; +import com.comphenix.tinyprotocol.Reflection; import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.chat.BaseComponent; import org.bukkit.Sound; import org.bukkit.entity.Player; +import java.util.UUID; + public class BountifulWrapper9 implements BountifulWrapper.IBountifulWrapper { @Override @@ -35,4 +38,49 @@ public class BountifulWrapper9 implements BountifulWrapper.IBountifulWrapper { public void sendMessage(Player player, ChatMessageType type, BaseComponent... msg) { player.spigot().sendMessage(type, msg); } + + private static final Class dataWatcherObject = Reflection.getClass("{nms.network.syncher}.DataWatcherObject"); + private static final Class dataWatcherRegistry = Reflection.getClass("{nms.network.syncher}.DataWatcherRegistry"); + private static final Class dataWatcherSerializer = Reflection.getClass("{nms.network.syncher}.DataWatcherSerializer"); + private static final Reflection.ConstructorInvoker dataWatcherObjectConstructor = Reflection.getConstructor(dataWatcherObject, int.class, dataWatcherSerializer); + @Override + public Object getDataWatcherObject(int index, Class type) { + return dataWatcherObjectConstructor.invoke(index, Reflection.getField(dataWatcherRegistry, dataWatcherSerializer, 0, type).get(null)); + } + + private static final Class item = Reflection.getClass("{nms.network.syncher}.DataWatcher$Item"); + private static final Reflection.ConstructorInvoker itemConstructor = Reflection.getConstructor(item, dataWatcherObject, Object.class); + @Override + public Object getDataWatcherItem(Object dwo, Object value) { + return itemConstructor.invoke(dwo, value); + } + + private static final Class teleportPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutEntityTeleport"); + private static final Reflection.FieldAccessor teleportX = Reflection.getField(teleportPacket, double.class, 0); + private static final Reflection.FieldAccessor teleportY = Reflection.getField(teleportPacket, double.class, 1); + private static final Reflection.FieldAccessor teleportZ = Reflection.getField(teleportPacket, double.class, 2); + @Override + public void setTeleportPacketPosition(Object packet, double x, double y, double z) { + teleportX.set(packet, x); + teleportY.set(packet, y); + teleportZ.set(packet, z); + } + + private static final Class spawnPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutSpawnEntity"); + private static final Reflection.FieldAccessor spawnUUID = Reflection.getField(spawnPacket, UUID.class, 0); + @Override + public void setSpawnPacketUUID(Object packet, UUID uuid) { + spawnUUID.set(packet, uuid); + } + + private static final Class namedSpawnPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutNamedEntitySpawn"); + private static final Reflection.FieldAccessor namedSpawnX = Reflection.getField(namedSpawnPacket, double.class, 0); + private static final Reflection.FieldAccessor namedSpawnY = Reflection.getField(namedSpawnPacket, double.class, 1); + private static final Reflection.FieldAccessor namedSpawnZ = Reflection.getField(namedSpawnPacket, double.class, 2); + @Override + public void setNamedSpawnPosition(Object packet, double x, double y, double z) { + namedSpawnX.set(packet, x); + namedSpawnY.set(packet, y); + namedSpawnZ.set(packet, z); + } } diff --git a/SpigotCore_Main/build.gradle b/SpigotCore_Main/build.gradle index a8b5675..c225092 100644 --- a/SpigotCore_Main/build.gradle +++ b/SpigotCore_Main/build.gradle @@ -48,6 +48,7 @@ dependencies { compileOnly 'com.mojang:authlib:1.5.25' compileOnly 'mysql:mysql-connector-java:5.1.49' compileOnly 'com.viaversion:viaversion-api:4.3.1' + compileOnly 'it.unimi.dsi:fastutil:8.5.6' compileOnly swdep("WorldEdit-1.12") implementation 'net.wesjd:anvilgui:1.5.3-SNAPSHOT' diff --git a/SpigotCore_Main/src/de/steamwar/core/BountifulWrapper.java b/SpigotCore_Main/src/de/steamwar/core/BountifulWrapper.java index 8b22c31..6d5bc69 100644 --- a/SpigotCore_Main/src/de/steamwar/core/BountifulWrapper.java +++ b/SpigotCore_Main/src/de/steamwar/core/BountifulWrapper.java @@ -23,6 +23,8 @@ import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.chat.BaseComponent; import org.bukkit.entity.Player; +import java.util.UUID; + public class BountifulWrapper { private BountifulWrapper() {} @@ -32,5 +34,11 @@ public class BountifulWrapper { void playPling(Player player); void sendMessage(Player player, ChatMessageType type, BaseComponent... msg); + + Object getDataWatcherObject(int index, Class type); + Object getDataWatcherItem(Object dataWatcherObject, Object value); + void setTeleportPacketPosition(Object packet, double x, double y, double z); + void setSpawnPacketUUID(Object packet, UUID uuid); + void setNamedSpawnPosition(Object packet, double x, double y, double z); } } diff --git a/SpigotCore_Main/src/de/steamwar/core/FlatteningWrapper.java b/SpigotCore_Main/src/de/steamwar/core/FlatteningWrapper.java index 9da4b20..0042207 100644 --- a/SpigotCore_Main/src/de/steamwar/core/FlatteningWrapper.java +++ b/SpigotCore_Main/src/de/steamwar/core/FlatteningWrapper.java @@ -38,5 +38,8 @@ public class FlatteningWrapper { Material getMaterial(String material); Material getDye(int colorCode); ItemStack setSkullOwner(String player); + + Object getPose(boolean sneaking); + void setNamedSpawnPacketDataWatcher(Object packet); } } diff --git a/SpigotCore_Main/src/de/steamwar/core/ProtocolWrapper.java b/SpigotCore_Main/src/de/steamwar/core/ProtocolWrapper.java new file mode 100644 index 0000000..b569428 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/core/ProtocolWrapper.java @@ -0,0 +1,43 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2021 SteamWar.de-Serverteam + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + */ + +package de.steamwar.core; + +import com.comphenix.tinyprotocol.Reflection; +import com.mojang.authlib.GameProfile; +import org.bukkit.entity.EntityType; + +public interface ProtocolWrapper { + + Class itemStack = Reflection.getClass("{nms.world.item}.ItemStack"); + Class spawnPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutSpawnEntity"); + Class equipmentPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutEntityEquipment"); + + // 0: hand, 1: offhand, 2: feet, 3: legs, 4: chest, 5: head + Object[] itemSlots = Core.getVersion() > 8 ? Reflection.getClass("{nms.world.entity}.EnumItemSlot").getEnumConstants() : new Integer[]{0, 0, 1, 2, 3, 4}; + + ProtocolWrapper impl = VersionDependent.getVersionImpl(Core.getInstance()); + + void setEquipmentPacketStack(Object packet, Object slot, Object stack); + + void setSpawnPacketType(Object packet, EntityType type); + + Object playerInfoDataConstructor(Object packet, GameProfile profile, Object mode); + +} diff --git a/SpigotCore_Main/src/de/steamwar/entity/REntity.java b/SpigotCore_Main/src/de/steamwar/entity/REntity.java new file mode 100644 index 0000000..d3c3bb6 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/entity/REntity.java @@ -0,0 +1,420 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2020 SteamWar.de-Serverteam + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package de.steamwar.entity; + +import com.comphenix.tinyprotocol.Reflection; +import com.mojang.authlib.GameProfile; +import de.steamwar.core.BountifulWrapper; +import de.steamwar.core.Core; +import de.steamwar.core.FlatteningWrapper; +import de.steamwar.core.ProtocolWrapper; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.EntityType; +import org.bukkit.inventory.ItemStack; + +import java.util.*; +import java.util.function.Consumer; + +public class REntity { + + private static final Object entityStatusWatcher = BountifulWrapper.impl.getDataWatcherObject(0, Byte.class); + private static final Object sneakingDataWatcher = BountifulWrapper.impl.getDataWatcherObject(Core.getVersion() > 12 ? 6 : 0, FlatteningWrapper.impl.getPose(true).getClass()); + private static final Object bowDrawnWatcher = BountifulWrapper.impl.getDataWatcherObject(Core.getVersion() > 12 ? 7 : 6, Byte.class); + private static final Object skinPartsDataWatcher = BountifulWrapper.impl.getDataWatcherObject(16, Byte.class); //TODO multiversioning + private static final Object nameWatcher = BountifulWrapper.impl.getDataWatcherObject(2, Optional.class); // Optional, first optional //TODO multiversioning (dataWatcherRegistry.getFields()[fieldIndex].get(null)) + private static final Object nameVisibleWatcher = BountifulWrapper.impl.getDataWatcherObject(3, Boolean.class); //TODO multiversioning + private static final Object sizeWatcher = BountifulWrapper.impl.getDataWatcherObject(14, Byte.class); //TODO multiversioning + + /* + private static final String SCOREBOARD_TEAMNAME = "Replay"; + private static final Team team; + + static { + if(FightScoreboard.getBukkit().getTeam(SCOREBOARD_TEAMNAME) == null) + team = FightScoreboard.getBukkit().registerNewTeam(SCOREBOARD_TEAMNAME); + else + team = FightScoreboard.getBukkit().getTeam(SCOREBOARD_TEAMNAME); + team.setNameTagVisibility(NameTagVisibility.NEVER); + } + */ + + private static int entityIdCounter = -1; + private static final Random random = new Random(); + + private final REntityServer server; + private final EntityType entityType; + private final int entityId; + private final UUID uuid; + private final String name; + + private double x; + private double y; + private double z; + private byte yaw; + private byte pitch; + private byte headYaw; + + private boolean invisible; + private boolean sneaks; + private int fireTick; + private String displayName; + private final Map itemSlots; + + //TODO packet caching + public REntity(REntityServer server, UUID uuid, String name, Location location) { + this(server, EntityType.PLAYER, uuid, name, location); + //team.addEntry(name); + } + + public REntity(REntityServer server, EntityType entityType, Location location) { + this(server, entityType, new UUID(random.nextLong() & -61441L | 16384L, random.nextLong() & 4611686018427387903L | -9223372036854775808L), null, location); + } + + private REntity(REntityServer server, EntityType entityType, UUID uuid, String name, Location location) { + this.server = server; + this.entityType = entityType; + this.entityId = entityIdCounter--; + this.uuid = uuid; + this.name = name; + + this.x = location.getX(); + this.y = location.getY(); + this.z = location.getZ(); + this.headYaw = this.yaw = rotToByte(location.getYaw()); + this.pitch = rotToByte(location.getPitch()); + + this.itemSlots = entityType == EntityType.PLAYER ? new HashMap<>() : null; + + server.addEntity(this); + } + + public void move(double locX, double locY, double locZ, float pitch, float yaw, byte headYaw) { + server.preEntityMove(this, locX, locZ); + double fromX = this.x; + double fromZ = this.z; + this.x = locX; + this.y = locY; + this.z = locZ; + this.yaw = rotToByte(yaw); + this.pitch = rotToByte(pitch); + this.headYaw = headYaw; + server.updateEntity(this, getTeleportPacket()); + server.updateEntity(this, getHeadRotationPacket()); + server.postEntityMove(this, fromX, fromZ); + } + + private static final Class animationPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutAnimation"); + private static final Reflection.FieldAccessor animationEntity = Reflection.getField(animationPacket, int.class, Core.getVersion() > 15 ? 6 : 0); + private static final Reflection.FieldAccessor animationAnimation = Reflection.getField(animationPacket, int.class, Core.getVersion() > 15 ? 7 : 1); + public void showAnimation(byte animation) { + Object packet = Reflection.newInstance(animationPacket); + animationEntity.set(packet, entityId); + animationAnimation.set(packet, (int) animation); + server.updateEntity(this, packet); + } + + private static final Class velocityPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutEntityVelocity"); + private static final Reflection.FieldAccessor velocityEntity = Reflection.getField(velocityPacket, int.class, 0); + private static final Reflection.FieldAccessor velocityX = Reflection.getField(velocityPacket, int.class, 1); + private static final Reflection.FieldAccessor velocityY = Reflection.getField(velocityPacket, int.class, 2); + private static final Reflection.FieldAccessor velocityZ = Reflection.getField(velocityPacket, int.class, 3); + public void setVelocity(double dX, double dY, double dZ) { + Object packet = Reflection.newInstance(velocityPacket); + velocityEntity.set(packet, entityId); + velocityX.set(packet, calcVelocity(dX)); + velocityY.set(packet, calcVelocity(dY)); + velocityZ.set(packet, calcVelocity(dZ)); + server.updateEntity(this, packet); + } + + private static final Class statusPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutEntityStatus"); + private static final Reflection.FieldAccessor statusEntity = Reflection.getField(statusPacket, int.class, 0); + private static final Reflection.FieldAccessor statusStatus = Reflection.getField(statusPacket, byte.class, 0); + public void showDamage() { + Object packet = Reflection.newInstance(statusPacket); + statusEntity.set(packet, entityId); + statusStatus.set(packet, (byte) 2); + server.updateEntity(this, packet); + } + + public void sneak(boolean sneaking) { + sneaks = sneaking; + server.updateEntity(this, getDataWatcherPacket(sneakingDataWatcher, FlatteningWrapper.impl.getPose(sneaking))); //TODO entityStatusData in 1.8! + } + + public void setOnFire(boolean perma) { + fireTick = perma ? -1 : 21; + server.updateEntity(this, getDataWatcherPacket(entityStatusWatcher, (byte) 1)); + } + + public void setInvisible(boolean invisible) { + this.invisible = invisible; + server.updateEntity(this, getDataWatcherPacket(entityStatusWatcher, (byte) 0x20)); + } + + public void setBowDrawn(boolean drawn, boolean offHand) { + if(Core.getVersion() > 8){ + server.updateEntity(this, getDataWatcherPacket(bowDrawnWatcher, (byte) ((drawn ? 1 : 0) + (offHand ? 2 : 0)))); + }else{ + server.updateEntity(this, getDataWatcherPacket(entityStatusWatcher, (byte)0x10)); + } + } + + private static final Class chatComponentText = Reflection.getClass("{nms}.ChatComponentText"); //TODO multiversioning + private static final Reflection.ConstructorInvoker chatComponentTextConstructor = Reflection.getConstructor(chatComponentText, String.class); //TODO multiversioning + public void setDisplayName(String displayName) { + this.displayName = displayName; + server.updateEntity(this, getDataWatcherPacket(nameWatcher, displayName != null ? Optional.of(chatComponentTextConstructor.invoke(displayName)) : Optional.empty())); + server.updateEntity(this, getDataWatcherPacket(nameVisibleWatcher, displayName != null)); + } + + @Deprecated + public void setItem(String item, boolean enchanted, String slot) { + ItemStack stack = new ItemStack(Material.valueOf(item.replace("minecraft:", "").toUpperCase()), 1); + if(enchanted) + stack.addUnsafeEnchantment(Enchantment.DURABILITY, 1); + + switch(slot){ + case "HEAD": + setItem(ProtocolWrapper.itemSlots[5], stack); + break; + case "CHEST": + setItem(ProtocolWrapper.itemSlots[4], stack); + break; + case "LEGS": + setItem(ProtocolWrapper.itemSlots[3], stack); + break; + case "FEET": + setItem(ProtocolWrapper.itemSlots[2], stack); + break; + case "OFFHAND": + setItem(ProtocolWrapper.itemSlots[1], stack); + break; + case "MAINHAND": + default: + setItem(ProtocolWrapper.itemSlots[0], stack); + } + } + + public void setItem(Object slot, ItemStack stack) { + itemSlots.put(slot, stack); + server.updateEntity(this, getEquipmentPacket(slot, stack)); + } + + public void close() { + server.removeEntity(this); + } + + void spawn(Consumer packetSink) { + if(entityType == EntityType.PLAYER){ + packetSink.accept(getPlayerInfoPacket(addPlayer)); + packetSink.accept(getNamedSpawnPacket()); + for (Map.Entry entry : itemSlots.entrySet()) { + packetSink.accept(getEquipmentPacket(entry.getKey(), entry.getValue())); + } + packetSink.accept(getDataWatcherPacket(skinPartsDataWatcher, (byte) 0x7F)); //TODO multiversioning + } else if(entityType == EntityType.ARMOR_STAND) { + packetSink.accept(getSpawnLivingEntityPacket()); + packetSink.accept(getDataWatcherPacket(sizeWatcher, (byte) 0x10)); // small size //TODO multiversioning + for (Map.Entry entry : itemSlots.entrySet()) { + packetSink.accept(getEquipmentPacket(entry.getKey(), entry.getValue())); + } + } else { + packetSink.accept(getSpawnEntityPacket()); + } + packetSink.accept(getTeleportPacket()); //TODO teleport necessary + packetSink.accept(getHeadRotationPacket()); //TODO head rotation for all types? + + //TODO merge MetadataPacket (DataWatcherPackets) + + if(sneaks) { + packetSink.accept(getDataWatcherPacket(entityStatusWatcher, FlatteningWrapper.impl.getPose(true))); + } + + if(fireTick != 0) { + packetSink.accept(getDataWatcherPacket(sneakingDataWatcher, getDataWatcherPacket(entityStatusWatcher, (byte)1))); + } + + if(invisible) { + packetSink.accept(getDataWatcherPacket(entityStatusWatcher, (byte) 0x20)); + } + + if(displayName != null) { + packetSink.accept(getDataWatcherPacket(nameWatcher, Optional.of(chatComponentTextConstructor.invoke(displayName)))); + packetSink.accept(getDataWatcherPacket(nameVisibleWatcher, true)); + } + } + + void tick() { + if(fireTick > 0) { + fireTick--; + if(fireTick == 0) { + server.updateEntity(this, getDataWatcherPacket(entityStatusWatcher, (byte)0)); + } + } + } + + private static final Class destroyPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutEntityDestroy"); + private static final Reflection.FieldAccessor destroyEntities; + static { + if(Core.getVersion() > 15) + destroyEntities = Reflection.getField(destroyPacket, IntList.class, 0); + else + destroyEntities = Reflection.getField(destroyPacket, int[].class, 0); + } + void despawn(Consumer packetSink){ + if(entityType == EntityType.PLAYER){ + packetSink.accept(getPlayerInfoPacket(removePlayer)); + //team.removeEntry(name); + } + + Object packet = Reflection.newInstance(destroyPacket); + destroyEntities.set(packet, Core.getVersion() > 15 ? new IntArrayList(new int[]{entityId}) : new int[]{entityId}); + packetSink.accept(packet); + } + + double x() { + return x; + } + + double z() { + return z; + } + + private static final Class metadataPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutEntityMetadata"); + private static final Reflection.FieldAccessor metadataEntity = Reflection.getField(metadataPacket, int.class, 0); + private static final Reflection.FieldAccessor metadataMetadata = Reflection.getField(metadataPacket, List.class, 0); + + private Object getDataWatcherPacket(Object... dataWatcherKeyValues) { + Object packet = Reflection.newInstance(metadataPacket); + metadataEntity.set(packet, entityId); + + ArrayList nativeWatchers = new ArrayList<>(1); + for(int i = 0; i < dataWatcherKeyValues.length; i+=2) { + nativeWatchers.add(BountifulWrapper.impl.getDataWatcherItem(dataWatcherKeyValues[i], dataWatcherKeyValues[i+1])); + } + metadataMetadata.set(packet, nativeWatchers); + + return packet; + } + + private static final Class teleportPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutEntityTeleport"); + private static final Reflection.FieldAccessor teleportEntity = Reflection.getField(teleportPacket, int.class, 0); + private static final Reflection.FieldAccessor teleportYaw = Reflection.getField(teleportPacket, byte.class, 0); + private static final Reflection.FieldAccessor teleportPitch = Reflection.getField(teleportPacket, byte.class, 1); + private Object getTeleportPacket(){ + Object packet = Reflection.newInstance(teleportPacket); + teleportEntity.set(packet, entityId); + BountifulWrapper.impl.setTeleportPacketPosition(packet, x, y, z); + teleportYaw.set(packet, yaw); + teleportPitch.set(packet, pitch); + return packet; + } + + private static final Class headRotationPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutEntityHeadRotation"); + private static final Reflection.FieldAccessor headRotationEntity = Reflection.getField(headRotationPacket, int.class, 0); + private static final Reflection.FieldAccessor headRotationYaw = Reflection.getField(headRotationPacket, byte.class, 0); + private Object getHeadRotationPacket(){ + Object packet = Reflection.newInstance(headRotationPacket); + headRotationEntity.set(packet, entityId); + headRotationYaw.set(packet, headYaw); + return packet; + } + + private static final Reflection.FieldAccessor equipmentEntity = Reflection.getField(ProtocolWrapper.equipmentPacket, int.class, 0); + + private static final Class craftItemStack = Reflection.getClass("{obc}.inventory.CraftItemStack"); + private static final Reflection.MethodInvoker asNMSCopy = Reflection.getTypedMethod(REntity.craftItemStack, "asNMSCopy", ProtocolWrapper.itemStack, ItemStack.class); + private Object getEquipmentPacket(Object slot, ItemStack stack){ + Object packet = Reflection.newInstance(ProtocolWrapper.equipmentPacket); + equipmentEntity.set(packet, entityId); + ProtocolWrapper.impl.setEquipmentPacketStack(packet, slot, asNMSCopy.invoke(null, stack)); //TODO grouped send (1.8 incompatible) + return packet; + } + + private static final Class playerInfoPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutPlayerInfo"); + private static final Class playerInfoActionClass = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutPlayerInfo$EnumPlayerInfoAction"); + private static final Object addPlayer = playerInfoActionClass.getEnumConstants()[0]; + private static final Reflection.FieldAccessor playerInfoAction = Reflection.getField(playerInfoPacket, playerInfoActionClass, 0); + private static final Object removePlayer = playerInfoActionClass.getEnumConstants()[4]; + private static final Reflection.FieldAccessor playerInfoData = Reflection.getField(playerInfoPacket, List.class, 0); + private static final Class enumGamemode = Reflection.getClass(Core.getVersion() > 9 ? "{nms.world.level}.EnumGamemode" : "{nms}.WorldSettings$EnumGamemode"); + private static final Object creative = enumGamemode.getEnumConstants()[Core.getVersion() > 15 ? 1 : 2]; + private Object getPlayerInfoPacket(Object action){ + Object packet = Reflection.newInstance(playerInfoPacket); + playerInfoAction.set(packet, action); + playerInfoData.set(packet, Collections.singletonList(ProtocolWrapper.impl.playerInfoDataConstructor(packet, new GameProfile(uuid, name), creative))); + return packet; + } + + private static final Class namedSpawnPacket = Reflection.getClass("{nms.network.protocol.game}.PacketPlayOutNamedEntitySpawn"); + private static final Reflection.FieldAccessor namedSpawnEntity = Reflection.getField(namedSpawnPacket, int.class, 0); + private static final Reflection.FieldAccessor namedSpawnUUID = Reflection.getField(namedSpawnPacket, UUID.class, 0); + private Object getNamedSpawnPacket() { + Object packet = Reflection.newInstance(namedSpawnPacket); + namedSpawnEntity.set(packet, entityId); + namedSpawnUUID.set(packet, uuid); + BountifulWrapper.impl.setNamedSpawnPosition(packet, x, y, z); + FlatteningWrapper.impl.setNamedSpawnPacketDataWatcher(packet); + return packet; + } + + private static final Class spawnLivingPacket = Reflection.getClass("{nms}.PacketPlayOutSpawnEntityLiving"); //TODO multiversioning + private static final Reflection.ConstructorInvoker spawnLivingPacketConstructor = Reflection.getConstructor(spawnLivingPacket); //TODO multiversioning + private static final Reflection.FieldAccessor spawnLivingEntityId = Reflection.getField(spawnLivingPacket, int.class, 0); //TODO multiversioning + private static final Reflection.FieldAccessor spawnLivingUUID = Reflection.getField(spawnLivingPacket, UUID.class, 0); //TODO multiversioning + private static final Reflection.FieldAccessor spawnLivingEntityType = Reflection.getField(spawnLivingPacket, int.class, 1); //TODO multiversioning + private static final Reflection.FieldAccessor spawnLivingEntityX = Reflection.getField(spawnLivingPacket, double.class, 0); //TODO multiversioning + private static final Reflection.FieldAccessor spawnLivingEntityY = Reflection.getField(spawnLivingPacket, double.class, 1); //TODO multiversioning + private static final Reflection.FieldAccessor spawnLivingEntityZ = Reflection.getField(spawnLivingPacket, double.class, 2); //TODO multiversioning + private Object getSpawnLivingEntityPacket() { + Object packet = spawnLivingPacketConstructor.invoke(); + spawnLivingEntityId.set(packet, entityId); + spawnLivingUUID.set(packet, uuid); + spawnLivingEntityType.set(packet, 1); //TODO set correct type + spawnLivingEntityX.set(packet, x); + spawnLivingEntityY.set(packet, y); + spawnLivingEntityZ.set(packet, z); + return packet; + } + + + private static final Reflection.FieldAccessor spawnEntity = Reflection.getField(ProtocolWrapper.spawnPacket, int.class, 0); + private Object getSpawnEntityPacket() { + Object packet = Reflection.newInstance(ProtocolWrapper.spawnPacket); + spawnEntity.set(packet, entityId); + BountifulWrapper.impl.setSpawnPacketUUID(packet, uuid); + ProtocolWrapper.impl.setSpawnPacketType(packet, entityType); + //TODO set position + return packet; + } + + private byte rotToByte(float rot) { + return (byte)((int)(rot * 256.0F / 360.0F)); + } + + private int calcVelocity(double value) { + return (int)(Math.max(-3.9, Math.min(value, 3.9)) * 8000); + } +} diff --git a/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java b/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java new file mode 100644 index 0000000..3a63b41 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java @@ -0,0 +1,217 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2022 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.entity; + +import com.comphenix.tinyprotocol.TinyProtocol; +import de.steamwar.core.Core; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.scheduler.BukkitTask; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + + +public class REntityServer implements Listener { + + private static final HashSet emptyEntities = new HashSet<>(0); + private static final HashSet emptyPlayers = new HashSet<>(0); + + private final HashMap> entities = new HashMap<>(); + private final HashMap> players = new HashMap<>(); + private final HashMap lastLocation = new HashMap<>(); + + private final BukkitTask tickTask; + + public REntityServer() { + Core.getInstance().getServer().getPluginManager().registerEvents(this, Core.getInstance()); + tickTask = Core.getInstance().getServer().getScheduler().runTaskTimer(Core.getInstance(), this::tick, 1, 1); + } + + public void addPlayer(Player player) { + Location location = player.getLocation(); + lastLocation.put(player, location); + forChunkInView(player, location, (x, z) -> addPlayerToChunk(player, x, z)); + } + + public void removePlayer(Player player) { + Location location = lastLocation.remove(player); + forChunkInView(player, location, (x, z) -> removePlayerFromChunk(player, x, z)); + } + + public void close() { + for(Player player : lastLocation.keySet().toArray(new Player[0])) { + removePlayer(player); + } + tickTask.cancel(); + HandlerList.unregisterAll(this); + } + + void addEntity(REntity entity) { + entities.computeIfAbsent(entityToId(entity), i -> new HashSet<>()).add(entity); + entity.spawn(packet -> updateEntity(entity, packet)); + } + + void preEntityMove(REntity entity, double toX, double toZ) { + long fromId = entityToId(entity); + long toId = posToId(toX, toZ); + if(fromId == toId) + return; + + onMissing(players.get(fromId), players.get(toId), entity::despawn); + } + + void postEntityMove(REntity entity, double fromX, double fromZ) { + long fromId = posToId(fromX, fromZ); + long toId = entityToId(entity); + if(fromId == toId) + return; + + onMissing(players.get(toId), players.get(fromId), entity::spawn); + } + + void updateEntity(REntity entity, Object packet) { + for(Player player : players.getOrDefault(entityToId(entity), emptyPlayers)) { + TinyProtocol.instance.sendPacket(player, packet); + } + } + + void removeEntity(REntity entity) { + entity.despawn(packet -> updateEntity(entity, packet)); + long id = entityToId(entity); + HashSet entitiesInChunk = entities.get(id); + entitiesInChunk.remove(entity); + if(entitiesInChunk.isEmpty()) + entities.remove(id); + } + + //TODO on settings, on respawn? on boatmove? + + @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) + public void onMove(PlayerMoveEvent e) { + Player player = e.getPlayer(); + Location from = lastLocation.get(player); + Location to = e.getTo(); + if(from == null || to == null) + return; + + int fromX = posToChunk(from.getX()); + int fromZ = posToChunk(from.getZ()); + int toX = posToChunk(to.getX()); + int toZ = posToChunk(to.getZ()); + if(fromX == toX && fromZ == toZ) + return; + + int viewDistance = viewRadius(player); + forChunkInView(player, from, (x, z) -> { + if(Math.abs(x - toX) > viewDistance || Math.abs(z - toX) > viewDistance) { + removePlayerFromChunk(player, x, z); + } + }); + forChunkInView(player, to, (x, z) -> { + if(Math.abs(x - fromX) > viewDistance || Math.abs(z - fromZ) > viewDistance) { + addPlayerToChunk(player, x, z); + } + }); + } + + @EventHandler + public void onQuit(PlayerQuitEvent e) { + Player player = e.getPlayer(); + Location location = lastLocation.remove(player); + if(location == null) + return; + + forChunkInView(player, location, (x, z) -> players.get(chunkToId(x, z)).remove(player)); + } + + private void onMissing(HashSet of, HashSet in, Consumer> packetProvider) { + for(Player player : of) { + if(!in.contains(player)) { + packetProvider.accept(packet -> TinyProtocol.instance.sendPacket(player, packet)); //TODO multi packet generation + } + } + } + + private void forChunkInView(Player player, Location location, BiConsumer func) { + int chunkX = posToChunk(location.getX()); + int chunkZ = posToChunk(location.getZ()); + int viewDistance = viewRadius(player); + + for(int x = chunkX - viewDistance; x <= chunkX + viewDistance; x++) { + for(int z = chunkZ - viewDistance; z <= chunkZ + viewDistance; z++) { + func.accept(x, z); + } + } + } + + private void addPlayerToChunk(Player player, int x, int z) { + long id = chunkToId(x, z); + players.computeIfAbsent(id, i -> new HashSet<>()).add(player); + for(REntity entity : entities.getOrDefault(id, emptyEntities)) { + entity.spawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); + } + } + + private void removePlayerFromChunk(Player player, int x, int z) { + long id = chunkToId(x, z); + players.get(id).remove(player); + for(REntity entity : entities.getOrDefault(id, emptyEntities)) { + entity.despawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); + } + } + + private void tick() { + for(HashSet entitiesInChunk : entities.values()) { + for(REntity entity : entitiesInChunk) { + entity.tick(); + } + } + } + + private int posToChunk(double coord) { + return (int)(coord / 16) - (coord < 0 ? 1 : 0); + } + + private int viewRadius(Player player) { + return player.getClientViewDistance() / 2; + } + + private long entityToId(REntity entity) { + return posToId(entity.x(), entity.z()); + } + + private long posToId(double x, double z) { + return chunkToId(posToChunk(x), posToChunk(z)); + } + + private long chunkToId(int x, int z) { + //TODO negative coord clash? + return (long) x << 32 + z; + } +}