|
| 1 | +package fr.black_eyes.lootchest.falleffect; |
| 2 | + |
| 3 | +import net.minecraft.world.item.Item; |
| 4 | +import net.minecraft.world.item.Items; |
| 5 | +import org.bukkit.Location; |
| 6 | +import org.bukkit.Material; |
| 7 | + |
| 8 | +import java.util.*; |
| 9 | +import java.util.stream.Stream; |
| 10 | +import java.util.stream.StreamSupport; |
| 11 | + |
| 12 | +import org.bukkit.plugin.java.JavaPlugin; |
| 13 | +import org.bukkit.scheduler.BukkitRunnable; |
| 14 | + |
| 15 | +import com.mojang.datafixers.util.Pair; |
| 16 | + |
| 17 | +import net.minecraft.server.MinecraftServer; |
| 18 | +import net.minecraft.server.level.ServerLevel; |
| 19 | +import net.minecraft.server.level.ServerPlayer; |
| 20 | +import net.minecraft.world.entity.EquipmentSlot; |
| 21 | +import net.minecraft.world.entity.decoration.ArmorStand; |
| 22 | +import net.minecraft.world.phys.Vec3; |
| 23 | +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; |
| 24 | +import net.minecraft.network.protocol.game.ClientboundMoveEntityPacket; |
| 25 | +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; |
| 26 | +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; |
| 27 | +import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket; |
| 28 | +import net.minecraft.world.item.ItemStack; |
| 29 | + |
| 30 | +/** |
| 31 | + * 1.17+ class to make an invisible armorstand fall from the sky with packets and a block on its head |
| 32 | + */ |
| 33 | +@SuppressWarnings("unused") |
| 34 | +public final class Fallv_1_21_11 implements IFallPacket { |
| 35 | + private final ClientboundAddEntityPacket spawnPacket; |
| 36 | + private final net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket dataPacket; |
| 37 | + private final ClientboundSetEquipmentPacket equipmentPacket; |
| 38 | + private final ClientboundMoveEntityPacket motionPacket; |
| 39 | + private final ArmorStand armorstand; |
| 40 | + private final Location startLocation; |
| 41 | + private final int height; |
| 42 | + private final double speed; |
| 43 | + private static ItemStack headItem; |
| 44 | + private long counter; |
| 45 | + private static final short SPEED_ONE_BLOCK_PER_SECOND = 410; // speed found after like 10 tests corresponding to one block fall per second |
| 46 | + private static final long COUNTER_ONE_BLOCK = 10; // after 10*2 ticks at speed 410, the armorstand falls one block |
| 47 | + private static final short SPEED_MULTIPLIER = 31; |
| 48 | + private final JavaPlugin instance; |
| 49 | + |
| 50 | + /** |
| 51 | + * Get the actual location of the armorstand |
| 52 | + * Getting it with the entity class won't work because I move the armorstand, but only client sees it moving, |
| 53 | + * so I need to get the location from the start location and the counter |
| 54 | + * @return Location of the armorstand |
| 55 | + */ |
| 56 | + @Override |
| 57 | + public Location getLocation() { |
| 58 | + Location loc = startLocation.clone(); |
| 59 | + loc.setY(loc.getY() - (height-((counter /(COUNTER_ONE_BLOCK/(this.speed*SPEED_MULTIPLIER)))-3)) ); |
| 60 | + return loc; |
| 61 | + } |
| 62 | + |
| 63 | + /** |
| 64 | + * Creates four packets to make an armorstand fall from the sky |
| 65 | + * The armorstand will have a head made of the headItem material |
| 66 | + * The armorstand will fall from its spawn location to {height} blocks below |
| 67 | + * One packet for the spawn of the armorstand |
| 68 | + * One packet for the equipment of the armorstand |
| 69 | + * One packet for the movement of the armorstand |
| 70 | + * One packet for its datas (invisible, etc.) |
| 71 | + * @param loc The location where the armorstand will spawn |
| 72 | + * @param headItem The material used for the head of the armorstand, the main reason for all of this |
| 73 | + * @param height The height of the fall, in blocks |
| 74 | + * @param speed The speed of the fall, does not have a clear meaning |
| 75 | + */ |
| 76 | + public Fallv_1_21_11(Location loc, Material headItem, int height, double speed, JavaPlugin plugin) { |
| 77 | + this.instance = plugin; |
| 78 | + this.speed = speed; |
| 79 | + this.height = height; |
| 80 | + this.startLocation = loc; |
| 81 | + @SuppressWarnings("deprecation") |
| 82 | + MinecraftServer server = MinecraftServer.getServer(); |
| 83 | + |
| 84 | + // stream all levels and filter the one that matches the world name |
| 85 | + org.bukkit.World world = loc.getWorld(); |
| 86 | + String worldName = (world != null) ? world.getName() : null; |
| 87 | + ServerLevel s = StreamSupport.stream(server.getAllLevels().spliterator(), false).filter(level -> level.getWorld().getName().equals(worldName)).findFirst().orElse(null); |
| 88 | + ArmorStand stand = new ArmorStand(s, loc.getX(), loc.getY(), loc.getZ()); |
| 89 | + stand.setInvisible(true); |
| 90 | + stand.setNoBasePlate(true); |
| 91 | + armorstand = stand; |
| 92 | + |
| 93 | + List<Pair<EquipmentSlot, ItemStack>> equipmentList = Collections.singletonList( |
| 94 | + new Pair<>(EquipmentSlot.HEAD, getNmsItemStackFromMaterial(headItem)) // nmsHeadItem is your helmet ItemStack |
| 95 | + ); |
| 96 | + |
| 97 | + equipmentPacket = new ClientboundSetEquipmentPacket(stand.getId(), equipmentList); |
| 98 | + spawnPacket = new ClientboundAddEntityPacket( |
| 99 | + stand.getId(), // Entity ID |
| 100 | + UUID.randomUUID(), // Unique ID |
| 101 | + loc.getX(), loc.getY(), loc.getZ(), // Position (X, Y, Z) |
| 102 | + loc.getYaw(), loc.getPitch(), // Rotation (Yaw, Pitch) |
| 103 | + stand.getType(), // Entity type (ArmorStand) |
| 104 | + 0, // No specific motion (use 0 for no velocity) |
| 105 | + new Vec3(0, 0, 0), // Velocity (none in this case) |
| 106 | + 0.0); |
| 107 | + dataPacket = new ClientboundSetEntityDataPacket(stand.getId(), stand.getEntityData().getNonDefaultValues()); |
| 108 | + short newSpeed = (short)(this.speed*SPEED_MULTIPLIER*SPEED_ONE_BLOCK_PER_SECOND); // the plugin had a default speed of 0.8 wich was quite fast, but it was never meaningful, 0.8 was like 5 blocks per seconds. |
| 109 | + // divide the counter by the speed multiplyer to get the number of ticks the armorstand will need to fall to get the fall ticks of one block for the new speed, then multiply it by the total height to fall |
| 110 | + counter = (int)((COUNTER_ONE_BLOCK/(this.speed*SPEED_MULTIPLIER))*(height+3)); |
| 111 | + // I added 3 to height, else packet is removed too fast |
| 112 | + motionPacket = new ClientboundMoveEntityPacket.Pos( |
| 113 | + armorstand.getId(), |
| 114 | + (short) (0), // Multiply by 4096 for correct movement scaling |
| 115 | + (short) (-newSpeed), // Adjust Y for gravity/fall (lower Y for falling) |
| 116 | + (short) (0), |
| 117 | + true // Yaw |
| 118 | + ); |
| 119 | + } |
| 120 | + |
| 121 | + /** |
| 122 | + * Sends the four packets to all players that are in a 100 blocks radius of the armorstand |
| 123 | + * An entity can't have gravity with packets, so we will manually move it to the ground, ticks after ticks |
| 124 | + */ |
| 125 | + @Override |
| 126 | + public void sendPacketToAll() { |
| 127 | + @SuppressWarnings("deprecation") |
| 128 | + MinecraftServer server = MinecraftServer.getServer(); |
| 129 | + Stream<ServerPlayer> players = server.getPlayerList().getPlayers().stream(); |
| 130 | + players.forEach(p -> { |
| 131 | + // check if player is in the same world as the armorstand |
| 132 | + if (!p.getBukkitEntity().getWorld().getName().equals(Objects.requireNonNull(startLocation.getWorld()).getName())) { |
| 133 | + return; |
| 134 | + } |
| 135 | + // check distance between player and armorstand |
| 136 | + if (p.distanceTo(armorstand) > 100) { |
| 137 | + return; |
| 138 | + } |
| 139 | + p.connection.send(spawnPacket); |
| 140 | + p.connection.send(dataPacket); |
| 141 | + p.connection.send(equipmentPacket); |
| 142 | + new BukkitRunnable() { |
| 143 | + @Override |
| 144 | + public void run() { |
| 145 | + p.connection.send(motionPacket); |
| 146 | + counter--; |
| 147 | + if(counter <= 0){ |
| 148 | + cancel(); |
| 149 | + removePacketToAll(); |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + }.runTaskTimer(instance, 0, 2L); |
| 154 | + }); |
| 155 | + } |
| 156 | + |
| 157 | + /** |
| 158 | + * Removes the armorstand from all players |
| 159 | + */ |
| 160 | + @Override |
| 161 | + public void removePacketToAll() { |
| 162 | + @SuppressWarnings("deprecation") |
| 163 | + MinecraftServer server = MinecraftServer.getServer(); |
| 164 | + Stream<ServerPlayer> players = server.getPlayerList().getPlayers().stream(); |
| 165 | + players.forEach(p -> p.connection.send(new ClientboundRemoveEntitiesPacket(armorstand.getId()))); |
| 166 | + } |
| 167 | + |
| 168 | + |
| 169 | + /** |
| 170 | + * Get an NMS ItemStack from a Bukkit Material |
| 171 | + */ |
| 172 | + public ItemStack getNmsItemStackFromMaterial(Material material) { |
| 173 | + String itemKey = "item."+material.getKey().toString().replace(":","."); |
| 174 | + String blockKey = "block."+material.getKey().toString().replace(":","."); |
| 175 | + if(headItem != null && (Objects.requireNonNull(headItem.getItem().getDescriptionId()).equals(itemKey) || headItem.getItem().getDescriptionId().equals(blockKey))) { |
| 176 | + return headItem; |
| 177 | + } |
| 178 | + for(Item item : Arrays.stream(Items.class.getFields()).map(field -> { |
| 179 | + try { |
| 180 | + return (Item) field.get(null); |
| 181 | + } catch (IllegalArgumentException | IllegalAccessException ignored) { |
| 182 | + } |
| 183 | + return null; |
| 184 | + }).toArray(Item[]::new)) { |
| 185 | + if (item == null) { |
| 186 | + continue; |
| 187 | + } |
| 188 | + if (Objects.requireNonNull(item.getDescriptionId()).equals(itemKey) || item.getDescriptionId().equals(blockKey)) { |
| 189 | + headItem = new ItemStack(item); |
| 190 | + return headItem; |
| 191 | + } |
| 192 | + |
| 193 | + } |
| 194 | + return ItemStack.EMPTY; // Return an empty item if reflection fails |
| 195 | + } |
| 196 | +} |
0 commit comments